Module: lib/bundler/treeshake

Decides which modules an application actually needs.

What was impossible before, and why

The build prepended dist/runtime.min.js — one pre-bundled blob — to every application. Nothing about that arrangement admits the question "does this application use the trace recorder?", because by the time the compiler ran, the recorder had already been fused into a single artifact. Shaking is not a feature that was missing; it was unaskable.

With the runtime consumed as modules the question is answerable, and this file answers it.

The rule, and its one deliberate departure from the specification

Two kinds of edge are treated differently, which is the whole mechanism:

  • A plain import always executes its target. That is what ES modules do, and the runtime depends on it: reactive/proxyHandler.js calls setPathResolver(getPropertyPath) at its top level, and that call has to happen. No analysis here will ever drop a module that something imports.

  • A re-export edge is followed only for the names that are needed. A barrel is a routing table, and export { startRecording } from './trace/recorder.js' in a barrel nobody asks startRecording of is a route to nowhere. Strictly, the specification says that re-export executes recorder.js; every bundler departs from it here, because otherwise no barrel is ever shakeable and avenx-core/runtime is a barrel.

The departure is bounded rather than blanket. A re-export target is still kept when it has a top-level side effect of its own — an expression statement rather than a declaration — or when its package declares itself effectful through sideEffects in package.json. Exactly one module in the Avenx runtime has such a statement, and it is reached by a plain import anyway.

What this does not do

It does not remove unused declarations inside a module that is kept. Statement-level elimination needs a real identifier analysis, and a bundler that guesses at that miscompiles code rather than shrinking it. The honest consequence is stated in the build's own reporting: what shaking removes here is whole modules, and the fixed cost of a module that something imports is its whole source.

Source:

Members

(inner, constant) ALL :string

The wildcard standing for "every export of this module is needed".

Type:
  • string
Source:

Methods

(static) hasTopLevelEffects(module) → {boolean}

Whether a module runs code at its top level beyond declaring things.

An expression statement at module scope is a side effect: it happens when the module is evaluated and nothing else will make it happen. A declaration is not, even when its initialiser calls something — that is the standard assumption every bundler makes, and abandoning it would keep every module that ever writes const x = new Thing().

Parameters:
Name Type Description
module object

A graph module.

Source:
Returns:

True when the module must run if it is reached at all.

Type
boolean

(static) shake(options) → {Set:.<string:>}

Works out which modules the bundle must contain.

Parameters:
Name Type Description
options object

Shake options.

Properties
Name Type Attributes Description
graph object

The linked module graph.

order Array:.<string:>

Modules in emission order.

entryNeeds Map:.<string:, Array:.<string:>> <optional>

Export names an entry's consumer requires, keyed by entry id. ['*'] means the whole namespace.

Source:
Returns:

Module ids to emit.

Type
Set:.<string:>

(inner) packageSideEffects(file, cache) → {boolean|null}

Reads the sideEffects declaration of the package a module belongs to.

"sideEffects": false is the convention a package uses to say its modules can be dropped when unused. It is honoured for third-party packages, where this analysis has no other way to know.

Parameters:
Name Type Description
file string

Absolute module path.

cache Map:.<string:, (boolean:|null:)>

Per-directory answers.

Source:
Returns:

False when the package declares itself pure, true when it declares effects, null when it says nothing.

Type
boolean | null