Summary
After the fix for #6, there are two remaining code-splitting edge cases in the current production builder:
- Alias-resolved modules outside the entry root can be assigned collector labels such as
_external/*, but chunk output selection currently looks up files by path suffix. That can omit an entry module from the chunk even though the chunk graph contains it.
- Dynamic CSS imports such as
import("./theme.css") can survive into the browser chunk path as runtime imports of synthetic CSS/data modules. Since Volt already owns stylesheet output, these should be inert browser-loadable promises rather than real dynamic imports.
Both cases show up when an entry combines an alias import with at least one dynamic import, or when user code dynamically imports a CSS file/package stylesheet.
Environment
- Volt
0.9.2 / current master after 3c6f117
- OXC
0.11.0
- Elixir
1.20.0-rc.4
- macOS Darwin
25.4.0
Repro 1: alias module outside the entry root
Given this source layout:
fixtures/
shared/rendered.ts
src/external_alias_entry.ts
src/lazy.ts
shared/rendered.ts:
export const rendered = "rendered-from-shared-root"
src/lazy.ts:
export const lazyValue = "lazy-loaded"
src/external_alias_entry.ts:
import { rendered } from "@shared/rendered"
document.body.dataset.rendered = rendered
import("./lazy").then((mod) => {
document.body.dataset.lazy = mod.lazyValue
})
Build shape:
Volt.Builder.build(
entry: Path.join(fixture_dir, "src/external_alias_entry.ts"),
outdir: outdir,
name: "external-alias-entry",
format: :esm,
hash: false,
minify: false,
sourcemap: false,
aliases: %{"@shared" => Path.join(fixture_dir, "shared")}
)
Actual
The alias-resolved module can be missed when selecting files for the entry chunk, because the chunk contains the original absolute module path while the generated JS file is keyed by the collector label.
Expected
The entry chunk should include the alias module, and the async chunk should remain separately imported:
external-alias-entry.js includes rendered-from-shared-root
external-alias-entry-lazy.js includes lazy-loaded
Repro 2: dynamic CSS import
src/dynamic_css_entry.ts:
import("./theme.css").then(() => {
document.body.dataset.css = "loaded"
})
src/theme.css:
Build shape:
Volt.Builder.build(
entry: Path.join(fixture_dir, "src/dynamic_css_entry.ts"),
outdir: outdir,
name: "dynamic-css-entry",
format: :esm,
hash: false,
minify: false,
sourcemap: false
)
Actual
The browser output can retain a runtime dynamic import path for CSS-like modules, even though Volt's CSS handling is not a browser JS module load.
Expected
The dynamic CSS import should become an inert fulfilled promise, similar in spirit to the existing static CSS import no-op handling. The emitted JS should not contain a runtime import("./theme.css"), CSS text, or a data:text/css module import.
Candidate fix
The minimal shape that fixed the two repros locally:
- select chunk files using the collector's original module path to label map, rather than suffix-matching module paths against generated labels;
- protect dynamic
import(...) before per-chunk OXC bundling, then restore it before Volt rewrites chunk URLs;
- rewrite dynamic CSS import expressions to an already-resolved no-op promise, while leaving static CSS imports on the existing no-op JS-module path.
I opened #10 with tests for both reduced cases and one candidate implementation.
AI disclosure
I used OpenAI Codex as a coding assistant to help reduce the repros and draft the candidate patch. I reviewed the generated issue text and patch before filing.
Summary
After the fix for #6, there are two remaining code-splitting edge cases in the current production builder:
_external/*, but chunk output selection currently looks up files by path suffix. That can omit an entry module from the chunk even though the chunk graph contains it.import("./theme.css")can survive into the browser chunk path as runtime imports of synthetic CSS/data modules. Since Volt already owns stylesheet output, these should be inert browser-loadable promises rather than real dynamic imports.Both cases show up when an entry combines an alias import with at least one dynamic import, or when user code dynamically imports a CSS file/package stylesheet.
Environment
0.9.2/ currentmasterafter3c6f1170.11.01.20.0-rc.425.4.0Repro 1: alias module outside the entry root
Given this source layout:
shared/rendered.ts:src/lazy.ts:src/external_alias_entry.ts:Build shape:
Actual
The alias-resolved module can be missed when selecting files for the entry chunk, because the chunk contains the original absolute module path while the generated JS file is keyed by the collector label.
Expected
The entry chunk should include the alias module, and the async chunk should remain separately imported:
Repro 2: dynamic CSS import
src/dynamic_css_entry.ts:src/theme.css:Build shape:
Actual
The browser output can retain a runtime dynamic import path for CSS-like modules, even though Volt's CSS handling is not a browser JS module load.
Expected
The dynamic CSS import should become an inert fulfilled promise, similar in spirit to the existing static CSS import no-op handling. The emitted JS should not contain a runtime
import("./theme.css"), CSS text, or adata:text/cssmodule import.Candidate fix
The minimal shape that fixed the two repros locally:
import(...)before per-chunk OXC bundling, then restore it before Volt rewrites chunk URLs;I opened #10 with tests for both reduced cases and one candidate implementation.
AI disclosure
I used OpenAI Codex as a coding assistant to help reduce the repros and draft the candidate patch. I reviewed the generated issue text and patch before filing.