Skip to content

Latest commit

 

History

History
94 lines (70 loc) · 5.26 KB

how-it-works.md

File metadata and controls

94 lines (70 loc) · 5.26 KB

How does it work?

This project abuses the internals implementation of rollup in a quite interesting way. See, rollup uses string manipulation to generate its output file, by changing and removing parts of the input file content. It also does quite extensive dead code elimination by walking the AST of the input code and figuring out which parts it can safely remove from the output bundle.

We can use this knowledge to specifically direct rollup to keep and remove parts of our input file, and to rename the correct Identifiers.

What we do, is to transform the Typescript code into a virtual AST, that is in itself just really strange code, but it makes rollup do what we would like it to do.

Creating declarations

For each export (class, function, interface or type), we will create a bogus FunctionDeclaration for rollup. The trick here is to annotate this FunctionDeclaration with a certain start and end. Rollup will then just remove all the bytes between start and end, without even looking into what those bytes actually are, if it figures out that the declaration is not referenced.

function foo() {}
export function bar() {}

See it live

Creating side-effects

Rollup will actually analyze functions for side-effects and happily remove functions which are side-effect free, even though they are referenced in other parts of your code.

In order for rollup to at least consider putting a function into our bundle, we have to introduce a side-effect into the function. How do we do that? The answer is to generate code that rollup can not see inside. For example by calling an unreferenced identifier. That identifier could potentially live in window and rollup does not know that. So it does not touch that code.

_()

See it live

Creating references

If someone has looked very carefully at the previous example, you will see that rollup actually inserts a semicolon after the CallExpression. This one took me a long time to figure out and work around.

In the end I decided to create references between different declarations as function argument defaults. That way rollup will not insert semicolons that would otherwise mess with out TypeScript code.

Again, all the Identifiers are annotated with correct start and end markers. So if rollup decides to rename them, it will touch the correct parts of the code. Also, the function name itself is part of the identifier list, because there might be identifiers before the function name, such as type parameters and maybe things we would want to remove.

function foo(_0 = foo) {}
function bar(_0 = bar, _1 = foo) {}
function baz(_0 = baz) {}
export function foobar(_0 = foobar, _1 = bar, _2 = baz) {}

See it live

Removing nested code

Building on the previous example, we can use the list of function argument defaults to, and the thing we learned before about removing top-level code to mark nested code for deletion.

For this case, we create an arrow function with some dead code inside. As you will see in the example, rollup will remove that code. Again, annotating it with start and end markers and you are done.

function foo(_0 = foo, _1 = () => {removeme}) {}
export function bar(_0 = bar, _1 = foo) {}

See it live

With that, we have all the tools to create roll-upd .d.ts files.