Repository navigation
Description
Native iOS development startup fails before the app mounts when an app dependency imports named exports from a minified ESM .js chunk: the dependency-bundle shim drops every named export from that chunk.
Environment: @nativescript/vite 8.0.20 (same code in 8.0.21), iOS HTTP module serving.
Reproduction
import { collectDepsModuleExportInfo } from '@nativescript/vite/hmr/server/deps-bundle.js';
// Write each fixture to disk, then call collectDepsModuleExportInfo(file, 'ios').| File | Contents | Observed result |
|---|---|---|
minified.js |
const value=1;export{value as g}; |
{ names: null, hasDefault: false, opaqueCjs: true } |
newline.js |
const value=1; then a newline then export{value as g}; |
{ names: ['g'], hasDefault: false } |
minified.mjs |
const value=1;export{value as g}; |
{ names: ['g'], hasDefault: false } |
Expected: all three expose g as ESM.
Live failure
A real dependency chain ends at a chunk containing export{...mt as g,...} after function declarations on the same line. Its /ns/m/node_modules/... response is the deps-registry shim with only export default __ns_dep_ns__; and no named exports, so the host ESM linker rejects the importer:
SyntaxError: The requested module './chunk-XXXX.js' does not provide an export named 'g'
Diagnosis
In hmr/server/deps-bundle.ts, ESM_SYNTAX_RE anchors import/export syntax to the start of a line:
const ESM_SYNTAX_RE = /^[ \t]*(?:import[\s{"'*]|export[\s{*])/m;collectDepsModuleExportInfo therefore misclassifies valid minified .js ESM as opaque CommonJS before reaching its export scanners. Two adjacent spots carry the same assumption:
starRe(export * fromfollowing) is also line-anchored — a mid-lineexport*from"./x"is silently dropped even after the classification check is unanchored.starAsRerequires whitespace around*— minifiedexport*as ns fromis missed.
The naive fix (dropping the ^ anchor) is not safe: it then matches export/import text inside string literals, comments, regex literals, $export-style identifiers ($ and . are non-word chars, so \b still matches in $export and obj.import), which flips real CJS files to ESM and breaks them the other way.
Proposed fix
Classify and scan on a masked copy of the source:
- Extend
maskJsComments(inhmr/server/websocket-core-bridge.ts) with a mode that also blanks string/template/regex literal interiors while keeping delimiters and character positions. While there: the template-literal scan must honor${}interpolation — a nested template or a literal/comment inside an interpolation currently desyncs the backtick pairing and masks real code after it (an apostrophe like`...it's...`inside a template can open a fake string running to EOF). - Run
ESM_SYNTAX_RE(unanchored) and theexport * as/export {...}/export * fromscanners against the masked copy, so mid-line ESM is found without matching look-alike text inside literals or comments. - Guard each keyword with an identifier/member lookbehind —
(?<![.$\u200c\u200d\p{ID_Continue}])— so$export/obj.export/obj.importcannot match. - For
export * from, find thefromposition on the masked copy and read the specifier literal back from the original text at the same offset.
NS_DEPS_PER_MODULE=1 (per-module serving) is the existing escape hatch; a bundled production build avoids the shim path entirely.
Reported downstream as octane-xplat/octane-xplat#181, which includes the same reproduction and a link-realm failure trace.
Activity
fix(vite): classify minified ESM deps chunks as ESM (NativeScript#11622…