create-octane-xplat 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,294 +1,564 @@
1
- diff --git a/hmr/server/core-bundle.js b/hmr/server/core-bundle.js
2
- index 993097b9d861a2a7c477f4e664213efec3eb6293..7a9b95180cbd47e1d9c71180bc4582a450333a7c 100644
3
- --- a/hmr/server/core-bundle.js
4
- +++ b/hmr/server/core-bundle.js
5
- @@ -607,17 +607,32 @@ export function buildCoreMainShimCode() {
6
- * class/function values. `exportNames` comes from the same disk-based export
7
- * discovery the per-module bridge relies on (collectStaticExportNamesFromFile).
8
- */
9
- -const RESERVED_EXPORT_NAMES = new Set(['default', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'let', 'static', 'yield', 'await', 'eval', 'arguments', '__ns_core_sub_ns__']);
10
- +// `default` is emitted separately (below); `__ns_core_sub_ns__` is this shim's
11
- +// own local — a module exporting that name can't collide with it.
12
- +const NON_DECLARABLE_EXPORT_NAMES = new Set(['default', '__ns_core_sub_ns__']);
13
- +// Reserved words can't be declaration bindings (`export const enum` is a
14
- +// syntax error), but they ARE legal export names — the specifier position in
15
- +// `export { x as name }` accepts any IdentifierName. Emit those through a
16
- +// temp + alias so a reserved-word export (zod's `enum`/`catch`/… shapes this
17
- +// same code path) still reaches the bridged namespace.
18
- +const RESERVED_EXPORT_NAMES = new Set(['break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'let', 'static', 'yield', 'await', 'eval', 'arguments']);
19
- export function buildCoreSubShimCode(sub, exportNames, hasOwnDefaultExport = false) {
20
- const canonical = normalizeCoreSub(sub);
21
- const registryKey = `@nativescript/core/${canonical}`;
22
- - const names = Array.from(new Set(exportNames)).filter((n) => /^[A-Za-z_$][\w$]*$/.test(n) && !RESERVED_EXPORT_NAMES.has(n));
23
- + const names = Array.from(new Set(exportNames)).filter((n) => /^[A-Za-z_$][\w$]*$/.test(n) && !NON_DECLARABLE_EXPORT_NAMES.has(n));
24
- const lines = [];
25
- lines.push(`/* @nativescript/core bridge (bundle mode) — sub: ${canonical} */`);
26
- lines.push(`import ${JSON.stringify(CORE_BUNDLE_PATH)};`);
27
- lines.push(`const __ns_core_sub_ns__ = (globalThis.__NS_CORE_MODULES__ || {})[${JSON.stringify(registryKey)}];`);
28
- lines.push(`if (!__ns_core_sub_ns__) { throw new Error(${JSON.stringify(`[ns-core] core bundle registry missing '${registryKey}' — restart the dev server or set NS_CORE_PER_MODULE=1`)}); }`);
29
- + let aliasIdx = 0;
30
- for (const name of names) {
31
- + if (RESERVED_EXPORT_NAMES.has(name)) {
32
- + const alias = `__ns_core_alias_${aliasIdx++}`;
33
- + lines.push(`const ${alias} = __ns_core_sub_ns__[${JSON.stringify(name)}];`);
34
- + lines.push(`export { ${alias} as ${name} };`);
35
- + continue;
36
- + }
37
- lines.push(`export const ${name} = __ns_core_sub_ns__.${name};`);
38
- }
39
- // Deep-subpath contract (see isDeepCoreSubpath / per-module bridge): a sub
40
1
  diff --git a/hmr/server/deps-bundle.js b/hmr/server/deps-bundle.js
41
- index 50e5abddddf80b35044d45eb108a64dd175bf6e7..799c7257a8a730515633f19f6221ad26ff8678eb 100644
2
+ index 50e5abddddf80b35044d45eb108a64dd175bf6e7..4f16f129b4c4271173169871f42645d16d3cfa0c 100644
42
3
  --- a/hmr/server/deps-bundle.js
43
4
  +++ b/hmr/server/deps-bundle.js
44
- @@ -219,8 +219,37 @@ function isPackageRootSpec(spec) {
45
- return false;
46
- return pkgSub.split('/').length === (pkgSub.startsWith('@') ? 2 : 1);
5
+ @@ -53,6 +53,7 @@
6
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { createHash } from 'node:crypto';
9
+ +import { createRequire } from 'node:module';
10
+ import * as esbuild from 'esbuild';
11
+ import { resolvePlatform } from '../../helpers/cli-flags.js';
12
+ import { getGlobalDefines, getUserDefineEntries } from '../../helpers/global-defines.js';
13
+ @@ -669,6 +670,29 @@ export async function generateDepsBundle(options) {
14
+ // the device cannot resolve.
15
+ const buildPlugins = () => [
16
+ createWebpackLoaderStubEsbuildPlugin(),
17
+ + // Bare `octane` is the DOM entry — a dep importing it (published
18
+ + // @octane-xplat/* dist files kept `from "octane"` through 0.6.0)
19
+ + // bundles a second runtime next to the universal one, and hooks read
20
+ + // an empty CURRENT_SCOPE ("Cannot read properties of null (reading
21
+ + // 'hooks')"). Vite resolve.alias never reaches these esbuild passes;
22
+ + // route exact `octane` imports to the host-neutral entry here.
23
+ + {
24
+ + name: 'ns-deps-bundle-octane-universal-alias',
25
+ + setup(build) {
26
+ + build.onResolve({ filter: /^octane$/ }, (args) => {
27
+ + for (const dir of [args.importer && path.dirname(args.importer), args.resolveDir, projectRoot]) {
28
+ + if (!dir)
29
+ + continue;
30
+ + try {
31
+ + return { path: createRequire(path.join(dir, 'noop.js')).resolve('octane/universal/native') };
32
+ + }
33
+ + catch {
34
+ + }
35
+ + }
36
+ + return null;
37
+ + });
38
+ + },
39
+ + },
40
+ createNodeBuiltinPolyfillEsbuildPlugin(projectRoot, NODE_BUILTINS),
41
+ createNativeClassEsbuildPlugin(platform),
42
+ createVendorEsbuildPlugin(projectRoot),
43
+ diff --git a/helpers/ts-config-paths.js b/helpers/ts-config-paths.js
44
+ index 27013b5c8f78a72b6e6df903ac36f72fc0d75c1a..53a2e7d489ab91593cbec47ee6fb640fc56c786f 100644
45
+ --- a/helpers/ts-config-paths.js
46
+ +++ b/helpers/ts-config-paths.js
47
+ @@ -113,15 +113,20 @@ function resolveTsConfigPath(fullPath, platform, verbose, debugId) {
48
+ if (verbose && debugId) {
49
+ console.log(`📁 TypeScript path candidate: ${debugId} -> ${fullPath}`);
50
+ }
51
+ + // `.mobile` is the shared ios/android suffix tier — it sits between the os
52
+ + // qualifier and the unsuffixed default. Probing it per extension keeps
53
+ + // paths-mapped imports consistent with the bundler's extension chain.
54
+ + const suffixes = platform === 'ios' || platform === 'android' ? [platform, 'mobile'] : platform ? [platform] : [];
55
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
56
+ - const platformIndexPatterns = [`index.${platform}.ts`, `index.${platform}.js`, `index.${platform}.mjs`];
57
+ - for (const indexFile of platformIndexPatterns) {
58
+ - const indexPath = path.join(fullPath, indexFile);
59
+ - if (fs.existsSync(indexPath)) {
60
+ - if (verbose) {
61
+ - console.log(`📁 Found platform-specific directory index: ${indexPath}`);
62
+ + for (const suffix of suffixes) {
63
+ + for (const ext of ['.ts', '.js', '.mjs']) {
64
+ + const indexPath = path.join(fullPath, `index.${suffix}${ext}`);
65
+ + if (fs.existsSync(indexPath)) {
66
+ + if (verbose) {
67
+ + console.log(`📁 Found platform-specific directory index: ${indexPath}`);
68
+ + }
69
+ + return indexPath;
70
+ }
71
+ - return indexPath;
72
+ }
73
+ }
74
+ for (const indexFile of ['index.ts', 'index.js', 'index.mjs']) {
75
+ @@ -135,12 +140,14 @@ function resolveTsConfigPath(fullPath, platform, verbose, debugId) {
76
+ }
77
+ }
78
+ for (const ext of ['.ts', '.js', '.mjs']) {
79
+ - const platformPath = `${fullPath}.${platform}${ext}`;
80
+ - if (fs.existsSync(platformPath)) {
81
+ - if (verbose) {
82
+ - console.log(`📁 Found platform-specific file: ${platformPath}`);
83
+ + for (const suffix of suffixes) {
84
+ + const platformPath = `${fullPath}.${suffix}${ext}`;
85
+ + if (fs.existsSync(platformPath)) {
86
+ + if (verbose) {
87
+ + console.log(`📁 Found platform-specific file: ${platformPath}`);
88
+ + }
89
+ + return platformPath;
90
+ }
91
+ - return platformPath;
92
+ }
93
+ const basePath = `${fullPath}${ext}`;
94
+ if (fs.existsSync(basePath)) {
95
+ diff --git a/hmr/server/device-transform-helpers.js b/hmr/server/device-transform-helpers.js
96
+ index 9364ce9942b84fffd0e3a6eb6700d8253fa66e7e..0ae5a89f0359afc3c700b43b2c80b176536100a6 100644
97
+ --- a/hmr/server/device-transform-helpers.js
98
+ +++ b/hmr/server/device-transform-helpers.js
99
+ @@ -89,7 +89,11 @@ function bareSpecifierFromFlatDepPath(depPath) {
100
+ return bareSpecifier;
47
101
  }
48
- -function resolveSpecToFile(spec, projectRoot, workspaceRoot, platform) {
102
+ function rewriteVitePrebundleImportsForDevice(code, preserveVendorImports) {
103
+ - const imports = collectTopLevelImportRecords(code);
104
+ + // Re-export statements (`export { x } from`/`export * from`) carry specifiers
105
+ + // too — vite's importAnalysis rewrites them to `.vite/deps?v=` URLs the same
106
+ + // as `import` declarations, so they need the same decode. Barrel modules
107
+ + // (e.g. a linked framework package's index) are nothing but re-exports.
108
+ + const imports = collectTopLevelImportRecords(code, true);
109
+ if (!imports.length) {
110
+ return code;
111
+ }
112
+ diff --git a/hmr/server/rewrite-imports.js b/hmr/server/rewrite-imports.js
113
+ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..dc70055007e26117b76fbb9d2f4f17fabfd8db00 100644
114
+ --- a/hmr/server/rewrite-imports.js
115
+ +++ b/hmr/server/rewrite-imports.js
116
+ @@ -253,11 +253,29 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
117
+ }
118
+ return `${prefix}${rewritten}${suffix}`;
119
+ }
120
+ - // Path resolves outside both roots — leave Vite's URL alone as a
121
+ - // last resort. The original behaviour was to fall through here
122
+ - // and let downstream branches (e.g. `normalizeNodeModulesSpecifier`)
123
+ - // handle paths whose abs form happens to contain `/node_modules/`,
124
+ - // so preserve that for the unrewritable case below.
125
+ + // Path resolves outside both roots — the `link:`/`workspace:` case,
126
+ + // where a package's realpath lives in another checkout entirely.
127
+ + // A raw /@fs URL makes the device fetch vite's standard middleware
128
+ + // response, bypassing the device transform: optimizer
129
+ + // `.vite/deps?v=` specifiers inside reach the runtime unrewritten
130
+ + // and 504 once the optimizer re-commits its browserHash. The /ns/m
131
+ + // route already resolves an absolute `/@fs` spec through the full
132
+ + // device pipeline, so route it there. Paths whose realpath still
133
+ + // contains /node_modules/ keep falling through to the bare-package
134
+ + // branch below (upstream behavior).
135
+ + if (!spec.includes('/node_modules/')) {
136
+ + const out = `/ns/m${spec}`;
137
+ + if (httpOriginSafe) {
138
+ + return `${prefix}${httpOriginSafe}${out}${suffix}`;
139
+ + }
140
+ + return `${prefix}${out}${suffix}`;
141
+ + }
142
+ + // Path resolves outside both roots but still names a node_modules
143
+ + // file — leave Vite's URL alone as a last resort. The original
144
+ + // behaviour was to fall through here and let downstream branches
145
+ + // (e.g. `normalizeNodeModulesSpecifier`) handle paths whose abs
146
+ + // form happens to contain `/node_modules/`, so preserve that for
147
+ + // the unrewritable case below.
148
+ }
149
+ // Vite 8 oxc runtime helpers (decorators etc.) — emitted as
150
+ // `/@id/__x00__@oxc-project+runtime@<ver>/helpers/<name>.js`. These are
151
+ diff --git a/hmr/server/websocket-core-bridge.js b/hmr/server/websocket-core-bridge.js
152
+ index 444ea4474645a10ce4f9ca3b81d96ec9c017c0cb..caa3724caeeb04b4d69d04a15a5899f835fec5ff 100644
153
+ --- a/hmr/server/websocket-core-bridge.js
154
+ +++ b/hmr/server/websocket-core-bridge.js
155
+ @@ -8,7 +8,76 @@ import { getGlobalScope } from '../shared/runtime/global-scope.js';
156
+ * an ASCII filter silently drops every such export from generated shims.
157
+ */
158
+ export const JS_IDENTIFIER_RE = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u;
49
159
  +/**
50
- + * `/node_modules/<pkg>/<sub...>` → the exports-mapped file on disk
51
- + * (`rouzer/http` → `rouzer/dist/http.js`). Without this, non-root specs fall
52
- + * back to `resolveId` and land on Vite's dep-optimizer artifacts under
53
- + * `.vite/deps/` — flat bundles whose generated shared chunks
54
- + * (`foo-<hash>.js`) this pipeline cannot serve to the device.
160
+ + * Blank out `//` and `/* ... *\/` comment bodies (positions and length
161
+ + * preserved — every matched index stays valid). Export-name scanners are
162
+ + * regex-based, so a commented-out declaration like ui-image's
163
+ + * `// export const roundRadiusProperty = ...` would otherwise register a
164
+ + * phantom export and the emitted explicit re-export list then fails to link
165
+ + * on device ("does not provide an export named ...").
166
+ + *
167
+ + * Strings and template literals are skipped so `//` inside quotes isn't
168
+ + * treated as a comment start. Regex literals are intentionally not tracked —
169
+ + * an unescaped `//` inside one (e.g. `[//]` char classes) still masks the
170
+ + * rest of the line; that is rare enough to accept in a dev-server path.
55
171
  + */
56
- +function resolvePackageSubpathFromSpecRoots(spec, roots, platform) {
57
- + const pkgSub = spec.replace(/^\/node_modules\//, '');
58
- + const parts = pkgSub.split('/');
59
- + const nameSegs = parts[0].startsWith('@') ? 2 : 1;
60
- + if (parts.length <= nameSegs)
61
- + return null;
62
- + const subpath = parts.slice(nameSegs).join('/');
63
- + for (const root of roots) {
64
- + const pkgDir = path.join(root, 'node_modules', ...parts.slice(0, nameSegs));
65
- + if (!existsSync(path.join(pkgDir, 'package.json')))
66
- + continue;
67
- + const resolved = resolvePackageSubpathEntry(pkgDir, subpath, platform);
68
- + if (resolved)
69
- + return resolved;
172
+ +export function maskJsComments(code) {
173
+ + if (!code || (code.indexOf('//') === -1 && code.indexOf('/*') === -1)) {
174
+ + return code;
175
+ + }
176
+ + const chars = code.split('');
177
+ + const n = code.length;
178
+ + let i = 0;
179
+ + while (i < n) {
180
+ + const c = code[i];
181
+ + const next = code[i + 1];
182
+ + if (c === '/' && next === '/') {
183
+ + chars[i] = ' ';
184
+ + chars[i + 1] = ' ';
185
+ + i += 2;
186
+ + while (i < n && code[i] !== '\n') {
187
+ + chars[i] = ' ';
188
+ + i++;
189
+ + }
190
+ + }
191
+ + else if (c === '/' && next === '*') {
192
+ + chars[i] = ' ';
193
+ + chars[i + 1] = ' ';
194
+ + i += 2;
195
+ + while (i < n && !(code[i] === '*' && code[i + 1] === '/')) {
196
+ + if (code[i] !== '\n')
197
+ + chars[i] = ' ';
198
+ + i++;
199
+ + }
200
+ + if (i < n) {
201
+ + chars[i] = ' ';
202
+ + chars[i + 1] = ' ';
203
+ + i += 2;
204
+ + }
205
+ + }
206
+ + else if (c === "'" || c === '"' || c === '`') {
207
+ + const quote = c;
208
+ + i++;
209
+ + while (i < n) {
210
+ + if (code[i] === '\\') {
211
+ + i += 2;
212
+ + continue;
213
+ + }
214
+ + if (code[i] === quote) {
215
+ + i++;
216
+ + break;
217
+ + }
218
+ + i++;
219
+ + }
220
+ + }
221
+ + else {
222
+ + i++;
223
+ + }
70
224
  + }
71
- + return null;
225
+ + return chars.join('');
72
226
  +}
73
- +export function resolveSpecToFile(spec, projectRoot, workspaceRoot, platform) {
74
- const roots = [projectRoot, ...(workspaceRoot && path.resolve(workspaceRoot) !== path.resolve(projectRoot) ? [workspaceRoot] : [])];
75
- + // `.vite/` is Vite's dep-optimizer store — generated artifacts with
76
- + // content-hashed names, not package files. Resolving them here would mint
77
- + // deps-bundle entries/shims for files whose names churn per optimization.
78
- + if (spec.includes('/.vite/'))
79
- + return null;
80
- // Package roots honor the declared entry (exports/module/main) FIRST —
81
- // matching esbuild's resolution of bare imports inside the bundle. The
82
- // index fan-out would otherwise pick stray root files like a shipped
83
- @@ -236,6 +265,12 @@ function resolveSpecToFile(spec, projectRoot, workspaceRoot, platform) {
84
- if (abs)
85
- return abs;
86
- }
87
- + // Non-root specs (`rouzer/http`, `qubu/sqlite`): consult the package's
88
- + // exports map so the real dist file wins — the alternative downstream is
89
- + // the dep optimizer's `.vite/deps` artifact, whose sibling chunks 404.
90
- + const subpathEntry = resolvePackageSubpathFromSpecRoots(spec, roots, platform);
91
- + if (subpathEntry)
92
- + return subpathEntry;
93
- return isPackageRootSpec(spec) ? null : resolvePackageRootEntry(spec, roots, platform);
94
- }
95
- // Never bundled: `@nativescript/core` belongs to the /ns/core bridge realm,
96
- @@ -869,7 +904,15 @@ export function collectDepsModuleExportInfo(absPath, platform, seen = new Set())
97
- names.delete('default');
98
- return { names: Array.from(names).sort(), hasDefault: hasExistingDefaultExport(code) };
99
- }
100
- -const RESERVED_EXPORT_NAMES = new Set(['default', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'let', 'static', 'yield', 'await', 'eval', 'arguments', '__ns_dep_ns__']);
101
- +// `default` is emitted separately (below); `__ns_dep_ns__` is this shim's own
102
- +// local — a package exporting that name can't collide with it.
103
- +const NON_DECLARABLE_EXPORT_NAMES = new Set(['default', '__ns_dep_ns__']);
104
- +// Reserved words can't be declaration bindings (`export const enum` is a
105
- +// syntax error), but they ARE legal export names — the specifier position in
106
- +// `export { x as name }` accepts any IdentifierName. Emit those through a
107
- +// temp + alias so packages like zod (`enum`, `catch`, `function`, …) keep
108
- +// their full surface on the bridged namespace.
109
- +const RESERVED_EXPORT_NAMES = new Set(['break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'let', 'static', 'yield', 'await', 'eval', 'arguments']);
110
- /**
111
- * Shim served at `/ns/m/node_modules/...` for a bundled module. The static
112
- * bundle import guarantees the registry is populated before the body reads it
113
- @@ -880,13 +923,20 @@ const RESERVED_EXPORT_NAMES = new Set(['default', 'break', 'case', 'catch', 'cla
114
- export function buildDepsShimCode(key, exportNames, hasOwnDefaultExport) {
115
- // JS_IDENTIFIER_RE (not ASCII \w): Angular exports `ɵɵdefineInjectable`-style
116
- // names — dropping them here breaks every `import * as i0` consumer.
117
- - const names = Array.from(new Set(exportNames)).filter((n) => JS_IDENTIFIER_RE.test(n) && !RESERVED_EXPORT_NAMES.has(n));
118
- + const names = Array.from(new Set(exportNames)).filter((n) => JS_IDENTIFIER_RE.test(n) && !NON_DECLARABLE_EXPORT_NAMES.has(n));
119
- const lines = [];
120
- lines.push(`/* ns-deps bridge (bundle mode) — ${key} */`);
121
- lines.push(`import ${JSON.stringify(DEPS_BUNDLE_PATH)};`);
122
- lines.push(`const __ns_dep_ns__ = (globalThis.__NS_DEPS_MODULES__ || {})[${JSON.stringify(key)}];`);
123
- lines.push(`if (!__ns_dep_ns__) { throw new Error(${JSON.stringify(`[ns-deps] deps bundle registry missing '${key}' — restart the dev server or set NS_DEPS_PER_MODULE=1`)}); }`);
124
- + let aliasIdx = 0;
125
- for (const name of names) {
126
- + if (RESERVED_EXPORT_NAMES.has(name)) {
127
- + const alias = `__ns_dep_alias_${aliasIdx++}`;
128
- + lines.push(`const ${alias} = __ns_dep_ns__[${JSON.stringify(name)}];`);
129
- + lines.push(`export { ${alias} as ${name} };`);
227
+ export function extractDirectExportedNames(code) {
228
+ + code = maskJsComments(code);
229
+ const names = new Set();
230
+ const declRe = /\bexport\s+(?:async\s+)?(?:function|class)\s+([$_\p{ID_Start}][$\p{ID_Continue}]*)/gu;
231
+ let match;
232
+ diff --git a/hmr/server/websocket-module-bindings.js b/hmr/server/websocket-module-bindings.js
233
+ index 54b8fba2def7ed0128e8e1dd4b2549d25559de82..fd276ae860d0805533be0bff4399247b38ebac01 100644
234
+ --- a/hmr/server/websocket-module-bindings.js
235
+ +++ b/hmr/server/websocket-module-bindings.js
236
+ @@ -7,6 +7,7 @@ import { getProjectRootPath } from '../../helpers/project.js';
237
+ import { extractRootPackageName } from '../shared/package-classifier.js';
238
+ import { isEsmFrameworkPackageSpecifier, isLikelyNativeScriptPluginSpecifier, isLikelyNativeScriptRuntimePluginSpecifier, normalizeNodeModulesSpecifier, resolveInternalRuntimePluginBareSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, shouldPreserveBareRuntimePluginSubpathImport } from './websocket-module-specifiers.js';
239
+ import { MODULE_IMPORT_ANALYSIS_PLUGINS } from './websocket-served-module-helpers.js';
240
+ +import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
241
+ const sourceImportIntentCache = new Map();
242
+ const EXPLICIT_RUNTIME_PLUGIN_SCRIPT_EXT_RE = /(?:\.(?:ios|android|visionos))?\.(?:ts|tsx|js|jsx|mjs|mts|cts)$/i;
243
+ function hasExplicitRuntimePluginScriptExtension(segment) {
244
+ @@ -109,6 +110,9 @@ export function ensureNativeScriptModuleBindings(code, options) {
245
+ }
246
+ const runtimePluginBareSpecifier = resolveInternalRuntimePluginBareSpecifier(specifier, getProjectRootPath());
247
+ let canonical = runtimePluginBareSpecifier || resolveVendorFromCandidate(specifier);
248
+ + if (canonical && getVendorManifest() && !resolveVendorSpecifier(canonical)) {
249
+ + canonical = null;
250
+ + }
251
+ const runtimePluginSpecifier = isLikelyNativeScriptRuntimePluginSpecifier(canonical || specifier, getProjectRootPath());
252
+ // Preserving a bare vendor id is only correct where the IMPORT MAP
253
+ // exists to route it (main realm). Worker realms have no import map —
254
+ @@ -120,7 +124,7 @@ export function ensureNativeScriptModuleBindings(code, options) {
255
+ preservedImports.push(original);
256
+ continue;
257
+ }
258
+ - if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier, getProjectRootPath())) {
259
+ + if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier, getProjectRootPath()) && (!getVendorManifest() || resolveVendorSpecifier(specifier))) {
260
+ canonical = specifier;
261
+ }
262
+ if (canonical && /^@nativescript\/core(\b|\/)/i.test(canonical)) {
263
+ @@ -163,6 +167,9 @@ export function ensureNativeScriptModuleBindings(code, options) {
264
+ }
265
+ const runtimePluginBareSpecifier = resolveInternalRuntimePluginBareSpecifier(specifier, getProjectRootPath());
266
+ let canonical = runtimePluginBareSpecifier || resolveVendorFromCandidate(specifier);
267
+ + if (canonical && getVendorManifest() && !resolveVendorSpecifier(canonical)) {
268
+ + canonical = null;
269
+ + }
270
+ const runtimePluginSpecifier = isLikelyNativeScriptRuntimePluginSpecifier(canonical || specifier, getProjectRootPath());
271
+ // See the side-effect branch above: bare preservation relies on the main
272
+ // realm's import map; in a worker serve (vendorImportsAsHttp) it must not
273
+ @@ -171,7 +178,7 @@ export function ensureNativeScriptModuleBindings(code, options) {
274
+ preservedImports.push(original);
275
+ continue;
276
+ }
277
+ - if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier, getProjectRootPath())) {
278
+ + if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier, getProjectRootPath()) && (!getVendorManifest() || resolveVendorSpecifier(specifier))) {
279
+ canonical = specifier;
280
+ }
281
+ if (canonical && /^@nativescript\/core(\b|\/)/i.test(canonical)) {
282
+ diff --git a/hmr/server/websocket-module-specifiers.js b/hmr/server/websocket-module-specifiers.js
283
+ index 2f1dd5d5d688076c5fc0ec8c5c888f9e834e7831..68cdd051144c1d69d8d20271558c8f8e79532d73 100644
284
+ --- a/hmr/server/websocket-module-specifiers.js
285
+ +++ b/hmr/server/websocket-module-specifiers.js
286
+ @@ -1,4 +1,4 @@
287
+ -import { existsSync, readFileSync, realpathSync } from 'fs';
288
+ +import { existsSync, readdirSync, readFileSync, realpathSync } from 'fs';
289
+ import * as path from 'path';
290
+ import * as PAT from './constants.js';
291
+ import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
292
+ @@ -331,6 +331,68 @@ export function resolveVendorFromCandidate(specifier) {
293
+ * request to the dev server's bridge fails with a "transform miss"
294
+ * 404 even though the file is right there at the workspace root.
295
+ */
296
+ +const workspacePackageNodeModulesDirsCache = new Map();
297
+ +/**
298
+ + * Enumerate `node_modules` directories that belong to workspace packages
299
+ + * (e.g. `<root>/packages/<pkg>/node_modules`, `<root>/apps/<app>/node_modules`)
300
+ + * plus the workspace root's own. pnpm's isolated linker gives each workspace
301
+ + * package a private `node_modules` — a dependency declared by a workspace
302
+ + * package (say a NativeScript plugin inside a library) is invisible from the
303
+ + * app root, so `/ns/m/node_modules/<pkg>/...` requests for transitive deps 404
304
+ + * without this fallback.
305
+ + *
306
+ + * Bounded: direct children of the root plus one level down
307
+ + * (`<root>/<group>/<pkg>/node_modules`) covers standard workspace layouts
308
+ + * without walking arbitrary depth. Cached for the dev-server lifetime.
309
+ + */
310
+ +function enumerateWorkspacePackageNodeModulesDirs(workspaceRoot) {
311
+ + const resolved = path.resolve(workspaceRoot);
312
+ + const cached = workspacePackageNodeModulesDirsCache.get(resolved);
313
+ + if (cached) {
314
+ + return cached;
315
+ + }
316
+ + const dirs = [];
317
+ + const pushIfPresent = (dir) => {
318
+ + const nm = path.join(dir, 'node_modules');
319
+ + try {
320
+ + if (existsSync(nm)) {
321
+ + dirs.push(nm);
322
+ + }
323
+ + }
324
+ + catch {
325
+ + // unreadable dirs are skipped — this is a best-effort fallback
326
+ + }
327
+ + };
328
+ + pushIfPresent(resolved);
329
+ + let groups = [];
330
+ + try {
331
+ + groups = readdirSync(resolved, { withFileTypes: true });
332
+ + }
333
+ + catch {
334
+ + groups = [];
335
+ + }
336
+ + for (const group of groups) {
337
+ + if (!group.isDirectory() || group.name === 'node_modules' || group.name.startsWith('.')) {
130
338
  + continue;
131
339
  + }
132
- lines.push(`export const ${name} = __ns_dep_ns__.${name};`);
133
- }
134
- if (hasOwnDefaultExport) {
135
- diff --git a/hmr/server/device-transform-helpers.js b/hmr/server/device-transform-helpers.js
136
- index 9364ce9942b84fffd0e3a6eb6700d8253fa66e7e..93ee2bf2570303277d7b4350018b00fbbb31e2a4 100644
137
- --- a/hmr/server/device-transform-helpers.js
138
- +++ b/hmr/server/device-transform-helpers.js
139
- @@ -81,12 +81,18 @@ function bareSpecifierFromFlatDepPath(depPath) {
340
+ + const groupDir = path.join(resolved, group.name);
341
+ + pushIfPresent(groupDir);
342
+ + let packages = [];
343
+ + try {
344
+ + packages = readdirSync(groupDir, { withFileTypes: true });
345
+ + }
346
+ + catch {
347
+ + packages = [];
348
+ + }
349
+ + for (const pkg of packages) {
350
+ + if (pkg.isDirectory() && !pkg.name.startsWith('.')) {
351
+ + pushIfPresent(path.join(groupDir, pkg.name));
352
+ + }
353
+ + }
354
+ + }
355
+ + workspacePackageNodeModulesDirsCache.set(resolved, dirs);
356
+ + return dirs;
357
+ +}
358
+ export function resolveCandidateFilePath(candidate, projectRoot, workspaceRoot) {
359
+ const cleaned = candidate.replace(PAT.QUERY_PATTERN, '');
360
+ if (!cleaned)
361
+ @@ -363,7 +425,24 @@ export function resolveCandidateFilePath(candidate, projectRoot, workspaceRoot)
362
+ const resolvedProject = path.resolve(projectRoot);
363
+ const resolvedWorkspace = path.resolve(workspaceRoot);
364
+ if (resolvedWorkspace !== resolvedProject) {
365
+ - return tryUnderRoot(resolvedWorkspace);
366
+ + const fromWorkspace = tryUnderRoot(resolvedWorkspace);
367
+ + if (fromWorkspace) {
368
+ + return fromWorkspace;
369
+ + }
370
+ + }
371
+ + // Transitive-dep fallback: the `/ns/m/node_modules/<pkg>/<sub>` URL
372
+ + // form drops the importer, so a package that only exists under a
373
+ + // workspace package's private node_modules (pnpm isolated linker)
374
+ + // can't be anchored to a root. Probe every workspace package's
375
+ + // node_modules for `<pkg>/<sub>` and serve the first hit.
376
+ + if (cleaned.includes('/node_modules/')) {
377
+ + const afterNodeModules = cleaned.slice(cleaned.lastIndexOf('/node_modules/') + '/node_modules/'.length);
378
+ + for (const nmDir of enumerateWorkspacePackageNodeModulesDirs(resolvedWorkspace)) {
379
+ + const absPath = path.join(nmDir, afterNodeModules);
380
+ + if (existsSync(absPath)) {
381
+ + return absPath;
382
+ + }
383
+ + }
140
384
  }
141
- catch { }
142
385
  }
143
- - const bareSpecifier = resolved || candidates[0];
144
- + // Unverified decode → NOT a package: `depPath` is a generated optimizer
145
- + // artifact (a hash-named shared chunk like `http-Dm_BQhab.js`, whose
146
- + // base64url '_' decodes to a bogus '/'). Returning null lets the caller
147
- + // keep the real `.vite/deps` URL instead of emitting a garbage specifier.
148
- + if (!resolved) {
149
- + return null;
150
- + }
151
- if (!warnedVendorMisses.has(depPath)) {
152
- warnedVendorMisses.add(depPath);
153
- - console.warn(`[ns-vite] vendor-manifest miss: prebundled dep '${depPath}' is not in the vendor bundle — serving '${bareSpecifier}' over HTTP via /ns/m/node_modules/${resolved ? '' : ' (could not verify the package on disk; the specifier is a best-effort decode of the flattened id)'}.`);
154
- + console.warn(`[ns-vite] vendor-manifest miss: prebundled dep '${depPath}' is not in the vendor bundle — serving '${resolved}' over HTTP via /ns/m/node_modules/${resolved}.`);
386
+ return null;
387
+ @@ -794,16 +873,24 @@ export function resolveVendorRouting(nodeModulesSpec, projectRoot) {
388
+ if (rootPackageName && pkgName !== rootPackageName) {
389
+ return { route: 'http' };
155
390
  }
156
- - return bareSpecifier;
157
- + return resolved;
158
- }
159
- function rewriteVitePrebundleImportsForDevice(code, preserveVendorImports) {
160
- const imports = collectTopLevelImportRecords(code);
161
- @@ -111,6 +117,15 @@ function rewriteVitePrebundleImportsForDevice(code, preserveVendorImports) {
162
- if (bareSpecifier) {
163
- replacement = imp.text.replace(source, bareSpecifier);
164
- }
165
- + else {
166
- + // Generated optimizer chunk (hash-named sibling of an
167
- + // optimized entry) — not a package and not decodable to one.
168
- + // Route the real artifact URL back through the bridge: the
169
- + // /ns/m route serves it from the dep-optimizer store (the
170
- + // `?v` stamp rides along as forcedVer) or emits an
171
- + // actionable optimizeDeps.exclude error.
172
- + replacement = imp.text.replace(source, `/ns/m/node_modules/.vite/deps/${depPath}`);
173
- + }
391
+ - if (isLikelyNativeScriptRuntimePluginSpecifier(pkgName, projectRoot) && (!subpath || isRootLevelMainEntry)) {
392
+ + // A plugin-patterned package absent from an active vendor manifest (a
393
+ + // transitive dep under pnpm's isolated linker, an NS_VENDOR_EXCLUDE/
394
+ + // flavor-excluded package) must not take the vendor route — the registry
395
+ + // has no entry and the sync require resolves to the missing-module stub.
396
+ + // Fall through to null so callers emit the per-module /ns/m HTTP form,
397
+ + // which resolves transitive deps through Vite's importer-aware resolution.
398
+ + // A null manifest (early boot) keeps the legacy behavior.
399
+ + const manifest = getVendorManifest();
400
+ + const pluginVendored = !manifest || !!manifest.modules?.[pkgName];
401
+ + if (isLikelyNativeScriptRuntimePluginSpecifier(pkgName, projectRoot) && pluginVendored && (!subpath || isRootLevelMainEntry)) {
402
+ return { route: 'vendor', bareSpec: pkgName };
403
+ }
404
+ - if (isLikelyNativeScriptRuntimePluginSpecifier(pkgName, projectRoot) && subpath.includes('/')) {
405
+ + if (isLikelyNativeScriptRuntimePluginSpecifier(pkgName, projectRoot) && pluginVendored && subpath.includes('/')) {
406
+ const exactBareSpecifier = resolveInternalRuntimePluginBareSpecifier(nodeModulesSpec, projectRoot);
407
+ if (exactBareSpecifier) {
408
+ return { route: 'vendor', bareSpec: exactBareSpecifier };
174
409
  }
175
- edits.push({
176
- start: imp.start,
410
+ }
411
+ - const manifest = getVendorManifest();
412
+ if (!manifest?.modules?.[pkgName]) {
413
+ return null;
414
+ }
177
415
  diff --git a/hmr/server/websocket-ns-m-request.js b/hmr/server/websocket-ns-m-request.js
178
- index b54aefcc839e95906d76d02e522c052a706ca76f..d49159de94ba05dcfc372aa7a3c513e0f5c0a0d3 100644
416
+ index b54aefcc839e95906d76d02e522c052a706ca76f..a9c38c32b8752765044cd8f2c1eac558a30052dd 100644
179
417
  --- a/hmr/server/websocket-ns-m-request.js
180
418
  +++ b/hmr/server/websocket-ns-m-request.js
181
- @@ -1,6 +1,9 @@
419
+ @@ -1,6 +1,41 @@
182
420
  import * as path from 'path';
183
- +import { realpathSync } from 'fs';
184
421
  import { filterExistingNodeModulesTransformCandidates, getBlockedDeviceNodeModulesReason, resolveCandidateFilePath, tryReadRawExplicitJavaScriptModule } from './websocket-module-specifiers.js';
185
422
  import { collapseLegacyNsMTags } from './websocket-ns-m-paths.js';
186
- +import { resolveSpecToFile } from './deps-bundle.js';
187
423
  +import { resolvePlatform } from '../../helpers/cli-flags.js';
188
- function normalizeAbsoluteFilesystemSpec(spec, serverRoot) {
189
- const toPosix = (value) => value.replace(/\\/g, '/');
190
- const rootPosix = toPosix(serverRoot);
191
- @@ -93,6 +96,29 @@ export function createNsMRequestContext(requestUrl, serverRoot, appVirtualWithSl
192
- return { kind: 'next' };
193
- }
194
- }
424
+ +let cachedPlatformSuffixes = undefined;
195
425
  +/**
196
- + * Canonical request URL for a resolved node_modules file. Project-root files
197
- + * keep the `/node_modules/...` form so module identity matches the /ns/m
198
- + * spec space; files outside the project root (hoisted/workspace) use the
199
- + * `/@fs` form Vite can load directly. Symlinked workspace-source packages
200
- + * resolve to their realpath so the TS transform applies — same policy as
201
- + * filterExistingNodeModulesTransformCandidates.
426
+ + * Platform-suffixed resolution tags (`ios`, `android`, `visionos`, plus the
427
+ + * shared `native` tag used by split-entry packages). Packages shipped with
428
+ + * platform-split entries — e.g. `@nativescript-community/*` plugins carrying
429
+ + * `index.ios.js`/`index.android.js` and a `main` like `./index` — 404 on the
430
+ + * device bridge when only unsuffixed candidates are probed. Candidates are
431
+ + * existence-filtered later, so probing every suffix is safe; the active
432
+ + * platform's variants go first so a package shipping several platforms picks
433
+ + * the right one.
202
434
  + */
203
- +function nodeModulesFileToRequestUrl(absPath, serverRoot) {
204
- + try {
205
- + const real = realpathSync(absPath);
206
- + if (real !== absPath && !real.split(path.sep).includes('node_modules')) {
207
- + return `/@fs${real.replace(/\\/g, '/')}`;
208
- + }
209
- + }
210
- + catch { }
211
- + const rootPosix = path.resolve(serverRoot).replace(/\\/g, '/').replace(/\/+$/, '');
212
- + const absPosix = absPath.replace(/\\/g, '/');
213
- + if (absPosix.startsWith(`${rootPosix}/`)) {
214
- + return absPosix.slice(rootPosix.length);
435
+ +function platformSuffixes() {
436
+ + if (cachedPlatformSuffixes === undefined) {
437
+ + const platform = resolvePlatform();
438
+ + const list = [platform, 'ios', 'android', 'visionos', 'native'].filter(Boolean);
439
+ + cachedPlatformSuffixes = [...new Set(list)];
215
440
  + }
216
- + return `/@fs${absPosix}`;
441
+ + return cachedPlatformSuffixes;
217
442
  +}
218
- async function tryTransformRequest(transformRequest, url, timeoutMs) {
219
- try {
220
- return await transformRequest(url, timeoutMs);
221
- @@ -146,6 +172,38 @@ export async function resolveNsMTransformedModule(options) {
222
- return { transformed, resolvedCandidate };
223
- }
224
- }
225
- + // Optimizer-artifact requests (`/node_modules/.vite/deps/<file>`): flat
226
- + // entries and hash-named shared chunks live in the dep-optimizer's store
227
- + // (often memory-only), keyed by the `?v=<browserHash>` stamp Vite emits —
228
- + // captured inbound as `forcedVer`. Replay it so Vite resolves the artifact
229
- + // as-is; a miss falls through to the route's transform-miss branch, which
230
- + // names optimizeDeps.exclude as the fix.
231
- + if (spec.includes('/.vite/deps/')) {
232
- + const depUrl = context.forcedVer ? `${spec}?v=${context.forcedVer}` : spec;
233
- + transformed = await tryTransformRequest(transformRequest, depUrl, timeoutMs);
234
- + if (transformed?.code) {
235
- + return { transformed, resolvedCandidate: depUrl };
236
- + }
237
- + return { transformed: null, resolvedCandidate };
238
- + }
239
- + // Bare or subpath node_modules specs (`rouzer/http`, `qubu/sqlite`) that
240
- + // survive the on-disk candidate fan-out would otherwise hit `resolveId`
241
- + // and land on Vite's dep-optimizer output: `/node_modules/.vite/deps/
242
- + // <flat>.js` bundles whose generated sibling chunks (`foo-<hash>.js`)
243
- + // this pipeline cannot serve — and whose flattened ids decode to bogus
244
- + // bare specifiers downstream (`http-Dm/BQhab`). Prefer the real package
245
- + // file (exports-map aware) so the device gets the same per-module source
246
- + // an `optimizeDeps.exclude` entry produces.
247
- + if (spec.includes('/node_modules/')) {
248
- + const realFile = resolveSpecToFile(spec.slice(spec.lastIndexOf('/node_modules/')), serverRoot, workspaceRoot, resolvePlatform() || 'ios');
249
- + const realUrl = realFile ? nodeModulesFileToRequestUrl(realFile, serverRoot) : null;
250
- + if (realUrl) {
251
- + transformed = await tryTransformRequest(transformRequest, realUrl, timeoutMs);
252
- + if (transformed?.code) {
253
- + return { transformed, resolvedCandidate: realUrl };
443
+ +const CANDIDATE_EXTS = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'mts', 'cts', 'vue'];
444
+ +function expandModuleCandidates(spec, hasExt, baseNoExt) {
445
+ + const candidates = [...(hasExt ? [spec] : [])];
446
+ + for (const base of [baseNoExt, `${baseNoExt}/index`]) {
447
+ + for (const suffix of platformSuffixes()) {
448
+ + for (const ext of CANDIDATE_EXTS) {
449
+ + candidates.push(`${base}.${suffix}.${ext}`);
254
450
  + }
255
451
  + }
452
+ + for (const ext of CANDIDATE_EXTS) {
453
+ + candidates.push(`${base}.${ext}`);
454
+ + }
256
455
  + }
257
- const resolvedId = await resolveId(spec);
258
- if (resolvedId) {
259
- transformed = await tryTransformRequest(transformRequest, resolvedId, timeoutMs);
456
+ + return candidates;
457
+ +}
458
+ function normalizeAbsoluteFilesystemSpec(spec, serverRoot) {
459
+ const toPosix = (value) => value.replace(/\\/g, '/');
460
+ const rootPosix = toPosix(serverRoot);
461
+ @@ -49,6 +84,14 @@ export function createNsMRequestContext(requestUrl, serverRoot, appVirtualWithSl
462
+ };
463
+ }
464
+ spec = spec.replace(/[?#].*$/, '');
465
+ + // urlObj.pathname keeps percent-encoding — device fetches encode
466
+ + // characters like `[`/`]` in file-based route names, so decode the
467
+ + // spec before resolving it to a filesystem path. Malformed escapes
468
+ + // keep the raw spelling rather than throwing.
469
+ + try {
470
+ + spec = decodeURIComponent(spec);
471
+ + }
472
+ + catch { }
473
+ const decorated = collapseLegacyNsMTags(spec, 'inbound-request-spec');
474
+ spec = decorated.cleanedSpec;
475
+ bootTaggedRequest = decorated.bootTaggedRequest;
476
+ @@ -73,7 +116,7 @@ export function createNsMRequestContext(requestUrl, serverRoot, appVirtualWithSl
477
+ }
478
+ const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
479
+ const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
480
+ - const candidates = [...(hasExt ? [spec] : []), `${baseNoExt}.ts`, `${baseNoExt}.js`, `${baseNoExt}.tsx`, `${baseNoExt}.jsx`, `${baseNoExt}.mjs`, `${baseNoExt}.mts`, `${baseNoExt}.cts`, `${baseNoExt}.vue`, `${baseNoExt}/index.ts`, `${baseNoExt}/index.js`, `${baseNoExt}/index.tsx`, `${baseNoExt}/index.jsx`, `${baseNoExt}/index.mjs`];
481
+ + const candidates = expandModuleCandidates(spec, hasExt, baseNoExt);
482
+ const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, serverRoot, workspaceRoot);
483
+ return {
484
+ kind: 'context',
260
485
  diff --git a/hmr/server/websocket-ns-m.js b/hmr/server/websocket-ns-m.js
261
- index 8eeb51ab59497e7f775da3117c4207e05f1814c7..346b676fbb67ce6bebe81cc379fdbe0d0aeac876 100644
486
+ index 8eeb51ab59497e7f775da3117c4207e05f1814c7..407c2a2484c843bc538ce6ef9cf7af0b9b883ad3 100644
262
487
  --- a/hmr/server/websocket-ns-m.js
263
488
  +++ b/hmr/server/websocket-ns-m.js
264
- @@ -265,6 +265,30 @@ export function registerNsModuleServerRoute(server, options) {
265
- catch { }
266
- // If transformRequest failed, handle bare-module vendor shims for 'vue' and 'pinia'
267
- if (!transformed?.code) {
268
- + // Dep-optimizer artifacts (`.vite/deps` entries + generated
269
- + // shared chunks) aren't package files — when one reaches the
270
- + // bridge unserved, the fix is keeping that dep out of
271
- + // optimizeDeps. Name the optimized set so the on-device
272
- + // error is actionable instead of a generic transform miss.
273
- + if (spec.includes('/.vite/deps/')) {
274
- + const optimizedIds = [];
275
- + try {
276
- + for (const env of Object.values(server.environments || {})) {
277
- + const meta = env?.depsOptimizer?.metadata;
278
- + for (const record of [meta?.optimized, meta?.discovered]) {
279
- + if (record)
280
- + optimizedIds.push(...Object.keys(record));
281
- + }
489
+ @@ -6,6 +6,7 @@ import { isRuntimeGraphExcludedPath } from './runtime-graph-filter.js';
490
+ import { buildPiniaVendorShim, buildVueVendorShim } from './vendor-bare-module-shims.js';
491
+ import { collapseLegacyNsMTags } from './websocket-ns-m-paths.js';
492
+ import { createNsMRequestContext, resolveNsMTransformedModule } from './websocket-ns-m-request.js';
493
+ +import { resolveCandidateFilePath } from './websocket-module-specifiers.js';
494
+ import { setDeviceModuleHeaders } from './route-helpers.js';
495
+ import { CSS_MODULE_RE, buildCssRegisterSnippetFromVar, normalizeCssForDevice } from './css-device-module.js';
496
+ import { assertNoOptimizedArtifacts, buildBootProgressSnippet, canonicalizeRtImports, dedupeRtNamedImportsAgainstDestructures, deduplicateLinkerImports, ensureDestructureCoreImports, ensureGuardPlainDynamicImports, ensureVariableDynamicImportHelper, expandStarExports, hoistTopLevelStaticImports, MODULE_IMPORT_ANALYSIS_PLUGINS, wrapCommonJsModuleForDevice, ensureWorkerEntryGlobalsImport, classifyServedRequest } from './websocket-served-module-helpers.js';
497
+ @@ -154,7 +155,22 @@ export function registerNsModuleServerRoute(server, options) {
498
+ // string for `import css from './x.css'`. `app.css` has its own path.
499
+ if (CSS_MODULE_RE.test(spec)) {
500
+ try {
501
+ - const inlineRes = await sharedTransformRequest(`${spec}?inline`);
502
+ + // `spec` is root-relative to the app package — workspace
503
+ + // css (e.g. `packages/ui/theme/tokens.css` in a monorepo)
504
+ + // resolves under the workspace root, so a bare `${spec}
505
+ + // ?inline` transform misses there. Anchor it to the real
506
+ + // file via /@fs/ first, like the module pipeline does.
507
+ + let inlineSpec = spec;
508
+ + const cssFsRoots = [serverRoot, ...(monorepoWorkspaceRoot && path.resolve(monorepoWorkspaceRoot) !== path.resolve(serverRoot) ? [monorepoWorkspaceRoot] : [])];
509
+ + for (const root of cssFsRoots) {
510
+ + const fsId = `/@fs${String(root).replace(/\\/g, '/').replace(/\/$/, '')}${spec.startsWith('/') ? '' : '/'}${spec}`;
511
+ + const abs = resolveCandidateFilePath(fsId, serverRoot, monorepoWorkspaceRoot);
512
+ + if (abs) {
513
+ + inlineSpec = `/@fs${abs.replace(/\\/g, '/')}`;
514
+ + break;
282
515
  + }
283
516
  + }
284
- + catch { }
285
- + const hint = optimizedIds.length
286
- + ? ` Currently optimized: ${Array.from(new Set(optimizedIds)).join(', ')} — add the package that pulls in this chunk to optimizeDeps.exclude in the native vite config (or restart with NS_DEPS_PER_MODULE=1).`
287
- + : ' Add the importing package to optimizeDeps.exclude in the native vite config.';
288
- + res.statusCode = 404;
289
- + res.end(`throw new Error(${JSON.stringify(`[ns/m] Vite optimizeDeps artifact '${spec}' is not servable on device — its shared chunks are generated files outside the package tree.${hint}`)});\nexport {};\n`);
290
- + return;
291
- + }
292
- const bare = spec.startsWith('/') ? spec.slice(1) : spec;
293
- const isBare = bare && !bare.includes('/') && !/\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(bare);
294
- if (isBare && (bare === 'vue' || bare === 'nativescript-vue' || bare === 'pinia')) {
517
+ + const inlineRes = await sharedTransformRequest(`${inlineSpec}?inline`);
518
+ const inlineCode = inlineRes?.code || '';
519
+ // `?inline` output is `export default "<compiled css>"`.
520
+ const m = inlineCode.match(/export\s+default\s+("(?:[^"\\]|\\.)*")\s*;?/s);
521
+ diff --git a/hmr/server/websocket-served-module-helpers.js b/hmr/server/websocket-served-module-helpers.js
522
+ index e65b1abaaf223cca4b11b65b8428c85c484f125d..c738f974ccdef53166365cf3f34d45136a4684f6 100644
523
+ --- a/hmr/server/websocket-served-module-helpers.js
524
+ +++ b/hmr/server/websocket-served-module-helpers.js
525
+ @@ -6,7 +6,7 @@ import * as PAT from './constants.js';
526
+ import { isDeepCoreSubpath } from './core-sanitize.js';
527
+ import { getCjsNamedExports } from '../helpers/cjs-named-exports.js';
528
+ import { getMonorepoWorkspaceRoot } from '../../helpers/project.js';
529
+ -import { extractDirectExportedNames, parseExportSpecList } from './websocket-core-bridge.js';
530
+ +import { extractDirectExportedNames, maskJsComments, parseExportSpecList } from './websocket-core-bridge.js';
531
+ import { resolveCandidateFilePath } from './websocket-module-specifiers.js';
532
+ let cachedWorkspaceCoreRoot;
533
+ /**
534
+ @@ -105,8 +105,8 @@ export function classifyServedRequest(spec, resolvedId) {
535
+ return classifyServedModule(spec);
536
+ }
537
+ export const MODULE_IMPORT_ANALYSIS_PLUGINS = ['typescript', 'jsx', 'importMeta', 'topLevelAwait', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'decorators-legacy'];
538
+ -export function collectTopLevelImportRecords(code) {
539
+ - if (!code || typeof code !== 'string' || !/\bimport\b/.test(code)) {
540
+ +export function collectTopLevelImportRecords(code, includeReExports = false) {
541
+ + if (!code || typeof code !== 'string' || !(includeReExports ? /\b(?:import|export)\b/ : /\bimport\b/).test(code)) {
542
+ return [];
543
+ }
544
+ try {
545
+ @@ -119,7 +119,7 @@ export function collectTopLevelImportRecords(code) {
546
+ return [];
547
+ }
548
+ return body
549
+ - .filter((node) => t.isImportDeclaration(node) && typeof node.start === 'number' && typeof node.end === 'number' && typeof node.source?.value === 'string')
550
+ + .filter((node) => (t.isImportDeclaration(node) || (includeReExports && (t.isExportNamedDeclaration(node) || t.isExportAllDeclaration(node)))) && typeof node.start === 'number' && typeof node.end === 'number' && typeof node.source?.value === 'string')
551
+ .map((node) => ({
552
+ start: node.start,
553
+ end: node.end,
554
+ @@ -366,6 +366,10 @@ function normalizeStarExportTargetPath(spec, parentVitePath) {
555
+ // - `extractExportMetadata` (below): does a module have a default export
556
+ // and which named ones — shape-only, for SFC route metadata.
557
+ function scanModuleExportSurface(code) {
558
+ + // Mask comments up front: the unanchored `export * as`/`export {} from`
559
+ + // regexes below would otherwise claim names from commented-out lines and
560
+ + // shadow the same name's real star-export source.
561
+ + code = maskJsComments(code);
562
+ const ownNames = new Set(extractDirectExportedNames(code));
563
+ for (const m of code.matchAll(/\bexport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*["'][^"']+["']/g)) {
564
+ ownNames.add(m[1]);