Skip to content

manualChunks

ZNTC implements Rollup’s manualChunks(id, meta) signature. Common production patterns — vendor/shared splitting, content-based grouping, and graph-topology-based grouping — are all supported.

import { build } from "@zntc/core";
await build({
entryPoints: ["./src/main.ts"],
splitting: true,
outdir: "./dist",
manualChunks: (id) => {
if (id.includes("node_modules")) return "vendor";
return null;
},
});

Modules are grouped into the chunk whose name manualChunks returns. Returning null / undefined falls back to automatic chunking.

The second argument meta exposes getModuleInfo(id) for graph lookups.

manualChunks: (id, meta) => {
const info = meta.getModuleInfo(id);
if (!info) return null;
// Only modules imported by 2+ other modules go to "shared"
if (info.importers.length >= 2) return "shared";
// External dependencies always split off
if (info.isExternal) return "vendor";
// Skip modules dropped by tree-shaking
if (!info.isIncluded) return null;
return null;
},
FieldTypeDescription
idstringAbsolute module path
isEntrybooleanWhether this module is an entry point
isExternalbooleanModule excluded from the bundle by an external pattern
hasModuleSideEffectsbooleanResult of package.json sideEffects / glob match
codestring | nullModule source. null for external / asset modules
isIncludedbooleanWhether the module survived tree-shaking
exportsstring[]Exported names (including default)
importersstring[]Absolute paths of modules that statically import this one
dynamicImportersstring[]Modules that reach this one via import()
importedIdsstring[]Static imports of this module (external included)
dynamicallyImportedIdsstring[]Dynamic imports of this module
syntheticNamedExportsbooleanPlugin-defined — currently always false
implicitlyLoadedAfterOneOfstring[]Plugin emitFile option — currently always []
implicitlyLoadedBeforestring[]Same as above

info.ast is not yet exposed (depends on the ESTree adapter).

Dynamic-import targets are absorbed into the importer’s chunk, approximating a single-file output.

await build({
entryPoints: ["./src/main.ts"],
splitting: true,
inlineDynamicImports: true, // inline dynamic imports too
outdir: "./dist",
});

Internally each dynamic-import target is wrapped with __esm and import("./x") calls are rewritten to Promise.resolve().then(() => (init_x(), exports_x)).

  • Namespace identity: (await import("./x")) === (await import("./x"))
  • Single execution: top-level side effects run exactly once (__esm caches)
  • Live bindings: mutations like export let counter; counter++ are visible to callers

Modules matched by external are excluded from the bundle but registered as phantom modules in the graph — Rollup parity.

await build({
entryPoints: ["./src/main.ts"],
external: ["react", "react-dom"],
manualChunks: (id, meta) => {
// externals are directly queryable
const reactInfo = meta.getModuleInfo("react");
console.log(reactInfo?.isExternal); // true
// entry.importedIds includes externals
const entry = meta.getModuleInfo(id);
console.log(entry?.importedIds.includes("react")); // true if entry imports react
return null;
},
});
manualChunks: (id) => {
if (id.includes("/node_modules/")) return "vendor";
if (id.includes("/src/components/")) return "components";
return null;
}

Group only modules whose source contains a @vendor marker:

manualChunks: (id, meta) => {
const info = meta.getModuleInfo(id);
if (info?.code?.includes("@vendor")) return "vendor";
return null;
}
manualChunks: (id, meta) => {
const info = meta.getModuleInfo(id);
if (!info) return null;
if (info.isEntry) return null;
if (info.importers.length >= 2) return "shared";
return null;
}

Pure / tree-shakable libraries into their own chunk

Section titled “Pure / tree-shakable libraries into their own chunk”
manualChunks: (id, meta) => {
const info = meta.getModuleInfo(id);
if (info && !info.hasModuleSideEffects) return "pure";
return null;
}
  • The manualChunks resolver is called exactly once per module (NAPI TSFN — minimal JS round-trips).
  • If the resolver throws, the module falls back to null (auto chunking); the build is not aborted.
  • Non-string returns (number, boolean) are treated as null (Rollup semantics).
  • external modules are not passed to the resolver — phantom modules are not chunk-assignment candidates.
  • Dynamic-import targets are excluded from manual chunks by default to preserve lazy-load semantics. Set inlineDynamicImports: true to absorb them into the importer’s chunk.

manualChunks is a function, so it cannot be expressed on the CLI directly — use the JS API (@zntc/core) or zntc.config.{js,ts}.

zntc.config.ts
import { defineConfig } from "@zntc/core";
export default defineConfig({
entryPoints: ["./src/main.ts"],
splitting: true,
inlineDynamicImports: true,
manualChunks: (id, meta) => {
if (meta.getModuleInfo(id)?.isExternal) return "vendor";
return null;
},
});

13 of 14 Rollup ModuleInfo fields are exposed. info.ast and plugin-context APIs (this.getModuleInfo / emitFile / resolve) are not available yet.