Skip to content

Tree Shaking

According to the official documentation, the main tree-shaking configuration options in rollup are as follows:

Annotations

The default value is true, indicating that rollup will determine whether to keep certain code based on annotations. The annotation format is @__PURE__, #__PURE__ or @__NO_SIDE_EFFECTS__, #__NO_SIDE_EFFECTS__. rollup will determine whether to keep certain code based on these annotations.

  1. The @__PURE__ or #__PURE__ comment marks specific function calls or constructor calls as side-effect free. This means rollup will perform tree-shaking optimization, i.e., remove the calls unless the return value is used in some code that is not tree-shaking optimized. These annotations need to be placed immediately before the call to take effect. The following code will be completely tree-shaken unless this option is set to false.

    js
    /*@__PURE__*/ console.log('side-effect');
    
    class Impure {
      constructor() {
        console.log('side-effect');
      }
    }
    
    /*@__PURE__ There may be additional text in the comment */ new Impure();
  2. The @__NO_SIDE_EFFECTS__ or #__NO_SIDE_EFFECTS__ comment marks the function declaration itself as side-effect free. When a function is marked as having no side effects, all calls to that function will be considered side-effect free. The following code will be completely tree-shaken.

    js
    /*@__NO_SIDE_EFFECTS__*/
    function impure() {
      console.log('side-effect');
    }
    
    /*@__NO_SIDE_EFFECTS__*/
    const impureArrowFn = () => {
      console.log('side-effect');
    };
    
    impure(); // <-- call will be considered as side effect free
    impureArrowFn(); // <-- call will be considered as side effect free

Implementation of annotations in rollup

rollup internally implements an ast class node for each node. After parsing the ast generated by native swc, it recursively instantiates each required ast class node based on the generated ast structure. The instantiated ast tree is what rollup operates on in subsequent steps, meaning all subsequent operations are based on the instantiated ast tree.

ts
export default class Module {
  async setSource({
    ast,
    code,
    customTransformCache,
    originalCode,
    originalSourcemap,
    resolvedIds,
    sourcemapChain,
    transformDependencies,
    transformFiles,
    ...moduleOptions
  }: TransformModuleJSON & {
    resolvedIds?: ResolvedIdMap;
    transformFiles?: EmittedFile[] | undefined;
  }): Promise<void> {
    // Omitted code
    this.ast = new nodeConstructors[ast.type](
      programParent,
      this.scope
    ).parseNode(ast) as Program;
    // Omitted code
  }
}

It's important to note that all ast node class inherit from NodeBase. In the constructor of ast node class, parseNode is called to further parse ast children node class. At this point, super.parseNode(esTreeNode); is called to invoke the parseNode method of NodeBase.

ts
export const ANNOTATION_KEY = '_rollupAnnotations';
export const INVALID_ANNOTATION_KEY = '_rollupRemoved';

export default class NodeBase {
  parseNode(esTreeNode: GenericEsTreeNode): this {
    for (const [key, value] of Object.entries(esTreeNode)) {
      if (key.charCodeAt(0) === 95 /* _ */) {
        if (key === ANNOTATION_KEY) {
          this.annotations = value as RollupAnnotation[];
        } else if (key === INVALID_ANNOTATION_KEY) {
          (this as unknown as Program).invalidAnnotations =
            value as RollupAnnotation[];
        }
      }
    }
  }
}

At this point, it collects the comments before the ast node and assigns them to this.annotations.

compat estree ast

It's worth noting that standard estree ast itself does not include comments.

rollup uses swc's capabilities in the rust layer to ultimately generate a compat estree ast. This ast structure contains all the information required by estree ast and extends some properties, such as _rollupAnnotations, which includes comment node information before statements. For details, refer to Optimize Ast Compatibility.

Manual Pure Functions

The default value is true, indicating that rollup will determine whether to keep certain code based on manually set pure functions.

Module Side Effects

Plays an important role in tree shaking. When set to false, tree shaking will be performed on unreferenced other modules and external dependency modules.

js
import { foo } from './foo.js';
import { unused } from 'external-a';
import 'external-b';
console.log(42);
js
import { bar } from './bar.js';
console.log(bar);
export const foo = 'foo';
js
export const bar = 'bar';

outputs:

js
// treeshake.moduleSideEffects === true
// import { foo } from './foo.js' is equivalent to import './foo.js';
import 'external-a';
import 'external-b';

const bar = 'bar';
console.log(bar);

console.log(42);
js
console.log(42);

If an external or internal dependency module with side effects is specified, and the dependency module's reference is not used, it is equivalent to resolving to:

js
import { a } from 'external-a';
// ==>
import 'external-b';

TIP

CSS modules are typically loaded using:

js
import './style.css';

If style.css is specified as having no side effects, the style.css module will be removed during tree-shaking, which is not reasonable.

Therefore, style.css is typically specified as having side effects to ensure that the side effects of the style.css module are preserved during tree-shaking.

If the reference variable of the dependency module is used, whether to scan for side effects in re-exported modules depends on how the variable is re-exported.

js
// Input file a.js
import { foo } from './b.js';
import { demo } from 'demo';
console.log(foo, demo);

// Input file b.js
// Direct re-export will ignore side effects
export { foo } from './c.js';
console.log('this side-effect is ignored');

// Input file c.js
// Non-direct re-export will include side effects
import { foo } from './d.js';
foo.mutated = true;
console.log('this side-effect and the mutation are retained');
export { foo };

// Input file d.js
console.log('d.js');
export const foo = 42;

This means that for directly re-exported modules that are specified as having no side effects, rollup will ignore their side effects, while for non-directly re-exported modules, rollup will preserve the module's side effects, even if the module is specified as having no side effects. This is different from the default value of moduleSideEffects being true, which preserves side effects of all reachable modules.

js
// Output when treeshake.moduleSideEffects === false
import { demo } from 'demo';

console.log('d.js');
const foo = 42;

foo.mutated = true;
console.log('this side-effect and the mutation are retained');

console.log(foo, demo);


// Output when treeshake.moduleSideEffects === true
import { demo } from 'demo';

console.log('d.js');
const foo = 42;

foo.mutated = true;
console.log('this side-effect and the mutation are retained');

console.log('this side-effect is ignored');

console.log(foo, demo);

Analysis of rollup's moduleSideEffects mechanism

moduleSideEffects config

The moduleSideEffects configuration can be specified to rollup in two ways:

  1. Plugin hooks (resolveId, load, transform): Specify side effects for local modules, with the highest priority, can override the treeshake.moduleSideEffects configuration.
  2. Through the treeshake.moduleSideEffects configuration: Specify side effects for global modules, with the lowest priority. By default, treeshake.moduleSideEffects = true, meaning rollup assumes all modules have side effects.

moduleSideEffects Notes

moduleSideEffects is essentially equivalent to the sideEffects field in package.json, but rollup itself does not respect the sideEffects field in package.json, see: rollup/rollup#2593.

The official @rollup/plugin-node-resolve plugin respects the sideEffects field in package.json, but this is essentially classified as side effects provided by the plugin.

js
import { foo } from './side-effect-1.js';
import { bar } from './side-effect-2.js';
import { noSideEffect } from './no-side-effect.js';
console.log(foo, noSideEffect);
js
console.log('side-effect-1');
export const foo = 42;
js
console.log('side-effect-2');
export const bar = 42;
js
console.log('no-side-effect');
export const bar = 42;
The Mechanism of moduleSideEffects

The moduleSideEffects configuration is common and affects side effects at the module level. Since rollup assumes by default that all top-level statements of all modules contain side effects, the moduleSideEffects configuration essentially tells rollup whether to assume that the top-level statements of specified modules have side effects.

Entry modules all have side effects and cannot be overridden by the sideEffects configuration, so the side effects of entry modules themselves will always be included in the final output.

When a module has no side effects, for rollup, it adopts a lazy evaluation optimization strategy, meaning it will only analyze and include the module's own side effects when the reference of the side-effect-free module is actually used. In detail, during the first tree-shaking, rollup will not include side effects of modules specified as having no side effects, only including side effects of modules specified as having side effects. When analyzing side effects of side-effect modules, if the side-effect statements depend on reference variables from modules specified as having no side effects, it determines that the side-effect-free module's side effects and related statements dependent on the side-effect module's references also have side effects, and activates side effect detection for the side-effect-free module. During the second tree-shaking, it will include side effects of the side-effect-free module and related statements dependent on the references.

When a module has side effects, even if the references exposed by the module with side effects are not actually used, it will further analyze and include its own side effects.

Property Read Side Effects

The default value is true

Indicates that rollup will determine whether to keep certain code based on property read side effects.

Try Catch Deoptimization

The default value is true

For workflows that rely on error throwing for feature detection, rollup will disable tree-shaking optimization in try statements by default, meaning rollup will keep all code in try statements. If function parameters are used in try statements, those parameters will not be optimized either.

js
function a(d, w) {
  d = d + 1;
  w = w + 1;
  try {
    const b = 1;
    const c = w;
  } catch (_) {}
}
const param1 = 1;
const param2 = 2;
a(param1, param2);

After tree-shaking optimization

js
function a(d, w) {
  w = w + 1;
  try {
    const b = 1;
    const c = w;
  } catch (_) {}
}
const param1 = 1;
const param2 = 2;
a(param1, param2);

In the above example, in function a, since parameter w is used in the try statement, rollup determines that statements related to w have side effects, while parameter d is used outside the try statement, so rollup will optimize away the side effects of parameter d.

AST Generation

rollup uses swc's capabilities in the rust side to generate babel ast, then translates it to compat ast, and finally passes the compat ast to the javascript side through an arraybuffer structure. For implementation details, refer to Native Parser.

rollup internally implements a class for each ast node. After obtaining the estree ast, rollup recursively instantiates each ast node class based on the estree ast structure, ultimately generating a complete ast instance. Subsequent tree-shaking operations are performed on the ast instance.

After the build is complete, it will cache the estree ast. In watch mode, by using the cache to reuse the estree ast, it skips communication with the rust side, thereby improving incremental build performance.

For changes related to switching from acorn parser to swc parser, refer to Native Parser.

Preparation

AST Context

rollup will instantiate ast for each module and also provide an astContext object to help rollup collect module information.

ts
this.astContext = {
  addDynamicImport: this.addDynamicImport.bind(this),
  addExport: this.addExport.bind(this),
  addImport: this.addImport.bind(this),
  addImportMeta: this.addImportMeta.bind(this),
  code, // Only needed for debugging
  deoptimizationTracker: this.graph.deoptimizationTracker,
  error: this.error.bind(this),
  fileName, // Needed for warnings
  getExports: this.getExports.bind(this),
  getModuleExecIndex: () => this.execIndex,
  getModuleName: this.basename.bind(this),
  getNodeConstructor: (name: string) =>
    nodeConstructors[name] || nodeConstructors.UnknownNode,
  getReexports: this.getReexports.bind(this),
  importDescriptions: this.importDescriptions,
  includeAllExports: () => this.includeAllExports(true),
  includeDynamicImport: this.includeDynamicImport.bind(this),
  includeVariableInModule: this.includeVariableInModule.bind(this),
  log: this.log.bind(this),
  magicString: this.magicString,
  manualPureFunctions: this.graph.pureFunctions,
  module: this,
  moduleContext: this.context,
  options: this.options,
  requestTreeshakingPass: () => (this.graph.needsTreeshakingPass = true),
  traceExport: (name: string) => this.getVariableForExportName(name)[0],
  traceVariable: this.traceVariable.bind(this),
  usesTopLevelAwait: false
};

this.scope = new ModuleScope(this.graph.scope, this.astContext);
this.namespace = new NamespaceVariable(this.astContext);
const programParent = { context: this.astContext, type: 'Module' };

timeEnd('generate ast', 3);
const astBuffer = await parseAsync(code, false, this.options.jsx !== false);
timeStart('generate ast', 3);
this.ast = convertProgram(astBuffer, programParent, this.scope);

rollup will recursively instantiate ast node classes based on the obtained compat estree ast structure, ultimately generating an ast instance tree. During this process, rollup will call methods in the astContext object to collect information about the current module.

For example:

  • Collect module's dynamic import information through the addDynamicImport method in the context object.
  • Collect import information through the addImport method in the context object.
  • Collect export and reexports information through the addExport method in the context object.
  • Collect import.meta information through the addImportMeta method in the context object.
  • Determine whether the current module uses top-level await and other information by checking the ast structure.

Let's look at how module information is collected through the compat estree ast structure.

Collect Module Info
  1. Dynamic Dependencies

    When the initialise method is executed in the ImportExpression instance, it means that child ast node classes have all been instantiated.

    js
    class ImportExpression extends NodeBase {
      initialise() {
        super.initialise();
        this.scope.context.addDynamicImport(this);
      }
      parseNode(esTreeNode: GenericEsTreeNode): this {
        this.sourceAstNode = esTreeNode.source;
        return super.parseNode(esTreeNode);
      }
    }

    Call the addDynamicImport method provided by the ast context to store the ImportExpression node information in the dynamicImports property of the current Module instance.

    ts
    class Module {
      readonly dynamicImports: DynamicImport[] = [];
    
      private addDynamicImport(node: ImportExpression) {
        let argument: AstNode | string = node.sourceAstNode;
        if (argument.type === NodeType.TemplateLiteral) {
          if (
            (argument as TemplateLiteralNode).quasis.length === 1 &&
            typeof (argument as TemplateLiteralNode).quasis[0].value
              .cooked === 'string'
          ) {
            argument = (argument as TemplateLiteralNode).quasis[0].value
              .cooked!;
          }
        } else if (
          argument.type === NodeType.Literal &&
          typeof (argument as LiteralStringNode).value === 'string'
        ) {
          argument = (argument as LiteralStringNode).value!;
        }
        this.dynamicImports.push({
          argument,
          id: null,
          node,
          resolution: null
        });
      }
    }
  2. Static Dependencies

    Same as above, when all child nodes of ImportDeclaration are instantiated, the initialise method of the ImportDeclaration instance will be called.

    js
    class ImportDeclaration extends NodeBase {
      initialise(): void {
        super.initialise();
        this.scope.context.addImport(this);
      }
    }

    It will call the addImport method to store the ImportDeclaration node information in the importDescriptions property of the current Module instance.

    ts
    class Module {
      readonly importDescriptions = new Map<string, ImportDescription>();
    
      private addImport(node: ImportDeclaration): void {
        const source = node.source.value;
        this.addSource(source, node);
    
        for (const specifier of node.specifiers) {
          const localName = specifier.local.name;
          if (
            this.scope.variables.has(localName) ||
            this.importDescriptions.has(localName)
          ) {
            this.error(
              logRedeclarationError(localName),
              specifier.local.start
            );
          }
    
          const name =
            specifier instanceof ImportDefaultSpecifier
              ? 'default'
              : specifier instanceof ImportNamespaceSpecifier
                ? '*'
                : specifier.imported instanceof Identifier
                  ? specifier.imported.name
                  : specifier.imported.value;
          this.importDescriptions.set(localName, {
            module: null as never, // filled in later
            name,
            source,
            start: specifier.start
          });
        }
      }
    }

    Since there are multiple ways to import, including default imports, named imports, and namespace imports, their ast node are different, so the storage method in importDescriptions needs to be specifically distinguished.

    • The ast node for default imports is the ImportDefaultSpecifier node.

      js
      import demo from 'demo.js';
      
      module.importDescriptions.set('demo', {
        module: null,
        name: 'default',
        source: 'demo.js',
        start: 7
      });

      Use default as the identifier in importDescriptions.

    • The ast node for named imports is the ImportSpecifier node.

      js
      import { demo } from 'demo.js';
      import { next as demo1 } from 'demo.js';
      
      module.importDescriptions.set('demo', {
        module: null, // filled in later
        name: 'demo',
        source: 'demo.js',
        start: 72
      });
      module.importDescriptions.set('demo1', {
        module: null, // filled in later
        name: 'next',
        source: 'demo.js',
        start: 80
      });

      Store imported.name or imported.value in importDescriptions

    • The ast node for namespace imports is the ImportNamespaceSpecifier node.

      js
      import * as demo from 'demo.js';
      
      module.importDescriptions.set('demo', {
        module: null,
        name: '*',
        source: 'demo.js',
        start: 36
      });

      Use * as the identifier in importDescriptions.

  3. Export Statement And Reexports Statements

    export involves the following three types of ast node

    • Default export (ExportDefaultDeclaration)
    • Named export (ExportNamedDeclaration)
    • Namespace export (ExportAllDeclaration)
    js
    // case 1: ExportDefaultDeclaration
    export default 'Y';
    // case 2: ExportAllDeclaration
    export * as name from './foo.js';
    // case 3: ExportAllDeclaration
    export * from './foo.js';
    // case 4: ExportNamedDeclaration
    export const a = 123;
    // case 5: ExportNamedDeclaration
    const demoA = 123;
    const demoB = 123;
    export { demoA as _a, demoB as _b };
    // case 6: ExportNamedDeclaration
    export function foo() {
      console.log('foo');
    }
    // case 7: ExportNamedDeclaration
    export { foo as _foo } from './foo.js';

    The three types of export ast node will call the addExport method during initialization to add the current module's export information to the exports, reexportDescriptions, and exportAllSources properties of the Module instance.

    ts
    class Module {
      private addExport(
        node:
          | ExportAllDeclaration
          | ExportNamedDeclaration
          | ExportDefaultDeclaration
      ): void {
        if (node instanceof ExportDefaultDeclaration) {
          // export default foo;
          this.assertUniqueExportName('default', node.start);
          this.exports.set('default', {
            identifier: node.variable.getAssignedVariableName(),
            localName: 'default'
          });
        } else if (node instanceof ExportAllDeclaration) {
          const source = node.source.value;
          this.addSource(source, node);
          if (node.exported) {
            // export * as name from './other'
            const name =
              node.exported instanceof Literal
                ? node.exported.value
                : node.exported.name;
            this.assertUniqueExportName(name, node.exported.start);
            this.reexportDescriptions.set(name, {
              localName: '*',
              module: null as never, // filled in later,
              source,
              start: node.start
            });
          } else {
            // export * from './other'
            this.exportAllSources.add(source);
          }
        } else if (node.source instanceof Literal) {
          // export { name } from './other'
          const source = node.source.value;
          this.addSource(source, node);
          for (const { exported, local, start } of node.specifiers) {
            const name =
              exported instanceof Literal ? exported.value : exported.name;
            this.assertUniqueExportName(name, start);
            this.reexportDescriptions.set(name, {
              localName: local instanceof Literal ? local.value : local.name,
              module: null as never, // filled in later,
              source,
              start
            });
          }
        } else if (node.declaration) {
          const declaration = node.declaration;
          if (declaration instanceof VariableDeclaration) {
            // export var { foo, bar } = ...
            // export var foo = 1, bar = 2;
            for (const declarator of declaration.declarations) {
              for (const localName of extractAssignedNames(declarator.id)) {
                this.assertUniqueExportName(localName, declarator.id.start);
                this.exports.set(localName, { identifier: null, localName });
              }
            }
          } else {
            // export function foo () {}
            const localName = (declaration.id as Identifier).name;
            this.assertUniqueExportName(localName, declaration.id!.start);
            this.exports.set(localName, { identifier: null, localName });
          }
        } else {
          // export { foo, bar, baz }
          for (const { local, exported } of node.specifiers) {
            // except for reexports, local must be an Identifier
            const localName = (local as Identifier).name;
            const exportedName =
              exported instanceof Identifier
                ? exported.name
                : exported.value;
            this.assertUniqueExportName(exportedName, exported.start);
            this.exports.set(exportedName, { identifier: null, localName });
          }
        }
      }
    }

    It can be seen that for reexport, rollup specifically distinguishes it in the Module instance through the reexportDescriptions and exportAllSources properties.

    The abstract parsing results for the above export-statement.js example are as follows:

    js
    // case 1: ExportDefaultDeclaration
    export default foo;
    // case 1 output
    module.exports.set('default', {
      identifier: null,
      localName: 'default'
    });
    
    // case 2: ExportAllDeclaration
    export * as name from './foo.js';
    // case 2 output
    module.reexportDescriptions.set('name', {
      localName: '*',
      module: null, // filled in later
      source: './foo.js',
      start: 72
    });
    
    // case 3: ExportAllDeclaration
    export * from './foo.js';
    // case 3 output
    module.exportAllSources.add('./foo.js');
    
    // case 4: ExportNamedDeclaration
    export const a = 123;
    // case 4 output
    module.exports.set('a', {
      identifier: null,
      localName: 'a'
    });
    
    // case 5: ExportNamedDeclaration
    const demoA = 123;
    const demoB = 123;
    export { demoA as _a, demoB as _b };
    // case 5 output
    module.exports.set('_a', {
      identifier: null,
      localName: 'demoA'
    });
    module.exports.set('_b', {
      identifier: null,
      localName: 'demoB'
    });
    
    // case 6: ExportNamedDeclaration
    export function foo() {
      console.log('foo');
    }
    // case 6 output
    module.exports.set('foo', {
      identifier: null,
      localName: 'foo'
    });
    
    // case 7: ExportNamedDeclaration
    export { foo as _foo } from './foo.js';
    // case 7 output
    module.reexportDescriptions.set('_foo', {
      localName: 'foo',
      module: null, // filled in later
      source: './foo.js',
      start: 289
    });

    Duplicate Declaration Reference Check

    rollup performs syntax analysis here. The reason for this can be found in the Optimize Syntax Analysis article. Both import and export check for duplicate variable names.

    export uses the assertUniqueExportName method to detect duplicate variable names.

    ts
    function assertUniqueExportName(name: string, nodeStart: number) {
      if (this.exports.has(name) || this.reexportDescriptions.has(name)) {
        this.error(logDuplicateExportError(name), nodeStart);
      }
    }

    It checks whether the exported variable already exists in the exports or reexportDescriptions properties of the Module instance to determine if it's a duplicate declaration.

    import checks in addImport whether the imported reference variable exists in scope-declared variables (scope.variables) or the importDescriptions property of the Module instance to determine if it's a duplicate declaration.

    ts
    if (
      this.scope.variables.has(localName) ||
      this.importDescriptions.has(localName)
    ) {
      this.error(logRedeclarationError(localName), specifier.local.start);
    }

    This means that rollup does not allow duplicate variable declarations in the same module, including top-level scope declared variables and import exported variables.

    If duplicate reference declarations occur, it will throw an error and terminate the build.

    Summary

    The information recorded about import and export nodes during ast instantiation is essentially the mapping relationship between the localName used in the current module's scope and the localName exported in the dependent module's scope.

    Whether it's import or export, the dependent modules are not filled in during the collection process, for the simple reason that the corresponding dependent modules have not started loading at this point, so they cannot be obtained.

    ts
    this.importDescriptions.set(localName, {
      module: null as never, // filled in later
      name,
      source,
      start: specifier.start
    });
    this.reexportDescriptions.set(name, {
      localName: local instanceof Literal ? local.value : local.name,
      module: null as never, // filled in later,
      source,
      start
    });

    When the child dependency module is loaded, the linkImports method will be called, and the addModulesToImportDescriptions method will be used to fill the child dependency module's references into the importer module's importDescriptions and reexportDescriptions.

    js
    class Module {
      addModulesToImportDescriptions(importDescription) {
        for (const specifier of importDescription.values()) {
          const { id } = this.resolvedIds[specifier.source];
          specifier.module = this.graph.modulesById.get(id);
        }
      }
      linkImports() {
        this.addModulesToImportDescriptions(this.importDescriptions);
        this.addModulesToImportDescriptions(this.reexportDescriptions);
        const externalExportAllModules = [];
        for (const source of this.exportAllSources) {
          const module = this.graph.modulesById.get(
            this.resolvedIds[source].id
          );
          if (module instanceof ExternalModule) {
            externalExportAllModules.push(module);
            continue;
          }
          this.exportAllModules.push(module);
        }
        this.exportAllModules.push(...externalExportAllModules);
      }
    }

    When the linkImports method is executed, all dependency modules of the current module have been resolved, so the references to all dependency modules of the current module can be obtained in this.graph.modulesById.

    At this point, all dependent module references corresponding to the importDescriptions and reexportDescriptions properties of the current module can be filled in. The source stored in exportAllSources also finds the corresponding dependent module through this.graph.modulesById and adds it to this.exportAllModules.

    Through layer-by-layer backtracking and binding collection, all dependent modules that were not filled in during the ast instantiation phase have been completely filled in.

  4. Import Meta

    The ast node node related to import.meta is MetaProperty. This is a declarative node, and since it's impossible to determine whether import.meta node information is needed during the ast instantiation phase.

    Therefore, node information is not processed during ast instantiation, but rather collected through the addImportMeta method after confirming that import.meta nodes need to be included during the tree shaking phase.

    js
    class MateProperty {
      include() {
        if (!this.included) {
          this.included = true;
          if (this.meta.name === IMPORT) {
            this.scope.context.addImportMeta(this);
            const parent = this.parent;
            const metaProperty = (this.metaProperty =
              parent instanceof MemberExpression &&
              typeof parent.propertyKey === 'string'
                ? parent.propertyKey
                : null);
            if (metaProperty?.startsWith(FILE_PREFIX)) {
              this.referenceId = metaProperty.slice(FILE_PREFIX.length);
            }
          }
        }
      }
    }
    
    class Module {
      addImportMeta(node) {
        this.importMetas.push(node);
      }
    }
  5. TLA

    This is to mark whether the current module contains Top Level Await. The implementation of Top Level Await in different bundlers has been explained in detail in Detailed Explanation And Implementation Of TLA, so it won't be repeated here.

    The ast node related to Top Level Await is AwaitExpression.

    js
    class AwaitExpression extends NodeBase {
      include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
        if (!this.deoptimized) this.applyDeoptimizations();
        if (!this.included) {
          this.included = true;
          checkTopLevelAwait: if (!this.scope.context.usesTopLevelAwait) {
            let parent = this.parent;
            do {
              if (parent instanceof FunctionNode || parent instanceof ArrowFunctionExpression)
                break checkTopLevelAwait;
            } while ((parent = (parent as Node).parent as Node));
            this.scope.context.usesTopLevelAwait = true;
          }
        }
        this.argument.include(context, includeChildrenRecursively);
      }
    }

    Like the import.meta node, AwaitExpression node information is also collected through the addAwaitExpression method after confirming that AwaitExpression nodes need to be included during the tree shaking phase.

    The way to determine whether a module contains AwaitExpression nodes is simple: traverse up to the parent level until encountering a FunctionNode or ArrowFunctionExpression node. If none are encountered, it means the current module has Top Level Await, and the usesTopLevelAwait property of the current module instance will be marked as true.

    js
    // usesTopLevelAwait: true
    await 1;
    
    // usesTopLevelAwait: false
    (async () => {
      await 1;
    })();
    
    // usesTopLevelAwait: false
    (function () {
      await 1;
    })();

All this information is collected during the ast instantiation phase. The includeAllExports, includeDynamicImport, and includeVariableInModule methods come into play during the tree shaking phase.

Determine Ast Node Scope

rollup will create a scope for each ast node node. Scopes are divided into global scope, function scope, module scope, parameter scope, catch scope, and with scope.

ts
class Scope {
  readonly children: ChildScope[] = [];
  readonly variables = new Map<string, Variable>();
  hoistedVariables?: Map<string, LocalVariable>;
}
class ChildScope extends Scope {
  constructor(
    readonly parent: Scope,
    readonly context: AstContext
  ) {
    super();
    parent.children.push(this);
  }
}
class ModuleScope extends ChildScope {
  declare parent: GlobalScope;

  constructor(parent: GlobalScope, context: AstContext) {
    super(parent, context);
    this.variables.set(
      'this',
      new LocalVariable(
        'this',
        null,
        UNDEFINED_EXPRESSION,
        context,
        'other'
      )
    );
  }
}
class Module {
  async setSource({
    ast,
    code,
    customTransformCache,
    originalCode,
    originalSourcemap,
    resolvedIds,
    sourcemapChain,
    transformDependencies,
    transformFiles,
    ...moduleOptions
  }: TransformModuleJSON & {
    resolvedIds?: ResolvedIdMap;
    transformFiles?: EmittedFile[] | undefined;
  }): Promise<void> {
    this.scope = new ModuleScope(this.graph.scope, this.astContext);
  }
}

rollup creates a ModuleScope instance (i.e., module scope) for the module, inheriting from the GlobalScope instance (i.e., global scope, which is the value of this.graph.scope).

During initialization, the this variable is added to the ModuleScope instance as the first variable in the current module scope.

ts
class ModuleScope extends ChildScope {
  declare parent: GlobalScope;

  constructor(parent: GlobalScope, context: AstContext) {
    super(parent, context);
    this.variables.set(
      'this',
      new LocalVariable(
        'this',
        null,
        UNDEFINED_EXPRESSION,
        context,
        'other'
      )
    );
  }
}

Next, we will use an example to explain how scope plays a role in the AST instantiation process.

js
const localVariable = 123;

function scopeDemo() {
  const scopeDemoLocalVariable = 345;
  const scopeDemoLocalFunction = function (params) {
    const d = 4445;
    return params + d;
  };
  console.log(scopeDemoLocalFunction(scopeDemoLocalVariable));
}

scopeDemo();
console.log(localVariable);

Since all AST nodes inherit from the NodeBase class, instantiating an AST node will trigger the execution of the NodeBase constructor, creating a scope for the current AST node.

js
class NodeBase extends ExpressionEntity {
  constructor(parent, parentScope) {
    super();
    this.parent = parent;
    this.scope = parentScope;
    this.createScope(parentScope);
  }
  createScope(parentScope) {
    this.scope = parentScope;
  }
}

Let's take the statement const localVariable = 123; as an example. After the VariableDeclaration node is initialized (meaning all AST nodes within the VariableDeclaration node have been initialized), the initialise method of the VariableDeclaration node will be called.

ts
class VariableDeclaration extends NodeBase {
  initialise() {
    super.initialise();
    this.isUsingDeclaration =
      this.kind === 'await using' || this.kind === 'using';
    for (const declarator of this.declarations) {
      declarator.declareDeclarator(this.kind, this.isUsingDeclaration);
    }
  }
}

In the initialise method of the VariableDeclaration node, a scope is created for the current VariableDeclaration node via the super.initialise() method.

Iterate through the declarations array of the VariableDeclaration node (each declared variable, for example, in const a = 1, b = 2;, the declarations array contains two VariableDeclarator nodes), and call the declareDeclarator method of the VariableDeclarator node.

ts
class VariableDeclarator extends NodeBase {
  declareDeclarator(kind, isUsingDeclaration) {
    this.isUsingDeclaration = isUsingDeclaration;
    this.id.declare(kind, this.init || UNDEFINED_EXPRESSION);
  }
}

In the declareDeclarator method, the declare method of the Identifier node (this.id) will be triggered, which in turn adds the Identifier node to scope.variables via the this.scope.addDeclaration method.

Since a scope was created in the VariableDeclaration node and no new scope was generated during the instantiation of the Identifier node, the scope of the Identifier node is consistent with the scope of the VariableDeclaration node, meaning the scope of the Identifier node is ModuleScope.

ts
class Identifier extends NodeBase {
  declare(kind, init) {
    let variable;
    const { treeshake } = this.scope.context.options;
    switch (kind) {
      case 'var': {
        variable = this.scope.addDeclaration(
          this,
          this.scope.context,
          init,
          kind
        );
        if (treeshake && treeshake.correctVarValueBeforeDeclaration) {
          // Necessary to make sure the init is deoptimized. We cannot call deoptimizePath here.
          variable.markInitializersForDeoptimization();
        }
        break;
      }
      case 'function': {
        // in strict mode, functions are only hoisted within a scope but not across block scopes
        variable = this.scope.addDeclaration(
          this,
          this.scope.context,
          init,
          kind
        );
        break;
      }
      case 'let':
      case 'const':
      case 'using':
      case 'await using':
      case 'class': {
        variable = this.scope.addDeclaration(
          this,
          this.scope.context,
          init,
          kind
        );
        break;
      }
      case 'parameter': {
        variable = this.scope.addParameterDeclaration(this);
        break;
      }
      /* istanbul ignore next */
      default: {
        /* istanbul ignore next */
        throw new Error(
          `Internal Error: Unexpected identifier kind ${kind}.`
        );
      }
    }
    return [(this.variable = variable)];
  }
}

So, for the statement const localVariable = 123;, the localVariable variable will eventually be added to the scope.variables of ModuleScope.

js
class Scope {
  addDeclaration(identifier, context, init, kind) {
    const name = identifier.name;
    const existingVariable =
      this.hoistedVariables?.get(name) || this.variables.get(name);
    if (existingVariable) {
      const existingKind = existingVariable.kind;
      if (kind === 'var' && existingKind === 'var') {
        existingVariable.addDeclaration(identifier, init);
        return existingVariable;
      }
      context.error(
        parseAst_js.logRedeclarationError(name),
        identifier.start
      );
    }
    const newVariable = new LocalVariable(
      identifier.name,
      identifier,
      init,
      context,
      kind
    );
    this.variables.set(name, newVariable);
    return newVariable;
  }
}
class ModuleScope extends ChildScope {
  addDeclaration(identifier, context, init, kind) {
    if (this.context.module.importDescriptions.has(identifier.name)) {
      context.error(
        parseAst_js.logRedeclarationError(identifier.name),
        identifier.start
      );
    }
    return super.addDeclaration(identifier, context, init, kind);
  }
}

Since the module scope contains already declared variables, it is easy to perform syntax checks for duplicate declarations.

If a variable declared in the current module scope conflicts with an imported variable declaration, a duplicate declaration exception is thrown.

If a variable not declared with the var keyword (e.g., let, const, etc.) is re-declared, an exception is thrown. After the check passes, the LocalVariable class is instantiated based on the Identifier's AST node information, and the instantiated variable is added to the current scope's scope.variables, preparing for subsequent syntax analysis and tree shaking.

Creating Scope

Previously, we discussed scopes created in top-level syntax, which belong to ModuleScope (module scope). It has also been mentioned that scopes are not limited to ModuleScope and GlobalScope; there are also FunctionScope, ParameterScope, CatchBodyScope, FunctionBodyScope, etc.

Function Scope

When a function declaration statement is encountered, the createScope method is called within the FunctionNode to create a new scope for the current FunctionNode.

ts
class FunctionNode extends FunctionBase {
  constructor() {
    super(...arguments);
    this.objectEntity = null;
  }
  createScope(parentScope) {
    this.scope = new FunctionScope(parentScope);
    this.constructedEntity = new ObjectEntity(
      Object.create(null),
      OBJECT_PROTOTYPE
    );
    // This makes sure that all deoptimizations of "this" are applied to the
    // constructed entity.
    this.scope.thisVariable.addEntityToBeDeoptimized(
      this.constructedEntity
    );
  }
}
ts
class Scope {
  constructor() {
    this.children = [];
    this.variables = new Map();
  }
}
class ChildScope extends Scope {
  constructor(parent, context) {
    super();
    this.parent = parent;
    this.context = context;
    this.accessedOutsideVariables = new Map();
    parent.children.push(this);
  }
}
class ParameterScope extends ChildScope {
  constructor(parent, isCatchScope) {
    super(parent, parent.context);
    this.parameters = [];
    this.hasRest = false;
    this.bodyScope = isCatchScope
      ? new CatchBodyScope(this)
      : new FunctionBodyScope(this);
  }
}
class ReturnValueScope extends ParameterScope {
  constructor() {
    super(...arguments);
    this.returnExpression = null;
    this.returnExpressions = [];
  }
}
class FunctionScope extends ReturnValueScope {
  constructor(parent) {
    const { context } = parent;
    super(parent, false);
    this.variables.set(
      'arguments',
      (this.argumentsVariable = new ArgumentsVariable(context))
    );
    this.variables.set(
      'this',
      (this.thisVariable = new ThisVariable(context))
    );
  }
}

In FunctionScope, a new FunctionScope that inherits from ModuleScope is created for the current function node.

In the new FunctionScope, this and arguments variables are created as initial variables for the current scope. Simultaneously, the new function scope is collected for the parent ModuleScope and stored in the children array of the parent ModuleScope.

ts
class ChildScope extends Scope {
  constructor(parent, context) {
    super();
    this.parent = parent;
    this.context = context;
    this.accessedOutsideVariables = new Map();
    parent.children.push(this);
  }
}

WARNING

Variables declared within a function scope are not added to FunctionScope's scope.variables, but rather to FunctionScope.bodyScope's scope.variables.

ts
function functionDeclaration(node: FunctionDeclaration, position, buffer) {
  const { scope } = node;
  const flags = buffer[position];
  node.async = (flags & 1) === 1;
  node.generator = (flags & 2) === 2;
  const annotations = (node.annotations = convertAnnotations(buffer[position + 1], buffer));
  node.annotationNoSideEffects = annotations.some(comment => comment.type === 'noSideEffects');
  const idPosition = buffer[position + 2];
  node.id =
    idPosition === 0 ? null : convertNode(node, scope.parent as ChildScope, idPosition, buffer);
  const parameters = (node.params = convertNodeList(node, scope, buffer[position + 3], buffer));
  scope.addParameterVariables(
    parameters.map(
      parameter => parameter.declare('parameter', UNKNOWN_EXPRESSION) as ParameterVariable[]
    ),
    parameters[parameters.length - 1] instanceof RestElement
  );
  node.body = convertNode(node, scope.bodyScope, buffer[position + 4], buffer);
}

So, where does bodyScope come from?

js
class ParameterScope extends ChildScope {
  constructor(parent, isCatchScope) {
    super(parent, parent.context);
    this.parameters = [];
    this.hasRest = false;
    this.bodyScope = isCatchScope ? new CatchBodyScope(this) : new FunctionBodyScope(this);
  }
}
class ReturnValueScope extends ParameterScope {}

class FunctionScope extends ReturnValueScope {}

It can be seen that when creating a function scope, a FunctionBodyScope is created as the bodyScope of the current function scope, and the parent of FunctionBodyScope is FunctionScope. Subsequently, variables declared in the function body will all be added to FunctionBodyScope's scope.variables.

Parameters of a function expression are added to FunctionScope's scope.variables via the scope.addParameterVariables method.

ts
function functionExpression(node: FunctionExpression, position, buffer) {
  const { scope } = node;
  const flags = buffer[position];
  node.async = (flags & 1) === 1;
  node.generator = (flags & 2) === 2;
  const annotations = (node.annotations = convertAnnotations(buffer[position + 1], buffer));
  node.annotationNoSideEffects = annotations.some(comment => comment.type === 'noSideEffects');
  const idPosition = buffer[position + 2];
  node.id = idPosition === 0 ? null : convertNode(node, node.idScope, idPosition, buffer);
  const parameters = (node.params = convertNodeList(node, scope, buffer[position + 3], buffer));
  scope.addParameterVariables(
    parameters.map(
      parameter => parameter.declare('parameter', UNKNOWN_EXPRESSION) as ParameterVariable[]
    ),
    parameters[parameters.length - 1] instanceof RestElement
  );
  node.body = convertNode(node, scope.bodyScope, buffer[position + 4], buffer);
}

Simultaneously, parameters are also added to the hoistedVariables of the function body scope (bodyScope, where bodyScope.parent is FunctionScope) under the function scope (FunctionScope), serving as hoisted variables.

ts
class Scope {
  addHoistedVariable(name: string, variable: LocalVariable) {
    (this.hoistedVariables ||= new Map()).set(name, variable);
  }
}
class ParameterScope extends ChildScope {
  addParameterDeclaration(identifier: Identifier): ParameterVariable {
    const { name, start } = identifier;
    const existingParameter = this.variables.get(name);
    if (existingParameter) {
      return this.context.error(logDuplicateArgumentNameError(name), start);
    }
    const variable = new ParameterVariable(name, identifier, this.context);
    this.variables.set(name, variable);
    // We also add it to the body scope to detect name conflicts with local
    // variables. We still need the intermediate scope, though, as parameter
    // defaults are NOT taken from the body scope but from the parameters or
    // outside scope.
    this.bodyScope.addHoistedVariable(name, variable);
    return variable;
  }
}

The purpose of hoistedVariables is to detect conflicts between identifiers and parameter variable declarations within the function body.

ts
function logRedeclarationError(name: string): RollupLog {
  return {
    code: REDECLARATION_ERROR,
    message: `Identifier "${name}" has already been declared`
  };
}
class FunctionBodyScope extends ChildScope {
  // There is stuff that is only allowed in function scopes, i.e. functions can
  // be redeclared, functions and var can redeclare each other
  addDeclaration(
    identifier: Identifier,
    context: AstContext,
    init: ExpressionEntity,
    kind: VariableKind
  ): LocalVariable {
    const name = identifier.name;
    const existingVariable =
      this.hoistedVariables?.get(name) || (this.variables.get(name) as LocalVariable);
    if (existingVariable) {
      const existingKind = existingVariable.kind;
      if (
        (kind === 'var' || kind === 'function') &&
        (existingKind === 'var' || existingKind === 'function' || existingKind === 'parameter')
      ) {
        existingVariable.addDeclaration(identifier, init);
        return existingVariable;
      }
      context.error(logRedeclarationError(name), identifier.start);
    }
    const newVariable = new LocalVariable(identifier.name, identifier, init, context, kind);
    this.variables.set(name, newVariable);
    return newVariable;
  }
}

It can be seen that if a variable declared with a keyword other than var or function has the same name as a parameter variable, an exception is thrown: Identifier xxx has already been declared. Therefore, during the AST instantiation process, duplicate variable declarations in the function body can be detected.

Instantiate AST

As mentioned above, module.ast is an instance of an AST node class implemented by Rollup, with a structure compatible with ESTree AST. Subsequent tree shaking operations are all performed on the module.ast instance.

So, the core question is: how does Rollup instantiate its implemented AST node class instances based on the compat ESTree AST structure?

Rollup internally implements a corresponding AST node class for each AST node type, and uses nodeConstructors to instantiate the corresponding AST node class.

ts
import type * as estree from 'estree';
type OmittedEstreeKeys =
  | 'loc'
  | 'range'
  | 'leadingComments'
  | 'trailingComments'
  | 'innerComments'
  | 'comments';
interface AstNodeLocation {
  end: number;
  start: number;
}
type RollupAstNode<T> = Omit<T, OmittedEstreeKeys> & AstNodeLocation;
type AstNode = RollupAstNode<estree.Node>;
interface GenericEsTreeNode extends AstNode {
  [key: string]: any;
}
type AnnotationType = 'pure' | 'noSideEffects';
interface RollupAnnotation {
  start: number;
  end: number;
  type: AnnotationType;
}

const nodeConstructors: Record<string, typeof NodeBase> = {
  ArrayExpression,
  ArrayPattern,
  ArrowFunctionExpression,
  AssignmentExpression,
  AssignmentPattern,
  AwaitExpression,
  BinaryExpression,
  BlockStatement,
  BreakStatement,
  CallExpression,
  CatchClause,
  ChainExpression,
  ClassBody,
  ClassDeclaration,
  ClassExpression,
  ConditionalExpression,
  ContinueStatement,
  DebuggerStatement,
  Decorator,
  DoWhileStatement,
  EmptyStatement,
  ExportAllDeclaration,
  ExportDefaultDeclaration,
  ExportNamedDeclaration,
  ExportSpecifier,
  ExpressionStatement,
  ForInStatement,
  ForOfStatement,
  ForStatement,
  FunctionDeclaration,
  FunctionExpression,
  Identifier,
  IfStatement,
  ImportAttribute,
  ImportDeclaration,
  ImportDefaultSpecifier,
  ImportExpression,
  ImportNamespaceSpecifier,
  ImportSpecifier,
  LabeledStatement,
  Literal,
  LogicalExpression,
  MemberExpression,
  MetaProperty,
  MethodDefinition,
  NewExpression,
  ObjectExpression,
  ObjectPattern,
  PanicError,
  ParseError,
  PrivateIdentifier,
  Program,
  Property,
  PropertyDefinition,
  RestElement,
  ReturnStatement,
  SequenceExpression,
  SpreadElement,
  StaticBlock,
  Super,
  SwitchCase,
  SwitchStatement,
  TaggedTemplateExpression,
  TemplateElement,
  TemplateLiteral,
  ThisExpression,
  ThrowStatement,
  TryStatement,
  UnaryExpression,
  UnknownNode,
  UpdateExpression,
  VariableDeclaration,
  VariableDeclarator,
  WhileStatement,
  YieldExpression
};

class NodeBase {
  parseNode(esTreeNode: GenericEsTreeNode): this {
    for (const [key, value] of Object.entries(esTreeNode)) {
      // Skip properties defined on the class already.
      // This way, we can override this function to add custom initialisation and then call super.parseNode
      // Note: this doesn't skip properties with defined getters/setters which we use to pack wrap booleans
      // in bitfields. Those are still assigned from the value in the esTreeNode.
      if (this.hasOwnProperty(key)) continue;

      if (key.charCodeAt(0) === 95 /* _ */) {
        if (key === ANNOTATION_KEY) {
          this.annotations = value as RollupAnnotation[];
        } else if (key === INVALID_ANNOTATION_KEY) {
          (this as unknown as Program).invalidAnnotations =
            value as RollupAnnotation[];
        }
      } else if (typeof value !== 'object' || value === null) {
        (this as GenericEsTreeNode)[key] = value;
      } else if (Array.isArray(value)) {
        (this as GenericEsTreeNode)[key] = [];
        for (const child of value) {
          (this as GenericEsTreeNode)[key]
            .push(
              child === null ? null : new nodeConstructors[child.type]()
            )(this, this.scope)
            .parseNode(child);
        }
      } else {
        (this as GenericEsTreeNode)[key] = new nodeConstructors[value.type](
          this,
          this.scope
        ).parseNode(value);
      }
    }
    // extend child keys for unknown node types
    childNodeKeys[esTreeNode.type] ||=
      createChildNodeKeysForNode(esTreeNode);
    this.initialise();
    return this;
  }
}

Each AST node class is instantiated recursively via DFS based on the compat ESTree AST structure.

AST Node Operations

getVariableForExportName

WARNING

During the generateModuleGraph phase, Module instantiation is performed, and the corresponding AST tree is parsed from the source code. It is important to note that at this point, the dependency modules of the module have not yet been instantiated. Therefore, references to child dependency modules cannot be obtained during AST tree parsing. As a result, the imported and re-exported variables collected in importDescriptions and reexportDescriptions do not have their dependency module references filled and are in an unfilled state.

Once all dependency modules have been instantiated, the module.linkImports() method is executed to fill in the dependency module references for importDescriptions and reexportDescriptions.

js
class Module {
  linkImports(): void {
    this.addModulesToImportDescriptions(this.importDescriptions);
    this.addModulesToImportDescriptions(this.reexportDescriptions);
    const externalExportAllModules: ExternalModule[] = [];
    for (const source of this.exportAllSources) {
      const module = this.graph.modulesById.get(this.resolvedIds[source].id)!;
      if (module instanceof ExternalModule) {
        externalExportAllModules.push(module);
        continue;
      }
      this.exportAllModules.push(module);
    }
    this.exportAllModules.push(...externalExportAllModules);
  }
}

During the tree-shaking phase, the module references for dependency items in importDescriptions and reexportDescriptions have already been filled. Therefore, in the tree-shaking phase, the module reference of a dependency can be obtained as follows.

js
// main.js

// export * as d from 'demo.js';
const reexportDeclaration = this.reexportDescriptions.get(name);
const childDependenciesReexportModule = reexportDeclaration.module;

// import { d } from 'demo.js';
const importDescription = this.importDescriptions.get(name);
const childDependenciesImportModule = importDescription.module;

Let's continue to analyze the implementation of the getVariableForExportName method.

ts
class Module {
  getVariableForExportName(
    name: string,
    {
      importerForSideEffects,
      isExportAllSearch,
      onlyExplicit,
      searchedNamesAndModules
    }: {
      importerForSideEffects?: Module;
      isExportAllSearch?: boolean;
      onlyExplicit?: boolean;
      searchedNamesAndModules?: Map<string, Set<Module | ExternalModule>>;
    } = EMPTY_OBJECT
  ): [variable: Variable | null, indirectExternal?: boolean] {
    if (name[0] === '*') {
      if (name.length === 1) {
        // export * from './other'
        return [this.namespace];
      }
      // export * from 'external'
      const module = this.graph.modulesById.get(
        name.slice(1)
      ) as ExternalModule;
      return module.getVariableForExportName('*');
    }

    // export { foo } from './other'
    const reexportDeclaration = this.reexportDescriptions.get(name);
    if (reexportDeclaration) {
      const [variable] = getVariableForExportNameRecursive(
        reexportDeclaration.module,
        reexportDeclaration.localName,
        importerForSideEffects,
        false,
        searchedNamesAndModules
      );
      if (!variable) {
        return this.error(
          logMissingExport(
            reexportDeclaration.localName,
            this.id,
            reexportDeclaration.module.id
          ),
          reexportDeclaration.start
        );
      }
      if (importerForSideEffects) {
        setAlternativeExporterIfCyclic(
          variable,
          importerForSideEffects,
          this
        );
        if (this.info.moduleSideEffects) {
          getOrCreate(
            importerForSideEffects.sideEffectDependenciesByVariable,
            variable,
            getNewSet<Module>
          ).add(this);
        }
      }
      return [variable];
    }

    const exportDeclaration = this.exports.get(name);
    if (exportDeclaration) {
      if (exportDeclaration === MISSING_EXPORT_SHIM_DESCRIPTION) {
        return [this.exportShimVariable];
      }
      const name = exportDeclaration.localName;
      const variable = this.traceVariable(name, {
        importerForSideEffects,
        searchedNamesAndModules
      })!;
      if (importerForSideEffects) {
        setAlternativeExporterIfCyclic(
          variable,
          importerForSideEffects,
          this
        );
        getOrCreate(
          importerForSideEffects.sideEffectDependenciesByVariable,
          variable,
          getNewSet<Module>
        ).add(this);
      }
      return [variable];
    }

    if (onlyExplicit) {
      return [null];
    }

    if (name !== 'default') {
      const foundNamespaceReexport =
        this.namespaceReexportsByName.get(name) ??
        this.getVariableFromNamespaceReexports(
          name,
          importerForSideEffects,
          searchedNamesAndModules
        );
      this.namespaceReexportsByName.set(name, foundNamespaceReexport);
      if (foundNamespaceReexport[0]) {
        return foundNamespaceReexport;
      }
    }

    if (this.info.syntheticNamedExports) {
      return [
        getOrCreate(
          this.syntheticExports,
          name,
          () =>
            new SyntheticNamedExportVariable(
              this.astContext,
              name,
              this.getSyntheticNamespace()
            )
        )
      ];
    }

    // we don't want to create shims when we are just
    // probing export * modules for exports
    if (!isExportAllSearch && this.options.shimMissingExports) {
      this.shimMissingExport(name);
      return [this.exportShimVariable];
    }
    return [null];
  }
}

It can be seen that the implementation differs slightly for acquiring different types of exports. The following sections will introduce the different implementation methods for obtaining Rollup AST Nodes for various export types.

  1. Retrieving the Rollup AST Node for Re-exported Variables

    It can be seen that acquiring the Rollup AST variable declaration node for re-exports involves calling the getVariableForEportNameRecursive method.

    ts
    const reexportDeclaration = this.reexportDescriptions.get(name);
    if (reexportDeclaration) {
      const [variable] = getVariableForExportNameRecursive(
        // Re-exported dependency module
        reexportDeclaration.module,
        reexportDeclaration.localName,
        importerForSideEffects,
        false,
        searchedNamesAndModules
      );
      // Other logic omitted
    }

    Inside the getVariableForExportNameRecursive method, the getVariableForExportName method of the re-exported dependency module is called recursively to search if the target variable is declared in the dependency module. If not, the recursive call continues until the Rollup AST Node of the target variable is found.

    ts
    function getVariableForExportNameRecursive(
      target: Module | ExternalModule,
      name: string,
      importerForSideEffects: Module | undefined,
      isExportAllSearch: boolean | undefined,
      searchedNamesAndModules = new Map<
        string,
        Set<Module | ExternalModule>
      >()
    ): [variable: Variable | null, indirectExternal?: boolean] {
      const searchedModules = searchedNamesAndModules.get(name);
      if (searchedModules) {
        if (searchedModules.has(target)) {
          return isExportAllSearch
            ? [null]
            : error(logCircularReexport(name, target.id));
        }
        searchedModules.add(target);
      } else {
        searchedNamesAndModules.set(name, new Set([target]));
      }
      return target.getVariableForExportName(name, {
        importerForSideEffects,
        isExportAllSearch,
        searchedNamesAndModules
      });
    }
  2. Retrieving the Rollup AST Node for Exported Variables

    To get the Rollup AST Node for an exported variable:

    ts
    class Module {
      getVariableForExportName(
        name: string,
        {
          importerForSideEffects,
          isExportAllSearch,
          onlyExplicit,
          searchedNamesAndModules
        }: {
          importerForSideEffects?: Module;
          isExportAllSearch?: boolean;
          onlyExplicit?: boolean;
          searchedNamesAndModules?: Map<
            string,
            Set<Module | ExternalModule>
          >;
        } = EMPTY_OBJECT
      ): [variable: Variable | null, indirectExternal?: boolean] {
        // Other logic omitted
        const exportDeclaration = this.exports.get(name);
        if (exportDeclaration) {
          if (exportDeclaration === MISSING_EXPORT_SHIM_DESCRIPTION) {
            return [this.exportShimVariable];
          }
          const name = exportDeclaration.localName;
          const variable = this.traceVariable(name, {
            importerForSideEffects,
            searchedNamesAndModules
          })!;
          if (importerForSideEffects) {
            setAlternativeExporterIfCyclic(
              variable,
              importerForSideEffects,
              this
            );
            getOrCreate(
              importerForSideEffects.sideEffectDependenciesByVariable,
              variable,
              getNewSet<Module>
            ).add(this);
          }
          return [variable];
        }
      }
      // Other logic omitted
    }

    It can be seen that the traceVariable method is used to get the exported variable declared in the target module.

    js
    class Module {
      traceVariable(
        name: string,
        {
          importerForSideEffects,
          isExportAllSearch,
          searchedNamesAndModules
        }: {
          importerForSideEffects?: Module;
          isExportAllSearch?: boolean;
          searchedNamesAndModules?: Map<string, Set<Module | ExternalModule>>;
        } = EMPTY_OBJECT
      ): Variable | null {
        const localVariable = this.scope.variables.get(name);
        if (localVariable) {
          return localVariable;
        }
    
        const importDescription = this.importDescriptions.get(name);
        if (importDescription) {
          const otherModule = importDescription.module;
    
          if (otherModule instanceof Module && importDescription.name === '*') {
            return otherModule.namespace;
          }
    
          const [declaration] = getVariableForExportNameRecursive(
            otherModule,
            importDescription.name,
            importerForSideEffects || this,
            isExportAllSearch,
            searchedNamesAndModules
          );
    
          if (!declaration) {
            return this.error(
              logMissingExport(importDescription.name, this.id, otherModule.id),
              importDescription.start
            );
          }
    
          return declaration;
        }
        return null;
      }
    }

    However, two special cases might need consideration:

    1. The exported variable is declared in the current module.

      js
      // target.js
      export const a = 1;

      This is a recursive exit point. For this case, the Rollup AST Node declared in the current scope can be obtained directly via this.scope.variables.get(name).

      js
      const localVariable = this.scope.variables.get(name);
      if (localVariable) {
        return localVariable;
      }
    2. The exported variable is declared in a dependency module.

      js
      // target.js
      import { a } from './other';
      export { a };

      This is essentially the same as a re-export. Therefore, the getVariableForExportNameRecursive method is used to recursively search other modules to obtain the variable declaration contained in the target module's scope.

      js
      if (importDescription) {
        const otherModule = importDescription.module;
      
        if (
          otherModule instanceof Module &&
          importDescription.name === '*'
        ) {
          return otherModule.namespace;
        }
      
        const [declaration] = getVariableForExportNameRecursive(
          otherModule,
          importDescription.name,
          importerForSideEffects || this,
          isExportAllSearch,
          searchedNamesAndModules
        );
      
        if (!declaration) {
          return this.error(
            logMissingExport(
              importDescription.name,
              this.id,
              otherModule.id
            ),
            importDescription.start
          );
        }
      
        return declaration;
      }

    Example

    js
    // main.js
    export { a } from './other';
    js
    // other.js
    export { a } from './other-next';
    js
    // other-next.js
    import { a } from './other-next-next.js';
    export { a };
    js
    // other-next-next.js
    export const a = 123;

    If the getVariableForExportName method is used in main.js to get the Rollup AST Node for variable a, it will recursively call getVariableForExportNameRecursive until variable a is found. This means it will ultimately obtain the Rollup AST Node for variable a declared in the other-next-next module, and the module property of this Rollup AST Node will be a reference to the other-next-next module.:::

Determining if Statements Have Side Effects

INFO

Each module contains a moduleSideEffects property (module.info.moduleSideEffects). This property is used to mark whether the current module has side effects, thereby influencing Rollup's tree-shaking behavior for the module.

ts
interface ModuleOptions {
  attributes: Record<string, string>;
  meta: CustomPluginOptions;
  moduleSideEffects: boolean | 'no-treeshake';
  syntheticNamedExports: boolean | string;
}
markdown
**The `moduleSideEffects` value can be one of the following three:**

1.  `moduleSideEffects = true`

    Indicates that the module has side effects. This is Rollup's default behavior; `Rollup` assumes all modules have side effects.

    ```ts{8-10,25-26}
    class ModuleLoader {
      constructor(
        private readonly graph: Graph,
        private readonly modulesById: Map<string, Module | ExternalModule>,
        private readonly options: NormalizedInputOptions,
        private readonly pluginDriver: PluginDriver
      ) {
        this.hasModuleSideEffects = options.treeshake
          ? options.treeshake.moduleSideEffects
          : () => true;
      }
      private getResolvedIdWithDefaults(
        resolvedId: NormalizedResolveIdWithoutDefaults | null,
        attributes: Record<string, string>
      ): ResolvedId | null {
        if (!resolvedId) {
          return null;
        }
        const external = resolvedId.external || false;
        return {
          attributes: resolvedId.attributes || attributes,
          external,
          id: resolvedId.id,
          meta: resolvedId.meta || {},
          moduleSideEffects:
            resolvedId.moduleSideEffects ?? this.hasModuleSideEffects(resolvedId.id, !!external),
          resolvedBy: resolvedId.resolvedBy ?? 'rollup',
          syntheticNamedExports: resolvedId.syntheticNamedExports ?? false
        };
      }
    }
    ```

    When executing Rollup's `resolveId` plugin hook, the `moduleSideEffects` property returned by the hook can be used to specify whether a module has side effects, thereby influencing Rollup's `tree-shaking` behavior for the module. If the hook does not return the `moduleSideEffects` property, the module is assumed to have side effects by default (i.e., `moduleSideEffects = true`). Of course, the `options.treeshake.moduleSideEffects` property can also be used to specify this default behavior.

    During the `tree-shaking` phase, `tree-shaking` operations are performed based on whether `statements` have side effects (`hasEffects`).

    ```ts{4}
    class Module {
      include(): void {
        const context = createInclusionContext();
        if (this.ast!.shouldBeIncluded(context)) this.ast!.include(context, false);
      }
    }
    ```

2.  `moduleSideEffects = false`: Indicates that the module has no side effects. The `tree-shaking` phase will directly remove this module.
3.  `moduleSideEffects = 'no-treeshake'`: Indicates that this module does not require `tree-shaking`. The content of this module and all its dependencies will be fully preserved.

    ```ts{12}
    class Graph {
      private includeStatements(): void {
        // 省略上方逻辑 (Logic above omitted)
        if (this.options.treeshake) {
          let treeshakingPass = 1;
          do {
            timeStart(`treeshaking pass ${treeshakingPass}`, 3);
            this.needsTreeshakingPass = false;
            for (const module of this.modules) {
              if (module.isExecuted) {
                if (module.info.moduleSideEffects === 'no-treeshake') {
                  module.includeAllInBundle();
                } else {
                  module.include();
                }
              }
            }
            if (treeshakingPass === 1) {
              // We only include exports after the first pass to avoid issues with
              // the TDZ detection logic
              for (const module of entryModules) {
                if (module.preserveSignature !== false) {
                  module.includeAllExports(false);
                  this.needsTreeshakingPass = true;
                }
              }
            }
            timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
          } while (this.needsTreeshakingPass);
        }
        // 省略下方逻辑 (Logic below omitted)
      }
    }
    ```

    It can be seen that if a module needs to be executed and its `moduleSideEffects` property is set to `'no-treeshake'`, Rollup will preserve all content in that module by executing the `module.includeAllInBundle()` method. A detailed explanation of `includeAllInBundle` can be found below.

The `moduleSideEffects` property value can also be specified by the plugin's **`load`** and **`transform`** hooks to indicate whether the current module has side effects. After the plugin execution, Rollup will update the module's `moduleSideEffects` property via the `updateOptions` method.

```ts{7-9}
class Module {
  updateOptions({
    meta,
    moduleSideEffects,
    syntheticNamedExports
  }: Partial<PartialNull<ModuleOptions>>): void {
    if (moduleSideEffects != null) {
      this.info.moduleSideEffects = moduleSideEffects;
    }
    if (syntheticNamedExports != null) {
      this.info.syntheticNamedExports = syntheticNamedExports;
    }
    if (meta != null) {
      Object.assign(this.info.meta, meta);
    }
  }
}
```

After completing Generate Module Graph and Sort Module Execution Order, the process officially enters the tree-shaking phase. The specific logic is implemented in the includeStatements method.

js
class Graph {
  async build(): Promise<void> {
    timeStart('generate module graph', 2);
    await this.generateModuleGraph();
    timeEnd('generate module graph', 2);

    timeStart('sort and bind modules', 2);
    this.phase = BuildPhase.ANALYSE;
    this.sortModules();
    timeEnd('sort and bind modules', 2);

    timeStart('mark included statements', 2);
    this.includeStatements();
    timeEnd('mark included statements', 2);

    this.phase = BuildPhase.GENERATE;
  }
}

includeStatements

Rollup uses the user's configuration (i.e., options.input) and implicitlyLoadedBefore as entry points. It traverses all dependencies of each entry module using a BFS algorithm. The traversed dependency modules need to be executed (i.e., module.isExecuted = true).

ts
function markModuleAndImpureDependenciesAsExecuted(
  baseModule: Module
): void {
  baseModule.isExecuted = true;
  const modules = [baseModule];
  const visitedModules = new Set<string>();
  for (const module of modules) {
    for (const dependency of [
      ...module.dependencies,
      ...module.implicitlyLoadedBefore
    ]) {
      if (
        !(dependency instanceof ExternalModule) &&
        !dependency.isExecuted &&
        (dependency.info.moduleSideEffects ||
          module.implicitlyLoadedBefore.has(dependency)) &&
        !visitedModules.has(dependency.id)
      ) {
        dependency.isExecuted = true;
        visitedModules.add(dependency.id);
        modules.push(dependency);
      }
    }
  }
}
class Graph {
  private includeStatements(): void {
    const entryModules = [
      ...this.entryModules,
      ...this.implicitEntryModules
    ];
    for (const module of entryModules) {
      markModuleAndImpureDependenciesAsExecuted(module);
    }
    // 省略下方逻辑 (Logic below omitted)
  }
}

After marking which modules need to be executed, the process officially enters the tree-shaking phase.

ts
class Graph {
  private includeStatements(): void {
    // 省略上方逻辑 (Logic above omitted)
    if (this.options.treeshake) {
      // 省略 tree-sharking 逻辑 (Tree-shaking logic omitted)
    } else {
      for (const module of this.modules) module.includeAllInBundle();
    }
    // 省略下方逻辑 (Logic below omitted)
  }
}

Based on the user's configuration (i.e., options.treeshake), it is determined whether to enable tree-shaking. If tree-shaking is not required, the includeAllInBundle method is executed for all modules. Let's first look at the logic of the includeAllInBundle method.

ts
class Module {
  includeAllInBundle(): void {
    this.ast!.include(createInclusionContext(), true);
    this.includeAllExports(false);
  }
}

The includeAllInBundle method does two things. Before that, one thing needs to be clarified: if an AST Node is confirmed to be included in the final output, node.include will be executed. In the subsequent ast.render phase, these not included AST nodes will be skipped, thus achieving the tree-shaking effect.

  1. Call the ast.include method to mark the current module's AST node as included.

    ts
    class Program {
      include(
        context: InclusionContext,
        includeChildrenRecursively: IncludeChildren
      ): void {
        this.included = true;
        for (const node of this.body) {
          if (includeChildrenRecursively || node.shouldBeIncluded(context)) {
            node.include(context, includeChildrenRecursively);
          }
        }
      }
    }

    The second parameter of ast.include, includeChildrenRecursively, being marked as true means that when recursing through the module's AST nodes, all descendant AST Nodes will be recursively included (confirming execution of node.include). If this flag is not present, whether an AST Node needs to be included in the final code is determined by whether the AST Node has side effects.

    js
    class NodeBase {
      shouldBeIncluded(context: InclusionContext): boolean {
        return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext()));
      }
    }
  2. Call the includeAllExports method.

The includeAllExports method also primarily does two things:

ts
function markModuleAndImpureDependenciesAsExecuted(
  baseModule: Module
): void {
  baseModule.isExecuted = true;
  const modules = [baseModule];
  const visitedModules = new Set<string>();
  for (const module of modules) {
    for (const dependency of [
      ...module.dependencies,
      ...module.implicitlyLoadedBefore
    ]) {
      if (
        !(dependency instanceof ExternalModule) &&
        !dependency.isExecuted &&
        (dependency.info.moduleSideEffects ||
          module.implicitlyLoadedBefore.has(dependency)) &&
        !visitedModules.has(dependency.id)
      ) {
        dependency.isExecuted = true;
        visitedModules.add(dependency.id);
        modules.push(dependency);
      }
    }
  }
}
class Module {
  getReexports(): string[] {
    if (this.transitiveReexports) {
      return this.transitiveReexports;
    }
    // to avoid infinite recursion when using circular `export * from X`
    this.transitiveReexports = [];

    const reexports = new Set(this.reexportDescriptions.keys());

    for (const module of this.exportAllModules) {
      if (module instanceof ExternalModule) {
        reexports.add(`*${module.id}`);
      } else {
        for (const name of [
          ...module.getReexports(),
          ...module.getExports()
        ]) {
          if (name !== 'default') reexports.add(name);
        }
      }
    }
    return (this.transitiveReexports = [...reexports]);
  }
  includeAllExports(includeNamespaceMembers: boolean): void {
    if (!this.isExecuted) {
      markModuleAndImpureDependenciesAsExecuted(this);
      this.graph.needsTreeshakingPass = true;
    }

    for (const exportName of this.exports.keys()) {
      if (
        includeNamespaceMembers ||
        exportName !== this.info.syntheticNamedExports
      ) {
        const variable = this.getVariableForExportName(exportName)[0];
        if (!variable) {
          return error(logMissingEntryExport(exportName, this.id));
        }
        variable.deoptimizePath(UNKNOWN_PATH);
        if (!variable.included) {
          this.includeVariable(variable);
        }
      }
    }

    for (const name of this.getReexports()) {
      const [variable] = this.getVariableForExportName(name);
      if (variable) {
        variable.deoptimizePath(UNKNOWN_PATH);
        if (!variable.included) {
          this.includeVariable(variable);
        }
        if (variable instanceof ExternalVariable) {
          variable.module.reexported = true;
        }
      }
    }

    if (includeNamespaceMembers) {
      this.namespace.setMergedNamespaces(
        this.includeAndGetAdditionalMergedNamespaces()
      );
    }
  }
}
  1. If the module is not marked as executed, mark the current module and its dependent modules as executed (i.e., module.isExecuted = true).
  2. Iterate through the current module's exports and reexports, marking the AST Nodes corresponding to all exported variables and re-exported variables as included (i.e., variable.included = true).

An interesting point here is the use of getVariableForExportName to obtain the AST Nodes corresponding to exported and re-exported variables. Let's see how it is implemented.

tree-shaking

tree-shaking is executed during the Chunk's render phase. At this point, it has been determined which modules need to be executed and which statements need to be preserved in the final output.

ts
class Chunk {
  // This method changes properties on the AST before rendering and must not be async
  private renderModules(fileName: string) {
    // 省略其他逻辑 (Other logic omitted)
    for (const module of orderedModules) {
      const renderedLength = 0;
      let source: MagicString | undefined;
      if (module.isIncluded() || includedNamespaces.has(module)) {
        const rendered = module.render(renderOptions);
        // 省略其他逻辑 (Other logic omitted)
      }
    }
    // 省略其他逻辑 (Other logic omitted)
  }
  async render(): Promise<ChunkRenderResult> {
    const {
      accessedGlobals,
      indent,
      magicString,
      renderedSource,
      usedModules,
      usesTopLevelAwait
    } = this.renderModules(preliminaryFileName.fileName);
    // 省略其他逻辑 (Other logic omitted)
  }
}

As known from the Generate Chunks article, a chunk corresponds to at least one module. Therefore, from the source code logic above, it can be seen that Rollup will iterate through all modules in the chunk and then call each module's render method sequentially to generate the final code.

WARNING

All modules contained in a chunk are sorted according to their execution order and stored in the orderedModules array.

ts
const compareExecIndex = <T extends OrderedExecutionUnit>(
  unitA: T,
  unitB: T
) => (unitA.execIndex > unitB.execIndex ? 1 : -1);
function sortByExecutionOrder(units: OrderedExecutionUnit[]): void {
  units.sort(compareExecIndex);
}
class Bundle {
  private async generateChunks(): Promise<void> {
    // 省略其他逻辑 (Other logic omitted)
    for (const { alias, modules } of inlineDynamicImports
      ? [{ alias: null, modules: includedModules }]
      : preserveModules
        ? includedModules.map(module => ({
            alias: null,
            modules: [module]
          }))
        : getChunkAssignments(
            this.graph.entryModules,
            manualChunkAliasByEntry,
            experimentalMinChunkSize,
            this.inputOptions.onLog
          )) {
      sortByExecutionOrder(modules);
      const chunk = new Chunk(
        modules,
        this.inputOptions,
        this.outputOptions,
        this.unsetOptions,
        this.pluginDriver,
        this.graph.modulesById,
        chunkByModule,
        externalChunkByModule,
        this.facadeChunkByModule,
        this.includedNamespaces,
        alias,
        getHashPlaceholder,
        bundle,
        inputBase,
        snippets
      );
      chunks.push(chunk);
    }
    // 省略其他逻辑 (Other logic omitted)
  }
  // 省略其他逻辑 (Other logic omitted)
}

It can be seen that tree-shaking is executed within the module.render(renderOptions) function. Let's delve deeper into the logic of the module.render(renderOptions) function.

ts
class Module {
  render(options: RenderOptions): {
    source: MagicString;
    usesTopLevelAwait: boolean;
  } {
    const source = this.magicString.clone();
    this.ast!.render(source, options);
    source.trim();
    const { usesTopLevelAwait } = this.astContext;
    if (
      usesTopLevelAwait &&
      options.format !== 'es' &&
      options.format !== 'system'
    ) {
      return error(logInvalidFormatForTopLevelAwait(this.id, options.format));
    }
    return { source, usesTopLevelAwait };
  }
}

From this, we can understand that TLA (Top-Level Await) syntax in Rollup does not support output formats other than ESM and system. For a detailed implementation plan of TLA syntax, please refer to Detailed Explanation and Implementation of TLA.

Additionally, the module.render(renderOptions) function calls the module.ast.render(source, options) method, thereby performing the tree-shaking operation.

The logic of the this.ast!.render(source, options) method is as follows:

ts
class Program {
  render(code: MagicString, options: RenderOptions): void {
    let start = this.start;
    if (code.original.startsWith('#!')) {
      start = Math.min(code.original.indexOf('\n') + 1, this.end);
      code.remove(0, start);
    }
    if (this.body.length > 0) {
      // Keep all consecutive lines that start with a comment
      while (
        code.original[start] === '/' &&
        /[*/]/.test(code.original[start + 1])
      ) {
        const firstLineBreak = findFirstLineBreakOutsideComment(
          code.original.slice(start, this.body[0].start)
        );
        if (firstLineBreak[0] === -1) {
          break;
        }
        start += firstLineBreak[1];
      }
      renderStatementList(this.body, code, start, this.end, options);
    } else {
      super.render(code, options);
    }
  }
}

The logic is straightforward: sequentially render the body nodes of the current module's AST, using the renderStatementList method to render each statement. In other words, the renderStatementList method contains the core logic of tree-shaking.

ts
function renderStatementList(
  statements: readonly StatementNode[],
  code: MagicString,
  start: number,
  end: number,
  options: RenderOptions
): void {
  let currentNode, currentNodeStart, currentNodeNeedsBoundaries, nextNodeStart;
  let nextNode = statements[0];
  let nextNodeNeedsBoundaries = !nextNode.included || nextNode.needsBoundaries;
  if (nextNodeNeedsBoundaries) {
    nextNodeStart =
      start +
      findFirstLineBreakOutsideComment(
        code.original.slice(start, nextNode.start)
      )[1];
  }

  for (let nextIndex = 1; nextIndex <= statements.length; nextIndex++) {
    currentNode = nextNode;
    currentNodeStart = nextNodeStart;
    currentNodeNeedsBoundaries = nextNodeNeedsBoundaries;
    nextNode = statements[nextIndex];
    nextNodeNeedsBoundaries =
      nextNode === undefined
        ? false
        : !nextNode.included || nextNode.needsBoundaries;
    if (currentNodeNeedsBoundaries || nextNodeNeedsBoundaries) {
      nextNodeStart =
        currentNode.end +
        findFirstLineBreakOutsideComment(
          code.original.slice(
            currentNode.end,
            nextNode === undefined ? end : nextNode.start
          )
        )[1];
      if (currentNode.included) {
        if (currentNodeNeedsBoundaries) {
          currentNode.render(code, options, {
            end: nextNodeStart,
            start: currentNodeStart
          });
        } else {
          currentNode.render(code, options);
        }
      } else {
        treeshakeNode(currentNode, code, currentNodeStart!, nextNodeStart);
      }
    } else {
      currentNode.render(code, options);
    }
  }
}

In the renderStatementList method, it is decided whether to call the node.render method or the treeshakeNode method based on whether the statement's AST Node is needed (node.included).

  1. If the node does not need to be included in the final output (i.e., node.included = false):

    Then, the treeshakeNode method will be used to execute the logic for removing this piece of code.

    ts
    function treeshakeNode(
      node: Node,
      code: MagicString,
      start: number,
      end: number
    ): void {
      code.remove(start, end);
      node.removeAnnotations(code);
    }
  2. If the node needs to be included in the final output (i.e., node.included = true):

    Then, the node.render method will be used to recursively process the AST tree and handle details. The logic mainly involves fine-tuning the source code, such as replacing variable names, deleting unused code blocks (by recursively calling renderStatementList), removing the export keyword from the source code corresponding to required export AST nodes, and adding semicolons at the end of statements.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)