@hyperfrontend/builder/bundle/dedupededupe
Shared-first-party dedupe: an additive post-emit pass that lifts first-party modules inlined into multiple per-entry bundles into shared _shared/<srcPath>/index.<fmt>.js chunks and rewrites each consuming entry to import them. Runs over the already-bundled esm/cjs outputs only, leaving per-entry isolated bundling intact.
hoistSharedFirstParty(...) is the entry point. It only hoists copies that planHoists proves safe — structurally identical, references resolvable, and initialization cycle-free — so the worst case is output identical to the input.
The pass is built from composable stages, each exported for direct use: module attribution (attribute, indexOwners, parseEntry), source reachability (collectReachableSources), chunk planning and extraction (planHoists, renderChunk, resolveModuleRefs), and entry rewriting (rewriteEntry).
Ownership is bounded to modules reachable from the entry points' sources, so a spec-only fixture can never own a name, and reference classification is scope-aware (collectFreeRefs), so a parameter that shadows an owned name never becomes an import edge.
API Reference§
ƒ Functions
Declarations whose base name is unowned (a dependency or unexported helper inlined into the bundle) are skipped, leaving them in place.
Parameters
| Name | Type | Description |
|---|---|---|
§parsed | ParsedEntry | A parsed entry bundle. |
§owners | OwnerIndex | The first-party ownership index. |
Example
Attributing an entry's declarations
const byModule = attribute(parseEntry(source, 'esm'), owners)$N collision-rename suffix from a local name, yielding the canonical source symbol name.Parameters
| Name | Type | Description |
|---|---|---|
§name | string | A local identifier from an emitted bundle. |
Returns
string$digits removed.Example
Undoing a collision rename
baseName('Store$1') // => 'Store'Parameters
| Name | Type | Description |
|---|---|---|
§format | ChunkFormat | Output module format. |
Returns
stringindex.esm.js / index.cjs.js).Example
Naming the CJS chunk
chunkFileName('cjs') // => 'index.cjs.js'.ts module file reachable through relative specifiers. Both static
import / export ... from declarations and string-literal dynamic import() calls are followed. Bare specifiers (dependencies and workspace packages) and non-.ts targets (e.g. imported .json data) are outside the graph, as is any resolution escaping srcRoot. Files a library carries but never imports from an entry point — spec-only fixtures above all — are unreachable by construction, which is what keeps their declarations out of the dedupe ownership index.Parameters
Returns
string[ ]Example
Reachable modules of a single-entry library
const files = collectReachableSources(['/abs/libs/foo/src/index.ts'], '/abs/libs/foo/src')Two copies of the same source that differ only in rollup's per-entry
$N collision suffixes compare equal, while genuinely different code never collides: $N is stripped from identifier nodes only, so string literals, comments, numeric tokens, and discriminating member names stay intact.Parameters
Returns
string$N stripped from every identifier node.Example
Stripping a dep-namespace local's collision suffix
fingerprintOf(statement, sourceFile) // 'const x = index_cjs_js.getType();' for both `$1` and `$2` copiesWhen
entryFiles is given, only modules reachable from those entry sources through the first-party import graph are indexed; a file the entries never import — a spec-only fixture above all — can then never own a name, so a bundle declaration merely named like one of its symbols (a tree-shaken JSON key, say) stays unattributed instead of forming a phantom chunk. Without entryFiles the whole <srcRoot>/** tree is scanned. Both exported and private top-level runtime declarations are indexed, so a module's private helpers hoist alongside the exports that use them. Types-only modules declare no runtime symbols and contribute nothing. A name declared by two different modules is ambiguous and dropped from the index, so the pass can never misattribute it.
Parameters
Returns
OwnerIndexExample
Indexing only what a library's entries reach
const owners = indexOwners('/abs/libs/foo/src', ['/abs/libs/foo/src/index.ts'])Classifies every top-level statement as an import binding, a removable runtime declaration, the export surface, or an opaque bare statement. Inline
export-modified declarations are treated as part of the export surface (never removable), so the bundle's published API is never disturbed.Parameters
| Name | Type | Description |
|---|---|---|
§source | string | Raw entry bundle source text. |
§format | ChunkFormat | Module format selecting ESM vs CJS import/export shapes. |
Returns
ParsedEntryExample
Modeling an ESM entry bundle
const parsed = parseEntry("import { x } from './_dependencies/a/index.esm.js'\nclass C {}", 'esm')A module qualifies only when it is inlined into at least two entries, is not entangled with a bare statement, presents structurally identical declarations (rename-insensitive) in a consistent order across every copy, references nothing unresolvable, and (after closure and acyclic peeling) depends only on other hoisted modules through a cycle-free graph. Anything failing these is left inlined, so the worst case equals the unmodified output.
Parameters
| Name | Type | Description |
|---|---|---|
§entries | EntryInput[ | Parsed entries with their per-module attribution. |
§owners | OwnerIndex | First-party ownership index. |
Returns
Map< string, PlannedModule>Example
Planning hoists for a set of entries
const plan = planHoists(entries, owners)Parameters
| Name | Type | Description |
|---|---|---|
§plan | ChunkPlan | The module's declarations plus resolved import edges. |
§format | ChunkFormat | Output module format. |
Returns
stringExample
Rendering an ESM chunk
const source = renderChunk({ decls, crossImports: [], depImports: [] }, 'esm')resolveModuleRefs( decls: EntryDecl[ ], owners: OwnerIndex, importBindings: Map< string, ImportBinding>, entryDeclNames: Set< string>, selfModuleKey: string): ModuleResolution
Reference collection is scope-aware: a name bound inside a declaration (a function parameter, a hoisted
var, a block-scoped local) is never a reference, so a callback parameter that happens to share an owned symbol's name can never fabricate a cross-module import edge. Identifiers the module declares itself are intra-chunk and ignored. A name that is neither owned, nor a dependency binding, nor a top-level entry declaration is a runtime global and needs no import. Cross-module references are always safe to lift because planHoists only keeps an acyclic subset, so every dependency chunk is fully evaluated before its dependent.Parameters
| Name | Type | Description |
|---|---|---|
§decls | EntryDecl[ | The module's canonical declarations. |
§owners | OwnerIndex | First-party ownership index. |
§importBindings | Map< | The consuming entry's import bindings. |
§entryDeclNames | Set< | Every top-level declaration name in the entry. |
§selfModuleKey | string | The module being resolved. |
Returns
ModuleResolutionExample
Resolving a module's references
const resolution = resolveModuleRefs(decls, owners, parsed.importBindings, parsed.declNames, 'events/events')_shared/ chunks instead of inlining them. Splices every hoisted declaration (and its leading comments) out of the entry and prepends an import/
require binding the chunk's base export to the entry's local name, aliasing when rollup renamed the local (foo as foo$1). Only the hoisted symbols the entry still references after splicing are re-imported: a private helper used solely by another hoisted export (e.g. a reducer's handlers, used only by the hoisted rootReducer) is seen as dead against the spliced body and omitted from the import, while the chunk it lives in keeps it. The bundle's export surface is left untouched: surviving spliced names are now supplied by the inserted imports, so the published API is byte-for-byte identical to before.Parameters
| Name | Type | Description |
|---|---|---|
§parsed | ParsedEntry | The parsed entry bundle. |
§hoists | EntryHoist[ | The modules hoisted out of this entry, each with its chunk specifier. |
§format | ChunkFormat | Output module format. |
Returns
stringhoists is empty.Example
Rewriting an entry to import a shared module
const rewritten = rewriteEntry(parseEntry(source, 'esm'), [{ decls, specifier: './_shared/state/index.esm.js' }], 'esm')◈ Interfaces
Properties
Properties
Properties
Properties
kind:"default" | "named" | "namespace" | "cjs-namespace" | "cjs-named"Binding shape governing how the import is re-emitted.Properties
Properties
fingerprint:stringRename-insensitive identity key: $N suffixes stripped from identifier nodes; the raw text is still the chunk-body source.Properties
Properties
Properties
Properties
kind:"default" | "named" | "namespace" | "cjs-namespace" | "cjs-named"Shape of the binding, governing how it is re-emitted into a shared chunk.Properties
Properties
◆ Types
src/, without extension and forward-slashed (e.g. events/events, models). Doubles as the directory name the module's hoisted chunk lives under: _shared/<moduleKey>/.type ModuleKey = string