Skip to content

feat(webpack,vite): keep ns: builtin module imports for the runtime - #11626

Merged
NathanWalker merged 2 commits into
mainfrom
feat/ns-builtin-module-externals
Nov 9, 2026
Merged

NathanWalker merged 2 commits into
mainfrom
feat/ns-builtin-module-externals

Conversation

@edusperoni

@edusperoni edusperoni commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

What is the current behavior?

The iOS and Android runtimes resolve ns:<name> builtin modules (ns:util, ns:module, ns:runtime, ns:url, ns:worker_threads, …) and a few node:<name> compatibility shims themselves, before any file or npm resolution. Neither bundler lets these imports through:

  • webpack treats ns:util as a URL scheme, which skips resolve.alias, and fails with UnhandledSchemeError. node:* fails the same way, because externalsPresets.node is off.
  • Vite fails with Rolldown failed to resolve import "ns:util", and silently replaces node:url / node:util / node:worker_threads with an empty browser stub. Under HMR, several dev-server passes also rewrite or stub these specifiers: /@id/ wrapping, routing to /ns/m/node_modules/ns:util, the esbuild dependency bundles, the dev import map, and the CommonJS require shim.

What is the new behavior?

Every ns: import is left verbatim in the output. Specifiers are matched by the ns: prefix, so modules added in future runtime versions work without changes here. ESM output keeps import … from "ns:util"; CommonJS output keeps require("ns:util").

Customization uses each bundler's own mechanisms:

webpack: the base config registers NativeScriptBuiltinModulesPlugin (default include: [/^ns:/]).

  • Change its include list with webpack-chain: config.plugin('NativeScriptBuiltinModulesPlugin').tap(...).
  • The plugin matches on the request after beforeResolve, so a stock NormalModuleReplacementPlugin can:
    • redirect a builtin to a polyfill (/^ns:url$/ → a file);
    • forward a bare name to a runtime shim (/^worker_threads$/ → node:worker_threads).

Vite: the base config adds nativescriptBuiltinModules().

  • Add another instance with { include: ['node:worker_threads'] } to opt in more specifiers.
  • resolve.alias handles polyfills and forwarding, since aliases resolve before the plugin.

node: stays opt-in rather than hardcoding the current shim list, because the set grows with each runtime version. Both READMEs show how to forward to the runtime's shims, using worker_threads as the example.

Vite coverage:

  • vite build: the plugin runs first in the main build, worker builds and the dependency optimizer.
  • HMR:
    • an esbuild counterpart runs first in the deps, vendor and core bundles;
    • /@id/<builtin> is unwrapped before the device rewriting passes;
    • guards in core rewriteSpec, worker-realm module bindings and the plugin-specifier check keep these imports untouched;
    • opted-in node: names are left out of the dev import map;
    • the CommonJS require shim tries the runtime's require first for ns: / node: ids.

Tests:

  • packages/webpack5/__tests__/plugins/NativeScriptBuiltinModulesPlugin.spec.ts: real webpack compiles checking ESM and CommonJS output, polyfill and forwarding through NormalModuleReplacementPlugin, and the build failure without the plugin.
  • packages/vite/helpers/builtin-modules.spec.ts: in-process vite build (ESM, import(), require() in a CommonJS dependency, alias forwarding and polyfill, and the failure without the plugin), plus the esbuild plugin.
  • packages/vite/hmr/server/builtin-modules-hmr.spec.ts: each HMR touch point. All four tests fail without the HMR changes.

Notes for reviewers:

  • The webpack config snapshots already fail on main: they contain absolute paths from another machine and a FixSourceMapUrlPlugin that no longer exists. I added only the new plugin entry to them rather than regenerating. The set of failing tests is identical to main. The mdn-data alias re-wrap in base.ts comes from the prettier pre-commit hook.
  • packages/vite/typescript-optional.spec.ts also fails on main (5 tests) and is unchanged by this PR.
  • Under HMR, the esbuild dependency bundles don't see Vite's resolve.alias. A ns: polyfill or bare-name forward set up with an alias applies to vite build and the Vite module graph, but not to dependencies bundled by esbuild during a dev session.
  • No on-device run yet. Manual check: import ns:util from app code, a CommonJS dependency and a worker. Opt in node:worker_threads and check that it reaches the runtime, under both ns run (HMR) and a release build.

The iOS and Android runtimes resolve `ns:<name>` builtin modules (and a
few `node:<name>` compatibility shims) themselves. Webpack failed on these
imports with an unhandled-scheme error, and Vite failed to resolve `ns:`
specifiers and replaced `node:` ones with an empty browser stub.

Every `ns:` specifier is now left verbatim in the output, matched by
prefix so modules added in future runtime versions work without changes.
Both packages expose the same helpers:

- addBuiltinModule(matcher, target?) opts more specifiers in, e.g. the
  `node:` shims a runtime version ships, optionally forwarding a bare
  specifier such as `worker_threads` to `node:worker_threads`.
- polyfillBuiltinModule(specifier, path) bundles a replacement instead,
  for runtimes that don't provide a builtin.

In Vite this covers `vite build`, workers, dep optimization and the HMR
dev server (esbuild dependency bundles, `/@id/` unwrapping, worker realm
routing, core rewriting, the dev import map and the CommonJS require
shim).
@nx-cloud

nx-cloud Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 8635dd3

Command Status Duration Result
nx test apps-automated -c=ios ✅ Succeeded 3m 15s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded 1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-11-09 18:10:50 UTC

@pkg-pr-new

pkg-pr-new Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11626
npm i https://pkg.pr.new/@nativescript/vite@11626
npm i https://pkg.pr.new/@nativescript/webpack@11626

commit: 8635dd3

…ive hooks

Replace the addBuiltinModule/polyfillBuiltinModule helpers with a single
plugin per bundler, customized the way each bundler is normally
customized:

- webpack: NativeScriptBuiltinModulesPlugin, registered by name so
  webpack-chain can `.tap()` its `include` list. It externalizes on the
  request after `beforeResolve`, so a NormalModuleReplacementPlugin can
  redirect a builtin to a polyfill or forward a bare specifier to one.
- vite: nativescriptBuiltinModules({ include }). Extra instances add
  specifiers; resolve.alias handles polyfills and forwarding, since
  aliases resolve before the plugin.
@NathanWalker
NathanWalker merged commit e532d62 into main Nov 9, 2026
10 checks passed
@NathanWalker
NathanWalker deleted the feat/ns-builtin-module-externals branch November 9, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants