Skip to content

ns dev: minified ESM .js chunks misclassified as CJS — deps shims lose named exports #11621

Closed

Description

@aleclarson

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 * from following) is also line-anchored — a mid-line export*from"./x" is silently dropped even after the classification check is unanchored.
  • starAsRe requires whitespace around * — minified export*as ns from is 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:

  1. Extend maskJsComments (in hmr/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).
  2. Run ESM_SYNTAX_RE (unanchored) and the export * as / export {...} / export * from scanners against the masked copy, so mid-line ESM is found without matching look-alike text inside literals or comments.
  3. Guard each keyword with an identifier/member lookbehind — (?<![.$\u200c\u200d\p{ID_Continue}]) — so $export/obj.export/obj.import cannot match.
  4. For export * from, find the from position 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

  1. added a commit that references this issue on Nov 10, 2026
    68765d7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions