@octane-xplat/patches 0.16.0 → 0.17.0

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,8 +1,8 @@
1
1
  diff --git a/dist/server/strategy.js b/dist/server/strategy.js
2
- index 7d84b37e2ed3a026da5b31c8a11027e0580877b2..f6594a913ba64837135e994cb183add13c6a0530 100644
2
+ index 7d84b37e2ed3a026da5b31c8a11027e0580877b2..1943f7c05e47d95950212789f84d5b7725da9ac9 100644
3
3
  --- a/dist/server/strategy.js
4
4
  +++ b/dist/server/strategy.js
5
- @@ -14,7 +14,9 @@ import { purgeTransformCachesForHotUpdate, runHotUpdatePrologue, typescriptServe
5
+ @@ -14,17 +14,26 @@ import { purgeTransformCachesForHotUpdate, runHotUpdatePrologue, typescriptServe
6
6
  * otherwise the re-fetch can be served from the previous save's transform and
7
7
  * the screen ends up one save behind.
8
8
  */
@@ -13,7 +13,16 @@ index 7d84b37e2ed3a026da5b31c8a11027e0580877b2..f6594a913ba64837135e994cb183add1
13
13
  export const octaneServerStrategy = {
14
14
  ...typescriptServerStrategy,
15
15
  flavor: 'octane',
16
- @@ -24,7 +26,7 @@ export const octaneServerStrategy = {
16
+ + matchesFile(id) {
17
+ + // Shared sources have workspace/@fs identities rather than /src IDs.
18
+ + // Source-root gating remains in runHotUpdatePrologue; dependency and
19
+ + // type/test files must not enter executable transaction batches.
20
+ + const clean = id.replace(/[?#].*$/, '');
21
+ + return SCRIPT_FILE_RE.test(clean) && !/(?:^|\/)(?:node_modules|__tests__|__mocks__)(?:\/|$)|\.(?:spec|test)\.[^/]+$|\.d\.(?:ts|mts|cts)$/i.test(clean);
22
+ + },
23
+ deferDeltaBroadcast: true,
24
+ async handleHotUpdate(ctx, deps) {
25
+ const state = await runHotUpdatePrologue(ctx, deps);
17
26
  if (!state)
18
27
  return;
19
28
  const { root, metrics, emitSummary } = state;
@@ -22,7 +31,29 @@ index 7d84b37e2ed3a026da5b31c8a11027e0580877b2..f6594a913ba64837135e994cb183add1
22
31
  const { file, server } = ctx;
23
32
  if (!SCRIPT_FILE_RE.test(file)) {
24
33
  emitSummary();
25
- @@ -71,6 +73,16 @@ export const octaneServerStrategy = {
34
+ @@ -37,9 +46,9 @@ export const octaneServerStrategy = {
35
+ .normalize(path.relative(root, file))
36
+ .split(path.sep)
37
+ .join('/');
38
+ - const normalizedId = moduleGraph.normalizeGraphId(rel);
39
+ + const normalizedId = moduleGraph.normalizeGraphId(file);
40
+ if (!moduleGraph.get(normalizedId)) {
41
+ - moduleGraph.upsert(rel, `/* octane-hmr ${Date.now()} */`, [], {
42
+ + moduleGraph.upsert(normalizedId, `/* octane-hmr ${Date.now()} */`, [], {
43
+ emitDeltaOnInsert: true,
44
+ });
45
+ }
46
+ @@ -51,7 +60,8 @@ export const octaneServerStrategy = {
47
+ label: 'octane',
48
+ });
49
+ try {
50
+ - const fresh = await sharedTransformRequest(rel, 30000);
51
+ + const transformId = path.relative(root, file).startsWith('..' + path.sep) ? '/@fs/' + file.replace(/\\/g, '/') : rel;
52
+ + const fresh = await sharedTransformRequest(transformId, 30000);
53
+ if (fresh?.code) {
54
+ const viteModule = server.moduleGraph.getModuleById(file);
55
+ const deps = viteModule
56
+ @@ -71,6 +81,16 @@ export const octaneServerStrategy = {
26
57
  const gm = moduleGraph.get(normalizedId);
27
58
  if (gm)
28
59
  moduleGraph.emitDelta([gm], []);
@@ -1,5 +1,5 @@
1
1
  diff --git a/helpers/project.js b/helpers/project.js
2
- index ef8a029f7e6e66cdeaa344a6af18af28145b8b05..b06cce4f6909d61b538295124ebb1d80a1eb31bf 100644
2
+ index f6777fa5e64e667cbc56c7e76c4c32c5ab76a911..fdc1f974884e26030c0084d75c1e51a1e5968db2 100644
3
3
  --- a/helpers/project.js
4
4
  +++ b/helpers/project.js
5
5
  @@ -73,7 +73,11 @@ function denoConfigHasWorkspace(dir) {
@@ -108,7 +108,7 @@ index a05696b679b3feecd1bafa267de44b59db559162..06648067a3fcf7f8c74437a82b94ec01
108
108
  return ['.native', ''];
109
109
  }
110
110
  diff --git a/hmr/helpers/ast-normalizer.js b/hmr/helpers/ast-normalizer.js
111
- index c6113c74247db616609b87d7f365c4d4c199c934..73651a1fd1892555cc5dd57bcefb9e93d30516b6 100644
111
+ index 77f5533a289902b644f2dba4c8a558f5a00c8314..5c1d57f34a5e863ae3e684012a99f9bb8796e974 100644
112
112
  --- a/hmr/helpers/ast-normalizer.js
113
113
  +++ b/hmr/helpers/ast-normalizer.js
114
114
  @@ -86,9 +86,10 @@ export function astNormalizeModuleImportsAndHelpers(code, opts) {
@@ -145,7 +145,7 @@ index 0d7cd968eac690a85411cf3d6a24323a3d3a3992..98094d6ccfb918d00249d8087aebdd99
145
145
  //# sourceMappingURL=css-device-module.js.map
146
146
 
147
147
  diff --git a/hmr/server/deps-bundle.js b/hmr/server/deps-bundle.js
148
- index 8f2fa15556244f7ef811b5e2ea0fc6a19bab74d3..80137262ec46d347d39a7ee12d66d649b5f909f5 100644
148
+ index 395c29e9b9b552fbf30c4a7131121a0d841df116..ca4d7beaf4ae0444ea947a8c97aa9d7fb1a254ef 100644
149
149
  --- a/hmr/server/deps-bundle.js
150
150
  +++ b/hmr/server/deps-bundle.js
151
151
  @@ -53,6 +53,7 @@
@@ -156,7 +156,7 @@ index 8f2fa15556244f7ef811b5e2ea0fc6a19bab74d3..80137262ec46d347d39a7ee12d66d649
156
156
  import * as esbuild from 'esbuild';
157
157
  import { resolvePlatform } from '../../helpers/cli-flags.js';
158
158
  import { getGlobalDefines, getUserDefineEntries } from '../../helpers/global-defines.js';
159
- @@ -698,6 +699,29 @@ export async function generateDepsBundle(options) {
159
+ @@ -699,6 +700,29 @@ export async function generateDepsBundle(options) {
160
160
  // the device cannot resolve.
161
161
  const buildPlugins = () => [
162
162
  createWebpackLoaderStubEsbuildPlugin(),
@@ -203,16 +203,119 @@ index 71574c48f655183af0c9601b020fd1febd63ccb3..0061d9c9e9fe9c6034576eae8449831d
203
203
  if (!imports.length) {
204
204
  return code;
205
205
  }
206
+ diff --git a/hmr/server/hmr-module-graph.js b/hmr/server/hmr-module-graph.js
207
+ index 8cff593b8ac8d8eff82ebdd398c1305a8408d308..92f6d194924b288864bcaca4fc42ef805d048e4f 100644
208
+ --- a/hmr/server/hmr-module-graph.js
209
+ +++ b/hmr/server/hmr-module-graph.js
210
+ @@ -1,3 +1,4 @@
211
+ +import { normalizeHmrModuleId } from './hmr-module-id.js';
212
+ import { matchesRuntimeGraphModuleId } from './runtime-graph-filter.js';
213
+ import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion } from './websocket-graph-upsert.js';
214
+ import { getProjectAppVirtualPath } from '../../helpers/utils.js';
215
+ @@ -39,25 +40,9 @@ export class HmrModuleGraph {
216
+ return ('00000000' + (h >>> 0).toString(16)).slice(-8);
217
+ }
218
+ normalizeGraphId(raw) {
219
+ - if (!raw)
220
+ - return raw;
221
+ - let id = raw.split('?')[0].replace(/\\/g, '/');
222
+ - id = id.replace(/^file:\/\//, '');
223
+ - const pluginRoot = this.deps.getPluginRoot();
224
+ - if (pluginRoot) {
225
+ - const rootNorm = pluginRoot.replace(/\\/g, '/');
226
+ - if (id.startsWith(rootNorm)) {
227
+ - id = id.slice(rootNorm.length);
228
+ - }
229
+ - }
230
+ - if (!id.startsWith('/'))
231
+ - id = '/' + id;
232
+ - // Collapse nested app root indicators when present (defensive)
233
+ - const idx = id.indexOf(APP_VIRTUAL_WITH_SLASH);
234
+ - if (idx !== -1)
235
+ - id = id.slice(idx);
236
+ - return id;
237
+ + return normalizeHmrModuleId(raw, this.deps.getPluginRoot());
238
+ }
239
+ +
240
+ // Dependency-closed, topologically sorted list of modules for a set of
241
+ // changed ids. Only includes application modules we can serve (under the
242
+ // app virtual root and known framework/.ts/.js entries already in the graph).
243
+ diff --git a/hmr/server/hmr-module-id.js b/hmr/server/hmr-module-id.js
244
+ new file mode 100644
245
+ index 0000000000000000000000000000000000000000..13a0c29d4b0fad502730062ddba081016514c440
246
+ --- /dev/null
247
+ +++ b/hmr/server/hmr-module-id.js
248
+ @@ -0,0 +1,30 @@
249
+ +import { existsSync } from 'node:fs';
250
+ +import * as path from 'node:path';
251
+ +import { getMonorepoWorkspaceRoot } from '../../helpers/project.js';
252
+ +import { rewriteFsAbsoluteToNsM } from './websocket-module-specifiers.js';
253
+ +
254
+ +// Use the same app/workspace/@fs spelling as static device imports. App
255
+ +// virtual IDs remain intact; an unrelated /src/ substring is never a root.
256
+ +export function normalizeHmrModuleId(raw, projectRoot) {
257
+ + if (!raw) return raw;
258
+ + let id = raw.split(/[?#]/)[0].replace(/\\/g, '/');
259
+ + if (id.startsWith('/ns/m/')) return id.slice('/ns/m'.length);
260
+ + const fileUrl = id.startsWith('file://');
261
+ + if (fileUrl) id = id.slice('file://'.length);
262
+ + const fsUrl = id.startsWith('/@fs/');
263
+ + if (fsUrl) id = id.slice('/@fs'.length);
264
+ + const root = projectRoot?.replace(/\\/g, '/').replace(/\/$/, '');
265
+ + const within = (base) => base && (id === base || id.startsWith(base + '/'));
266
+ + // Preserve ordinary app IDs, including Windows drive paths, without
267
+ + // passing a drive-letter source through POSIX /@fs parsing.
268
+ + if (within(root)) return id.slice(root.length) || '/';
269
+ + const workspace = root ? getMonorepoWorkspaceRoot(root) : null;
270
+ + if (root && id.startsWith('/../')) id = path.resolve(root, '.' + id).replace(/\\/g, '/');
271
+ + if (root && (fileUrl || fsUrl || within(root) || within(workspace) || (path.isAbsolute(id) && existsSync(id)))) {
272
+ + const url = rewriteFsAbsoluteToNsM('/@fs' + id, root, workspace);
273
+ + // The module route serves explicit /@fs paths for linked sources
274
+ + // outside both roots, matching rewriteImports' existing fallback.
275
+ + return url ? url.slice('/ns/m'.length) : '/@fs' + id;
276
+ + }
277
+ + return id.startsWith('/') ? id : '/' + id;
278
+ +}
206
279
  diff --git a/hmr/server/process-code-for-device.js b/hmr/server/process-code-for-device.js
207
- index c71d457806a8add919f7e2368e7ebc349bd908e7..998eb268ed0db1200f8b5ce30fc7da211125406f 100644
280
+ index 1f02ba5ebb7e89d40eff58d1c395b5ec838f45fe..5bbb6e4827da640624624ef80f94a7e5eb5c28ce 100644
208
281
  --- a/hmr/server/process-code-for-device.js
209
282
  +++ b/hmr/server/process-code-for-device.js
210
- @@ -1,3 +1,4 @@
283
+ @@ -1,3 +1,5 @@
284
+ +import { normalizeHmrModuleId } from './hmr-module-id.js';
211
285
  +import { CSS_MODULE_RE } from './css-device-module.js';
212
286
  // Device code-transform: cleans Vite/framework noise out of a served module,
213
287
  // injects NativeScript build globals + the process.env shim, normalizes
214
288
  // imports/helpers for the on-device ESM runtime, and collects an importer's
215
- @@ -451,13 +452,13 @@ function processCodeForDevice(code, isVitePreBundled, preserveVendorImports = fa
289
+ @@ -125,25 +127,10 @@ export function inlineNsConfigVirtualImports(code) {
290
+ // `hmr/client/hot-context.ts` so cold-boot evaluation and HMR re-evaluation
291
+ // resolve the same `hot.data` entry.
292
+ export function computeHotContextId(sourceId, projectRoot) {
293
+ - if (!sourceId || typeof sourceId !== 'string')
294
+ - return null;
295
+ - let id = sourceId.split('?')[0].split('#')[0].replace(/\\/g, '/');
296
+ - if (!id)
297
+ - return null;
298
+ - id = id.replace(/^\/@fs/, '');
299
+ - if (projectRoot) {
300
+ - const rootPosix = projectRoot.replace(/\\/g, '/').replace(/\/$/, '');
301
+ - if (rootPosix && id.startsWith(rootPosix + '/')) {
302
+ - id = id.slice(rootPosix.length);
303
+ - }
304
+ - }
305
+ - if (id.startsWith('/ns/m/'))
306
+ - id = id.slice('/ns/m'.length);
307
+ - id = id.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '');
308
+ - if (!id.startsWith('/'))
309
+ - id = '/' + id;
310
+ - return id;
311
+ + if (!sourceId || typeof sourceId !== 'string') return null;
312
+ + return normalizeHmrModuleId(sourceId, projectRoot).replace(/\.(ts|tsx|tsrx|js|jsx|mjs|mts|cts)$/i, '');
313
+ }
314
+ +
315
+ // One-line `import.meta.hot` prelude. Single-statement, no internal `;` or
316
+ // newline, so that if any later sanitize pass re-applies
317
+ // `IMPORT_META_HOT_ASSIGNMENT` the whole statement strips cleanly (module
318
+ @@ -449,13 +436,13 @@ function processCodeForDevice(code, isVitePreBundled, preserveVendorImports = fa
216
319
  // Unlike other virtual ids these are real, servable dev-server URLs returning
217
320
  // self-contained ESM — stripping them leaves `_decorate`/`_decorateMetadata`
218
321
  // undefined at runtime. They're origin-prefixed for the device in rewriteImports.
@@ -229,7 +332,7 @@ index c71d457806a8add919f7e2368e7ebc349bd908e7..998eb268ed0db1200f8b5ce30fc7da21
229
332
  result = `// [hmr-sanitize] stripped virtual /@id/ side-effect imports\n${result}`;
230
333
  }
231
334
  diff --git a/hmr/server/rewrite-imports.js b/hmr/server/rewrite-imports.js
232
- index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..2876cf3694c278249b9db182e2f89df34cc464f3 100644
335
+ index 51ad398cefe75c918e887bd9506d9152259b957e..235b7cd49cbfb18c8cca87edf62e53c4771b304e 100644
233
336
  --- a/hmr/server/rewrite-imports.js
234
337
  +++ b/hmr/server/rewrite-imports.js
235
338
  @@ -1,3 +1,4 @@
@@ -331,7 +434,7 @@ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..2876cf3694c278249b9db182e2f89df3
331
434
  export function prepareAngularEntryForDevice(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp = false) {
332
435
  const rewrittenCode = rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp);
333
436
  return rewriteAngularEntryRegisterOnly(rewrittenCode, resolveAngularCoreHmrImportSource(rewrittenCode, httpOrigin));
334
- @@ -236,6 +323,19 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
437
+ @@ -239,6 +326,19 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
335
438
  }
336
439
  return `${prefix}${stub}${suffix}`;
337
440
  }
@@ -351,7 +454,7 @@ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..2876cf3694c278249b9db182e2f89df3
351
454
  spec = normalizeNativeScriptCoreSpecifier(spec);
352
455
  // Pull `/@fs/<abs-path>` URLs back into the `/ns/m/` pipeline so they
353
456
  // hit our CJS/UMD-wrapping handler. Vite emits `/@fs/...` for any
354
- @@ -253,17 +353,39 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
457
+ @@ -256,17 +356,39 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
355
458
  }
356
459
  return `${prefix}${rewritten}${suffix}`;
357
460
  }
@@ -397,159 +500,45 @@ index 4a6575cb00a6aeecbbb7ac5ccac3268cb10d7f54..2876cf3694c278249b9db182e2f89df3
397
500
  if (httpOriginSafe) {
398
501
  return `${prefix}${httpOriginSafe}${spec}${suffix}`;
399
502
  diff --git a/hmr/server/websocket-module-specifiers.js b/hmr/server/websocket-module-specifiers.js
400
- index 74f50a77f48f3873aebf375a29d06b16c29440a3..0f06b6dc997118473e4a1ac6d3a3c48432311c71 100644
503
+ index 0081819906b9df7480408697b1afadb149e26e31..562bd0f7f982cd90d22c945f369d2581201597a2 100644
401
504
  --- a/hmr/server/websocket-module-specifiers.js
402
505
  +++ b/hmr/server/websocket-module-specifiers.js
403
- @@ -1,8 +1,8 @@
404
- -import { existsSync, readdirSync, readFileSync, realpathSync } from 'fs';
405
- +import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
406
- import * as path from 'path';
407
- import * as PAT from './constants.js';
408
- import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
409
- -import { getProjectRootPath } from '../../helpers/project.js';
410
- +import { getMonorepoWorkspaceRoot, getProjectRootPath } from '../../helpers/project.js';
411
- import { extractRootPackageName, getPackageRuntimeInfo } from '../shared/package-classifier.js';
412
- import { getFlavorClientPackages } from '../framework-flavors.js';
413
- const ESM_FRAMEWORK_PACKAGE_ROOTS = new Set(['@nativescript/angular', 'nativescript-angular']);
414
- @@ -328,6 +328,76 @@ export function resolveVendorFromCandidate(specifier) {
506
+ @@ -328,6 +328,24 @@ export function resolveVendorFromCandidate(specifier) {
415
507
  }
416
508
  return null;
417
509
  }
418
- +const depFallbackDirsCache = new Map();
419
- +/**
420
- + * `node_modules` dirs reachable through the app's dependency specs rather than
421
- + * its workspace layout — consulted when neither the project root nor the
422
- + * workspace root can anchor a served `/node_modules/` tail:
423
- + * - `link:`/`file:`/`portal:` deps keep their own `node_modules` at the real
424
- + * target; their workspace siblings can own transitive deps too. Reuse the
425
- + * bounded workspace scan rather than recursively walking dependency trees.
426
- + * - pnpm's isolated virtual store keeps every non-hoisted package under
427
- + * `node_modules/.pnpm/<name>@<ver>/node_modules/`.
428
- + * Importer info is gone from the URL — first hit wins, same as the workspace
429
- + * scan above. Cached per project root; the set only changes on install.
430
- + */
431
- +function listDepFallbackDirs(projectRoot) {
432
- + const cached = depFallbackDirsCache.get(projectRoot);
433
- + if (cached) {
434
- + return cached;
435
- + }
510
+ +const pnpmStoreDirsCache = new Map();
511
+ +// Upstream scans linked checkouts; retain only the app-local pnpm store fallback.
512
+ +function listPnpmStoreNodeModulesDirs(projectRoot) {
513
+ + const cached = pnpmStoreDirsCache.get(projectRoot);
514
+ + if (cached) return cached;
436
515
  + const dirs = [];
437
- + const seen = new Set();
438
- + const linkedWorkspaces = new Set();
439
- + const pushIfPresent = (dir) => {
440
- + if (!seen.has(dir) && existsSync(dir)) {
441
- + seen.add(dir);
442
- + dirs.push(dir);
443
- + }
444
- + };
445
- + const pnpmStore = path.join(projectRoot, 'node_modules', '.pnpm');
516
+ + const store = path.join(projectRoot, 'node_modules', '.pnpm');
446
517
  + try {
447
- + for (const entry of readdirSync(pnpmStore, { withFileTypes: true })) {
518
+ + for (const entry of readdirSync(store, { withFileTypes: true })) {
448
519
  + if (entry.isDirectory() && !entry.name.startsWith('.')) {
449
- + pushIfPresent(path.join(pnpmStore, entry.name, 'node_modules'));
450
- + }
451
- + }
452
- + }
453
- + catch {
454
- + }
455
- + try {
456
- + const pkg = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
457
- + for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
458
- + for (const spec of Object.values(pkg[section] ?? {})) {
459
- + const target = /^(?:link|file|portal):(.+)$/.exec(String(spec))?.[1];
460
- + if (!target)
461
- + continue;
462
- + try {
463
- + const depDir = realpathSync(path.resolve(projectRoot, target));
464
- + if (statSync(depDir).isDirectory()) {
465
- + pushIfPresent(path.join(depDir, 'node_modules'));
466
- + const workspace = getMonorepoWorkspaceRoot(depDir);
467
- + if (workspace)
468
- + linkedWorkspaces.add(workspace);
469
- + }
470
- + }
471
- + catch {
472
- + }
520
+ + const dir = path.join(store, entry.name, 'node_modules');
521
+ + if (existsSync(dir)) dirs.push(dir);
473
522
  + }
474
523
  + }
475
- + }
476
- + catch {
477
- + }
478
- + // Preserve direct-dependency precedence. Scan each linked workspace once,
479
- + // using the existing two-level package scan and its per-workspace cache.
480
- + for (const workspace of linkedWorkspaces) {
481
- + pushIfPresent(path.join(workspace, 'node_modules'));
482
- + for (const dir of listWorkspaceNodeModulesDirs(workspace))
483
- + pushIfPresent(dir);
484
- + }
485
- + depFallbackDirsCache.set(projectRoot, dirs);
524
+ + } catch {}
525
+ + pnpmStoreDirsCache.set(projectRoot, dirs);
486
526
  + return dirs;
487
527
  +}
488
528
  const workspaceNodeModulesDirsCache = new Map();
489
529
  /**
490
530
  * `node_modules` dirs that can hold a package the served URL no longer
491
- @@ -428,6 +498,16 @@ export function resolveCandidateFilePath(candidate, projectRoot, workspaceRoot)
492
- }
531
+ @@ -485,6 +503,8 @@ export function resolveCandidateFilePath(candidate, projectRoot, workspaceRoot)
532
+ return fromWorkspaceScan;
493
533
  }
494
534
  }
495
- + // Deps reachable only through the app's dependency specs — linked-package
496
- + // node_modules, pnpm's virtual store — still resolve to real files.
497
- + if (cleaned.includes('/node_modules/')) {
498
- + const tail = cleaned.slice(cleaned.lastIndexOf('/node_modules/') + '/node_modules/'.length);
499
- + for (const nmDir of listDepFallbackDirs(projectRoot)) {
500
- + const absPath = path.resolve(nmDir, tail);
501
- + if (absPath.startsWith(nmDir + path.sep) && existsSync(absPath))
502
- + return absPath;
503
- + }
504
- + }
505
- return null;
506
- }
507
- /**
508
- @@ -512,8 +592,8 @@ export function rewriteFsAbsoluteToNsM(spec, projectRoot, workspaceRoot) {
509
- * `export const X: string = '...'`. Routing through the realpath makes
510
- * Vite see a non-`node_modules` path and the TS transform applies.
511
- *
512
- - * App-local candidates whose real path is also app-local are returned
513
- - * unchanged so existing behaviour is preserved bit-for-bit.
514
- + * Candidates are returned unchanged only when their URL addresses the file
515
- + * found. Fallback files need filesystem anchors even inside the app root.
516
- */
517
- export function filterExistingNodeModulesTransformCandidates(spec, candidates, projectRoot, workspaceRoot) {
518
- const cleanedSpec = spec.replace(PAT.QUERY_PATTERN, '');
519
- @@ -557,22 +637,18 @@ export function filterExistingNodeModulesTransformCandidates(spec, candidates, p
520
- // through to the existing project/workspace logic.
521
- }
522
- if (mapped === null) {
523
- - if (!hasDistinctWorkspaceRoot) {
524
- + const candidatePath = cleanedCandidate.startsWith('/@fs/')
525
- + ? path.resolve(cleanedCandidate.slice('/@fs'.length))
526
- + : path.resolve(resolvedProjectRoot, cleanedCandidate.replace(/^\/+/, ''));
527
- + if (candidatePath === resolved) {
528
- mapped = candidate;
529
- }
530
- else {
531
- - const relFromProject = path.relative(resolvedProjectRoot, resolved);
532
- - const insideProjectRoot = relFromProject !== '' && !relFromProject.startsWith('..') && !path.isAbsolute(relFromProject);
533
- - if (insideProjectRoot) {
534
- - mapped = candidate;
535
- - }
536
- - else {
537
- - // Hoisted `node_modules/<pkg>/...` file lives above projectRoot.
538
- - // Rewrite the URL to its fs-anchored form so Vite reads it directly
539
- - // instead of bouncing through bare-specifier resolution that the
540
- - // hoisted package's `exports` field may refuse.
541
- - mapped = `/@fs${resolved.replace(/\\/g, '/')}${querySuffix}`;
542
- - }
543
- + // The candidate URL does not address the discovered file. A
544
- + // fallback may live inside the project (nested link: dependency
545
- + // or pnpm store) as well as outside it (hoisted dependency).
546
- + // Anchor the actual path so Vite loads the same file we found.
547
- + mapped = `/@fs${resolved.replace(/\\/g, '/')}${querySuffix}`;
548
- }
549
- }
550
- if (seen.has(mapped))
535
+ + const fromPnpmStore = probeNodeModulesTail(listPnpmStoreNodeModulesDirs(projectRoot));
536
+ + if (fromPnpmStore) return fromPnpmStore;
537
+ // Linked source checkouts keep private node_modules outside both roots.
538
+ // Only packages the app's own install lacks may come from them: each
539
+ // extension/index variant is resolved separately, so a variant missing
551
540
  diff --git a/hmr/server/websocket-ns-m-request.js b/hmr/server/websocket-ns-m-request.js
552
- index a11fbd5d24579cdf572a91f70cb06561bfa43cd0..c887334efce2c2f6cf85c7508276109467ff3737 100644
541
+ index 0879633dc5ee172f16de82d5d24bebe2f6f7878f..b489d7f1524eb35ba87206d86cdb9448a95c188e 100644
553
542
  --- a/hmr/server/websocket-ns-m-request.js
554
543
  +++ b/hmr/server/websocket-ns-m-request.js
555
544
  @@ -27,10 +27,15 @@ const MODULE_CANDIDATE_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mjs', 'mts', 'cts', 'v
@@ -570,26 +559,25 @@ index a11fbd5d24579cdf572a91f70cb06561bfa43cd0..c887334efce2c2f6cf85c75082761094
570
559
  }
571
560
  candidates.push(...MODULE_CANDIDATE_EXTS.map((ext) => `${base}.${ext}`));
572
561
  }
573
- @@ -149,8 +154,12 @@ export function resolveFsAnchoredSpec(spec, serverRoot, workspaceRoot) {
574
- for (const root of roots) {
575
- const rootPosix = root.replace(/\\/g, '/').replace(/\/$/, '');
576
- const fsId = `/@fs${rootPosix}${spec.startsWith('/') ? '' : '/'}${spec}`;
577
- - if (resolveCandidateFilePath(fsId, serverRoot, workspaceRoot)) {
578
- - return fsId;
579
- + // The fs id is a guess composed under a root — serve the path the
580
- + // resolver actually found (it may live in a dep's own node_modules,
581
- + // outside both roots).
582
- + const resolved = resolveCandidateFilePath(fsId, serverRoot, workspaceRoot);
583
- + if (resolved) {
584
- + return `/@fs${resolved.replace(/\\/g, '/')}`;
562
+ @@ -216,8 +221,11 @@ export async function resolveNsMTransformedModule(options) {
563
+ }
585
564
  }
586
565
  }
587
- return null;
566
+ - const fsId = resolveFsAnchoredSpec(spec, serverRoot, workspaceRoot);
567
+ - if (fsId) {
568
+ + // A workspace-relative extensionless URL is not a Vite app URL.
569
+ + // Probe the same extension/platform candidates under both source roots.
570
+ + for (const candidate of new Set([spec, ...transformCandidates])) {
571
+ + const fsId = resolveFsAnchoredSpec(candidate, serverRoot, workspaceRoot);
572
+ + if (!fsId) continue;
573
+ transformed = await tryTransformRequest(transformRequest, fsId, timeoutMs);
574
+ if (transformed?.code) {
575
+ return { transformed, resolvedCandidate: fsId };
588
576
  diff --git a/hmr/server/websocket-ns-m.js b/hmr/server/websocket-ns-m.js
589
- index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d9579078d93 100644
577
+ index fcdcd69776b3f82d3fc7d8dd26e03856177c933e..ac33b017b65489472c3573ce45f89bb94859777b 100644
590
578
  --- a/hmr/server/websocket-ns-m.js
591
579
  +++ b/hmr/server/websocket-ns-m.js
592
- @@ -97,7 +97,7 @@ export function registerNsModuleServerRoute(server, options) {
580
+ @@ -98,7 +98,7 @@ export function registerNsModuleServerRoute(server, options) {
593
581
  // internal virtual modules (e.g., \0nsvite:nsconfig-json for
594
582
  // ~/package.json) that don't exist on disk and must NOT run
595
583
  // through the device post-processing pipeline below.
@@ -598,7 +586,7 @@ index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d95
598
586
  try {
599
587
  // First try Vite's transform pipeline directly
600
588
  const vr = await sharedTransformRequest(spec);
601
- @@ -157,8 +157,21 @@ export function registerNsModuleServerRoute(server, options) {
589
+ @@ -167,8 +167,21 @@ export function registerNsModuleServerRoute(server, options) {
602
590
  // Workspace css outside the vite root must be anchored to /@fs or
603
591
  // `?inline` is denied and the DOM css module (__vite__updateStyle)
604
592
  // is served instead.
@@ -622,7 +610,7 @@ index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d95
622
610
  const inlineCode = inlineRes?.code || '';
623
611
  // `?inline` output is `export default "<compiled css>"`.
624
612
  const m = inlineCode.match(/export\s+default\s+("(?:[^"\\]|\\.)*")\s*;?/s);
625
- @@ -168,7 +181,14 @@ export function registerNsModuleServerRoute(server, options) {
613
+ @@ -178,7 +191,14 @@ export function registerNsModuleServerRoute(server, options) {
626
614
  css = JSON.parse(m[1]);
627
615
  }
628
616
  catch { }
@@ -638,7 +626,7 @@ index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d95
638
626
  const out = `// [ns:m][css] ${spec}\n` + `const __ns_css_text__ = ${JSON.stringify(css)};\n` + buildCssRegisterSnippetFromVar(spec, '__ns_css_text__') + `export default __ns_css_text__;\n`;
639
627
  res.statusCode = 200;
640
628
  res.end(out);
641
- @@ -181,7 +201,10 @@ export function registerNsModuleServerRoute(server, options) {
629
+ @@ -191,7 +211,10 @@ export function registerNsModuleServerRoute(server, options) {
642
630
  if (verbose)
643
631
  console.warn('[ns:m][css] inline compile failed', spec, cssErr?.message);
644
632
  }
@@ -650,7 +638,7 @@ index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d95
650
638
  }
651
639
  // Queue and dedupe transformRequest calls so heavy app graphs do not
652
640
  // overwhelm Vite with concurrent work. Slow-transform warnings start only
653
- @@ -503,6 +526,8 @@ export function registerNsModuleServerRoute(server, options) {
641
+ @@ -513,6 +536,8 @@ export function registerNsModuleServerRoute(server, options) {
654
642
  continue;
655
643
  }
656
644
  else if (u.pathname.startsWith('/ns/m')) {
@@ -659,6 +647,25 @@ index 63b5d9a4886d8da03b8e7deac8a16d53347ea5d7..0f92b3377e6d697f5e5c9eafad130d95
659
647
  // Resolve the import target through the SAME
660
648
  // canonical pipeline the /ns/m route itself uses
661
649
  // (websocket-ns-m-request.ts) instead of a third
650
+ @@ -546,7 +571,17 @@ export function registerNsModuleServerRoute(server, options) {
651
+ catch { }
652
+ if (!targetCode)
653
+ continue;
654
+ - const hasDefault = /\bexport\s+default\b/.test(targetCode) || /export\s*\{\s*default\s*(?:as\s*default)?\s*\}/.test(targetCode);
655
+ + // Inspect exports, not source text: compilers emit aliases such as
656
+ + // `export { Screen as default }`, and comments/strings are not exports.
657
+ + const targetAst = babelParse(targetCode, {
658
+ + sourceType: 'module',
659
+ + plugins: MODULE_IMPORT_ANALYSIS_PLUGINS,
660
+ + });
661
+ + const hasDefault = targetAst.program.body.some((node) =>
662
+ + node.type === 'ExportDefaultDeclaration' ||
663
+ + (node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type' &&
664
+ + node.specifiers.some((specifier) => specifier.exportKind !== 'type' &&
665
+ + (specifier.exported?.name === 'default' || specifier.exported?.value === 'default'))));
666
+ if (!hasDefault) {
667
+ // CJS/UMD modules won't have `export default` — they get CJS-wrapped
668
+ // by the serving pipeline. Only warn, don't fatally block the importer.
662
669
  diff --git a/hmr/server/websocket-served-module-helpers.js b/hmr/server/websocket-served-module-helpers.js
663
670
  index b9e3ab67eb4ae7c9b12bc4b02b9aa3660d712644..f857f4a5a5956af229e447ed04bf1a52a90c6399 100644
664
671
  --- a/hmr/server/websocket-served-module-helpers.js