@nativescript/vite 8.0.0-alpha.4 → 8.0.0-alpha.6
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.
- package/configuration/base.js +57 -0
- package/configuration/base.js.map +1 -1
- package/helpers/config-as-json.js +10 -0
- package/helpers/config-as-json.js.map +1 -1
- package/helpers/global-defines.d.ts +55 -0
- package/helpers/global-defines.js +81 -0
- package/helpers/global-defines.js.map +1 -1
- package/helpers/logging.d.ts +1 -0
- package/helpers/logging.js +36 -3
- package/helpers/logging.js.map +1 -1
- package/helpers/main-entry.js +167 -13
- package/helpers/main-entry.js.map +1 -1
- package/helpers/ns-core-url.d.ts +84 -0
- package/helpers/ns-core-url.js +168 -0
- package/helpers/ns-core-url.js.map +1 -0
- package/hmr/client/hmr-pending-overlay.d.ts +27 -0
- package/hmr/client/hmr-pending-overlay.js +50 -0
- package/hmr/client/hmr-pending-overlay.js.map +1 -0
- package/hmr/client/index.js +72 -1
- package/hmr/client/index.js.map +1 -1
- package/hmr/client/utils.d.ts +5 -0
- package/hmr/client/utils.js +153 -15
- package/hmr/client/utils.js.map +1 -1
- package/hmr/entry-runtime.js +95 -31
- package/hmr/entry-runtime.js.map +1 -1
- package/hmr/frameworks/angular/client/index.d.ts +1 -0
- package/hmr/frameworks/angular/client/index.js +424 -11
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/server/core-sanitize.d.ts +8 -4
- package/hmr/server/core-sanitize.js +71 -41
- package/hmr/server/core-sanitize.js.map +1 -1
- package/hmr/server/import-map.js +7 -3
- package/hmr/server/import-map.js.map +1 -1
- package/hmr/server/ns-core-cjs-shape.d.ts +206 -0
- package/hmr/server/ns-core-cjs-shape.js +273 -0
- package/hmr/server/ns-core-cjs-shape.js.map +1 -0
- package/hmr/server/perf-instrumentation.d.ts +118 -0
- package/hmr/server/perf-instrumentation.js +198 -0
- package/hmr/server/perf-instrumentation.js.map +1 -0
- package/hmr/server/shared-transform-request.js +12 -5
- package/hmr/server/shared-transform-request.js.map +1 -1
- package/hmr/server/websocket-angular-hot-update.d.ts +16 -0
- package/hmr/server/websocket-angular-hot-update.js +163 -1
- package/hmr/server/websocket-angular-hot-update.js.map +1 -1
- package/hmr/server/websocket-core-bridge.d.ts +0 -2
- package/hmr/server/websocket-core-bridge.js +60 -58
- package/hmr/server/websocket-core-bridge.js.map +1 -1
- package/hmr/server/websocket-graph-upsert.d.ts +15 -0
- package/hmr/server/websocket-graph-upsert.js +20 -0
- package/hmr/server/websocket-graph-upsert.js.map +1 -1
- package/hmr/server/websocket-hmr-pending.d.ts +43 -0
- package/hmr/server/websocket-hmr-pending.js +55 -0
- package/hmr/server/websocket-hmr-pending.js.map +1 -0
- package/hmr/server/websocket-module-specifiers.js +12 -0
- package/hmr/server/websocket-module-specifiers.js.map +1 -1
- package/hmr/server/websocket-ns-m-finalize.d.ts +0 -10
- package/hmr/server/websocket-ns-m-finalize.js +26 -11
- package/hmr/server/websocket-ns-m-finalize.js.map +1 -1
- package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
- package/hmr/server/websocket-ns-m-paths.js +59 -13
- package/hmr/server/websocket-ns-m-paths.js.map +1 -1
- package/hmr/server/websocket-ns-m-request.js +1 -16
- package/hmr/server/websocket-ns-m-request.js.map +1 -1
- package/hmr/server/websocket-runtime-compat.js.map +1 -1
- package/hmr/server/websocket-served-module-helpers.d.ts +36 -0
- package/hmr/server/websocket-served-module-helpers.js +613 -0
- package/hmr/server/websocket-served-module-helpers.js.map +1 -0
- package/hmr/server/websocket-vue-sfc.d.ts +0 -8
- package/hmr/server/websocket-vue-sfc.js +17 -19
- package/hmr/server/websocket-vue-sfc.js.map +1 -1
- package/hmr/server/websocket.d.ts +5 -5
- package/hmr/server/websocket.js +2867 -252
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/runtime/boot-timeline.d.ts +17 -0
- package/hmr/shared/runtime/boot-timeline.js +54 -0
- package/hmr/shared/runtime/boot-timeline.js.map +1 -0
- package/hmr/shared/runtime/dev-overlay.d.ts +49 -2
- package/hmr/shared/runtime/dev-overlay.js +587 -12
- package/hmr/shared/runtime/dev-overlay.js.map +1 -1
- package/hmr/shared/runtime/session-bootstrap.js +49 -0
- package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
- package/hmr/shared/vendor/manifest.js +114 -12
- package/hmr/shared/vendor/manifest.js.map +1 -1
- package/package.json +1 -1
package/hmr/server/websocket.js
CHANGED
|
@@ -1,53 +1,64 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { normalizeStrayCoreStringLiterals, fixDanglingCoreFrom, normalizeAnyCoreSpecToBridge, isDeepCoreSubpath, rewriteSpecifiersForDevice } from './core-sanitize.js';
|
|
3
|
+
import { buildDefaultExportFooter, buildShapeInstallHeader, hasNamespaceReExport, rewriteNamespaceReExportsForShape } from './ns-core-cjs-shape.js';
|
|
3
4
|
// AST tooling for robust transformations
|
|
4
5
|
import { parse as babelParse } from '@babel/parser';
|
|
5
6
|
import { genCode } from '../helpers/babel.js';
|
|
7
|
+
import babelCore from '@babel/core';
|
|
6
8
|
import traverse from '@babel/traverse';
|
|
7
9
|
// Ensure traverse callable across CJS/ESM builds
|
|
8
10
|
const babelTraverse = traverse?.default || traverse;
|
|
9
11
|
import * as t from '@babel/types';
|
|
10
12
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
11
13
|
import { astNormalizeModuleImportsAndHelpers, astVerifyAndAnnotateDuplicates } from '../helpers/ast-normalizer.js';
|
|
12
|
-
import { stripDanglingViteCjsImports } from '../helpers/sanitize.js';
|
|
14
|
+
import { stripRtCoreSentinel, stripDanglingViteCjsImports } from '../helpers/sanitize.js';
|
|
13
15
|
import { WebSocketServer } from 'ws';
|
|
14
16
|
import * as path from 'path';
|
|
15
17
|
import { createHash } from 'crypto';
|
|
16
18
|
import * as PAT from './constants.js';
|
|
17
19
|
import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
|
|
18
|
-
import { getProjectRootPath } from '../../helpers/project.js';
|
|
20
|
+
import { getPackageJson, getProjectFilePath, getProjectRootPath } from '../../helpers/project.js';
|
|
19
21
|
import { loadPrebuiltVendorManifest } from '../shared/vendor/manifest-loader.js';
|
|
20
22
|
import '../vendor-bootstrap.js';
|
|
23
|
+
import { NS_NATIVE_TAGS } from './compiler.js';
|
|
24
|
+
import { vueSfcCompiler } from '../frameworks/vue/server/compiler.js';
|
|
21
25
|
import { linkAngularPartialsIfNeeded } from '../frameworks/angular/server/linker.js';
|
|
22
26
|
import { vueServerStrategy } from '../frameworks/vue/server/strategy.js';
|
|
23
27
|
import { angularServerStrategy } from '../frameworks/angular/server/strategy.js';
|
|
24
28
|
import { solidServerStrategy } from '../frameworks/solid/server/strategy.js';
|
|
25
29
|
import { typescriptServerStrategy } from '../frameworks/typescript/server/strategy.js';
|
|
26
|
-
import { createProcessSfcCode } from '../frameworks/vue/server/sfc-transforms.js';
|
|
30
|
+
import { buildInlineTemplateBlock, createProcessSfcCode, extractTemplateRender, processTemplateVariantMinimal } from '../frameworks/vue/server/sfc-transforms.js';
|
|
31
|
+
import { astExtractImportsAndStripTypes } from '../helpers/ast-extract.js';
|
|
27
32
|
import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from '../../helpers/utils.js';
|
|
28
33
|
import { buildRuntimeConfig, generateImportMap } from './import-map.js';
|
|
29
34
|
import { getCliFlags } from '../../helpers/cli-flags.js';
|
|
35
|
+
import { normalizeCoreSub as normalizeCoreSubCanonical } from '../../helpers/ns-core-url.js';
|
|
30
36
|
import { isRuntimeGraphExcludedPath, matchesRuntimeGraphModuleId, normalizeRuntimeGraphPath, shouldIncludeRuntimeGraphFile, shouldSkipRuntimeGraphDirectoryName } from './runtime-graph-filter.js';
|
|
31
37
|
import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
|
|
32
|
-
import { canonicalizeTransformRequestCacheKey, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
|
|
33
|
-
import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta } from './websocket-graph-upsert.js';
|
|
38
|
+
import { angularSourceHasSemanticDecorator, canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
|
|
39
|
+
import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion } from './websocket-graph-upsert.js';
|
|
40
|
+
import { classifyBootRoute, classifyHmrUpdateKind, createColdBootRequestCounter, formatHmrUpdateSummary, formatPopulateInitialGraphSummary, formatServerStartupBanner } from './perf-instrumentation.js';
|
|
41
|
+
import { createHmrPendingMessage } from './websocket-hmr-pending.js';
|
|
34
42
|
import { extractVitePrebundleId, filterExistingNodeModulesTransformCandidates, getBlockedDeviceNodeModulesReason, getFlattenedManifestMap, isCoreGlobalsReference, isEsmFrameworkPackageSpecifier, isLikelyNativeScriptPluginSpecifier, isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveCandidateFilePath, resolveInternalRuntimePluginBareSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, resolveVendorRouting, shouldPreserveBareRuntimePluginSubpathImport, stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule, viteDepsPathToBareSpecifier, } from './websocket-module-specifiers.js';
|
|
35
43
|
import { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
|
|
36
|
-
import {
|
|
37
|
-
import { finalizeNsMServedModule } from './websocket-ns-m-finalize.js';
|
|
38
|
-
import { createNsMRequestContext, resolveNsMTransformedModule } from './websocket-ns-m-request.js';
|
|
39
|
-
import { getNumericServeVersionTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
|
|
40
|
-
import { registerRuntimeCompatHandlers } from './websocket-runtime-compat.js';
|
|
41
|
-
import { registerTxnHandler } from './websocket-txn.js';
|
|
42
|
-
import { registerVendorUnifierHandler } from './websocket-vendor-unifier.js';
|
|
43
|
-
import { registerVueSfcHandlers } from './websocket-vue-sfc.js';
|
|
44
|
+
import { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, extractDirectExportedNames, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest, resolveRuntimeCoreModulePath } from './websocket-core-bridge.js';
|
|
44
45
|
import { createSharedTransformRequestRunner } from './shared-transform-request.js';
|
|
46
|
+
import { formatNsMHmrServeTag, getNumericServeVersionTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
|
|
47
|
+
import { ensureDynamicHmrImportHelper } from './websocket-served-module-helpers.js';
|
|
45
48
|
export { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
|
|
46
49
|
export { stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule } from './websocket-module-specifiers.js';
|
|
47
|
-
export {
|
|
48
|
-
export { rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
|
|
50
|
+
export { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest } from './websocket-core-bridge.js';
|
|
49
51
|
export { rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
|
|
50
|
-
export
|
|
52
|
+
// Re-export the canonical URL rewriter from `websocket-ns-m-paths.js` so the
|
|
53
|
+
// existing test suites (which import from `./websocket.js`) keep working
|
|
54
|
+
// without churn while the implementation lives in a focused module.
|
|
55
|
+
export { formatNsMHmrServeTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
|
|
56
|
+
export { angularSourceHasSemanticDecorator, canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, createSharedTransformRequestRunner, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload, classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion };
|
|
57
|
+
const pluginTransformTypescript = (() => {
|
|
58
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
59
|
+
const loaded = requireFromHere('@babel/plugin-transform-typescript');
|
|
60
|
+
return loaded?.default || loaded;
|
|
61
|
+
})();
|
|
51
62
|
// Build a serialized process.env object from CLI --env.* flags.
|
|
52
63
|
// This is injected into every HTTP-served module so app code referencing
|
|
53
64
|
// process.env.TEST_ENV (etc.) works on device in HMR dev mode.
|
|
@@ -63,11 +74,40 @@ try {
|
|
|
63
74
|
}
|
|
64
75
|
catch { }
|
|
65
76
|
const __processEnvJson = JSON.stringify(__processEnvEntries);
|
|
77
|
+
const { parse, compileTemplate, compileScript } = vueSfcCompiler;
|
|
66
78
|
const APP_ROOT_DIR = getProjectAppPath();
|
|
67
79
|
const APP_VIRTUAL_PREFIX = getProjectAppVirtualPath();
|
|
68
80
|
const APP_VIRTUAL_WITH_SLASH = `${APP_VIRTUAL_PREFIX}/`;
|
|
69
81
|
const DEFAULT_MAIN_ENTRY = getProjectAppRelativePath('app.ts');
|
|
70
82
|
const DEFAULT_MAIN_ENTRY_VIRTUAL = getProjectAppVirtualPath('app.ts');
|
|
83
|
+
// alpha.59 — Stable URL + Explicit Invalidation:
|
|
84
|
+
// Memoized resolver for the project bootstrap entry as a posix
|
|
85
|
+
// project-relative path (e.g. `/src/main.ts`). This mirrors the resolution
|
|
86
|
+
// the cold-boot wrapper performs (`getPackageJson().main` →
|
|
87
|
+
// project-relative under `/<APP_ROOT_DIR>/`) so the eviction set for HMR
|
|
88
|
+
// always lines up with the URL the runtime actually re-imports. Resolved
|
|
89
|
+
// at first call and cached: `package.json` is read at startup and never
|
|
90
|
+
// changes during a dev session, so it's safe to memoize.
|
|
91
|
+
let __ns_bootstrap_entry_rel_cached = null;
|
|
92
|
+
function getBootstrapEntryRelPath() {
|
|
93
|
+
if (__ns_bootstrap_entry_rel_cached)
|
|
94
|
+
return __ns_bootstrap_entry_rel_cached;
|
|
95
|
+
let entry = DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
96
|
+
try {
|
|
97
|
+
const pkg = getPackageJson();
|
|
98
|
+
const main = (pkg && pkg.main) || DEFAULT_MAIN_ENTRY;
|
|
99
|
+
const abs = getProjectFilePath(main).replace(/\\/g, '/');
|
|
100
|
+
const marker = `/${APP_ROOT_DIR}/`;
|
|
101
|
+
const idx = abs.indexOf(marker);
|
|
102
|
+
entry = idx >= 0 ? abs.substring(idx) : DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
if (!entry.startsWith('/')) {
|
|
106
|
+
entry = '/' + entry;
|
|
107
|
+
}
|
|
108
|
+
__ns_bootstrap_entry_rel_cached = entry;
|
|
109
|
+
return entry;
|
|
110
|
+
}
|
|
71
111
|
const STRATEGY_REGISTRY = new Map([
|
|
72
112
|
['vue', vueServerStrategy],
|
|
73
113
|
['angular', angularServerStrategy],
|
|
@@ -314,44 +354,12 @@ function ensureGuardPlainDynamicImports(code, origin) {
|
|
|
314
354
|
return code;
|
|
315
355
|
}
|
|
316
356
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const helper = 'const __nsDynamicHmrImport = (spec) => {\n' +
|
|
324
|
-
" const __nsm = '/ns' + '/m';\n" +
|
|
325
|
-
" const __nsBootPrefix = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' && import.meta.url.includes('/__ns_boot__/b1/') ? '/__ns_boot__/b1' : '';\n" +
|
|
326
|
-
" const __nsImporterTagMatch = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' ? import.meta.url.match(/\\/__ns_hmr__\\/([^/]+)\\//) : null;\n" +
|
|
327
|
-
" const __nsImporterTag = __nsImporterTagMatch && __nsImporterTagMatch[1] ? decodeURIComponent(__nsImporterTagMatch[1]) : '';\n" +
|
|
328
|
-
" try { if (!spec || spec === '@') { return import(new URL(__nsm + '/__invalid_at__.mjs', import.meta.url).href); } } catch {}\n" +
|
|
329
|
-
' try {\n' +
|
|
330
|
-
" if (typeof spec === 'string' && spec.startsWith(__nsm + '/')) {\n" +
|
|
331
|
-
' const g = globalThis;\n' +
|
|
332
|
-
" const graphVersion = typeof g.__NS_HMR_GRAPH_VERSION__ === 'number' ? g.__NS_HMR_GRAPH_VERSION__ : 0;\n" +
|
|
333
|
-
" const nonce = typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0;\n" +
|
|
334
|
-
" const __nsActiveBootPrefix = graphVersion || nonce ? '' : __nsBootPrefix;\n" +
|
|
335
|
-
" if (spec.includes('/__ns_hmr__/')) {\n" +
|
|
336
|
-
" const __preservedSpec = !nonce && __nsBootPrefix && spec.startsWith(__nsm + '/__ns_hmr__/') && !spec.includes('/node_modules/') ? __nsm + __nsBootPrefix + spec.slice(__nsm.length) : spec;\n" +
|
|
337
|
-
' return import(new URL(__preservedSpec, import.meta.url).href);\n' +
|
|
338
|
-
' }\n' +
|
|
339
|
-
" if (spec.startsWith(__nsm + '/node_modules/')) { return import(new URL(spec, import.meta.url).href); }\n" +
|
|
340
|
-
" const tag = nonce ? `n${nonce}` : (graphVersion ? `v${graphVersion}` : (__nsImporterTag || 'live'));\n" +
|
|
341
|
-
" const nextPath = __nsm + __nsActiveBootPrefix + '/__ns_hmr__/' + encodeURIComponent(tag) + spec.slice(__nsm.length);\n" +
|
|
342
|
-
" const origin = typeof g.__NS_HTTP_ORIGIN__ === 'string' && /^https?:\\/\\//.test(g.__NS_HTTP_ORIGIN__) ? g.__NS_HTTP_ORIGIN__ : '';\n" +
|
|
343
|
-
' return import(origin ? origin + nextPath : new URL(nextPath, import.meta.url).href);\n' +
|
|
344
|
-
' }\n' +
|
|
345
|
-
' } catch {}\n' +
|
|
346
|
-
' return import(spec);\n' +
|
|
347
|
-
'};\n';
|
|
348
|
-
return helper + code;
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
return code;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
async function expandStarExports(code, server, projectRoot, verbose) {
|
|
357
|
+
// alpha.59 — `ensureDynamicHmrImportHelper` was previously duplicated
|
|
358
|
+
// here. Single source of truth now lives in
|
|
359
|
+
// `./websocket-served-module-helpers.js`. See that file for the
|
|
360
|
+
// architectural rationale and the current (much smaller) helper
|
|
361
|
+
// implementation.
|
|
362
|
+
async function expandStarExports(code, server, projectRoot, verbose, sharedTransformer) {
|
|
355
363
|
const STAR_RE = /^[ \t]*(export\s+\*\s+from\s+["'])([^"']+)(["'];?)[ \t]*$/gm;
|
|
356
364
|
let match;
|
|
357
365
|
const replacements = [];
|
|
@@ -363,25 +371,41 @@ async function expandStarExports(code, server, projectRoot, verbose) {
|
|
|
363
371
|
}
|
|
364
372
|
if (!replacements.length)
|
|
365
373
|
return code;
|
|
366
|
-
|
|
374
|
+
// Pull target URLs through the shared runner when it's available so each
|
|
375
|
+
// node_modules path shares the 60s TTL cache with the main /ns/m pipeline
|
|
376
|
+
// and respects the global concurrency gate. Fan them out in parallel —
|
|
377
|
+
// this block used to be a serial `for await` loop, which dominated cold
|
|
378
|
+
// boot on apps with dozens of star-re-exports.
|
|
379
|
+
const transformer = sharedTransformer ?? ((url) => server.transformRequest(url));
|
|
380
|
+
const resolved = await Promise.all(replacements.map(async (rep) => {
|
|
367
381
|
try {
|
|
368
382
|
let vitePath = rep.url.replace(/^https?:\/\/[^/]+/, '');
|
|
369
383
|
vitePath = vitePath.replace(/^\/ns\/m\//, '/');
|
|
370
384
|
vitePath = vitePath.replace(/^\/__ns_boot__\/[^/]+/, '');
|
|
371
385
|
vitePath = vitePath.replace(/\/__ns_hmr__\/[^/]+/, '');
|
|
372
|
-
const result = await
|
|
386
|
+
const result = await transformer(vitePath);
|
|
373
387
|
if (!result?.code)
|
|
374
|
-
|
|
388
|
+
return null;
|
|
375
389
|
const names = extractExportedNames(result.code);
|
|
376
390
|
if (!names.length)
|
|
377
|
-
|
|
378
|
-
const explicit = `export { ${names.join(', ')} } from ${JSON.stringify(rep.url)};`;
|
|
379
|
-
code = code.replace(rep.full, explicit);
|
|
391
|
+
return null;
|
|
380
392
|
if (verbose) {
|
|
381
|
-
|
|
393
|
+
try {
|
|
394
|
+
console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
|
|
395
|
+
}
|
|
396
|
+
catch { }
|
|
382
397
|
}
|
|
398
|
+
return { rep, names };
|
|
383
399
|
}
|
|
384
|
-
catch {
|
|
400
|
+
catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}));
|
|
404
|
+
for (const entry of resolved) {
|
|
405
|
+
if (!entry)
|
|
406
|
+
continue;
|
|
407
|
+
const explicit = `export { ${entry.names.join(', ')} } from ${JSON.stringify(entry.rep.url)};`;
|
|
408
|
+
code = code.replace(entry.rep.full, explicit);
|
|
385
409
|
}
|
|
386
410
|
return code;
|
|
387
411
|
}
|
|
@@ -890,6 +914,14 @@ function toNodeModulesHttpModuleId(importPath) {
|
|
|
890
914
|
}
|
|
891
915
|
return `/ns/m/node_modules/${nodeModulesSpecifier}`;
|
|
892
916
|
}
|
|
917
|
+
// alpha.59 — `rewriteNsMImportPathForHmr` and `getNumericServeVersionTag`
|
|
918
|
+
// previously lived here as duplicates of the implementations in
|
|
919
|
+
// `./websocket-ns-m-paths.js`. The path rewriter is part of the
|
|
920
|
+
// "Stable URL + Explicit Invalidation" architecture (see
|
|
921
|
+
// HMR_STABLE_URL_INVALIDATION_PLAN.md) and must be a single source of
|
|
922
|
+
// truth so the canonicalization rules can't drift between the two files.
|
|
923
|
+
// They are imported above and re-exported below for tests / external
|
|
924
|
+
// callers that historically reached them through this module.
|
|
893
925
|
function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
|
|
894
926
|
if (!spec || typeof spec !== 'string') {
|
|
895
927
|
return null;
|
|
@@ -1049,8 +1081,31 @@ export function wrapCommonJsModuleForDevice(code) {
|
|
|
1049
1081
|
`var __ns_cjs_require_kind = (typeof globalThis.__nsBaseRequire === 'function' ? 'base-require' : (typeof globalThis.__nsRequire === 'function' ? 'vendor-require' : 'global-require'));\n` +
|
|
1050
1082
|
`var require = function(spec) {\n` +
|
|
1051
1083
|
` if (!__ns_cjs_require_base) { throw new Error('require is not defined'); }\n` +
|
|
1052
|
-
|
|
1053
|
-
|
|
1084
|
+
// Resolve relative specifiers against the HTTP-served module's URL
|
|
1085
|
+
// before delegating to NS's runtime require. Without this step,
|
|
1086
|
+
// \`require('./base64-vlq')\` inside a CJS module served from
|
|
1087
|
+
// \`http://.../ns/m/node_modules/source-map-js/lib/source-map-generator.js\`
|
|
1088
|
+
// would pass a literal '"./base64-vlq"' to the native require, which
|
|
1089
|
+
// has no notion of the current HTTP-module's location and either
|
|
1090
|
+
// throws "Module not found" or fetches an arbitrary filesystem path
|
|
1091
|
+
// that happens to parse as code (producing misleading syntax errors
|
|
1092
|
+
// like "missing ) after argument list" from unrelated modules).
|
|
1093
|
+
` var __nsResolvedSpec = spec;\n` +
|
|
1094
|
+
` try {\n` +
|
|
1095
|
+
` if (typeof spec === 'string' && (spec.indexOf('./') === 0 || spec.indexOf('../') === 0)) {\n` +
|
|
1096
|
+
` var __nsParentUrl = (typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string') ? import.meta.url : null;\n` +
|
|
1097
|
+
` if (__nsParentUrl) {\n` +
|
|
1098
|
+
` var __nsResolvedUrl = new URL(spec, __nsParentUrl);\n` +
|
|
1099
|
+
` // Common Node-style bare extensions: prefer .js if the resolved URL lacks an extension in its last path segment.\n` +
|
|
1100
|
+
` if (!/\\.[A-Za-z0-9]+$/.test(__nsResolvedUrl.pathname.split('/').pop() || '')) {\n` +
|
|
1101
|
+
` __nsResolvedUrl.pathname = __nsResolvedUrl.pathname.replace(/\\/+$/, '') + '.js';\n` +
|
|
1102
|
+
` }\n` +
|
|
1103
|
+
` __nsResolvedSpec = __nsResolvedUrl.href;\n` +
|
|
1104
|
+
` }\n` +
|
|
1105
|
+
` }\n` +
|
|
1106
|
+
` } catch (e) {}\n` +
|
|
1107
|
+
` try { var __nsRecord = globalThis.__NS_RECORD_MODULE_PROVENANCE__; if (typeof __nsRecord === 'function') { __nsRecord(String(__nsResolvedSpec), { kind: __ns_cjs_require_kind, specifier: String(spec), url: __nsResolvedSpec !== spec ? __nsResolvedSpec : undefined, via: 'cjs-wrapper', parent: (typeof import.meta !== 'undefined' && import.meta && import.meta.url) ? import.meta.url : undefined }); } } catch (e) {}\n` +
|
|
1108
|
+
` var mod = __ns_cjs_require_base(__nsResolvedSpec);\n` +
|
|
1054
1109
|
` try {\n` +
|
|
1055
1110
|
` if (mod && (typeof mod === 'object' || typeof mod === 'function') && mod.default !== undefined) {\n` +
|
|
1056
1111
|
` var keys = [];\n` +
|
|
@@ -2096,6 +2151,27 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
|
|
|
2096
2151
|
return `${prefix}./${depFile}${suffix}`;
|
|
2097
2152
|
}
|
|
2098
2153
|
}
|
|
2154
|
+
// Bare npm package specifier fallback — route to /ns/m/node_modules/.
|
|
2155
|
+
// This catches specifiers like `source-map-js/lib/source-map-generator.js`
|
|
2156
|
+
// emitted by helpers such as the CommonJS compat transform, which Vite
|
|
2157
|
+
// would normally resolve to an absolute path but which pass through the
|
|
2158
|
+
// rewriter as bare strings here. Under HMR (core external) bundle.mjs
|
|
2159
|
+
// depends on these resolving over HTTP rather than via a filesystem
|
|
2160
|
+
// bare-specifier lookup, which iOS can't satisfy and which crashes with
|
|
2161
|
+
// "Module not found".
|
|
2162
|
+
if (spec && !spec.startsWith('/') && !spec.startsWith('./') && !spec.startsWith('../') && !/^https?:\/\//i.test(spec) && !spec.startsWith('ns-vendor:') && !spec.startsWith('@nativescript/core')) {
|
|
2163
|
+
// Only treat as a package spec if it looks like one — disallow
|
|
2164
|
+
// plain identifiers like `moment` unresolved (those are left alone
|
|
2165
|
+
// for existing vendor-routing paths to handle).
|
|
2166
|
+
const bareNpmRe = /^(?:@[A-Za-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*(?:\/[\w.\-/]+)?$/;
|
|
2167
|
+
if (bareNpmRe.test(spec)) {
|
|
2168
|
+
const httpSpec = `/ns/m/node_modules/${spec}`;
|
|
2169
|
+
if (httpOriginSafe) {
|
|
2170
|
+
return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
|
|
2171
|
+
}
|
|
2172
|
+
return `${prefix}${httpSpec}${suffix}`;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2099
2175
|
// Leave everything else unchanged (vendor imports, etc.)
|
|
2100
2176
|
return `${prefix}${spec}${suffix}`;
|
|
2101
2177
|
};
|
|
@@ -2237,10 +2313,21 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2237
2313
|
let registrySent = false;
|
|
2238
2314
|
let vendorBootstrapDone = false;
|
|
2239
2315
|
let pluginRoot;
|
|
2240
|
-
|
|
2316
|
+
// graphVersion starts at 1 so the very first /ns/m response uses a stable
|
|
2317
|
+
// `v1` URL tag (see dynamic-import helper at lines 398-432). Keeping it
|
|
2318
|
+
// stable during cold boot prevents double-loads when the graph fills up
|
|
2319
|
+
// lazily as modules are served.
|
|
2320
|
+
let graphVersion = 1;
|
|
2241
2321
|
// Transactional HMR batches: map graphVersion -> ordered list of changed ids for that version
|
|
2242
2322
|
const txnBatches = new Map();
|
|
2243
2323
|
const graph = new Map();
|
|
2324
|
+
// Tracks the background initial-graph population so handleHotUpdate can
|
|
2325
|
+
// await completion before computing delta roots for the first HMR event.
|
|
2326
|
+
let graphInitialPopulationPromise = null;
|
|
2327
|
+
// Cold-boot /ns/m request counter — populated the first time a /ns/m
|
|
2328
|
+
// request arrives, finalized when the request window goes idle.
|
|
2329
|
+
// See Shared across requests so a single counter spans the whole cold boot.
|
|
2330
|
+
let coldBootCounter = null;
|
|
2244
2331
|
function rememberAngularReloadSuppression(root, file, ttlMs = 3000) {
|
|
2245
2332
|
const absPath = normalizeHotReloadMatchPath(file);
|
|
2246
2333
|
const relPath = normalizeHotReloadMatchPath(file, root);
|
|
@@ -2412,12 +2499,17 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2412
2499
|
const classification = classifyGraphUpsert(existing, hash, normDeps);
|
|
2413
2500
|
if (classification === 'unchanged')
|
|
2414
2501
|
return existing;
|
|
2415
|
-
|
|
2502
|
+
// Version bumps are only meaningful for live edits — serve-time graph
|
|
2503
|
+
// warm-ups and the initial bulk walk should leave graphVersion stable.
|
|
2504
|
+
const bumpVersion = shouldBumpGraphVersion(classification, options?.bumpVersion !== false);
|
|
2505
|
+
if (bumpVersion) {
|
|
2506
|
+
graphVersion++;
|
|
2507
|
+
}
|
|
2416
2508
|
const gm = { id, deps: normDeps, hash };
|
|
2417
2509
|
graph.set(id, gm);
|
|
2418
2510
|
if (verbose) {
|
|
2419
2511
|
try {
|
|
2420
|
-
console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification });
|
|
2512
|
+
console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification, bumpVersion });
|
|
2421
2513
|
console.log('[hmr-ws][graph] size', graph.size);
|
|
2422
2514
|
}
|
|
2423
2515
|
catch { }
|
|
@@ -2445,6 +2537,8 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2445
2537
|
async function populateInitialGraph(server) {
|
|
2446
2538
|
if (graph.size)
|
|
2447
2539
|
return; // already populated
|
|
2540
|
+
const tStart = Date.now();
|
|
2541
|
+
const versionAtStart = graphVersion;
|
|
2448
2542
|
const root = server.config.root || process.cwd();
|
|
2449
2543
|
// Avoid direct require in ESM build: lazily obtain fs & path via createRequire or dynamic import
|
|
2450
2544
|
let fs;
|
|
@@ -2460,6 +2554,18 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2460
2554
|
fs = await import('fs');
|
|
2461
2555
|
pathMod = await import('path');
|
|
2462
2556
|
}
|
|
2557
|
+
// Route every bulk transform through `sharedTransformRequest` when it's
|
|
2558
|
+
// already been wired up — this way the background walk shares the 60s
|
|
2559
|
+
// TTL cache with live /ns/m requests, so the device sees cached results
|
|
2560
|
+
// for any file the walker already visited. The fallback keeps the
|
|
2561
|
+
// walker working during server tests where the shared runner isn't
|
|
2562
|
+
// constructed yet.
|
|
2563
|
+
const bulkTransform = (rel) => {
|
|
2564
|
+
if (sharedTransformRequest) {
|
|
2565
|
+
return sharedTransformRequest(rel);
|
|
2566
|
+
}
|
|
2567
|
+
return server.transformRequest(rel);
|
|
2568
|
+
};
|
|
2463
2569
|
async function walk(dir) {
|
|
2464
2570
|
for (const name of fs.readdirSync(dir)) {
|
|
2465
2571
|
if (name === 'node_modules' || name.startsWith('.') || shouldSkipRuntimeGraphDirectoryName(name))
|
|
@@ -2474,7 +2580,7 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2474
2580
|
const rel = '/' + pathMod.relative(root, full).split(pathMod.sep).join('/');
|
|
2475
2581
|
// Transform via Vite to gather deps (ignore failures)
|
|
2476
2582
|
try {
|
|
2477
|
-
const transformed = await
|
|
2583
|
+
const transformed = await bulkTransform(rel);
|
|
2478
2584
|
const code = transformed?.code || '';
|
|
2479
2585
|
const deps = [];
|
|
2480
2586
|
// fallback to import relationships via moduleGraph
|
|
@@ -2485,7 +2591,10 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2485
2591
|
deps.push(m.id.split('?')[0]);
|
|
2486
2592
|
}
|
|
2487
2593
|
}
|
|
2488
|
-
|
|
2594
|
+
// bumpVersion: false — the initial walk is a bulk load, not a live
|
|
2595
|
+
// edit. Keeping graphVersion stable during cold boot avoids double
|
|
2596
|
+
// cache-key drift described in Track 1.3 of the HMR plan.
|
|
2597
|
+
upsertGraphModule(rel, code, deps, { bumpVersion: false });
|
|
2489
2598
|
}
|
|
2490
2599
|
catch { }
|
|
2491
2600
|
}
|
|
@@ -2498,6 +2607,36 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2498
2607
|
await walk(pathMod.join(root, 'src'));
|
|
2499
2608
|
}
|
|
2500
2609
|
catch { }
|
|
2610
|
+
// Diagnostic summary: Always-on so slow
|
|
2611
|
+
// cold-boot walks surface even when verbose is off. A `bumpedVersion=no`
|
|
2612
|
+
// result is the happy path (Track 1.3); `yes` indicates a regression.
|
|
2613
|
+
try {
|
|
2614
|
+
console.info(formatPopulateInitialGraphSummary({
|
|
2615
|
+
moduleCount: graph.size,
|
|
2616
|
+
durationMs: Date.now() - tStart,
|
|
2617
|
+
graphVersion,
|
|
2618
|
+
bumpedVersion: graphVersion !== versionAtStart,
|
|
2619
|
+
}));
|
|
2620
|
+
}
|
|
2621
|
+
catch { }
|
|
2622
|
+
}
|
|
2623
|
+
// Kick off `populateInitialGraph` in the background (non-awaited) so /ns/m
|
|
2624
|
+
// responses are never blocked on a full tree walk. Returns the shared
|
|
2625
|
+
// promise so hot-update code paths can await completion before computing
|
|
2626
|
+
// delta roots for the first HMR event.
|
|
2627
|
+
function ensureInitialGraphPopulationStarted(server) {
|
|
2628
|
+
if (graphInitialPopulationPromise) {
|
|
2629
|
+
return graphInitialPopulationPromise;
|
|
2630
|
+
}
|
|
2631
|
+
if (graph.size) {
|
|
2632
|
+
graphInitialPopulationPromise = Promise.resolve();
|
|
2633
|
+
return graphInitialPopulationPromise;
|
|
2634
|
+
}
|
|
2635
|
+
graphInitialPopulationPromise = populateInitialGraph(server).catch((error) => {
|
|
2636
|
+
if (verbose)
|
|
2637
|
+
console.warn('[hmr-ws][graph] background initial population failed', error);
|
|
2638
|
+
});
|
|
2639
|
+
return graphInitialPopulationPromise;
|
|
2501
2640
|
}
|
|
2502
2641
|
return {
|
|
2503
2642
|
name: 'nativescript-hmr-websocket',
|
|
@@ -2526,12 +2665,22 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2526
2665
|
return originalSend(payload, ...rest);
|
|
2527
2666
|
});
|
|
2528
2667
|
}
|
|
2529
|
-
//
|
|
2530
|
-
//
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2668
|
+
// Transform concurrency. Historically we defaulted to 1 to avoid
|
|
2669
|
+
// race conditions during HTTP HMR startup, but the shared runner
|
|
2670
|
+
// already has per-URL coalescing and an async-cached result map,
|
|
2671
|
+
// so higher fan-out is safe and dramatically reduces cold-boot
|
|
2672
|
+
// time (Track 1.4 in HMR_CORE_REALM_DETERMINISTIC_PLAN.md). We cap
|
|
2673
|
+
// at 8 by default to match typical dev machines and respect Vite's
|
|
2674
|
+
// internal worker pool limits. Override via the env var when needed.
|
|
2675
|
+
const configuredTransformConcurrency = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CONCURRENCY || '', 10);
|
|
2676
|
+
const transformConcurrency = Number.isFinite(configuredTransformConcurrency) && configuredTransformConcurrency > 0 ? configuredTransformConcurrency : 8;
|
|
2677
|
+
// Keep transformed code cached for longer across HMR updates so that
|
|
2678
|
+
// unchanged neighbours of an edited file don't re-run through the
|
|
2679
|
+
// Angular/TypeScript/Vite transform pipeline. The HMR flow
|
|
2680
|
+
// explicitly invalidates affected URLs, so a longer TTL is safe.
|
|
2681
|
+
// See Track 2.2 in HMR_CORE_REALM_DETERMINISTIC_PLAN.md.
|
|
2682
|
+
const configuredTransformCacheMs = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CACHE_MS || '', 10);
|
|
2683
|
+
const transformCacheMs = Number.isFinite(configuredTransformCacheMs) && configuredTransformCacheMs >= 0 ? configuredTransformCacheMs : 60000;
|
|
2535
2684
|
sharedTransformRequest = createSharedTransformRequestRunner((url) => server.transformRequest(url), (url, timeoutMs) => {
|
|
2536
2685
|
try {
|
|
2537
2686
|
console.warn('[ns:m] slow transformRequest for', url, '(>' + timeoutMs + 'ms)');
|
|
@@ -2542,6 +2691,120 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2542
2691
|
resultCacheTtlMs: transformCacheMs,
|
|
2543
2692
|
getResultCacheKey: (url) => canonicalizeTransformRequestCacheKey(url, pluginRoot || process.cwd()),
|
|
2544
2693
|
});
|
|
2694
|
+
// Always-on startup banner — prints once per dev server process so
|
|
2695
|
+
// anyone investigating perf can immediately see which build is live
|
|
2696
|
+
// and what knobs are active. See HMR_CORE_REALM_DETERMINISTIC_PLAN.md
|
|
2697
|
+
// ("Track 1 + 2 — diagnostic instrumentation").
|
|
2698
|
+
try {
|
|
2699
|
+
let pkgVersion = 'unknown';
|
|
2700
|
+
try {
|
|
2701
|
+
const req = createRequire(import.meta.url);
|
|
2702
|
+
const pkg = req('@nativescript/vite/package.json');
|
|
2703
|
+
if (pkg && typeof pkg.version === 'string')
|
|
2704
|
+
pkgVersion = pkg.version;
|
|
2705
|
+
}
|
|
2706
|
+
catch {
|
|
2707
|
+
// `@nativescript/vite/package.json` is not always exported; fall
|
|
2708
|
+
// back to reading the file from disk next to this module.
|
|
2709
|
+
try {
|
|
2710
|
+
const here = new URL(import.meta.url).pathname;
|
|
2711
|
+
const pkgPath = path.resolve(path.dirname(here), '..', '..', 'package.json');
|
|
2712
|
+
if (existsSync(pkgPath)) {
|
|
2713
|
+
const parsed = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
2714
|
+
if (parsed && typeof parsed.version === 'string')
|
|
2715
|
+
pkgVersion = parsed.version;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
catch { }
|
|
2719
|
+
}
|
|
2720
|
+
console.info(formatServerStartupBanner({
|
|
2721
|
+
version: pkgVersion,
|
|
2722
|
+
transformConcurrency,
|
|
2723
|
+
transformCacheMs,
|
|
2724
|
+
lazyInitialGraph: true,
|
|
2725
|
+
graphVersion,
|
|
2726
|
+
}));
|
|
2727
|
+
}
|
|
2728
|
+
catch { }
|
|
2729
|
+
// Always-on cold-boot request trace. Runs in front of every other
|
|
2730
|
+
// middleware so it catches all NS dev routes (/ns/m/*, /ns/rt/*,
|
|
2731
|
+
// /ns/core/*, /__ns_boot__/*, etc.) with a single hook. Closes
|
|
2732
|
+
// itself after an idle window so HMR edits don't get rolled into
|
|
2733
|
+
// the cold-boot numbers. The idle window is generous by default
|
|
2734
|
+
// (5s) because V8's HTTP ESM resolver pauses between dep levels
|
|
2735
|
+
// while parsing — a too-tight window was closing after the first
|
|
2736
|
+
// wave and under-reporting boot by 100x. Override via
|
|
2737
|
+
// NS_VITE_HMR_BOOT_TRACE_IDLE_MS when profiling something tricky.
|
|
2738
|
+
// See HMR_CORE_REALM_DETERMINISTIC_PLAN.md ("Track 1 + 2 — round
|
|
2739
|
+
// three, 2026-04").
|
|
2740
|
+
try {
|
|
2741
|
+
const configuredIdleMs = Number.parseInt(process.env.NS_VITE_HMR_BOOT_TRACE_IDLE_MS || '', 10);
|
|
2742
|
+
const idleWindowMs = Number.isFinite(configuredIdleMs) && configuredIdleMs > 0 ? configuredIdleMs : 5000;
|
|
2743
|
+
const configuredSummaryEvery = Number.parseInt(process.env.NS_VITE_HMR_BOOT_TRACE_PROGRESS_EVERY || '', 10);
|
|
2744
|
+
const summaryEvery = Number.isFinite(configuredSummaryEvery) && configuredSummaryEvery >= 0 ? configuredSummaryEvery : 25;
|
|
2745
|
+
if (!coldBootCounter) {
|
|
2746
|
+
coldBootCounter = createColdBootRequestCounter({
|
|
2747
|
+
summaryEvery,
|
|
2748
|
+
idleWindowMs,
|
|
2749
|
+
log: (line) => {
|
|
2750
|
+
try {
|
|
2751
|
+
console.info(line);
|
|
2752
|
+
}
|
|
2753
|
+
catch { }
|
|
2754
|
+
},
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
catch { }
|
|
2759
|
+
server.middlewares.use((req, res, next) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
2762
|
+
const route = classifyBootRoute(urlObj.pathname);
|
|
2763
|
+
if (route === 'other')
|
|
2764
|
+
return next();
|
|
2765
|
+
if (!coldBootCounter)
|
|
2766
|
+
return next();
|
|
2767
|
+
const handle = coldBootCounter.record(urlObj.pathname);
|
|
2768
|
+
const finishOnce = () => {
|
|
2769
|
+
try {
|
|
2770
|
+
handle.finish();
|
|
2771
|
+
}
|
|
2772
|
+
catch { }
|
|
2773
|
+
};
|
|
2774
|
+
try {
|
|
2775
|
+
res.once('finish', finishOnce);
|
|
2776
|
+
res.once('close', finishOnce);
|
|
2777
|
+
}
|
|
2778
|
+
catch { }
|
|
2779
|
+
}
|
|
2780
|
+
catch { }
|
|
2781
|
+
next();
|
|
2782
|
+
});
|
|
2783
|
+
// Give `populateInitialGraph` a head start. Previously this only
|
|
2784
|
+
// kicked off on the first /ns/m hit, which meant populate was
|
|
2785
|
+
// competing with the device for the same 8 transform slots
|
|
2786
|
+
// throughout the first 4-5 seconds of cold boot. Starting at
|
|
2787
|
+
// `configureServer` time gives populate the full app build/launch
|
|
2788
|
+
// window (typically 2-3s on simulator) as a head start, so more
|
|
2789
|
+
// of its work lands before the device even connects. Disable via
|
|
2790
|
+
// NS_VITE_HMR_DISABLE_POPULATE=1 when profiling whether populate
|
|
2791
|
+
// is helping or hurting a specific app. See
|
|
2792
|
+
// HMR_CORE_REALM_DETERMINISTIC_PLAN.md ("Track 1 + 2 — round
|
|
2793
|
+
// three").
|
|
2794
|
+
try {
|
|
2795
|
+
const disablePopulate = process.env.NS_VITE_HMR_DISABLE_POPULATE === '1' || process.env.NS_VITE_HMR_DISABLE_POPULATE === 'true';
|
|
2796
|
+
if (disablePopulate) {
|
|
2797
|
+
if (verbose)
|
|
2798
|
+
console.info('[hmr-ws][populate] disabled via NS_VITE_HMR_DISABLE_POPULATE');
|
|
2799
|
+
// Short-circuit: mark as resolved so /ns/m never schedules it and
|
|
2800
|
+
// HMR still works (handleHotUpdate just has no pre-warmed graph).
|
|
2801
|
+
graphInitialPopulationPromise = Promise.resolve();
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
ensureInitialGraphPopulationStarted(server);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
catch { }
|
|
2545
2808
|
// Attempt early vendor manifest bootstrap once per server.
|
|
2546
2809
|
if (!vendorBootstrapDone) {
|
|
2547
2810
|
vendorBootstrapDone = true;
|
|
@@ -2816,27 +3079,137 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2816
3079
|
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
2817
3080
|
if (!urlObj.pathname.startsWith('/ns/m'))
|
|
2818
3081
|
return next();
|
|
3082
|
+
// Previously we awaited `populateInitialGraph(server)` here so
|
|
3083
|
+
// graphVersion would be non-zero for the first /ns/m request.
|
|
3084
|
+
// That gave deterministic URL tags but blocked the cold boot on a
|
|
3085
|
+
// full src/ tree walk (hundreds of transformRequest calls, 3-6s).
|
|
3086
|
+
//
|
|
3087
|
+
// Track 1.3: graphVersion now starts at 1 and stays stable during
|
|
3088
|
+
// cold boot (see `upsertGraphModule`'s bumpVersion option and the
|
|
3089
|
+
// inline comment at the graphVersion declaration). We kick off the
|
|
3090
|
+
// initial population in the background so it doesn't block the first
|
|
3091
|
+
// response. `handleHotUpdate` awaits the same promise so the first
|
|
3092
|
+
// HMR event still sees a fully populated graph.
|
|
3093
|
+
ensureInitialGraphPopulationStarted(server);
|
|
3094
|
+
// Cold-boot counter is now hooked via the leading boot-trace
|
|
3095
|
+
// middleware (see `configureServer` — it records the request
|
|
3096
|
+
// and tracks finish() via res.on('close'/'finish')). This
|
|
3097
|
+
// handler used to record here but that missed the
|
|
3098
|
+
// round-trip timing and didn't track per-route breakdowns.
|
|
2819
3099
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
2820
3100
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
2821
3101
|
// Disable caching for dev ESM endpoints to avoid device-side stale module reuse
|
|
2822
3102
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
2823
3103
|
res.setHeader('Pragma', 'no-cache');
|
|
2824
3104
|
res.setHeader('Expires', '0');
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
3105
|
+
// Support both query (?path=/abs) and path-style (/ns/m/abs)
|
|
3106
|
+
let spec = urlObj.searchParams.get('path') || '';
|
|
3107
|
+
// Optional graph version pin for deterministic boot
|
|
3108
|
+
let forcedVer = urlObj.searchParams.get('v');
|
|
3109
|
+
let bootTaggedRequest = false;
|
|
3110
|
+
if (!spec) {
|
|
3111
|
+
const base = '/ns/m';
|
|
3112
|
+
let rest = urlObj.pathname.slice(base.length);
|
|
3113
|
+
if (rest && rest !== '/')
|
|
3114
|
+
spec = rest;
|
|
3115
|
+
}
|
|
3116
|
+
// Special-case stub for anomalous '@' imports emitted as '/__invalid_at__.mjs'
|
|
3117
|
+
if (spec === '/__invalid_at__.mjs' || spec === '__invalid_at__.mjs') {
|
|
3118
|
+
res.statusCode = 200;
|
|
3119
|
+
res.end("// invalid '@' import stub\nexport {}\n");
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (!spec) {
|
|
3123
|
+
res.statusCode = 200;
|
|
3124
|
+
res.end('export {}\n');
|
|
2832
3125
|
return;
|
|
2833
3126
|
}
|
|
2834
|
-
|
|
3127
|
+
const serverRoot = (server.config?.root || process.cwd());
|
|
3128
|
+
spec = spec.replace(/[?#].*$/, '');
|
|
3129
|
+
// Accept path-based boot/HMR prefixes:
|
|
3130
|
+
// /ns/m/__ns_boot__/b1/<real-spec>
|
|
3131
|
+
// /ns/m/__ns_hmr__/<tag>/<real-spec>
|
|
3132
|
+
// /ns/m/__ns_boot__/b1/__ns_hmr__/<tag>/<real-spec>
|
|
3133
|
+
// The iOS HTTP ESM loader canonicalizes cache keys by stripping query params,
|
|
3134
|
+
// so we must carry the cache-buster in the path.
|
|
3135
|
+
try {
|
|
3136
|
+
const decorated = stripDecoratedServePrefixes(spec);
|
|
3137
|
+
spec = decorated.cleanedSpec;
|
|
3138
|
+
bootTaggedRequest = decorated.bootTaggedRequest;
|
|
3139
|
+
forcedVer || (forcedVer = decorated.forcedVer);
|
|
3140
|
+
}
|
|
3141
|
+
catch { }
|
|
3142
|
+
// Normalize absolute filesystem paths back to project-relative ids (e.g. /src/app.ts)
|
|
3143
|
+
try {
|
|
3144
|
+
const toPosix = (p) => p.replace(/\\/g, '/');
|
|
3145
|
+
const rootPosix = toPosix(serverRoot);
|
|
3146
|
+
const specPosix = toPosix(spec);
|
|
3147
|
+
// If spec is an absolute path under the project root, convert to '/'+relative
|
|
3148
|
+
const isAbsFs = /^\//.test(specPosix) || /^[A-Za-z]:\//.test(spec); // posix or win drive
|
|
3149
|
+
if (isAbsFs) {
|
|
3150
|
+
let rel = specPosix.startsWith(rootPosix) ? specPosix.slice(rootPosix.length) : require('path').posix.relative(rootPosix, specPosix);
|
|
3151
|
+
if (!rel.startsWith('..')) {
|
|
3152
|
+
if (!rel.startsWith('/'))
|
|
3153
|
+
rel = '/' + rel;
|
|
3154
|
+
// Ensure leading '/src' style when path maps into src
|
|
3155
|
+
spec = rel;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
catch { }
|
|
2835
3160
|
// Serve Vite virtual modules (/@id/ prefix). These are internal
|
|
2836
3161
|
// virtual modules (e.g., \0nsvite:nsconfig-json for ~/package.json)
|
|
2837
3162
|
// that don't exist on disk. Decode the ID and load via plugin container.
|
|
3163
|
+
if (spec.startsWith('/@id/')) {
|
|
3164
|
+
try {
|
|
3165
|
+
// First try Vite's transform pipeline directly
|
|
3166
|
+
const vr = await sharedTransformRequest(spec);
|
|
3167
|
+
if (vr?.code) {
|
|
3168
|
+
res.statusCode = 200;
|
|
3169
|
+
res.end(vr.code);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
catch { }
|
|
3174
|
+
try {
|
|
3175
|
+
// Fallback: decode the virtual module ID (__x00__ → \0) and
|
|
3176
|
+
// load through the plugin container directly
|
|
3177
|
+
const rawId = spec.slice('/@id/'.length).replace(/__x00__/g, '\0');
|
|
3178
|
+
const loadResult = await server.pluginContainer.load(rawId);
|
|
3179
|
+
if (loadResult) {
|
|
3180
|
+
const code = typeof loadResult === 'string' ? loadResult : loadResult.code;
|
|
3181
|
+
if (code) {
|
|
3182
|
+
res.statusCode = 200;
|
|
3183
|
+
res.end(code);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
catch { }
|
|
3189
|
+
}
|
|
3190
|
+
if (spec.startsWith('@/'))
|
|
3191
|
+
spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
|
|
3192
|
+
if (spec.startsWith('./'))
|
|
3193
|
+
spec = spec.slice(1);
|
|
3194
|
+
const blockedNodeModulesReason = getBlockedDeviceNodeModulesReason(spec);
|
|
3195
|
+
if (blockedNodeModulesReason) {
|
|
3196
|
+
res.statusCode = 404;
|
|
3197
|
+
res.end(`// [ns:m] blocked device import\nthrow new Error(${JSON.stringify(`[ns/m] ${blockedNodeModulesReason}`)});\nexport {};\n`);
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
if (!spec.startsWith('/'))
|
|
3201
|
+
spec = '/' + spec;
|
|
3202
|
+
const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
|
|
3203
|
+
const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
|
|
3204
|
+
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'];
|
|
3205
|
+
const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, serverRoot);
|
|
2838
3206
|
let transformed = null;
|
|
2839
3207
|
let resolvedCandidate = null;
|
|
3208
|
+
const rawExplicitModule = tryReadRawExplicitJavaScriptModule(spec, serverRoot);
|
|
3209
|
+
if (rawExplicitModule) {
|
|
3210
|
+
transformed = { code: rawExplicitModule.code };
|
|
3211
|
+
resolvedCandidate = rawExplicitModule.resolvedId;
|
|
3212
|
+
}
|
|
2840
3213
|
// Queue and dedupe transformRequest calls so heavy app graphs do not
|
|
2841
3214
|
// overwhelm Vite with concurrent work. Slow-transform warnings start only
|
|
2842
3215
|
// when the transform actually begins executing, and requests stay pending
|
|
@@ -2844,15 +3217,86 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2844
3217
|
const transformWithTimeout = (url, timeoutMs = 120000) => {
|
|
2845
3218
|
return sharedTransformRequest(url, timeoutMs);
|
|
2846
3219
|
};
|
|
2847
|
-
(
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3220
|
+
if (!transformed?.code) {
|
|
3221
|
+
for (const cand of transformCandidates) {
|
|
3222
|
+
try {
|
|
3223
|
+
const r = await transformWithTimeout(cand);
|
|
3224
|
+
if (r?.code) {
|
|
3225
|
+
transformed = r;
|
|
3226
|
+
resolvedCandidate = cand;
|
|
3227
|
+
break;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
catch { }
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
// Fallback 1: ask Vite to resolve the id, then transform the resolved id (handles aliases and virtual ids)
|
|
3234
|
+
if (!transformed?.code) {
|
|
3235
|
+
try {
|
|
3236
|
+
const rid = await server.pluginContainer?.resolveId?.(spec, undefined);
|
|
3237
|
+
const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
|
|
3238
|
+
if (ridStr) {
|
|
3239
|
+
const r = await transformWithTimeout(ridStr);
|
|
3240
|
+
if (r?.code) {
|
|
3241
|
+
transformed = r;
|
|
3242
|
+
resolvedCandidate = ridStr;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
catch { }
|
|
3247
|
+
}
|
|
3248
|
+
// Fallback 1b: if spec is a /node_modules/ path, extract bare specifier
|
|
3249
|
+
// and try resolveId with that. This handles package.json "exports" field
|
|
3250
|
+
// resolution (e.g., solid-js/jsx-runtime → solid-js/dist/solid.js).
|
|
3251
|
+
if (!transformed?.code && spec.includes('/node_modules/')) {
|
|
3252
|
+
try {
|
|
3253
|
+
const nmIdx = spec.lastIndexOf('/node_modules/');
|
|
3254
|
+
const bare = spec.slice(nmIdx + '/node_modules/'.length);
|
|
3255
|
+
if (bare && !bare.startsWith('.')) {
|
|
3256
|
+
const rid = await server.pluginContainer?.resolveId?.(bare, undefined);
|
|
3257
|
+
const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
|
|
3258
|
+
if (ridStr) {
|
|
3259
|
+
const r = await sharedTransformRequest(ridStr);
|
|
3260
|
+
if (r?.code) {
|
|
3261
|
+
transformed = r;
|
|
3262
|
+
resolvedCandidate = ridStr;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
catch { }
|
|
3268
|
+
}
|
|
3269
|
+
// Fallback 2: try /@fs absolute path under project root (Vite file system alias)
|
|
3270
|
+
if (!transformed?.code) {
|
|
3271
|
+
try {
|
|
3272
|
+
const toPosix = (p) => p.replace(/\\/g, '/');
|
|
3273
|
+
const rootPosix = toPosix(serverRoot).replace(/\/$/, '');
|
|
3274
|
+
const absPosix = `${rootPosix}${spec.startsWith('/') ? '' : '/'}${spec}`;
|
|
3275
|
+
const fsId = `/@fs${absPosix}`;
|
|
3276
|
+
if (resolveCandidateFilePath(fsId, serverRoot)) {
|
|
3277
|
+
const r = await transformWithTimeout(fsId);
|
|
3278
|
+
if (r?.code) {
|
|
3279
|
+
transformed = r;
|
|
3280
|
+
resolvedCandidate = fsId;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
catch { }
|
|
3285
|
+
}
|
|
3286
|
+
// Fallback 3: try adding ?import to hint Vite's transform pipeline
|
|
3287
|
+
if (!transformed?.code) {
|
|
3288
|
+
for (const cand of transformCandidates) {
|
|
3289
|
+
try {
|
|
3290
|
+
const r = await transformWithTimeout(`${cand}${cand.includes('?') ? '&' : '?'}import`);
|
|
3291
|
+
if (r?.code) {
|
|
3292
|
+
transformed = r;
|
|
3293
|
+
resolvedCandidate = `${cand}?import`;
|
|
3294
|
+
break;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
catch { }
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
2856
3300
|
// Solid HMR: patch @@solid-refresh's $$refreshESM to do inline patching
|
|
2857
3301
|
// during module re-evaluation instead of deferring to hot.accept() callback.
|
|
2858
3302
|
// In NativeScript's HTTP ESM environment, accept callbacks are registered
|
|
@@ -2930,7 +3374,9 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2930
3374
|
}
|
|
2931
3375
|
catch { }
|
|
2932
3376
|
}
|
|
2933
|
-
|
|
3377
|
+
// Serve-time warm-up: no live edit happened, so don't bump
|
|
3378
|
+
// graphVersion. See Track 1.3 in HMR_CORE_REALM_DETERMINISTIC_PLAN.md.
|
|
3379
|
+
upsertGraphModule(id, code, deps, { bumpVersion: false });
|
|
2934
3380
|
}
|
|
2935
3381
|
}
|
|
2936
3382
|
}
|
|
@@ -3055,47 +3501,200 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3055
3501
|
}
|
|
3056
3502
|
}
|
|
3057
3503
|
}
|
|
3504
|
+
let code = transformed.code;
|
|
3505
|
+
// Prepend guard to capture any URL-based require attempts
|
|
3506
|
+
code = REQUIRE_GUARD_SNIPPET + code;
|
|
3507
|
+
code = cleanCode(code);
|
|
3508
|
+
const isNodeMod = /(?:^|\/)node_modules\//.test(resolvedCandidate || spec || '');
|
|
3509
|
+
code = processCodeForDevice(code, false, true, isNodeMod, resolvedCandidate || spec);
|
|
3510
|
+
// Solid HMR: The NativeScript iOS/Android runtime provides import.meta.hot
|
|
3511
|
+
// natively (via InitializeImportMetaHot in HMRSupport.mm) with C++-backed
|
|
3512
|
+
// persistent hot.data that survives across module re-evaluations.
|
|
3513
|
+
// cleanCode() strips Vite's __vite__createHotContext assignment, which is
|
|
3514
|
+
// correct — the runtime's native hot context is better.
|
|
3058
3515
|
const projectRoot = server.config?.root || process.cwd();
|
|
3059
3516
|
const serverOrigin = getServerOrigin(server);
|
|
3060
|
-
|
|
3517
|
+
if (ACTIVE_STRATEGY?.flavor === 'angular') {
|
|
3518
|
+
code = prepareAngularEntryForDevice(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
|
|
3519
|
+
}
|
|
3520
|
+
else {
|
|
3521
|
+
code = rewriteImports(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
|
|
3522
|
+
}
|
|
3523
|
+
// Expand `export * from "url"` into explicit named re-exports.
|
|
3524
|
+
// NativeScript's HTTP ESM loader may not propagate star-re-exports across
|
|
3525
|
+
// HTTP module boundaries (the namespace object gets direct exports but
|
|
3526
|
+
// misses re-exported names). By expanding to `export { a, b } from "url"`,
|
|
3527
|
+
// the engine sees explicit named exports and resolves them correctly.
|
|
3061
3528
|
try {
|
|
3062
|
-
code = await
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3529
|
+
code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose, sharedTransformRequest);
|
|
3530
|
+
}
|
|
3531
|
+
catch (e) {
|
|
3532
|
+
if (verbose)
|
|
3533
|
+
console.warn('[ns/m] export* expansion failed:', e?.message);
|
|
3534
|
+
}
|
|
3535
|
+
// Dedupe any /ns/rt named imports that duplicate destructured bindings off default /ns/rt
|
|
3536
|
+
try {
|
|
3537
|
+
code = dedupeRtNamedImportsAgainstDestructures(code);
|
|
3538
|
+
}
|
|
3539
|
+
catch { }
|
|
3540
|
+
code = ensureVariableDynamicImportHelper(code);
|
|
3541
|
+
// Final safety: guard any plain dynamic import(...) occurrences to reroute anomalous '@' specs
|
|
3542
|
+
try {
|
|
3543
|
+
code = ensureGuardPlainDynamicImports(code, getServerOrigin(server));
|
|
3544
|
+
}
|
|
3545
|
+
catch { }
|
|
3546
|
+
// Extra hardening: normalize any remaining core references to the unified bridge
|
|
3547
|
+
// - Stray string-literals
|
|
3548
|
+
// - Dangling `from` merges
|
|
3549
|
+
// - Any spec (including /node_modules resolves) that still references '@nativescript/core'
|
|
3550
|
+
// Do this right before the final fast-fail assertion. If a rewrite occurred, add a small marker for diagnostics.
|
|
3551
|
+
try {
|
|
3552
|
+
const __before = code;
|
|
3553
|
+
code = normalizeStrayCoreStringLiterals(code);
|
|
3554
|
+
code = fixDanglingCoreFrom(code);
|
|
3555
|
+
code = normalizeAnyCoreSpecToBridge(code);
|
|
3556
|
+
if (code !== __before) {
|
|
3557
|
+
code = `// [hmr-sanitize] core-literal->bridge\n` + code;
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
catch { }
|
|
3561
|
+
// Final pass: deduplicate/resolve any bare-specifier imports that slipped
|
|
3562
|
+
// through the pipeline (e.g., extracted from JSDoc comments by import-splitting
|
|
3563
|
+
// regexes, or injected by the Angular linker on already-resolved code).
|
|
3564
|
+
try {
|
|
3565
|
+
code = deduplicateLinkerImports(code);
|
|
3566
|
+
}
|
|
3567
|
+
catch { }
|
|
3568
|
+
// CJS/UMD wrapping: if a module uses module.exports but has no ESM export default,
|
|
3569
|
+
// wrap it with CJS shims so the device HTTP ESM loader can consume it.
|
|
3570
|
+
// This handles npm packages that use CommonJS but aren't pre-bundled by Vite.
|
|
3571
|
+
//
|
|
3572
|
+
// Key constraints this must handle:
|
|
3573
|
+
// - CJS modules often declare local vars with the same names as their exports
|
|
3574
|
+
// (e.g. `function createLTTB() {...}; exports.createLTTB = createLTTB;`)
|
|
3575
|
+
// so `export var { createLTTB }` would cause a duplicate declaration.
|
|
3576
|
+
// - UMD modules reference `this` at top level (undefined in ESM) but
|
|
3577
|
+
// typically fall back to `self` or `globalThis`.
|
|
3578
|
+
// - `module`, `exports` must be shims since they don't exist in ESM.
|
|
3579
|
+
try {
|
|
3580
|
+
code = wrapCommonJsModuleForDevice(code);
|
|
3581
|
+
}
|
|
3582
|
+
catch { }
|
|
3583
|
+
try {
|
|
3584
|
+
assertNoOptimizedArtifacts(code, `NS M ${resolvedCandidate || spec}`);
|
|
3094
3585
|
}
|
|
3095
3586
|
catch (e) {
|
|
3096
3587
|
res.statusCode = 500;
|
|
3097
3588
|
return void res.end(`throw new Error(${JSON.stringify(e?.message || String(e))});\nexport {};`);
|
|
3098
3589
|
}
|
|
3590
|
+
// Defensive export normalization: if a module defines `routes` and only exports it named,
|
|
3591
|
+
// add a default export alias so both `import { routes }` and `import routes` work.
|
|
3592
|
+
try {
|
|
3593
|
+
if (!/\bexport\s+default\b/.test(code)) {
|
|
3594
|
+
const hasNamedRoutes = /\bexport\s*\{\s*routes\s*\}/.test(code);
|
|
3595
|
+
const hasConstRoutes = /\bconst\s+routes\s*=/.test(code) || /\bvar\s+routes\s*=/.test(code) || /\blet\s+routes\s*=/.test(code);
|
|
3596
|
+
if (hasNamedRoutes && hasConstRoutes) {
|
|
3597
|
+
code += `\nexport default routes;\n`;
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
catch { }
|
|
3602
|
+
// alpha.59 — `/ns/rt` and `/ns/core` URL versioning.
|
|
3603
|
+
//
|
|
3604
|
+
// Pre-alpha.59 these URLs were emitted as `/ns/rt/<ver>`
|
|
3605
|
+
// and `/ns/core/<ver>` so V8's HTTP module cache would see
|
|
3606
|
+
// a fresh URL on every save. The runtime canonicalizer
|
|
3607
|
+
// (HMRSupport.mm `CanonicalizeHttpUrlKey`) collapses these
|
|
3608
|
+
// version segments to the bare `/ns/rt` and `/ns/core`
|
|
3609
|
+
// keys before lookup, so V8 actually saw a single cache
|
|
3610
|
+
// entry — but the server was doing extra work to inject a
|
|
3611
|
+
// version segment that the runtime then immediately
|
|
3612
|
+
// stripped. Now that alpha.59 has explicit eviction (and
|
|
3613
|
+
// these bridge endpoints don't change at HMR time
|
|
3614
|
+
// anyway), the version segment is purely vestigial.
|
|
3615
|
+
//
|
|
3616
|
+
// Rather than rip the helpers out (which would touch
|
|
3617
|
+
// every ensureVersionedImports caller and risk bumping
|
|
3618
|
+
// older runtimes), we keep them but pass `verNum=0`. The
|
|
3619
|
+
// helpers still normalize URL shape (strip the absolute
|
|
3620
|
+
// origin prefix when present) but emit a stable
|
|
3621
|
+
// `/ns/rt/0` / `/ns/core/0` URL — which collapses to
|
|
3622
|
+
// `/ns/rt` / `/ns/core` in the runtime.
|
|
3623
|
+
try {
|
|
3624
|
+
const verNum = 0;
|
|
3625
|
+
code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
|
|
3626
|
+
code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
|
|
3627
|
+
code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
|
|
3628
|
+
}
|
|
3629
|
+
catch { }
|
|
3630
|
+
// alpha.59 — `/ns/m` URL finalize step.
|
|
3631
|
+
//
|
|
3632
|
+
// `rewriteNsMImportPathForHmr` (Phase 3a) is now a
|
|
3633
|
+
// canonicalizer: it strips legacy `__ns_hmr__/<tag>/`
|
|
3634
|
+
// segments and adds `__ns_boot__/b1/` only for boot-tagged
|
|
3635
|
+
// requests. The `ver` parameter is preserved on the
|
|
3636
|
+
// signature for API compatibility but is ignored for app
|
|
3637
|
+
// modules (cache busting is driven by
|
|
3638
|
+
// `__nsInvalidateModules`, not URL versioning). We pass
|
|
3639
|
+
// `'v0'` as a stable placeholder — the canonicalizer
|
|
3640
|
+
// emits the same URL regardless of this value, but a
|
|
3641
|
+
// constant placeholder makes the contract explicit.
|
|
3642
|
+
//
|
|
3643
|
+
// SFC URLs (line below, `/ns/sfc/${verTag}/...`) still
|
|
3644
|
+
// embed a version because the Vue SFC pathway does not
|
|
3645
|
+
// yet have an eviction protocol. The runtime canonicalizer
|
|
3646
|
+
// does NOT strip `/ns/sfc/<ver>/`, so Vue users still see
|
|
3647
|
+
// per-save SFC re-fetches — that's a known follow-up
|
|
3648
|
+
// (HMR_STABLE_URL_INVALIDATION_PLAN.md "Vue Follow-up").
|
|
3649
|
+
try {
|
|
3650
|
+
const verTag = (() => {
|
|
3651
|
+
const numeric = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
|
|
3652
|
+
return numeric > 0 ? `v${numeric}` : 'v0';
|
|
3653
|
+
})();
|
|
3654
|
+
const origin = getServerOrigin(server);
|
|
3655
|
+
const rewritePath = (p) => rewriteNsMImportPathForHmr(p, 'v0', bootTaggedRequest);
|
|
3656
|
+
// /ns/m URL forms — all collapse to canonical stable
|
|
3657
|
+
// URLs via the Phase 3a rewriter.
|
|
3658
|
+
// 1) Static imports: import ... from "/ns/m/..."
|
|
3659
|
+
code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3660
|
+
// 2) Side-effect imports: import "/ns/m/..."
|
|
3661
|
+
code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3662
|
+
// 3) Dynamic imports: import("/ns/m/...")
|
|
3663
|
+
code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3664
|
+
// 4) new URL("/ns/m/...", import.meta.url)
|
|
3665
|
+
code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3666
|
+
// 5) __ns_import(new URL('/ns/m/...', import.meta.url).href)
|
|
3667
|
+
code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\)\.href)/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3668
|
+
// 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → canonical stable URL.
|
|
3669
|
+
try {
|
|
3670
|
+
code = code.replace(/new\s+URL\(\s*["'](\/ns\/m\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}${rewritePath(p1)}`)}`);
|
|
3671
|
+
}
|
|
3672
|
+
catch { }
|
|
3673
|
+
// 7) SFC URLs (Vue) — still versioned. See header comment.
|
|
3674
|
+
try {
|
|
3675
|
+
code = code.replace(/new\s+URL\(\s*["']\/ns\/sfc(\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}/ns/sfc/${verTag}${p1}`)}`);
|
|
3676
|
+
}
|
|
3677
|
+
catch { }
|
|
3678
|
+
}
|
|
3679
|
+
catch { }
|
|
3680
|
+
// Final guard: eliminate any lingering named imports from /ns/core to avoid
|
|
3681
|
+
// evaluation-time "does not provide an export named ..." in the device runtime.
|
|
3682
|
+
try {
|
|
3683
|
+
code = ensureDestructureCoreImports(code);
|
|
3684
|
+
}
|
|
3685
|
+
catch { }
|
|
3686
|
+
// Boot-time module graph progress: while the app is still replacing the
|
|
3687
|
+
// placeholder, emit lightweight progress updates as /ns/m modules begin
|
|
3688
|
+
// evaluating. This keeps the overlay moving during large initial graphs.
|
|
3689
|
+
try {
|
|
3690
|
+
if (bootTaggedRequest) {
|
|
3691
|
+
const bootModuleLabel = String(spec || '').replace(/\\/g, '/');
|
|
3692
|
+
const bootProgressSnippet = buildBootProgressSnippet(bootModuleLabel);
|
|
3693
|
+
code = bootProgressSnippet + code;
|
|
3694
|
+
code = hoistTopLevelStaticImports(code);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
catch { }
|
|
3099
3698
|
// Dev-only: link-check static imports to surface missing bindings early
|
|
3100
3699
|
try {
|
|
3101
3700
|
const devCheck = process.env.NODE_ENV !== 'production';
|
|
@@ -3224,147 +3823,1912 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3224
3823
|
res.end('export {}\n');
|
|
3225
3824
|
}
|
|
3226
3825
|
});
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3826
|
+
// 2.5) ESM runtime bridge for NativeScript-Vue: GET /ns/rt
|
|
3827
|
+
// Provides a single authoritative source of Vue helpers bound to the NativeScript renderer.
|
|
3828
|
+
// V2.1: Lazy ensure bridge — does not statically import vue. It lazily resolves helpers from
|
|
3829
|
+
// globalThis or vendor registry/require on first evaluation, then exports references so SFCs
|
|
3830
|
+
// can immediately call them during module evaluation.
|
|
3831
|
+
server.middlewares.use(async (req, res, next) => {
|
|
3832
|
+
try {
|
|
3833
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
3834
|
+
// Accept only /ns/rt and /ns/rt/<ver> for cache-busting semantics
|
|
3835
|
+
if (!(urlObj.pathname === '/ns/rt' || /^\/ns\/rt\/[\d]+$/.test(urlObj.pathname)))
|
|
3836
|
+
return next();
|
|
3837
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
3838
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
3839
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
3840
|
+
res.setHeader('Pragma', 'no-cache');
|
|
3841
|
+
res.setHeader('Expires', '0');
|
|
3842
|
+
const rtVerSeg = urlObj.pathname.replace(/^\/ns\/rt\/?/, '');
|
|
3843
|
+
const rtVer = /^[0-9]+$/.test(rtVerSeg) ? rtVerSeg : String(graphVersion || 0);
|
|
3844
|
+
const origin = getServerOrigin(server);
|
|
3845
|
+
let code = `// [ns-rt][v2.3] NativeScript-Vue runtime bridge (module-scoped cache, no globals)\n` +
|
|
3846
|
+
`const __origin = ((typeof globalThis !== 'undefined' && globalThis && globalThis.__NS_HTTP_ORIGIN__) || (new URL(import.meta.url)).origin);\n` +
|
|
3847
|
+
`let __ns_core_bridge = null; try { import(__origin + "/ns/core/${rtVer}").then(m => { __ns_core_bridge = m; }).catch(() => {}); } catch {}\n` +
|
|
3848
|
+
`const g = globalThis;\n` +
|
|
3849
|
+
`const reg = (g.__nsVendorRegistry ||= new Map());\n` +
|
|
3850
|
+
`const req = reg && reg.get ? (g.__nsVendorRequire || g.__nsRequire || g.require) : (g.__nsRequire || g.require);\n` +
|
|
3851
|
+
`let __cached_rt = null;\n` +
|
|
3852
|
+
`let __cached_vm = null;\n` +
|
|
3853
|
+
`const __RT_REALM_TAG = (globalThis.__NS_RT_REALM__ ||= Math.random().toString(36).slice(2));\n` +
|
|
3854
|
+
// Unconditional one-shot evaluation marker to confirm bridge is executed on device
|
|
3855
|
+
`try { if (!(globalThis.__NS_RT_ONCE__ && globalThis.__NS_RT_ONCE__.eval)) { (globalThis.__NS_RT_ONCE__ ||= {}).eval = true; console.log('[ns-rt] evaluated', { rtRealm: __RT_REALM_TAG }); } } catch {}\n` +
|
|
3856
|
+
`function __ensure(){\n` +
|
|
3857
|
+
` if (__cached_rt) return __cached_rt;\n` +
|
|
3858
|
+
` let vm = null;\n` +
|
|
3859
|
+
` try { vm = reg && reg.has && reg.has('nativescript-vue') ? reg.get('nativescript-vue') : (typeof req==='function' ? req('nativescript-vue') : null); } catch {}\n` +
|
|
3860
|
+
` if (!vm) { try { vm = reg && reg.has && reg.has('vue') ? reg.get('vue') : (typeof req==='function' ? req('vue') : null); } catch {} }\n` +
|
|
3861
|
+
` const rt = (vm && (vm.default ?? vm)) || {};\n` +
|
|
3862
|
+
` __cached_vm = vm;\n` +
|
|
3863
|
+
` __cached_rt = rt;\n` +
|
|
3864
|
+
` return rt;\n` +
|
|
3865
|
+
`}\n` +
|
|
3866
|
+
`// Soft-globals for @nativescript/core when missing (dev-only safety)\n` +
|
|
3867
|
+
`try {\n` +
|
|
3868
|
+
` const dev = typeof __DEV__ !== 'undefined' ? __DEV__ : true;\n` +
|
|
3869
|
+
` if (dev) {\n` +
|
|
3870
|
+
` const ns = (__ns_core_bridge && (__ns_core_bridge.__esModule && __ns_core_bridge.default ? __ns_core_bridge.default : (__ns_core_bridge.default || __ns_core_bridge))) || __ns_core_bridge || {};\n` +
|
|
3871
|
+
` if (ns) {\n` +
|
|
3872
|
+
` if (!g.Frame && ns.Frame) g.Frame = ns.Frame;\n` +
|
|
3873
|
+
` if (!g.Page && ns.Page) g.Page = ns.Page;\n` +
|
|
3874
|
+
` if (!g.Application && (ns.Application||ns.app||ns.application)) g.Application = (ns.Application||ns.app||ns.application);\n` +
|
|
3875
|
+
` }\n` +
|
|
3876
|
+
` }\n` +
|
|
3877
|
+
`} catch {}\n` +
|
|
3878
|
+
`const __get = (k) => { const rt = __ensure(); const v = rt && rt[k]; if (typeof v !== 'function' && v === undefined) { throw new Error('[ns-rt] missing export '+k); } return v; };\n` +
|
|
3879
|
+
`export const __realm = __RT_REALM_TAG;\n` +
|
|
3880
|
+
`export const defineComponent = (...a) => (__get('defineComponent'))(...a);\n` +
|
|
3881
|
+
`export const resolveComponent = (...a) => (__ensure().resolveComponent)(...a);\n` +
|
|
3882
|
+
`export const createVNode = (...a) => (__ensure().createVNode)(...a);\n` +
|
|
3883
|
+
`export const createTextVNode = (...a) => (__ensure().createTextVNode)(...a);\n` +
|
|
3884
|
+
`export const createCommentVNode = (...a) => (__ensure().createCommentVNode)(...a);\n` +
|
|
3885
|
+
`export const Fragment = (__ensure().Fragment);\n` +
|
|
3886
|
+
`export const Teleport = (__ensure().Teleport);\n` +
|
|
3887
|
+
`export const Transition = (__ensure().Transition);\n` +
|
|
3888
|
+
`export const TransitionGroup = (__ensure().TransitionGroup);\n` +
|
|
3889
|
+
`export const KeepAlive = (__ensure().KeepAlive);\n` +
|
|
3890
|
+
`export const Suspense = (__ensure().Suspense);\n` +
|
|
3891
|
+
`export const withCtx = (...a) => (__ensure().withCtx)(...a);\n` +
|
|
3892
|
+
`export const openBlock = (...a) => (__ensure().openBlock)(...a);\n` +
|
|
3893
|
+
`export const createBlock = (...a) => (__ensure().createBlock)(...a);\n` +
|
|
3894
|
+
`export const createElementVNode = (...a) => (__ensure().createElementVNode)(...a);\n` +
|
|
3895
|
+
`export const createElementBlock = (...a) => (__ensure().createElementBlock)(...a);\n` +
|
|
3896
|
+
`export const renderSlot = (...a) => (__ensure().renderSlot)(...a);\n` +
|
|
3897
|
+
`export const mergeProps = (...a) => (__ensure().mergeProps)(...a);\n` +
|
|
3898
|
+
`export const toHandlers = (...a) => (__ensure().toHandlers)(...a);\n` +
|
|
3899
|
+
`export const renderList = (...a) => (__ensure().renderList)(...a);\n` +
|
|
3900
|
+
`export const normalizeProps = (...a) => (__ensure().normalizeProps)(...a);\n` +
|
|
3901
|
+
`export const guardReactiveProps = (...a) => (__ensure().guardReactiveProps)(...a);\n` +
|
|
3902
|
+
`export const normalizeClass = (...a) => (__ensure().normalizeClass)(...a);\n` +
|
|
3903
|
+
`export const normalizeStyle = (...a) => (__ensure().normalizeStyle)(...a);\n` +
|
|
3904
|
+
`export const toDisplayString = (...a) => (__ensure().toDisplayString)(...a);\n` +
|
|
3905
|
+
`export const withDirectives = (...a) => (__ensure().withDirectives)(...a);\n` +
|
|
3906
|
+
`export const resolveDirective = (...a) => (__ensure().resolveDirective)(...a);\n` +
|
|
3907
|
+
`export const withModifiers = (...a) => (__ensure().withModifiers)(...a);\n` +
|
|
3908
|
+
`export const withKeys = (...a) => (__ensure().withKeys)(...a);\n` +
|
|
3909
|
+
`export const resolveDynamicComponent = (...a) => (__ensure().resolveDynamicComponent)(...a);\n` +
|
|
3910
|
+
`export const isVNode = (...a) => (__ensure().isVNode)(...a);\n` +
|
|
3911
|
+
`export const cloneVNode = (...a) => (__ensure().cloneVNode)(...a);\n` +
|
|
3912
|
+
`export const isRef = (...a) => (__ensure().isRef)(...a);\n` +
|
|
3913
|
+
`export const ref = (...a) => (__ensure().ref)(...a);\n` +
|
|
3914
|
+
`export const shallowRef = (...a) => (__ensure().shallowRef)(...a);\n` +
|
|
3915
|
+
`export const unref = (...a) => (__ensure().unref)(...a);\n` +
|
|
3916
|
+
`export const computed = (...a) => (__ensure().computed)(...a);\n` +
|
|
3917
|
+
`export const reactive = (...a) => (__ensure().reactive)(...a);\n` +
|
|
3918
|
+
`export const readonly = (...a) => (__ensure().readonly)(...a);\n` +
|
|
3919
|
+
`export const isReactive = (...a) => (__ensure().isReactive)(...a);\n` +
|
|
3920
|
+
`export const isReadonly = (...a) => (__ensure().isReadonly)(...a);\n` +
|
|
3921
|
+
`export const toRaw = (...a) => (__ensure().toRaw)(...a);\n` +
|
|
3922
|
+
`export const markRaw = (...a) => (__ensure().markRaw)(...a);\n` +
|
|
3923
|
+
`export const shallowReactive = (...a) => (__ensure().shallowReactive)(...a);\n` +
|
|
3924
|
+
`export const shallowReadonly = (...a) => (__ensure().shallowReadonly)(...a);\n` +
|
|
3925
|
+
`export const watch = (...a) => (__ensure().watch)(...a);\n` +
|
|
3926
|
+
`export const watchEffect = (...a) => (__ensure().watchEffect)(...a);\n` +
|
|
3927
|
+
`export const watchPostEffect = (...a) => (__ensure().watchPostEffect)(...a);\n` +
|
|
3928
|
+
`export const watchSyncEffect = (...a) => (__ensure().watchSyncEffect)(...a);\n` +
|
|
3929
|
+
`export const onBeforeMount = (...a) => (__ensure().onBeforeMount)(...a);\n` +
|
|
3930
|
+
`export const onMounted = (...a) => (__ensure().onMounted)(...a);\n` +
|
|
3931
|
+
`export const onBeforeUpdate = (...a) => (__ensure().onBeforeUpdate)(...a);\n` +
|
|
3932
|
+
`export const onUpdated = (...a) => (__ensure().onUpdated)(...a);\n` +
|
|
3933
|
+
`export const onBeforeUnmount = (...a) => (__ensure().onBeforeUnmount)(...a);\n` +
|
|
3934
|
+
`export const onUnmounted = (...a) => (__ensure().onUnmounted)(...a);\n` +
|
|
3935
|
+
`export const onActivated = (...a) => (__ensure().onActivated)(...a);\n` +
|
|
3936
|
+
`export const onDeactivated = (...a) => (__ensure().onDeactivated)(...a);\n` +
|
|
3937
|
+
`export const onErrorCaptured = (...a) => (__ensure().onErrorCaptured)(...a);\n` +
|
|
3938
|
+
`export const onRenderTracked = (...a) => (__ensure().onRenderTracked)(...a);\n` +
|
|
3939
|
+
`export const onRenderTriggered = (...a) => (__ensure().onRenderTriggered)(...a);\n` +
|
|
3940
|
+
`export const nextTick = (...a) => (__ensure().nextTick)(...a);\n` +
|
|
3941
|
+
`export const h = (...a) => (__ensure().h)(...a);\n` +
|
|
3942
|
+
`export const provide = (...a) => (__ensure().provide)(...a);\n` +
|
|
3943
|
+
`export const inject = (...a) => (__ensure().inject)(...a);\n` +
|
|
3944
|
+
`export const vShow = (__ensure().vShow);\n` +
|
|
3945
|
+
`export const createApp = (...a) => (__ensure().createApp)(...a);\n` +
|
|
3946
|
+
`export const registerElement = (...a) => (__ensure().registerElement)(...a);\n` +
|
|
3947
|
+
`export const $navigateTo = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); try { if (!(g && g.Frame)) { const ns = (__ns_core_bridge && (__ns_core_bridge.__esModule && __ns_core_bridge.default ? __ns_core_bridge.default : (__ns_core_bridge.default || __ns_core_bridge))) || __ns_core_bridge || {}; if (ns) { if (!g.Frame && ns.Frame) g.Frame = ns.Frame; if (!g.Page && ns.Page) g.Page = ns.Page; if (!g.Application && (ns.Application||ns.app||ns.application)) g.Application = (ns.Application||ns.app||ns.application); } } } catch {} try { const hmrRealm = (g && g.__NS_HMR_REALM__) || 'unknown'; const hasTop = !!(g && g.Frame && g.Frame.topmost && g.Frame.topmost()); const top = hasTop ? g.Frame.topmost() : null; const ctor = top && top.constructor && top.constructor.name; } catch {} if (g && typeof g.__nsNavigateUsingApp === 'function') { try { return g.__nsNavigateUsingApp(...a); } catch (e) { try { console.error('[ns-rt] $navigateTo app navigator error', e); } catch {} throw e; } } try { console.error('[ns-rt] $navigateTo unavailable: app navigator missing'); } catch {} throw new Error('$navigateTo unavailable: app navigator missing'); } ;\n` +
|
|
3948
|
+
`export const $navigateBack = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); const impl = (vm && (vm.$navigateBack || (vm.default && vm.default.$navigateBack))) || (rt && (rt.$navigateBack || (rt.runtimeHelpers && rt.runtimeHelpers.navigateBack))); let res; try { const via = (impl && (impl === (vm && vm.$navigateBack) || impl === (vm && vm.default && vm.default.$navigateBack))) ? 'vm' : (impl ? 'rt' : 'none'); } catch {} try { if (typeof impl === 'function') res = impl(...a); } catch {} try { const top = (g && g.Frame && g.Frame.topmost && g.Frame.topmost()); if (!res && top && top.canGoBack && top.canGoBack()) { res = top.goBack(); } } catch {} try { const hook = g && (g.__NS_HMR_ON_NAVIGATE_BACK || g.__NS_HMR_ON_BACK || g.__nsAttemptBackRemount); if (typeof hook === 'function') hook(); } catch {} return res; }\n` +
|
|
3949
|
+
`export const $showModal = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); const impl = (vm && (vm.$showModal || (vm.default && vm.default.$showModal))) || (rt && (rt.$showModal || (rt.runtimeHelpers && rt.runtimeHelpers.showModal))); try { if (typeof impl === 'function') return impl(...a); } catch (e) { } return undefined; }\n` +
|
|
3950
|
+
`export default {\n` +
|
|
3951
|
+
` defineComponent, resolveComponent, createVNode, createTextVNode, createCommentVNode,\n` +
|
|
3952
|
+
` Fragment, Teleport, Transition, TransitionGroup, KeepAlive, Suspense, withCtx, openBlock,\n` +
|
|
3953
|
+
` createBlock, createElementVNode, createElementBlock, renderSlot, mergeProps, toHandlers,\n` +
|
|
3954
|
+
` renderList, normalizeProps, guardReactiveProps, normalizeClass, normalizeStyle, toDisplayString,\n` +
|
|
3955
|
+
` withDirectives, resolveDirective, withModifiers, withKeys, resolveDynamicComponent,\n` +
|
|
3956
|
+
` isVNode, cloneVNode, isRef, ref, shallowRef, unref, computed, reactive, readonly, isReactive, isReadonly, toRaw, markRaw, shallowReactive, shallowReadonly,\n` +
|
|
3957
|
+
` watch, watchEffect, watchPostEffect, watchSyncEffect, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,\n` +
|
|
3958
|
+
` onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured, onRenderTracked, onRenderTriggered, nextTick, h, provide, inject, vShow, createApp, registerElement,\n` +
|
|
3959
|
+
` $navigateTo, $navigateBack, $showModal\n` +
|
|
3960
|
+
`};\n`;
|
|
3961
|
+
// Prepend guard and ship (harmless, keeps diagnostics consistent)
|
|
3962
|
+
code = REQUIRE_GUARD_SNIPPET + code;
|
|
3963
|
+
res.statusCode = 200;
|
|
3964
|
+
res.end(code);
|
|
3965
|
+
}
|
|
3966
|
+
catch (e) {
|
|
3967
|
+
res.statusCode = 500;
|
|
3968
|
+
res.end('export {}\n');
|
|
3969
|
+
}
|
|
3235
3970
|
});
|
|
3236
3971
|
// 2.55) Dev-only vendor import unifier: rewrite 'vue'/'nativescript-vue' to /ns/rt/<ver>
|
|
3237
3972
|
// This ensures plugins and app share a single Vue/NativeScript-Vue instance/realm.
|
|
3238
|
-
registerVendorUnifierHandler(server, {
|
|
3239
|
-
getGraphVersion: () => Number(graphVersion || 0),
|
|
3240
|
-
getServerOrigin,
|
|
3241
|
-
getStrategy: () => ACTIVE_STRATEGY,
|
|
3242
|
-
});
|
|
3243
|
-
// 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
|
|
3244
3973
|
server.middlewares.use(async (req, res, next) => {
|
|
3245
3974
|
try {
|
|
3246
3975
|
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3976
|
+
const p = urlObj.pathname || '';
|
|
3977
|
+
// Ignore our own core/rt bridge endpoints and non-JS assets, but DO allow /ns/m/* through
|
|
3978
|
+
if (/^\/ns\/(?:rt|core)(?:\/|$)/.test(p))
|
|
3979
|
+
return next();
|
|
3980
|
+
if (!/(\.m?js$|\.ts$|\/node_modules\/|\/\.vite\/deps\/|^\/@id\/|^\/@fs\/)/.test(p))
|
|
3981
|
+
return next();
|
|
3982
|
+
if (/\.css($|\?)/.test(p))
|
|
3983
|
+
return next();
|
|
3984
|
+
const reqUrl = req.url || '';
|
|
3985
|
+
const transformed = await server.transformRequest(reqUrl);
|
|
3986
|
+
if (!transformed?.code)
|
|
3987
|
+
return next();
|
|
3988
|
+
const origin = getServerOrigin(server);
|
|
3989
|
+
const ver = Number(graphVersion || 0);
|
|
3990
|
+
const rewrite = ACTIVE_STRATEGY.rewriteVendorSpec;
|
|
3991
|
+
if (!rewrite)
|
|
3992
|
+
return next();
|
|
3993
|
+
const before = transformed.code;
|
|
3994
|
+
const code = rewrite(before, origin, ver);
|
|
3995
|
+
if (code === before)
|
|
3249
3996
|
return next();
|
|
3250
3997
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
3251
3998
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
3252
3999
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
3253
4000
|
res.setHeader('Pragma', 'no-cache');
|
|
3254
4001
|
res.setHeader('Expires', '0');
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
4002
|
+
res.statusCode = 200;
|
|
4003
|
+
res.end(code);
|
|
4004
|
+
}
|
|
4005
|
+
catch {
|
|
4006
|
+
return next();
|
|
4007
|
+
}
|
|
4008
|
+
});
|
|
4009
|
+
// 2.5.1) Catch-all redirect for stray /node_modules/@nativescript/core/*
|
|
4010
|
+
// requests — route them to the /ns/core bridge so they get the same
|
|
4011
|
+
// __DEV__/__IOS__ preamble and specifier rewriting. Without this,
|
|
4012
|
+
// Vite's default /node_modules/ handler serves the raw file, which
|
|
4013
|
+
// references bare __DEV__ and crashes at module eval.
|
|
4014
|
+
server.middlewares.use((req, _res, next) => {
|
|
4015
|
+
try {
|
|
4016
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4017
|
+
const coreNmPrefix = '/node_modules/@nativescript/core';
|
|
4018
|
+
if (!urlObj.pathname.startsWith(coreNmPrefix))
|
|
4019
|
+
return next();
|
|
4020
|
+
const sub = urlObj.pathname.slice(coreNmPrefix.length).replace(/^\/+/, '');
|
|
4021
|
+
if (sub === '' || sub === 'index.js' || sub === 'index') {
|
|
4022
|
+
req.url = `/ns/core`;
|
|
4023
|
+
}
|
|
4024
|
+
else {
|
|
4025
|
+
req.url = `/ns/core/${sub}`;
|
|
4026
|
+
}
|
|
4027
|
+
return next();
|
|
4028
|
+
}
|
|
4029
|
+
catch {
|
|
4030
|
+
return next();
|
|
4031
|
+
}
|
|
4032
|
+
});
|
|
4033
|
+
// 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
|
|
4034
|
+
//
|
|
4035
|
+
// Since bundle.mjs no longer bundles @nativescript/core (see
|
|
4036
|
+
// HMR_CORE_REALM_DETERMINISTIC_PLAN.md — external in the rolldown
|
|
4037
|
+
// config under HMR), this endpoint is the ONE place core is
|
|
4038
|
+
// evaluated. Every consumer — bundle.mjs's own `@nativescript/core*`
|
|
4039
|
+
// imports (resolved to full HTTP URLs in the entry virtual module),
|
|
4040
|
+
// externalized vendor packages, HTTP-served app modules — all end
|
|
4041
|
+
// up here. No more proxy bridge, no enumeration, no namespace
|
|
4042
|
+
// detection, no prototype-polluted maps. We just serve Vite's
|
|
4043
|
+
// authoritative transformed module content.
|
|
4044
|
+
//
|
|
4045
|
+
// iOS caches by URL path, so each unique URL is evaluated exactly
|
|
4046
|
+
// once per app lifetime. Every class identity is shared, every
|
|
4047
|
+
// `register()` side effect runs once, every `Application` reference
|
|
4048
|
+
// is the same iosApp singleton. The entire class of "does not
|
|
4049
|
+
// provide an export named X" and "Cannot redefine property" errors
|
|
4050
|
+
// is eliminated by construction.
|
|
4051
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4052
|
+
try {
|
|
4053
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4054
|
+
const coreRequest = parseCoreBridgeRequest(urlObj.pathname, urlObj.searchParams, Number(graphVersion || 0));
|
|
4055
|
+
if (!coreRequest)
|
|
4056
|
+
return next();
|
|
4057
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4058
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
4059
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4060
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4061
|
+
res.setHeader('Expires', '0');
|
|
4062
|
+
const { normalizedSub, sub, ver } = coreRequest;
|
|
4063
|
+
const resolveModuleId = async (moduleId) => {
|
|
4064
|
+
const resolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
|
|
4065
|
+
return typeof resolved === 'string' ? resolved : resolved?.id || null;
|
|
4066
|
+
};
|
|
4067
|
+
let modulePath = null;
|
|
4068
|
+
if (sub) {
|
|
4069
|
+
const resolvedSubpath = normalizedSub || sub;
|
|
4070
|
+
modulePath = await resolveRuntimeCoreModulePath(resolvedSubpath, resolveModuleId);
|
|
4071
|
+
if (!modulePath) {
|
|
4072
|
+
modulePath = `/node_modules/@nativescript/core/${resolvedSubpath}`;
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
else {
|
|
4076
|
+
modulePath = (await resolveModuleId('@nativescript/core')) || '/node_modules/@nativescript/core/index.js';
|
|
4077
|
+
}
|
|
4078
|
+
const transformed = await sharedTransformRequest(modulePath);
|
|
4079
|
+
if (!transformed?.code) {
|
|
4080
|
+
res.statusCode = 500;
|
|
4081
|
+
res.setHeader('Content-Type', 'application/json');
|
|
4082
|
+
res.end(JSON.stringify({ error: 'core-transform-failed', modulePath, sub: sub || null }));
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
// Vite's transform output references module IDs with /@fs,
|
|
4086
|
+
// relative specifiers, or absolute project paths. Rewrite
|
|
4087
|
+
// those to URLs iOS can fetch over HTTP.
|
|
4088
|
+
let rewritten = rewriteSpecifiersForDevice(transformed.code, getServerOrigin(server), Number(ver));
|
|
4089
|
+
// Invariant D (CJS/ESM interop shape) — EXPORT-SIDE fix.
|
|
4090
|
+
//
|
|
4091
|
+
// `@nativescript/core/index.js` declares namespace
|
|
4092
|
+
// re-exports like:
|
|
4093
|
+
// export * as Utils from './utils';
|
|
4094
|
+
// The ES spec says these produce Module Namespace Objects
|
|
4095
|
+
// with [[Prototype]] = null. Consumers that reach them
|
|
4096
|
+
// via direct ESM import — `import { Utils } from
|
|
4097
|
+
// '@nativescript/core'` — get the raw null-proto value,
|
|
4098
|
+
// bypassing any CJS `require` shim we install. Most
|
|
4099
|
+
// consumers tolerate this, but CJS-style interop (most
|
|
4100
|
+
// notably zone.js's `patchMethod`) calls
|
|
4101
|
+
// `hasOwnProperty` on the target and crashes on
|
|
4102
|
+
// null-proto.
|
|
4103
|
+
//
|
|
4104
|
+
// We rewrite the re-export to a shape-wrapped const:
|
|
4105
|
+
// import * as __ns_re_Utils__ from './utils';
|
|
4106
|
+
// export const Utils = __NS_CJS_SHAPE__(__ns_re_Utils__);
|
|
4107
|
+
// so the EXPORT itself is a plain object — visible to
|
|
4108
|
+
// both ESM and CJS consumers consistently.
|
|
4109
|
+
//
|
|
4110
|
+
// We only pay the rewrite cost when the module actually
|
|
4111
|
+
// contains namespace re-exports (i.e., the main
|
|
4112
|
+
// `index.js`). Subpaths (`/utils`, `/http`, …) don't
|
|
4113
|
+
// re-export via `export * as`; they expose named
|
|
4114
|
+
// exports directly, so the rewrite is a no-op on them.
|
|
4115
|
+
if (hasNamespaceReExport(rewritten)) {
|
|
4116
|
+
rewritten = rewriteNamespaceReExportsForShape(rewritten);
|
|
4117
|
+
}
|
|
4118
|
+
// Prepend the build-time defines (__DEV__, __IOS__, __ANDROID__,
|
|
4119
|
+
// __APPLE__, …) that @nativescript/core source references directly.
|
|
4120
|
+
// Vite's `define` config substitutes these in user-code transforms but
|
|
4121
|
+
// skips node_modules by default; since core is now external and served
|
|
4122
|
+
// over HTTP from this endpoint, the served transformed code still has
|
|
4123
|
+
// bare identifiers like `if (__DEV__) …`. Without these consts, V8
|
|
4124
|
+
// hits `ReferenceError: __DEV__ is not defined` at module eval because
|
|
4125
|
+
// globalThis.__DEV__ is set by bundle.mjs's body AFTER all static
|
|
4126
|
+
// imports (including these core modules) have resolved.
|
|
4127
|
+
//
|
|
4128
|
+
// We inject LITERAL boolean values based on CLI flags + dev-server
|
|
4129
|
+
// mode rather than reading from globalThis, so the defines are
|
|
4130
|
+
// resolved even before bundle.mjs's body runs.
|
|
4131
|
+
const __cliFlags = getCliFlags() || {};
|
|
4132
|
+
const __platformIsAndroid = !!__cliFlags.android;
|
|
4133
|
+
const __platformIsVisionOS = !!__cliFlags.visionos;
|
|
4134
|
+
const __platformIsIOS = !__platformIsAndroid && !__platformIsVisionOS;
|
|
4135
|
+
const preamble = [
|
|
4136
|
+
`const __ANDROID__ = ${__platformIsAndroid ? 'true' : 'false'};`,
|
|
4137
|
+
`const __IOS__ = ${__platformIsIOS ? 'true' : 'false'};`,
|
|
4138
|
+
`const __VISIONOS__ = ${__platformIsVisionOS ? 'true' : 'false'};`,
|
|
4139
|
+
`const __APPLE__ = __IOS__ || __VISIONOS__;`,
|
|
4140
|
+
`const __DEV__ = ${server.config?.mode === 'development' ? 'true' : 'false'};`,
|
|
4141
|
+
`const __COMMONJS__ = false;`,
|
|
4142
|
+
`const __NS_WEBPACK__ = false;`,
|
|
4143
|
+
`const __NS_ENV_VERBOSE__ = globalThis.__NS_ENV_VERBOSE__ !== undefined ? !!globalThis.__NS_ENV_VERBOSE__ : false;`,
|
|
4144
|
+
`const __CSS_PARSER__ = 'css-tree';`,
|
|
4145
|
+
`const __UI_USE_XML_PARSER__ = true;`,
|
|
4146
|
+
`const __UI_USE_EXTERNAL_RENDERER__ = false;`,
|
|
4147
|
+
`const __TEST__ = false;`,
|
|
4148
|
+
].join('\n');
|
|
4149
|
+
// Boot-time instrumentation + module self-registration.
|
|
4150
|
+
// See HMR_CORE_REALM_DETERMINISTIC_PLAN.md:
|
|
4151
|
+
// - Invariant A (URL canonicalization): the same
|
|
4152
|
+
// logical module must always resolve to byte-
|
|
4153
|
+
// identical URLs across every emitter. The /ns/core
|
|
4154
|
+
// handler records the first URL seen for each
|
|
4155
|
+
// canonical sub (or '' for main) in
|
|
4156
|
+
// `globalThis.__NS_CORE_FIRST_URL__` and fails hard
|
|
4157
|
+
// on mismatch so drift in any emitter surfaces
|
|
4158
|
+
// immediately, before the realm splits.
|
|
4159
|
+
// - Invariant C (boot-order): CommonJS
|
|
4160
|
+
// `require('@nativescript/core/...')` calls from
|
|
4161
|
+
// vendor install() hooks must resolve to the SAME
|
|
4162
|
+
// ESM namespace that ran this side-effect preamble.
|
|
4163
|
+
// The registration below keys the namespace object
|
|
4164
|
+
// under BOTH the bare specifier and the canonical
|
|
4165
|
+
// subpath (and raw subpath for back-compat) so the
|
|
4166
|
+
// vendor shim's `createRequire` and the main-entry
|
|
4167
|
+
// `_nsReq` hit on any lookup form.
|
|
4168
|
+
const rawSub = normalizedSub || sub || '';
|
|
4169
|
+
const canonicalSub = normalizeCoreSubCanonical(rawSub);
|
|
4170
|
+
const registrationKeySet = new Set();
|
|
4171
|
+
registrationKeySet.add(canonicalSub ? `@nativescript/core/${canonicalSub}` : '@nativescript/core');
|
|
4172
|
+
registrationKeySet.add(canonicalSub);
|
|
4173
|
+
if (rawSub && rawSub !== canonicalSub) {
|
|
4174
|
+
registrationKeySet.add(`@nativescript/core/${rawSub}`);
|
|
4175
|
+
registrationKeySet.add(rawSub);
|
|
4176
|
+
}
|
|
4177
|
+
const registrationKeys = Array.from(registrationKeySet).map((k) => JSON.stringify(k));
|
|
4178
|
+
const canonicalUrl = `${getServerOrigin(server)}` + (canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core');
|
|
4179
|
+
const instrumentationHeader = [
|
|
4180
|
+
`/* @nativescript/core bridge — canonical URL: ${canonicalUrl} */`,
|
|
4181
|
+
`try { if (typeof globalThis !== 'undefined') {`,
|
|
4182
|
+
` const __nsFirst = globalThis.__NS_CORE_FIRST_URL__ || (globalThis.__NS_CORE_FIRST_URL__ = Object.create(null));`,
|
|
4183
|
+
` const __nsSeen = globalThis.__NS_CORE_FETCHED_URLS__ || (globalThis.__NS_CORE_FETCHED_URLS__ = []);`,
|
|
4184
|
+
` const __nsKey = ${JSON.stringify(canonicalSub)};`,
|
|
4185
|
+
` const __nsUrl = ${JSON.stringify(canonicalUrl)};`,
|
|
4186
|
+
` __nsSeen.push(__nsUrl);`,
|
|
4187
|
+
` if (typeof __nsFirst[__nsKey] === 'string' && __nsFirst[__nsKey] !== __nsUrl) {`,
|
|
4188
|
+
` throw new Error('[ns-core] URL drift for sub=' + __nsKey + ': first=' + __nsFirst[__nsKey] + ' now=' + __nsUrl + ' (see HMR_CORE_REALM_DETERMINISTIC_PLAN.md Invariant A)');`,
|
|
4189
|
+
` }`,
|
|
4190
|
+
` if (!__nsFirst[__nsKey]) __nsFirst[__nsKey] = __nsUrl;`,
|
|
4191
|
+
` globalThis.__NS_CORE_EVAL_COUNT__ = (globalThis.__NS_CORE_EVAL_COUNT__ || 0) + 1;`,
|
|
4192
|
+
`} } catch (e) { try { console.warn('[ns-core] instrumentation failed:', (e && e.message) || e); } catch {} }`,
|
|
4193
|
+
].join('\n');
|
|
4194
|
+
// Invariant D (CJS/ESM interop shape) — REGISTRATION side.
|
|
4195
|
+
//
|
|
4196
|
+
// The actual shape installer runs earlier in the module
|
|
4197
|
+
// body (between preamble and selfImport; see
|
|
4198
|
+
// buildShapeInstallHeader). At this point we just read
|
|
4199
|
+
// globalThis.__NS_CJS_SHAPE__ and apply it to the self
|
|
4200
|
+
// namespace before registering under the CJS key space.
|
|
4201
|
+
//
|
|
4202
|
+
// Why shape self at registration: consumers that reach
|
|
4203
|
+
// `@nativescript/core` via `require()` (legacy vendors,
|
|
4204
|
+
// `globalThis.require` shim) look up the registry. They
|
|
4205
|
+
// expect a plain object (Object.prototype in chain) so
|
|
4206
|
+
// `.hasOwnProperty` / `.toString` work. Shaping once on
|
|
4207
|
+
// registration — the shape function is identity-preserving
|
|
4208
|
+
// via WeakMap — gives a stable, shared, CJS-compatible
|
|
4209
|
+
// view without copying on every require.
|
|
4210
|
+
//
|
|
4211
|
+
// See HMR_CORE_REALM_DETERMINISTIC_PLAN.md § "Invariant D"
|
|
4212
|
+
// for the full rationale.
|
|
4213
|
+
const registrationFooter = [
|
|
4214
|
+
`try { if (typeof globalThis !== 'undefined') {`,
|
|
4215
|
+
` const __nsReg = globalThis.__NS_CORE_MODULES__ || (globalThis.__NS_CORE_MODULES__ = Object.create(null));`,
|
|
4216
|
+
` const __nsShapeFn = typeof globalThis.__NS_CJS_SHAPE__ === 'function' ? globalThis.__NS_CJS_SHAPE__ : function (x) { return x; };`,
|
|
4217
|
+
` const __nsSelfRaw = (typeof __ns_core_self_ns__ !== 'undefined') ? __ns_core_self_ns__ : { default: undefined };`,
|
|
4218
|
+
` const __nsSelf = __nsShapeFn(__nsSelfRaw);`,
|
|
4219
|
+
...registrationKeys.map((k) => ` __nsReg[${k}] = __nsSelf;`),
|
|
4220
|
+
`} } catch (e) { try { console.warn('[ns-core] self-register failed:', (e && e.message) || e); } catch {} }`,
|
|
4221
|
+
].join('\n');
|
|
4222
|
+
// Bind `import * as __ns_core_self_ns__` to the module's
|
|
4223
|
+
// own export namespace so the footer can stash it into
|
|
4224
|
+
// the registry. Self-import is a no-op at eval time —
|
|
4225
|
+
// V8 resolves it to the module record we're already
|
|
4226
|
+
// evaluating and the final namespace is the same object
|
|
4227
|
+
// the registry receives. We use the CANONICAL URL here
|
|
4228
|
+
// so the self-import participates in Invariant A along
|
|
4229
|
+
// with every other @nativescript/core URL.
|
|
4230
|
+
const canonicalUrlForSelf = canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core';
|
|
4231
|
+
const selfImport = `import * as __ns_core_self_ns__ from ${JSON.stringify(canonicalUrlForSelf)};`;
|
|
4232
|
+
// Invariant D — SHAPE INSTALLER.
|
|
4233
|
+
//
|
|
4234
|
+
// Emits idempotent body-code that installs
|
|
4235
|
+
// globalThis.__NS_CJS_SHAPE__ BEFORE `rewritten`'s body
|
|
4236
|
+
// runs. This matters because the rewrite step above may
|
|
4237
|
+
// have produced statements like
|
|
4238
|
+
// `export const Utils = (typeof globalThis.__NS_CJS_SHAPE__ ...)(__ns_re_Utils__);`
|
|
4239
|
+
// that execute during module evaluation. Without the
|
|
4240
|
+
// installer running first, the ternary falls back to
|
|
4241
|
+
// identity — still safe, but the null-proto namespace
|
|
4242
|
+
// leaks through and consumers that expect a plain
|
|
4243
|
+
// object would still crash.
|
|
4244
|
+
//
|
|
4245
|
+
// Placement is important: BEFORE selfImport in the
|
|
4246
|
+
// concatenation. ESM imports are hoisted regardless of
|
|
4247
|
+
// textual position, but body code executes in source
|
|
4248
|
+
// order. Placing the installer first guarantees it
|
|
4249
|
+
// runs before any body statement in `rewritten`.
|
|
4250
|
+
//
|
|
4251
|
+
// Install is idempotent: `|| (globalThis.X = ...)` so
|
|
4252
|
+
// whichever /ns/core module evaluates first wins and
|
|
4253
|
+
// every subsequent module becomes a no-op.
|
|
4254
|
+
const shapeInstallHeader = buildShapeInstallHeader();
|
|
4255
|
+
// Invariant D — DEFAULT EXPORT BRIDGE.
|
|
4256
|
+
//
|
|
4257
|
+
// See `buildDefaultExportFooter` in ns-core-cjs-shape.ts
|
|
4258
|
+
// for the full rationale (consumer matrix, skip conditions,
|
|
4259
|
+
// why the default isn't shaped). The short version:
|
|
4260
|
+
// upstream rewrites turn `import { X } from '@nativescript/core'`
|
|
4261
|
+
// into a DEFAULT import, and the bridge has to provide one.
|
|
4262
|
+
const defaultExportFooter = buildDefaultExportFooter(rewritten);
|
|
4263
|
+
const moduleCode = [instrumentationHeader, preamble, shapeInstallHeader, selfImport, rewritten, defaultExportFooter, registrationFooter].join('\n');
|
|
4264
|
+
res.statusCode = 200;
|
|
4265
|
+
res.end(moduleCode);
|
|
4266
|
+
}
|
|
4267
|
+
catch (e) {
|
|
4268
|
+
try {
|
|
4269
|
+
console.warn('[ns-core-bridge] serve failed:', e?.message);
|
|
4270
|
+
}
|
|
4271
|
+
catch { }
|
|
4272
|
+
next();
|
|
4273
|
+
}
|
|
4274
|
+
});
|
|
4275
|
+
// 2.6a) Serve compiled entry runtime module: GET /ns/entry-rt[?v=<ver>]
|
|
4276
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4277
|
+
try {
|
|
4278
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4279
|
+
if (!(urlObj.pathname === '/ns/entry-rt'))
|
|
4280
|
+
return next();
|
|
4281
|
+
try {
|
|
4282
|
+
if (verbose) {
|
|
4283
|
+
const ra = req.socket?.remoteAddress;
|
|
4284
|
+
const rp = req.socket?.remotePort;
|
|
4285
|
+
console.log('[hmr-http] GET /ns/entry-rt from', ra + (rp ? ':' + rp : ''));
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
catch { }
|
|
4289
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4290
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
4291
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4292
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4293
|
+
res.setHeader('Expires', '0');
|
|
4294
|
+
let content = '';
|
|
4295
|
+
try {
|
|
4296
|
+
const _req = createRequire(import.meta.url);
|
|
4297
|
+
const entryRtPath = _req.resolve('@nativescript/vite/hmr/entry-runtime.js');
|
|
4298
|
+
content = readFileSync(entryRtPath, 'utf-8');
|
|
4299
|
+
}
|
|
4300
|
+
catch (e) {
|
|
4301
|
+
// .js not found (source tree without build) — transform .ts on the fly
|
|
4302
|
+
try {
|
|
4303
|
+
const tsPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'entry-runtime.ts');
|
|
4304
|
+
if (existsSync(tsPath)) {
|
|
4305
|
+
const tsSource = readFileSync(tsPath, 'utf-8');
|
|
4306
|
+
const result = babelCore.transformSync(tsSource, {
|
|
4307
|
+
filename: tsPath,
|
|
4308
|
+
plugins: [[pluginTransformTypescript, { isTSX: false, allowDeclareFields: true }]],
|
|
4309
|
+
sourceType: 'module',
|
|
4310
|
+
});
|
|
4311
|
+
if (result?.code) {
|
|
4312
|
+
content = result.code;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
catch (e2) {
|
|
4317
|
+
if (verbose)
|
|
4318
|
+
console.warn('[hmr-http] entry-runtime.ts transform failed', e2);
|
|
4319
|
+
}
|
|
4320
|
+
if (!content) {
|
|
4321
|
+
content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
console.log('[hmr-http] /ns/entry-rt serving', content.length, 'bytes');
|
|
4325
|
+
res.statusCode = 200;
|
|
4326
|
+
res.end(content);
|
|
4327
|
+
}
|
|
4328
|
+
catch (e) {
|
|
4329
|
+
console.warn('[hmr-http] /ns/entry-rt error', e);
|
|
4330
|
+
next();
|
|
4331
|
+
}
|
|
4332
|
+
});
|
|
4333
|
+
// 2.6b) HTTP-only app entry endpoint: GET /ns/entry[/<ver>]
|
|
4334
|
+
// Thin wrapper that imports the compiled entry runtime and starts it with parameters.
|
|
4335
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4336
|
+
try {
|
|
4337
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4338
|
+
if (!(urlObj.pathname === '/ns/entry' || /^\/ns\/entry\/[\d]+$/.test(urlObj.pathname)))
|
|
4339
|
+
return next();
|
|
4340
|
+
try {
|
|
4341
|
+
if (verbose) {
|
|
4342
|
+
const ra = req.socket?.remoteAddress;
|
|
4343
|
+
const rp = req.socket?.remotePort;
|
|
4344
|
+
console.log('[hmr-http] GET /ns/entry from', ra + (rp ? ':' + rp : ''));
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
catch { }
|
|
4348
|
+
const verSeg = urlObj.pathname.replace(/^\/ns\/entry\/?/, '');
|
|
4349
|
+
// Resolve app main entry to an absolute path-like key used by /ns/m
|
|
4350
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4351
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4352
|
+
res.setHeader('Expires', '0');
|
|
4353
|
+
const ver = /^[0-9]+$/.test(verSeg) ? verSeg : String(graphVersion || 0);
|
|
4354
|
+
const origin = getServerOrigin(server) || `${urlObj.protocol}//${urlObj.host}`;
|
|
4355
|
+
// Resolve app main entry to an absolute path-like key used by /ns/m
|
|
4356
|
+
let mainEntry = '/';
|
|
4357
|
+
try {
|
|
4358
|
+
const pkg = getPackageJson();
|
|
4359
|
+
const main = pkg?.main || DEFAULT_MAIN_ENTRY;
|
|
4360
|
+
const abs = getProjectFilePath(main).replace(/\\/g, '/');
|
|
4361
|
+
// Normalize to '/app/...'
|
|
4362
|
+
const marker = `/${APP_ROOT_DIR}/`;
|
|
4363
|
+
const idx = abs.indexOf(marker);
|
|
4364
|
+
mainEntry = idx >= 0 ? abs.substring(idx) : DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
4365
|
+
}
|
|
4366
|
+
catch { }
|
|
4367
|
+
// Build a tiny wrapper that imports the compiled entry runtime from the dev server
|
|
4368
|
+
let code = REQUIRE_GUARD_SNIPPET +
|
|
4369
|
+
`// [ns-entry][v${ver}] wrapper (script-safe) bytes will follow\n` +
|
|
4370
|
+
`(async function(){\n` +
|
|
4371
|
+
` let origin = ${JSON.stringify(origin)}; const main = ${JSON.stringify(mainEntry)}; const __ns_graph_ver = ${JSON.stringify(ver)};\n` +
|
|
4372
|
+
` try { const __b = (globalThis && globalThis.__NS_ENTRY_BASE__) ? String(globalThis.__NS_ENTRY_BASE__) : ''; if (__b) { try { const __o = new URL(__b).origin; if (__o) origin = __o; } catch {} } } catch {}\n` +
|
|
4373
|
+
` const __VERBOSE__ = (typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__) || (globalThis && globalThis.process && globalThis.process.env && globalThis.process.env.verbose) || (globalThis && globalThis.__NS_ENV_VERBOSE__) || ${JSON.stringify(!!verbose)};\n` +
|
|
4374
|
+
` if (__VERBOSE__) console.info('[ns-entry][wrapper] start', { origin, main, ver: __ns_graph_ver });\n` +
|
|
4375
|
+
` async function __ns_import_entry_rt(u){\n` +
|
|
4376
|
+
` // Prefer fetch+eval script transformation to avoid module import limitations on device\n` +
|
|
4377
|
+
` try { const r = await fetch(u); const t = await r.text(); if (__VERBOSE__) console.info('[ns-entry][wrapper] entry-rt fetched bytes', (t&&t.length)||0);\n` +
|
|
4378
|
+
` // Transform 'export default function' or 'export default async function' into global assignment\n` +
|
|
4379
|
+
` let s = t.replace(/export\\s+default\\s+async\\s+function\\s+([A-Za-z0-9_$]+)?/,'globalThis.__NS_START_ENTRY__=async function $1')\n` +
|
|
4380
|
+
` .replace(/export\\s+default\\s+function\\s+([A-Za-z0-9_$]+)?/,'globalThis.__NS_START_ENTRY__=function $1');\n` +
|
|
4381
|
+
` // Fallback: if function-form replacements didn't run, handle expression default export too\n` +
|
|
4382
|
+
` if (String(s).indexOf('__NS_START_ENTRY__') === -1) { s = 'globalThis.__NS_START_ENTRY__=' + s.replace(/export\\s+default\\s*/,''); }\n` +
|
|
4383
|
+
` try { (0,eval)(s); } catch (ee) { console.error('[ns-entry][wrapper] eval entry-rt failed', ee && (ee.message||ee)); throw ee; }\n` +
|
|
4384
|
+
` const fn = globalThis.__NS_START_ENTRY__; if (!fn) { throw new Error('entry-rt missing __NS_START_ENTRY__'); }\n` +
|
|
4385
|
+
` return { default: fn };\n` +
|
|
4386
|
+
` } catch(e) { console.error('[ns-entry][wrapper] entry-rt fetch/eval failed', e && (e.message||e)); throw e; }\n` +
|
|
4387
|
+
` }\n` +
|
|
4388
|
+
` const __entryRtUrl = '/ns/entry-rt?v=' + String(__ns_graph_ver);\n` +
|
|
4389
|
+
` let __mod; try { __mod = await __ns_import_entry_rt(__entryRtUrl); if (__VERBOSE__) console.info('[ns-entry][wrapper] entry-rt ready'); } catch (e) { console.error('[ns-entry][wrapper] failed to prepare entry-rt', e && (e.message||e)); throw e; }\n` +
|
|
4390
|
+
` const startEntry = (__mod && (__mod.default || __mod));\n` +
|
|
4391
|
+
` try { await startEntry({ origin, main, ver: __ns_graph_ver, verbose: !!__VERBOSE__ }); if (__VERBOSE__) console.info('[ns-entry][wrapper] startEntry() resolved'); } catch (e) { console.error('[ns-entry][wrapper] startEntry() failed', e && (e.message||e)); throw e; }\n` +
|
|
4392
|
+
`})();\n`;
|
|
4393
|
+
code = code + `\n//# sourceURL=${origin}/ns/entry`;
|
|
4394
|
+
res.statusCode = 200;
|
|
4395
|
+
res.end(code);
|
|
4396
|
+
}
|
|
4397
|
+
catch (e) {
|
|
4398
|
+
next();
|
|
4399
|
+
}
|
|
4400
|
+
});
|
|
4401
|
+
// 2.6) Transactional HMR endpoint: GET /ns/txn/<ver>
|
|
4402
|
+
// Returns a single ESM that sequentially imports all changed modules for the given graphVersion.
|
|
4403
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4404
|
+
try {
|
|
4405
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4406
|
+
const p = urlObj.pathname || '';
|
|
4407
|
+
if (!p.startsWith('/ns/txn'))
|
|
4408
|
+
return next();
|
|
4409
|
+
let verStr = p.replace('/ns/txn', '').replace(/^\//, '');
|
|
4410
|
+
const ver = Number(verStr || urlObj.searchParams.get('v') || 0);
|
|
4411
|
+
let ids = txnBatches.get(ver) || [];
|
|
4412
|
+
if (!ids.length) {
|
|
4413
|
+
// Attempt to rebuild from any changed modules at this version if present in graph history is unavailable.
|
|
4414
|
+
// Fallback heuristic: use all modules with latest hash change equal to this version (we don't store per-module version, so use any changedIds from query 'ids' if provided)
|
|
4415
|
+
try {
|
|
4416
|
+
const q = (urlObj.searchParams.get('ids') || '')
|
|
4417
|
+
.split(',')
|
|
4418
|
+
.map((s) => s.trim())
|
|
4419
|
+
.filter(Boolean);
|
|
4420
|
+
if (q.length)
|
|
4421
|
+
ids = computeTxnOrderForChanged(q);
|
|
4422
|
+
}
|
|
4423
|
+
catch { }
|
|
4424
|
+
}
|
|
4425
|
+
const origin = getServerOrigin(server) || `${urlObj.protocol}//${urlObj.host}`;
|
|
4426
|
+
const lines = [];
|
|
4427
|
+
lines.push(`// [txn] version=${ver} count=${ids.length}`);
|
|
4428
|
+
if (!ids.length) {
|
|
4429
|
+
lines.push(`export default true;`);
|
|
4430
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4431
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
4432
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4433
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4434
|
+
res.setHeader('Expires', '0');
|
|
4435
|
+
res.statusCode = 200;
|
|
4436
|
+
res.end(lines.join('\n'));
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
for (const id of ids) {
|
|
4440
|
+
const isVue = /\.vue$/i.test(id);
|
|
4441
|
+
const safe = id.startsWith('/') ? id : '/' + id;
|
|
4442
|
+
const abs = isVue ? `/ns/asm/${ver}?path=${encodeURIComponent(safe)}` : `/ns/m${safe}`;
|
|
4443
|
+
lines.push(`await import(${JSON.stringify(abs)});`);
|
|
4444
|
+
}
|
|
4445
|
+
lines.push(`export default true;`);
|
|
4446
|
+
const code = lines.join('\n');
|
|
4447
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4448
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
4449
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4450
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4451
|
+
res.setHeader('Expires', '0');
|
|
4452
|
+
res.statusCode = 200;
|
|
4453
|
+
res.end(code);
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
catch (e) {
|
|
4457
|
+
/* fallthrough */
|
|
4458
|
+
}
|
|
4459
|
+
return next();
|
|
4460
|
+
});
|
|
4461
|
+
// 3) ESM endpoint for SFC modules: GET /ns/sfc?path=/src/Comp.vue[?vue&type=*] OR /ns/sfc/src/Comp.vue[?vue&type=*]
|
|
4462
|
+
// Also accept alias /ns/sfc
|
|
4463
|
+
// Preserves variant queries (?vue&type=script|template|style) and adds a diagnostic signature comment.
|
|
4464
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4465
|
+
try {
|
|
4466
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4467
|
+
const p = urlObj.pathname;
|
|
4468
|
+
// Only match exactly "/ns/sfc" or paths under it.
|
|
4469
|
+
const isNs = p === '/ns/sfc' || p.startsWith('/ns/sfc/');
|
|
4470
|
+
if (!isNs)
|
|
4471
|
+
return next();
|
|
4472
|
+
if (p.startsWith('/ns/asm') || p.startsWith('/ns/sfc-meta'))
|
|
4473
|
+
return next();
|
|
4474
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4475
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
4476
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4477
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4478
|
+
res.setHeader('Expires', '0');
|
|
4479
|
+
const base = '/ns/sfc';
|
|
4480
|
+
// Determine request spec, preserving variant query when present and handling optional version in path
|
|
4481
|
+
let pathParam = urlObj.searchParams.get('path') || ''; // may include its own query
|
|
4482
|
+
const rawRemainder = urlObj.pathname.slice(base.length) || '';
|
|
4483
|
+
let verFromPath = null;
|
|
4484
|
+
let pathStyle = rawRemainder;
|
|
4485
|
+
if (rawRemainder && rawRemainder.startsWith('/')) {
|
|
4486
|
+
const parts = rawRemainder.split('/'); // ["", maybe "<ver>", ...]
|
|
4487
|
+
if (parts.length > 2 && /^[0-9]+$/.test(parts[1] || '')) {
|
|
4488
|
+
verFromPath = parts[1];
|
|
4489
|
+
pathStyle = '/' + parts.slice(2).join('/');
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
if (pathStyle && pathStyle !== '/' && !pathParam) {
|
|
4493
|
+
if (!pathStyle.startsWith('/'))
|
|
4494
|
+
pathStyle = '/' + pathStyle;
|
|
4495
|
+
// Include endpoint query for variant-style requests (e.g. /ns/sfc/Comp.vue?vue&type=template)
|
|
4496
|
+
pathParam = pathStyle + (urlObj.search || '');
|
|
4497
|
+
}
|
|
4498
|
+
let fullSpec = pathParam || '';
|
|
4499
|
+
if (!fullSpec) {
|
|
4500
|
+
res.statusCode = 200;
|
|
4501
|
+
res.end('export {}\n');
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
if (fullSpec.startsWith('@/'))
|
|
4505
|
+
fullSpec = APP_VIRTUAL_WITH_SLASH + fullSpec.slice(2);
|
|
4506
|
+
if (!fullSpec.startsWith('/'))
|
|
4507
|
+
fullSpec = '/' + fullSpec;
|
|
4508
|
+
const isVariant = /[?&]vue&type=/.test(fullSpec);
|
|
4509
|
+
const variantTypeMatch = /[?&]type=([^&]+)/.exec(fullSpec);
|
|
4510
|
+
const variantType = variantTypeMatch?.[1] || null;
|
|
4511
|
+
const isStyleVariant = /[?&]type=style\b/.test(fullSpec);
|
|
4512
|
+
// Determine candidate for transformRequest
|
|
4513
|
+
// For full SFCs we prefer a clean base path + '?vue'; if that fails, try base without query as fallback.
|
|
4514
|
+
let candidate = fullSpec;
|
|
4515
|
+
let transformed = null;
|
|
4516
|
+
if (!isVariant) {
|
|
4517
|
+
const basePath = fullSpec.replace(/[?#].*$/, '');
|
|
4518
|
+
const candidates = [basePath + (basePath.includes('?') ? '&' : '?') + 'vue', basePath];
|
|
4519
|
+
for (const c of candidates) {
|
|
4520
|
+
try {
|
|
4521
|
+
const r = await server.transformRequest(c);
|
|
4522
|
+
if (r?.code) {
|
|
4523
|
+
transformed = r;
|
|
4524
|
+
candidate = c;
|
|
4525
|
+
break;
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
catch { }
|
|
4529
|
+
}
|
|
4530
|
+
if (!transformed?.code) {
|
|
4531
|
+
if (verbose) {
|
|
4532
|
+
try {
|
|
4533
|
+
console.warn(`[sfc][serve] transform miss for`, fullSpec);
|
|
4534
|
+
}
|
|
4535
|
+
catch { }
|
|
4536
|
+
}
|
|
4537
|
+
// Emit an erroring module to surface the failure at import site with helpful hints
|
|
4538
|
+
try {
|
|
4539
|
+
const tried = candidates.slice(0, 8);
|
|
4540
|
+
const out = `// [sfc] transform miss kind=full path=${fullSpec.replace(/\n/g, '')} tried=${tried.length}\n` + `throw new Error(${JSON.stringify('[ns/sfc] transform failed for full SFC: ' + fullSpec + ' (tried ' + tried.length + ')')});\nexport {}\n`;
|
|
4541
|
+
res.statusCode = 404;
|
|
4542
|
+
res.end(out);
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
catch {
|
|
4546
|
+
res.statusCode = 404;
|
|
4547
|
+
res.end('export {}\n');
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
else {
|
|
4553
|
+
try {
|
|
4554
|
+
transformed = await server.transformRequest(candidate);
|
|
4555
|
+
}
|
|
4556
|
+
catch { }
|
|
4557
|
+
if (!transformed?.code) {
|
|
4558
|
+
try {
|
|
4559
|
+
const out = `// [sfc] transform miss kind=variant path=${fullSpec.replace(/\n/g, '')}\n` + `throw new Error(${JSON.stringify('[ns/sfc] transform failed for variant: ' + fullSpec)});\nexport {}\n`;
|
|
4560
|
+
res.statusCode = 404;
|
|
4561
|
+
res.end(out);
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
catch {
|
|
4565
|
+
res.statusCode = 404;
|
|
4566
|
+
res.end('export {}\n');
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
// For style variants, return an empty module immediately
|
|
4572
|
+
if (isStyleVariant) {
|
|
4573
|
+
const sig = `// [sfc] kind=variant:style path=${fullSpec.replace(/\n/g, '')} len=0 default=false\n`;
|
|
4574
|
+
res.statusCode = 200;
|
|
4575
|
+
res.end(`${sig}export {}\n`);
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4578
|
+
let code = transformed.code;
|
|
4579
|
+
// Prepend guard to capture any URL-based require attempts
|
|
4580
|
+
code = REQUIRE_GUARD_SNIPPET + code;
|
|
4581
|
+
const projectRoot = server.config?.root || process.cwd();
|
|
4582
|
+
// IMPORTANT: Do not run cleanCode() on template variant; it can strip required pieces.
|
|
4583
|
+
// We'll handle script/full SFC below, and treat template minimally right away.
|
|
4584
|
+
// Full SFCs delegate to deterministic assembler module; variants (script/template) still go through processing
|
|
4585
|
+
if (!isVariant) {
|
|
4586
|
+
const importerPath = fullSpec.replace(/[?#].*$/, '');
|
|
4587
|
+
const origin = getServerOrigin(server);
|
|
4588
|
+
const ver = verFromPath || '0';
|
|
4589
|
+
const asmPath = `/ns/asm/${ver}?path=${encodeURIComponent(importerPath)}`;
|
|
4590
|
+
const delegated = `// [sfc] kind=full (delegated to assembler) path=${importerPath}\nexport * from ${JSON.stringify(asmPath)};\nexport { default } from ${JSON.stringify(asmPath)};\n`;
|
|
4591
|
+
res.statusCode = 200;
|
|
4592
|
+
res.end(delegated);
|
|
4593
|
+
return;
|
|
4594
|
+
}
|
|
4595
|
+
else {
|
|
4596
|
+
// Variants
|
|
4597
|
+
if (variantType === 'template') {
|
|
4598
|
+
const preferSelfCompile = !!process.env.NS_HMR_SELF_COMPILE_TEMPLATE;
|
|
4599
|
+
// Compile the template ourselves to guarantee no Vite HMR code and stable output
|
|
4600
|
+
if (preferSelfCompile)
|
|
4601
|
+
try {
|
|
4602
|
+
const projectRootT = server.config?.root || process.cwd();
|
|
4603
|
+
const basePath = fullSpec.replace(/[?#].*$/, '');
|
|
4604
|
+
const abs = path.join(projectRootT, basePath.replace(/^\//, ''));
|
|
4605
|
+
let sfcSrc = '';
|
|
4606
|
+
try {
|
|
4607
|
+
sfcSrc = readFileSync(abs, 'utf-8');
|
|
4608
|
+
}
|
|
4609
|
+
catch { }
|
|
4610
|
+
if (sfcSrc) {
|
|
4611
|
+
const { descriptor } = parse(sfcSrc, { filename: abs });
|
|
4612
|
+
const id = createHash('md5').update(abs).digest('hex').slice(0, 8);
|
|
4613
|
+
let bindingMetadata = undefined;
|
|
4614
|
+
try {
|
|
4615
|
+
const s = compileScript(descriptor, {
|
|
4616
|
+
id,
|
|
4617
|
+
inlineTemplate: false,
|
|
4618
|
+
reactivityTransform: false,
|
|
4619
|
+
});
|
|
4620
|
+
bindingMetadata = s?.bindings;
|
|
4621
|
+
}
|
|
4622
|
+
catch { }
|
|
4623
|
+
const tpl = descriptor.template?.content || '';
|
|
4624
|
+
const ct = compileTemplate({
|
|
4625
|
+
source: tpl,
|
|
4626
|
+
id,
|
|
4627
|
+
filename: abs,
|
|
4628
|
+
isProd: false,
|
|
4629
|
+
ssr: false,
|
|
4630
|
+
compilerOptions: {
|
|
4631
|
+
bindingMetadata,
|
|
4632
|
+
isCustomElement: (tag) => NS_NATIVE_TAGS.has(tag),
|
|
4633
|
+
},
|
|
4634
|
+
});
|
|
4635
|
+
let out = (ct && (ct.code || '')) || '';
|
|
4636
|
+
// Map Vue helper imports to runtime bridge
|
|
4637
|
+
try {
|
|
4638
|
+
out = out.replace(/from\s+["'](?:nativescript-vue|vue)[^"']*["']/g, 'from "/ns/rt"');
|
|
4639
|
+
}
|
|
4640
|
+
catch { }
|
|
4641
|
+
// No import.meta.hot present when compiling ourselves, but keep minimal sanitizer just in case
|
|
4642
|
+
out = processTemplateVariantMinimal(out);
|
|
4643
|
+
code = out;
|
|
4644
|
+
}
|
|
4645
|
+
else {
|
|
4646
|
+
code = 'export {}\n';
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
catch (eTplSelf) {
|
|
4650
|
+
if (verbose) {
|
|
4651
|
+
try {
|
|
4652
|
+
console.warn('[sfc][template][self-compile][fail]', fullSpec, eTplSelf?.message);
|
|
4653
|
+
}
|
|
4654
|
+
catch { }
|
|
4655
|
+
}
|
|
4656
|
+
code = transformed.code || 'export {}\n';
|
|
4657
|
+
code = processTemplateVariantMinimal(code);
|
|
4658
|
+
}
|
|
4659
|
+
else {
|
|
4660
|
+
// Prefer using Vite's template transform and apply minimal sanitization; avoids compiler mismatches and warnings
|
|
4661
|
+
code = transformed.code || 'export {}\n';
|
|
4662
|
+
code = processTemplateVariantMinimal(code);
|
|
4663
|
+
}
|
|
4664
|
+
// fall through to shared post-processing (versioning, signature, etc.)
|
|
4665
|
+
}
|
|
4666
|
+
// Script variants still need vendor mappings and general device processing (no SFC assembly)
|
|
4667
|
+
// IMPORTANT: Use a Babel AST transform to remove imports of the template variant and
|
|
4668
|
+
// neutralize their usage without brittle regex.
|
|
4669
|
+
try {
|
|
4670
|
+
const ast = babelParse(code, {
|
|
4671
|
+
sourceType: 'module',
|
|
4672
|
+
plugins: ['typescript'],
|
|
4673
|
+
});
|
|
4674
|
+
const templateBindings = new Set();
|
|
4675
|
+
const navToLocals = [];
|
|
4676
|
+
const navBackLocals = [];
|
|
4677
|
+
babelTraverse(ast, {
|
|
4678
|
+
ImportDeclaration(path) {
|
|
4679
|
+
const spec = path.node.source.value || '';
|
|
4680
|
+
// Remove template variant imports and collect their local identifiers for neutralization
|
|
4681
|
+
if (typeof spec === 'string' && /\.vue\?[^\n]*type=template/.test(spec)) {
|
|
4682
|
+
const ids = [];
|
|
4683
|
+
for (const s of path.node.specifiers) {
|
|
4684
|
+
if (t.isImportSpecifier(s)) {
|
|
4685
|
+
const imported = t.isIdentifier(s.imported) ? s.imported.name : undefined;
|
|
4686
|
+
const local = t.isIdentifier(s.local) ? s.local.name : undefined;
|
|
4687
|
+
if ((imported === 'render' || imported === undefined) && local)
|
|
4688
|
+
ids.push(local);
|
|
4689
|
+
}
|
|
4690
|
+
else if (t.isImportDefaultSpecifier(s) || t.isImportNamespaceSpecifier(s)) {
|
|
4691
|
+
if (t.isIdentifier(s.local))
|
|
4692
|
+
ids.push(s.local.name);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
ids.forEach((n) => templateBindings.add(n));
|
|
4696
|
+
path.remove();
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
// Rewrite $navigateTo/$navigateBack imports from nativescript-vue (or prebundle) to use globals
|
|
4700
|
+
const isNsVue = typeof spec === 'string' && (/nativescript-vue/.test(spec) || /vendor\.mjs$/.test(spec) || /\/node_modules\/\.vite\/deps\/nativescript-vue\.js/.test(spec));
|
|
4701
|
+
if (isNsVue) {
|
|
4702
|
+
const remain = [];
|
|
4703
|
+
for (const s of path.node.specifiers) {
|
|
4704
|
+
if (t.isImportSpecifier(s)) {
|
|
4705
|
+
const imported = t.isIdentifier(s.imported) ? s.imported.name : undefined;
|
|
4706
|
+
const local = t.isIdentifier(s.local) ? s.local.name : undefined;
|
|
4707
|
+
if (local && (imported === '$navigateTo' || imported === 'navigateTo')) {
|
|
4708
|
+
navToLocals.push(local);
|
|
4709
|
+
continue;
|
|
4710
|
+
}
|
|
4711
|
+
if (local && (imported === '$navigateBack' || imported === 'navigateBack')) {
|
|
4712
|
+
navBackLocals.push(local);
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
remain.push(s);
|
|
4717
|
+
}
|
|
4718
|
+
if (remain.length) {
|
|
4719
|
+
path.node.specifiers = remain;
|
|
4720
|
+
}
|
|
4721
|
+
else {
|
|
4722
|
+
path.remove();
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
},
|
|
4726
|
+
});
|
|
4727
|
+
if (templateBindings.size) {
|
|
4728
|
+
babelTraverse(ast, {
|
|
4729
|
+
Identifier(path) {
|
|
4730
|
+
if (templateBindings.has(path.node.name)) {
|
|
4731
|
+
path.replaceWith(t.identifier('undefined'));
|
|
4732
|
+
}
|
|
4733
|
+
},
|
|
4734
|
+
AssignmentExpression(path) {
|
|
4735
|
+
// Guard component.render = <alias> to avoid TDZ when alias is undefined
|
|
4736
|
+
if (t.isMemberExpression(path.node.left) &&
|
|
4737
|
+
t.isIdentifier(path.node.left.property, {
|
|
4738
|
+
name: 'render',
|
|
4739
|
+
})) {
|
|
4740
|
+
const e = t.identifier('__e');
|
|
4741
|
+
const guarded = t.tryStatement(t.blockStatement([t.variableDeclaration('const', [t.variableDeclarator(e, path.node.right)]), t.ifStatement(t.logicalExpression('&&', t.binaryExpression('!==', t.unaryExpression('typeof', path.node.left.object, true), t.stringLiteral('undefined')), t.binaryExpression('!==', t.unaryExpression('typeof', e, true), t.stringLiteral('undefined'))), t.blockStatement([t.expressionStatement(t.assignmentExpression('=', path.node.left, e))]))]), t.catchClause(t.identifier('_e'), t.blockStatement([])));
|
|
4742
|
+
path.replaceWithMultiple([guarded]);
|
|
4743
|
+
}
|
|
4744
|
+
},
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4747
|
+
let outCode = genCode(ast).code;
|
|
4748
|
+
if (navToLocals.length || navBackLocals.length) {
|
|
4749
|
+
const shimLines = [];
|
|
4750
|
+
for (const n of navToLocals)
|
|
4751
|
+
shimLines.push(`import __ns_rt_nav_to_mod from "/ns/rt";\nconst ${n} = (...args) => __ns_rt_nav_to_mod.$navigateTo(...args);`);
|
|
4752
|
+
for (const n of navBackLocals)
|
|
4753
|
+
shimLines.push(`import __ns_rt_nav_back_mod from "/ns/rt";\nconst ${n} = (...args) => __ns_rt_nav_back_mod.$navigateBack(...args);`);
|
|
4754
|
+
outCode = shimLines.join('\n') + '\n' + outCode;
|
|
4755
|
+
}
|
|
4756
|
+
code = outCode;
|
|
4757
|
+
}
|
|
4758
|
+
catch { }
|
|
4759
|
+
code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(fullSpec), fullSpec);
|
|
4760
|
+
// Transform static .vue imports into static imports from the assembler (no TLA) via AST
|
|
4761
|
+
try {
|
|
4762
|
+
const importerPath = fullSpec.replace(/[?#].*$/, '');
|
|
4763
|
+
const origin = getServerOrigin(server);
|
|
4764
|
+
const ver = verFromPath || '0';
|
|
4765
|
+
const ast2 = babelParse(code, {
|
|
4766
|
+
sourceType: 'module',
|
|
4767
|
+
plugins: ['typescript'],
|
|
4768
|
+
});
|
|
4769
|
+
babelTraverse(ast2, {
|
|
4770
|
+
ImportDeclaration(p) {
|
|
4771
|
+
const src = p.node.source.value || '';
|
|
4772
|
+
if (typeof src !== 'string')
|
|
4773
|
+
return;
|
|
4774
|
+
if (/^https?:\/\//.test(src))
|
|
4775
|
+
return; // leave absolute URLs
|
|
4776
|
+
if (/\.vue(?:$|\?)/.test(src)) {
|
|
4777
|
+
let spec = src;
|
|
4778
|
+
// Resolve to absolute project path
|
|
4779
|
+
if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
4780
|
+
spec = path.posix.normalize(path.posix.join(path.posix.dirname(importerPath), spec));
|
|
4781
|
+
if (!spec.startsWith('/'))
|
|
4782
|
+
spec = '/' + spec;
|
|
4783
|
+
}
|
|
4784
|
+
else if (!spec.startsWith('/')) {
|
|
4785
|
+
// Handle '@/'
|
|
4786
|
+
if (spec.startsWith('@@/'))
|
|
4787
|
+
spec = '/' + spec.slice(2);
|
|
4788
|
+
if (spec.startsWith('@/'))
|
|
4789
|
+
spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
|
|
4790
|
+
}
|
|
4791
|
+
// Strip query for plain .vue (keep variant imports intact)
|
|
4792
|
+
if (!/\bvue&type=/.test(src)) {
|
|
4793
|
+
spec = spec.replace(/[?#].*$/, '');
|
|
4794
|
+
const asmUrl = `/ns/asm/${ver}?path=${encodeURIComponent(spec)}&mode=inline`;
|
|
4795
|
+
p.node.source = t.stringLiteral(asmUrl);
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
},
|
|
4799
|
+
});
|
|
4800
|
+
code = genCode(ast2).code;
|
|
4801
|
+
}
|
|
4802
|
+
catch { }
|
|
4803
|
+
// After rewrites, strip any TypeScript syntax from the script variant to avoid device-side parse errors
|
|
4804
|
+
try {
|
|
4805
|
+
const importerPath = fullSpec.replace(/[?#].*$/, '');
|
|
4806
|
+
const tsRes = await babelCore.transformAsync(code, {
|
|
4807
|
+
plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
|
|
4808
|
+
sourceType: 'module',
|
|
4809
|
+
// Help Babel infer TS parsing even if the virtual filename isn't .ts
|
|
4810
|
+
filename: importerPath.endsWith('.vue') ? importerPath.replace(/\.vue$/, '.ts') : importerPath + '.ts',
|
|
4811
|
+
comments: true,
|
|
4812
|
+
configFile: false,
|
|
4813
|
+
babelrc: false,
|
|
4814
|
+
});
|
|
4815
|
+
if (tsRes?.code) {
|
|
4816
|
+
code = tsRes.code;
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
catch (eTsVar) {
|
|
4820
|
+
if (verbose) {
|
|
4821
|
+
try {
|
|
4822
|
+
console.warn('[sfc][variant:script][babel-ts][fail]', fullSpec, eTsVar?.message);
|
|
4823
|
+
}
|
|
4824
|
+
catch { }
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
const importerPath = fullSpec.replace(/[?#].*$/, '');
|
|
4829
|
+
// Only run cleanCode for non-template cases (script/full). Template code must remain intact.
|
|
4830
|
+
if (!isVariant || variantType !== 'template') {
|
|
4831
|
+
code = cleanCode(code);
|
|
4832
|
+
}
|
|
4833
|
+
code = rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, getServerOrigin(server));
|
|
4834
|
+
code = ensureVariableDynamicImportHelper(code);
|
|
4835
|
+
try {
|
|
4836
|
+
// For variant requests under /ns/sfc, prefer the version from the path segment when present
|
|
4837
|
+
// so that any internal '/ns/rt', '/ns/core', or '/ns/sfc' imports are aligned with the same version.
|
|
4838
|
+
const verNum = Number(verFromPath || '0');
|
|
4839
|
+
if (Number.isFinite(verNum) && verNum > 0) {
|
|
4840
|
+
code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
|
|
4841
|
+
code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
|
|
4842
|
+
code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
|
|
4843
|
+
}
|
|
4844
|
+
else {
|
|
4845
|
+
code = ensureVersionedRtImports(code, getServerOrigin(server), graphVersion);
|
|
4846
|
+
code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), graphVersion);
|
|
4847
|
+
code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
catch { }
|
|
4851
|
+
// Final guard for SFC variant output as well
|
|
4852
|
+
try {
|
|
4853
|
+
code = ensureDestructureCoreImports(code);
|
|
4854
|
+
}
|
|
4855
|
+
catch { }
|
|
4856
|
+
// CRITICAL: As a last step for script/template variants, re-run AST normalization and strip
|
|
4857
|
+
// any sentinel destructures that could cause duplicate locals, then re-apply core versioning.
|
|
4858
|
+
try {
|
|
4859
|
+
code = astNormalizeModuleImportsAndHelpers(code);
|
|
4860
|
+
}
|
|
4861
|
+
catch { }
|
|
4862
|
+
try {
|
|
4863
|
+
// Remove any rt->core sentinel destructures that slipped in late
|
|
4864
|
+
code = stripRtCoreSentinel(code);
|
|
4865
|
+
}
|
|
4866
|
+
catch { }
|
|
4867
|
+
try {
|
|
4868
|
+
const verNum = Number(verFromPath || '0');
|
|
4869
|
+
if (Number.isFinite(verNum) && verNum > 0) {
|
|
4870
|
+
code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
|
|
4871
|
+
code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
|
|
4872
|
+
}
|
|
4873
|
+
else {
|
|
4874
|
+
code = ensureVersionedRtImports(code, getServerOrigin(server), graphVersion);
|
|
4875
|
+
code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
catch { }
|
|
4879
|
+
// Last-chance sanitizer for dangling Vite CJS import helper usages that may surface after late transforms
|
|
4880
|
+
try {
|
|
4881
|
+
code = stripDanglingViteCjsImports(code);
|
|
4882
|
+
}
|
|
4883
|
+
catch { }
|
|
4884
|
+
const hasDefault = /\bexport\s+default\b/.test(code);
|
|
4885
|
+
const kind = isVariant ? `variant:${variantType || 'unknown'}` : 'full';
|
|
4886
|
+
const sig = `// [sfc] kind=${kind} path=${importerPath} len=${code.length} default=${hasDefault} wrapped=${false}\n`;
|
|
4887
|
+
if (verbose) {
|
|
4888
|
+
try {
|
|
4889
|
+
console.log(`[sfc][serve] ${fullSpec} kind=${kind} default=${hasDefault} bytes=${code.length}`);
|
|
4890
|
+
}
|
|
4891
|
+
catch { }
|
|
4892
|
+
}
|
|
4893
|
+
// Ensure script variants always provide a default export if they declare a component
|
|
4894
|
+
if (!hasDefault) {
|
|
4895
|
+
// Prefer an explicit identifier if present
|
|
4896
|
+
const m = code.match(/\b(?:const|let|var)\s+(__ns_sfc__|_sfc_main)\b/);
|
|
4897
|
+
if (m && m[1]) {
|
|
4898
|
+
code += `\nexport default ${m[1]};`;
|
|
4899
|
+
}
|
|
4900
|
+
else if (/\b_defineComponent\s*\(|\bdefineComponent\s*\(/.test(code)) {
|
|
4901
|
+
// Fallback: export whichever is defined at runtime without throwing on missing identifiers
|
|
4902
|
+
code += `\nexport default (typeof __ns_sfc__ !== "undefined" ? __ns_sfc__ : (typeof _sfc_main !== "undefined" ? _sfc_main : undefined));`;
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
res.statusCode = 200;
|
|
4906
|
+
res.end(sig + code);
|
|
4907
|
+
}
|
|
4908
|
+
catch (e) {
|
|
4909
|
+
res.statusCode = 500;
|
|
4910
|
+
res.end('export {}\n');
|
|
4911
|
+
}
|
|
4912
|
+
});
|
|
4913
|
+
// 4) JSON metadata endpoint for SFCs: GET /ns/sfc-meta?path=/src/Comp.vue OR /ns/sfc-meta/<ver>?path=/src/Comp.vue
|
|
4914
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4915
|
+
try {
|
|
4916
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4917
|
+
if (!urlObj.pathname.startsWith('/ns/sfc-meta'))
|
|
4918
|
+
return next();
|
|
4919
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4920
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
4921
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
4922
|
+
res.setHeader('Pragma', 'no-cache');
|
|
4923
|
+
res.setHeader('Expires', '0');
|
|
4924
|
+
// Accept optional version segment similar to /ns/sfc
|
|
4925
|
+
{
|
|
4926
|
+
const metaBase = '/ns/sfc-meta';
|
|
4927
|
+
if (urlObj.pathname.startsWith(metaBase + '/')) {
|
|
4928
|
+
const rawRemainder = urlObj.pathname.slice(metaBase.length);
|
|
4929
|
+
const parts = rawRemainder.split('/');
|
|
4930
|
+
if (parts.length > 2 && /^[0-9]+$/.test(parts[1] || '')) {
|
|
4931
|
+
// consume version but we don't need it server-side
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
let spec = urlObj.searchParams.get('path') || '';
|
|
4936
|
+
if (!spec) {
|
|
4937
|
+
res.statusCode = 400;
|
|
4938
|
+
res.end(JSON.stringify({ error: 'missing path' }));
|
|
4939
|
+
return;
|
|
4940
|
+
}
|
|
4941
|
+
if (spec.startsWith('@/'))
|
|
4942
|
+
spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
|
|
4943
|
+
if (!spec.startsWith('/'))
|
|
4944
|
+
spec = '/' + spec;
|
|
4945
|
+
const base = spec.replace(/[?#].*$/, '');
|
|
4946
|
+
// Transform variants to inspect exports
|
|
4947
|
+
const [scriptR, templateR] = await Promise.all([server.transformRequest(base + '?vue&type=script'), server.transformRequest(base + '?vue&type=template')]);
|
|
4948
|
+
const scriptCode = scriptR?.code || '';
|
|
4949
|
+
const templateCode = templateR?.code || '';
|
|
4950
|
+
const scriptMeta = extractExportMetadata(scriptCode);
|
|
4951
|
+
// Robust render detection: Vue compiler may emit several shapes:
|
|
4952
|
+
// 1) export function render(_ctx, _cache) { ... }
|
|
4953
|
+
// 2) function render(_ctx,_cache) { ... } (later exported)
|
|
4954
|
+
// 3) export const render = (_ctx,_cache) => { ... }
|
|
4955
|
+
// 4) const render = (...) => { ... } (later exported)
|
|
4956
|
+
// 5) export { render } or export { render as render }
|
|
4957
|
+
// 6) Object property forms (rare in template output) render: (...) => {}
|
|
4958
|
+
const hasRender = /export\s+function\s+render\s*\(/.test(templateCode) || /(?:^|\n)\s*function\s+render\s*\(/.test(templateCode) || /export\s+(?:const|let|var)\s+render\s*=/.test(templateCode) || /(?:^|\n)\s*(?:const|let|var)\s+render\s*=/.test(templateCode) || /\brender\s*[:=]\s*/.test(templateCode) || /export\s*\{\s*render\s*(?:as\s*render)?\s*\}/.test(templateCode);
|
|
4959
|
+
if (hasRender && verbose) {
|
|
4960
|
+
try {
|
|
4961
|
+
console.log('[sfc-meta] detected render for', base);
|
|
4962
|
+
}
|
|
4963
|
+
catch { }
|
|
4964
|
+
}
|
|
4965
|
+
else if (!hasRender && verbose) {
|
|
4966
|
+
try {
|
|
4967
|
+
console.warn('[sfc-meta] render NOT detected for', base);
|
|
4968
|
+
}
|
|
4969
|
+
catch { }
|
|
4970
|
+
}
|
|
4971
|
+
const hash = createHash('md5').update(base).digest('hex').slice(0, 8);
|
|
4972
|
+
const payload = {
|
|
4973
|
+
path: base,
|
|
4974
|
+
hasScript: !!scriptCode,
|
|
4975
|
+
hasTemplate: !!templateCode,
|
|
4976
|
+
hasStyle: false,
|
|
4977
|
+
scriptExports: scriptMeta.named,
|
|
4978
|
+
scriptHasDefault: scriptMeta.hasDefault,
|
|
4979
|
+
templateHasRender: hasRender,
|
|
4980
|
+
hmrId: hash,
|
|
4981
|
+
};
|
|
4982
|
+
res.statusCode = 200;
|
|
4983
|
+
res.end(JSON.stringify(payload));
|
|
4984
|
+
}
|
|
4985
|
+
catch (e) {
|
|
4986
|
+
res.statusCode = 500;
|
|
4987
|
+
res.end(JSON.stringify({ error: e?.message || String(e) }));
|
|
4988
|
+
}
|
|
4989
|
+
});
|
|
4990
|
+
// 5) Deterministic SFC assembler: GET /ns/asm?path=/src/Comp.vue
|
|
4991
|
+
// Place BEFORE any broader /ns/sfc* handlers that might accidentally match and delegate.
|
|
4992
|
+
server.middlewares.use(async (req, res, next) => {
|
|
4993
|
+
try {
|
|
4994
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
4995
|
+
if (!urlObj.pathname.startsWith('/ns/asm'))
|
|
4996
|
+
return next();
|
|
4997
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4998
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
4999
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
5000
|
+
res.setHeader('Pragma', 'no-cache');
|
|
5001
|
+
res.setHeader('Expires', '0');
|
|
5002
|
+
// Optional version segment as first path component after /ns/asm
|
|
5003
|
+
const asmBase = '/ns/asm';
|
|
5004
|
+
const asmRemainder = urlObj.pathname.slice(asmBase.length) || '';
|
|
5005
|
+
let verFromPath = null;
|
|
5006
|
+
if (asmRemainder && asmRemainder.startsWith('/')) {
|
|
5007
|
+
const p = asmRemainder.split('/');
|
|
5008
|
+
if (p.length > 1 && /^[0-9]+$/.test(p[1] || '')) {
|
|
5009
|
+
verFromPath = p[1];
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
let spec = urlObj.searchParams.get('path') || '';
|
|
5013
|
+
const diag = urlObj.searchParams.get('diag') === '1';
|
|
5014
|
+
if (!spec) {
|
|
5015
|
+
res.statusCode = 400;
|
|
5016
|
+
res.end('export {}\n');
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
if (spec.startsWith('@/'))
|
|
5020
|
+
spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
|
|
5021
|
+
if (!spec.startsWith('/'))
|
|
5022
|
+
spec = '/' + spec;
|
|
5023
|
+
const base = spec.replace(/[?#].*$/, '');
|
|
5024
|
+
if (diag) {
|
|
5025
|
+
const code = `// [sfc-asm] ${base} (diag)\n` + `// vue shim for diag-only instantiation\n` + `var _createElementVNode = globalThis.createElementVNode || globalThis._createElementVNode;\n` + `const __ns_sfc__ = { name: ${JSON.stringify(base.split('/').pop() || 'DiagComp')}, render(){ return _createElementVNode ? _createElementVNode('StackLayout') : (globalThis.createElementVNode ? globalThis.createElementVNode('StackLayout') : {}); } };\nexport default __ns_sfc__;\n`;
|
|
5026
|
+
res.statusCode = 200;
|
|
5027
|
+
res.end(code);
|
|
5028
|
+
return;
|
|
5029
|
+
}
|
|
5030
|
+
const projectRoot = server.config?.root || process.cwd();
|
|
5031
|
+
// Ensure variant transforms exist so imports resolve (avoid Promise.all short-circuit on single failure)
|
|
5032
|
+
const safeTransform = async (cand) => {
|
|
5033
|
+
try {
|
|
5034
|
+
return await server.transformRequest(cand);
|
|
5035
|
+
}
|
|
5036
|
+
catch {
|
|
5037
|
+
return null;
|
|
5038
|
+
}
|
|
5039
|
+
};
|
|
5040
|
+
const scriptR = await safeTransform(base + '?vue&type=script');
|
|
5041
|
+
const templateR = await safeTransform(base + '?vue&type=template');
|
|
5042
|
+
const fullR = await safeTransform(base + '?vue');
|
|
5043
|
+
const hasScript = !!scriptR?.code;
|
|
5044
|
+
const hasTemplate = !!templateR?.code;
|
|
5045
|
+
const origin = getServerOrigin(server);
|
|
5046
|
+
const ver = String(verFromPath || graphVersion || Date.now());
|
|
5047
|
+
const scriptUrl = `${origin}/ns/sfc/${ver}${base}?vue&type=script`;
|
|
5048
|
+
const templateCode = templateR?.code || '';
|
|
5049
|
+
// INLINE-FIRST assembler: compile SFC source into a self-contained ESM module (enhanced diagnostics)
|
|
5050
|
+
try {
|
|
5051
|
+
const root = server.config?.root || process.cwd();
|
|
5052
|
+
const abs = path.join(root, base.replace(/^\//, ''));
|
|
5053
|
+
let sfcSrc = '';
|
|
5054
|
+
try {
|
|
5055
|
+
sfcSrc = readFileSync(abs, 'utf-8');
|
|
5056
|
+
}
|
|
5057
|
+
catch { }
|
|
5058
|
+
if (sfcSrc) {
|
|
5059
|
+
const { descriptor } = parse(sfcSrc, { filename: abs });
|
|
5060
|
+
const id = createHash('md5').update(abs).digest('hex').slice(0, 8);
|
|
5061
|
+
// 1) Compile script (prefer inlineTemplate for a complete module)
|
|
5062
|
+
let compiledScript = '';
|
|
5063
|
+
let bindingMetadata = undefined;
|
|
5064
|
+
let triedInlineTemplate = false;
|
|
5065
|
+
let hadScriptDefaultPre = false;
|
|
5066
|
+
let usedInlineScript = false;
|
|
5067
|
+
try {
|
|
5068
|
+
// First try inlineTemplate for a holistic, self-contained module with render + hoists
|
|
5069
|
+
// Use a strict NativeScript native element detector for inlineTemplate that does NOT treat generic PascalCase as native.
|
|
5070
|
+
// This ensures imported components like PageWrapper remain true components and get referenced via bindings.
|
|
5071
|
+
const isNSNative = (tag) => NS_NATIVE_TAGS.has(tag);
|
|
5072
|
+
const sInline = compileScript(descriptor, {
|
|
5073
|
+
id,
|
|
5074
|
+
inlineTemplate: true,
|
|
5075
|
+
reactivityTransform: false,
|
|
5076
|
+
// Pass only strict NS native element predicate; avoid broad PascalCase heuristic here.
|
|
5077
|
+
templateOptions: {
|
|
5078
|
+
compilerOptions: { isCustomElement: isNSNative },
|
|
5079
|
+
},
|
|
5080
|
+
});
|
|
5081
|
+
triedInlineTemplate = true;
|
|
5082
|
+
if (/export\s+default/.test(sInline?.content || '')) {
|
|
5083
|
+
compiledScript = sInline.content;
|
|
5084
|
+
bindingMetadata = sInline?.bindings;
|
|
5085
|
+
hadScriptDefaultPre = true;
|
|
5086
|
+
usedInlineScript = true;
|
|
5087
|
+
}
|
|
5088
|
+
else {
|
|
5089
|
+
// Fallback to standard script (no inline) and attempt separate template compile
|
|
5090
|
+
const s = compileScript(descriptor, {
|
|
5091
|
+
id,
|
|
5092
|
+
inlineTemplate: false,
|
|
5093
|
+
reactivityTransform: false,
|
|
5094
|
+
});
|
|
5095
|
+
compiledScript = s?.content || '';
|
|
5096
|
+
bindingMetadata = s?.bindings;
|
|
5097
|
+
hadScriptDefaultPre = /export\s+default/.test(compiledScript);
|
|
5098
|
+
usedInlineScript = false;
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
catch (eScript) {
|
|
5102
|
+
if (verbose) {
|
|
5103
|
+
try {
|
|
5104
|
+
console.warn('[sfc-asm][compileScript] failed', base, eScript?.message);
|
|
5105
|
+
}
|
|
5106
|
+
catch { }
|
|
5107
|
+
}
|
|
5108
|
+
// Retry without inlineTemplate
|
|
5109
|
+
try {
|
|
5110
|
+
const s = compileScript(descriptor, {
|
|
5111
|
+
id,
|
|
5112
|
+
inlineTemplate: false,
|
|
5113
|
+
reactivityTransform: false,
|
|
5114
|
+
});
|
|
5115
|
+
compiledScript = s?.content || '';
|
|
5116
|
+
bindingMetadata = s?.bindings;
|
|
5117
|
+
hadScriptDefaultPre = /export\s+default/.test(compiledScript);
|
|
5118
|
+
usedInlineScript = false;
|
|
5119
|
+
}
|
|
5120
|
+
catch (eNoInline) {
|
|
5121
|
+
if (verbose) {
|
|
5122
|
+
try {
|
|
5123
|
+
console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
|
|
5124
|
+
}
|
|
5125
|
+
catch { }
|
|
5126
|
+
}
|
|
3277
5127
|
}
|
|
3278
|
-
res.statusCode = 200;
|
|
3279
|
-
res.end(buildVersionedCoreSubpathAliasModule(resolvedSubpath, ver));
|
|
3280
|
-
return;
|
|
3281
5128
|
}
|
|
3282
|
-
if
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
res.statusCode = 200;
|
|
3290
|
-
res.end(moduleCode);
|
|
3291
|
-
return;
|
|
5129
|
+
// Final fallback: if script compile yielded nothing, use the variant-transformed script
|
|
5130
|
+
if (!compiledScript && scriptR?.code) {
|
|
5131
|
+
try {
|
|
5132
|
+
compiledScript = scriptR.code;
|
|
5133
|
+
hadScriptDefaultPre = /export\s+default/.test(compiledScript);
|
|
5134
|
+
}
|
|
5135
|
+
catch { }
|
|
3292
5136
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
5137
|
+
// If inlineTemplate produced a default export AND visibly contains a render, allow early-return.
|
|
5138
|
+
// Visible render forms we accept:
|
|
5139
|
+
// - export function render(...) { ... }
|
|
5140
|
+
// - setup(...) { ... return (_ctx, _cache) => { ... } }
|
|
5141
|
+
const hasInlineRender = /(^|\n)\s*export\s+function\s+render\s*\(/.test(compiledScript || '') || /\breturn\s*\(\s*_ctx\s*,\s*_cache\s*\)\s*=>\s*\{/.test(compiledScript || '');
|
|
5142
|
+
// Always use canonical assembler path; avoid inlineTemplate early-return which can miss render attachment
|
|
5143
|
+
// If we reached here, we are going to assemble canonically. Ensure the script we use does NOT include inlineTemplate render.
|
|
5144
|
+
if (usedInlineScript) {
|
|
5145
|
+
try {
|
|
5146
|
+
const sNoInline = compileScript(descriptor, {
|
|
5147
|
+
id,
|
|
5148
|
+
inlineTemplate: false,
|
|
5149
|
+
reactivityTransform: false,
|
|
5150
|
+
});
|
|
5151
|
+
compiledScript = sNoInline?.content || compiledScript;
|
|
5152
|
+
bindingMetadata = sNoInline?.bindings || bindingMetadata;
|
|
5153
|
+
}
|
|
5154
|
+
catch (eNoInline) {
|
|
5155
|
+
if (verbose) {
|
|
5156
|
+
try {
|
|
5157
|
+
console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
|
|
5158
|
+
}
|
|
5159
|
+
catch { }
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
// 2) Compile template
|
|
5164
|
+
let compiledTplCode = '';
|
|
5165
|
+
let templateErr = null;
|
|
3295
5166
|
try {
|
|
3296
|
-
|
|
5167
|
+
const tplSrc = descriptor.template?.content || '';
|
|
5168
|
+
if (tplSrc) {
|
|
5169
|
+
const ct = compileTemplate({
|
|
5170
|
+
source: tplSrc,
|
|
5171
|
+
id,
|
|
5172
|
+
filename: abs,
|
|
5173
|
+
isProd: false,
|
|
5174
|
+
ssr: false,
|
|
5175
|
+
compilerOptions: {
|
|
5176
|
+
bindingMetadata,
|
|
5177
|
+
isCustomElement: (tag) => NS_NATIVE_TAGS.has(tag),
|
|
5178
|
+
},
|
|
5179
|
+
});
|
|
5180
|
+
compiledTplCode = (ct && (ct.code || '')) || '';
|
|
5181
|
+
if (ct?.errors?.length && verbose) {
|
|
5182
|
+
try {
|
|
5183
|
+
console.warn('[sfc-asm][compileTemplate][errors]', base, ct.errors);
|
|
5184
|
+
}
|
|
5185
|
+
catch { }
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
3297
5188
|
}
|
|
3298
|
-
catch {
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
5189
|
+
catch (eTpl) {
|
|
5190
|
+
templateErr = eTpl;
|
|
5191
|
+
if (verbose) {
|
|
5192
|
+
try {
|
|
5193
|
+
console.warn('[sfc-asm][compileTemplate] failed', base, eTpl?.message);
|
|
5194
|
+
}
|
|
5195
|
+
catch { }
|
|
5196
|
+
}
|
|
5197
|
+
// Fallback: use the variant-transformed template code if available
|
|
5198
|
+
try {
|
|
5199
|
+
if (templateR?.code)
|
|
5200
|
+
compiledTplCode = templateR.code;
|
|
5201
|
+
}
|
|
5202
|
+
catch { }
|
|
5203
|
+
}
|
|
5204
|
+
// If still no template code, synthesize a minimal render stub so the module is valid
|
|
5205
|
+
if (!compiledTplCode) {
|
|
5206
|
+
try {
|
|
5207
|
+
compiledTplCode = "export function render(){ const _ = (globalThis.createElementVNode||globalThis._createElementVNode); return _? _('StackLayout') : {}; }\n";
|
|
5208
|
+
}
|
|
5209
|
+
catch { }
|
|
5210
|
+
}
|
|
5211
|
+
// 3) Sanitize script and rewrite .vue imports to inline assembler
|
|
5212
|
+
let scriptBody = compiledScript || '';
|
|
5213
|
+
if (scriptBody) {
|
|
5214
|
+
// Do NOT strip Vue/nativescript-vue imports; retarget them to the runtime bridge so helpers (e.g., onMounted) are bound.
|
|
5215
|
+
// Preserve the import clause and only rewrite the source to '/ns/rt'.
|
|
5216
|
+
scriptBody = scriptBody.replace(/(^|\n)\s*import\s+([^;\n]+)\s+from\s+["'](?:vue|nativescript-vue|~\/vendor\.mjs)(?:\/[^"]*)?["'];?/g, (_m, pfx, clause) => `${pfx}import ${clause} from "/ns/rt";`);
|
|
5217
|
+
try {
|
|
5218
|
+
const importerDir = path.posix.dirname(base);
|
|
5219
|
+
scriptBody = scriptBody.replace(/(^|\n)\s*import\s+([^;\n]+)\s+from\s+["']([^"'\n]+\.vue)(?:\?[^"'\n]*)?["'];?/g, (_m, pfx, clause, spec) => {
|
|
5220
|
+
let absImp = spec;
|
|
5221
|
+
if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
5222
|
+
absImp = path.posix.normalize(path.posix.join(importerDir, spec));
|
|
5223
|
+
if (!absImp.startsWith('/'))
|
|
5224
|
+
absImp = '/' + absImp;
|
|
5225
|
+
}
|
|
5226
|
+
else if (!spec.startsWith('/')) {
|
|
5227
|
+
if (absImp.startsWith('@/'))
|
|
5228
|
+
absImp = APP_VIRTUAL_WITH_SLASH + absImp.slice(2);
|
|
5229
|
+
}
|
|
5230
|
+
const asmUrl = `/ns/asm/${ver}?path=${encodeURIComponent(absImp)}&mode=inline`;
|
|
5231
|
+
return `${pfx}import ${clause} from ${JSON.stringify(asmUrl)};`;
|
|
5232
|
+
});
|
|
5233
|
+
}
|
|
5234
|
+
catch { }
|
|
5235
|
+
}
|
|
5236
|
+
// 4) Extract render from compiled template and prepare a full inline template block
|
|
5237
|
+
let helperBindings = '';
|
|
5238
|
+
let renderDecl = '';
|
|
5239
|
+
let inlineBlock = undefined;
|
|
5240
|
+
let renderOk = false;
|
|
5241
|
+
if (compiledTplCode) {
|
|
5242
|
+
try {
|
|
5243
|
+
// Build a full inline template block to preserve hoists where possible
|
|
5244
|
+
inlineBlock = buildInlineTemplateBlock(compiledTplCode) || undefined;
|
|
5245
|
+
if (!inlineBlock) {
|
|
5246
|
+
const extracted = extractTemplateRender(compiledTplCode);
|
|
5247
|
+
helperBindings = extracted.helperBindings;
|
|
5248
|
+
renderDecl = extracted.renderDecl;
|
|
5249
|
+
inlineBlock = extracted.inlineBlock;
|
|
5250
|
+
renderOk = extracted.ok;
|
|
5251
|
+
}
|
|
5252
|
+
else {
|
|
5253
|
+
renderOk = true;
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
catch (eExtract) {
|
|
5257
|
+
if (verbose) {
|
|
5258
|
+
try {
|
|
5259
|
+
console.warn('[sfc-asm][extractTemplateRender] failed', base, eExtract?.message);
|
|
5260
|
+
}
|
|
5261
|
+
catch { }
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
// Final guard: if no inline render extracted, attempt to import template variant or synthesize a no-op render
|
|
5266
|
+
if (!renderOk && !inlineBlock) {
|
|
5267
|
+
try {
|
|
5268
|
+
const templateUrl = `${origin}/ns/sfc/${ver}${base}?vue&type=template`;
|
|
5269
|
+
const importLine = `import * as __template from ${JSON.stringify(templateUrl)};`;
|
|
5270
|
+
// Attach only if scriptTransformed produces __ns_sfc__ later
|
|
5271
|
+
helperBindings += `\n${importLine}`;
|
|
5272
|
+
renderDecl += `\nfunction __ns_getRender(){\n try {\n if (__template && __template.render) return __template.render;\n } catch (_e) {}\n try {\n const _ = globalThis.createElementVNode || globalThis._createElementVNode;\n return _ ? function(){ return _('StackLayout'); } : function(){ return {}; };\n } catch (_e) { return function(){ return {}; }; }\n}\n`;
|
|
5273
|
+
renderOk = true;
|
|
5274
|
+
}
|
|
5275
|
+
catch { }
|
|
5276
|
+
}
|
|
5277
|
+
// 5) Convert default export to const __ns_sfc__
|
|
5278
|
+
let scriptTransformed = scriptBody;
|
|
5279
|
+
if (scriptTransformed) {
|
|
5280
|
+
scriptTransformed = scriptTransformed.replace(/(^|\n)\s*export\s+default\s+/g, '$1const __ns_sfc__ = ').replace(/(^|\n)\s*export\s*\{[^}]*\}\s*;?\s*/g, '\n/* removed named exports for inline asm */\n');
|
|
5281
|
+
// Normalize any prior declaration of __ns_sfc__ to a plain assignment to avoid redeclare
|
|
5282
|
+
// Accept a semicolon before the declaration too
|
|
5283
|
+
scriptTransformed = scriptTransformed.replace(/(^|[\n;])\s*(?:const|let|var)\s+__ns_sfc__\s*=\s*/g, '$1__ns_sfc__ = ');
|
|
5284
|
+
// Ensure a single declaration appears once before first assignment
|
|
5285
|
+
if (!/(^|[\n;])\s*(?:const|let|var)\s+__ns_sfc__\b/.test(scriptTransformed)) {
|
|
5286
|
+
scriptTransformed = `let __ns_sfc__;\n` + scriptTransformed;
|
|
5287
|
+
}
|
|
5288
|
+
// Remove stray leading braces (artifact defense)
|
|
5289
|
+
scriptTransformed = scriptTransformed.replace(/^\s*\}+(?=\s*[^}])/, (m) => `/* [asm-fix] removed ${m.length} stray leading braces */\n`);
|
|
3317
5290
|
}
|
|
3318
5291
|
else {
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
5292
|
+
try {
|
|
5293
|
+
const compName = (base.split('/').pop() || 'Component').replace(/\.vue$/i, '') || 'Component';
|
|
5294
|
+
scriptTransformed = `import { defineComponent as _defineComponent } from "/ns/rt";\nlet __ns_sfc__;\n__ns_sfc__ = /*@__PURE__*/_defineComponent({ __name: ${JSON.stringify(compName)} });`;
|
|
5295
|
+
}
|
|
5296
|
+
catch {
|
|
5297
|
+
scriptTransformed = `import { defineComponent as _defineComponent } from "/ns/rt";\nlet __ns_sfc__;\n__ns_sfc__ = /*@__PURE__*/_defineComponent({});`;
|
|
3323
5298
|
}
|
|
3324
5299
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
5300
|
+
// 6) Emit final inline module with diagnostics comment
|
|
5301
|
+
const parts = [];
|
|
5302
|
+
parts.push(`// [sfc-asm] ${base} (inline-compiled)`);
|
|
5303
|
+
// Deterministic path: always use extracted helperBindings + renderDecl + scriptTransformed (ignore inlineBlock)
|
|
5304
|
+
// Emit hoisted template bindings first
|
|
5305
|
+
if (helperBindings)
|
|
5306
|
+
parts.push(helperBindings);
|
|
5307
|
+
// IMPORTANT: place script (with its imports) BEFORE renderDecl so imports never appear inside the render function.
|
|
5308
|
+
parts.push(scriptTransformed);
|
|
5309
|
+
parts.push(renderDecl);
|
|
5310
|
+
parts.push(`try { if (!__ns_sfc__.render) Object.defineProperty(__ns_sfc__, 'render', { configurable: true, enumerable: true, get(){ const r = (typeof __ns_getRender==='function' ? __ns_getRender() : undefined); Object.defineProperty(__ns_sfc__, 'render', { value: r, writable: true, configurable: true, enumerable: true }); return r; }, set(v){ Object.defineProperty(__ns_sfc__, 'render', { value: v, writable: true, configurable: true, enumerable: true }); } }); } catch(_e){}`);
|
|
5311
|
+
parts.push(`export function render(){ const f = (typeof __ns_getRender==='function' ? __ns_getRender() : (__ns_sfc__ && __ns_sfc__.render)); return typeof f==='function' ? f.apply(this, arguments) : undefined; }`);
|
|
5312
|
+
parts.push(`export default __ns_sfc__`);
|
|
5313
|
+
let inlineCode = parts.filter(Boolean).join('\n');
|
|
5314
|
+
inlineCode = processCodeForDevice(inlineCode, false, true);
|
|
3327
5315
|
try {
|
|
3328
|
-
|
|
5316
|
+
inlineCode = ensureVersionedCoreImports(inlineCode, getServerOrigin(server), Number(ver));
|
|
3329
5317
|
}
|
|
3330
5318
|
catch { }
|
|
5319
|
+
try {
|
|
5320
|
+
inlineCode = ensureDestructureCoreImports(inlineCode);
|
|
5321
|
+
}
|
|
5322
|
+
catch { }
|
|
5323
|
+
// Replace legacy mutation pipeline with canonical assembler for reliability
|
|
5324
|
+
{
|
|
5325
|
+
// First: strip TypeScript robustly using Babel transform
|
|
5326
|
+
try {
|
|
5327
|
+
const tsRes = await babelCore.transformAsync(scriptTransformed, {
|
|
5328
|
+
plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
|
|
5329
|
+
ast: false,
|
|
5330
|
+
sourceType: 'module',
|
|
5331
|
+
configFile: false,
|
|
5332
|
+
babelrc: false,
|
|
5333
|
+
});
|
|
5334
|
+
if (tsRes?.code)
|
|
5335
|
+
scriptTransformed = tsRes.code;
|
|
5336
|
+
}
|
|
5337
|
+
catch (eTs) {
|
|
5338
|
+
if (verbose) {
|
|
5339
|
+
try {
|
|
5340
|
+
console.warn('[sfc-asm][babel-ts][fail]', base, eTs?.message);
|
|
5341
|
+
}
|
|
5342
|
+
catch { }
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
// Hoist imports + strip residual TS via AST
|
|
5346
|
+
let importLines = [];
|
|
5347
|
+
try {
|
|
5348
|
+
const astRes = astExtractImportsAndStripTypes(scriptTransformed);
|
|
5349
|
+
importLines = astRes.imports;
|
|
5350
|
+
scriptTransformed = astRes.body;
|
|
5351
|
+
if (astRes.diagnostics.length && verbose) {
|
|
5352
|
+
try {
|
|
5353
|
+
console.warn('[sfc-asm][ast]', base, astRes.diagnostics.join('; '));
|
|
5354
|
+
}
|
|
5355
|
+
catch { }
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
catch (eAst) {
|
|
5359
|
+
if (verbose) {
|
|
5360
|
+
try {
|
|
5361
|
+
console.warn('[sfc-asm][ast][fail]', base, eAst?.message);
|
|
5362
|
+
}
|
|
5363
|
+
catch { }
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
// Ensure renderDecl ends with closing brace ONLY for function declaration forms
|
|
5367
|
+
// Avoid appending to const-assignment forms like: const __ns_render = (function(){ ... })();
|
|
5368
|
+
if (renderDecl && /(^|\n)\s*(?:export\s+)?function\s+__ns_render\s*\(/.test(renderDecl) && !/\}\s*$/.test(renderDecl)) {
|
|
5369
|
+
renderDecl = renderDecl.trimEnd() + '\n}';
|
|
5370
|
+
}
|
|
5371
|
+
const outParts = [];
|
|
5372
|
+
outParts.push(`// [sfc-asm] ${base} (inline-compiled)`);
|
|
5373
|
+
outParts.push('// [sfc-asm][canonical]');
|
|
5374
|
+
if (importLines.length)
|
|
5375
|
+
outParts.push(Array.from(new Set(importLines)).join('\n'));
|
|
5376
|
+
// Place component script first so the component object exists before we attach render.
|
|
5377
|
+
outParts.push(scriptTransformed);
|
|
5378
|
+
// Prefer full template block to guarantee presence of all hoisted constants.
|
|
5379
|
+
if (inlineBlock) {
|
|
5380
|
+
outParts.push(inlineBlock);
|
|
5381
|
+
}
|
|
5382
|
+
else {
|
|
5383
|
+
if (helperBindings)
|
|
5384
|
+
outParts.push(helperBindings);
|
|
5385
|
+
if (renderDecl && renderDecl.trim())
|
|
5386
|
+
outParts.push(renderDecl);
|
|
5387
|
+
}
|
|
5388
|
+
outParts.push(`try { if (!__ns_sfc__.render) Object.defineProperty(__ns_sfc__, 'render', { configurable: true, enumerable: true, get(){ const r = (typeof __ns_getRender==='function' ? __ns_getRender() : (typeof __ns_render==='function' ? __ns_render : undefined)); Object.defineProperty(__ns_sfc__, 'render', { value: r, writable: true, configurable: true, enumerable: true }); return r; }, set(v){ Object.defineProperty(__ns_sfc__, 'render', { value: v, writable: true, configurable: true, enumerable: true }); } }); } catch(_e){}`);
|
|
5389
|
+
// Export named render as a function that resolves lazily
|
|
5390
|
+
outParts.push('export function render(){ const f = (typeof __ns_getRender==="function" ? __ns_getRender() : (typeof __ns_render==="function" ? __ns_render : (__ns_sfc__ && __ns_sfc__.render))); return typeof f === "function" ? f.apply(this, arguments) : undefined; }');
|
|
5391
|
+
outParts.push('export default __ns_sfc__');
|
|
5392
|
+
let inlineCode2 = outParts.filter(Boolean).join('\n');
|
|
5393
|
+
inlineCode2 = processCodeForDevice(inlineCode2, false, true);
|
|
5394
|
+
try {
|
|
5395
|
+
inlineCode2 = ensureVersionedCoreImports(inlineCode2, getServerOrigin(server), Number(ver));
|
|
5396
|
+
}
|
|
5397
|
+
catch { }
|
|
5398
|
+
try {
|
|
5399
|
+
inlineCode2 = ensureDestructureCoreImports(inlineCode2);
|
|
5400
|
+
}
|
|
5401
|
+
catch { }
|
|
5402
|
+
// Hoist any late imports that accidentally landed after render or script assembly
|
|
5403
|
+
try {
|
|
5404
|
+
const lateImportRe = /^(?!\/\/).*^\s*import\s+[^;]+;?$/gm;
|
|
5405
|
+
const allImports = [];
|
|
5406
|
+
inlineCode2 = inlineCode2.replace(lateImportRe, (imp) => {
|
|
5407
|
+
allImports.push(imp);
|
|
5408
|
+
return '';
|
|
5409
|
+
});
|
|
5410
|
+
if (allImports.length) {
|
|
5411
|
+
// Place after helperBindings sentinel
|
|
5412
|
+
inlineCode2 = inlineCode2.replace(/(\/\/ \[sfc-asm\]\[canonical\]\n)/, `$1${Array.from(new Set(allImports)).join('\n')}\n/* [asm-fix] re-hoisted ${allImports.length} imports */\n`);
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
catch { }
|
|
5416
|
+
// After hoisting, re-run AST normalization and duplicate-binding verification.
|
|
5417
|
+
// This guards against freshly hoisted imports reintroducing identifiers that collide
|
|
5418
|
+
// with earlier destructures (e.g., __ns_core_ns_1), which would otherwise surface at device runtime.
|
|
5419
|
+
try {
|
|
5420
|
+
inlineCode2 = astNormalizeModuleImportsAndHelpers(inlineCode2);
|
|
5421
|
+
}
|
|
5422
|
+
catch { }
|
|
5423
|
+
try {
|
|
5424
|
+
inlineCode2 = astVerifyAndAnnotateDuplicates(inlineCode2);
|
|
5425
|
+
if (/^\s*\/\/ \[ast-verify\]\[duplicate-bindings\]/m.test(inlineCode2)) {
|
|
5426
|
+
const diagnosticLine = (inlineCode2.match(/^\s*\/\/ \[ast-verify\]\[duplicate-bindings\][^\n]*/m) || [])[0] || '// [ast-verify][duplicate-bindings]';
|
|
5427
|
+
const brief = diagnosticLine.replace(/^[^:]*:?\s?/, '');
|
|
5428
|
+
const escaped = brief.replace(/["\\]/g, '\\$&');
|
|
5429
|
+
const thrower = `throw new Error("[nsv-hmr] Duplicate top-level bindings detected post-hoist: ${escaped}");`;
|
|
5430
|
+
inlineCode2 = `${thrower}\n` + inlineCode2;
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
catch { }
|
|
5434
|
+
// Minimal cleanup only (avoid destructive type stripping breaking object literal property defaults)
|
|
5435
|
+
try {
|
|
5436
|
+
// Heal cases where a TS type strip earlier removed initializer: plain 'default' inside props objects
|
|
5437
|
+
// becomes 'default: undefined'. We only match when followed by ',' or '}' or newline to avoid 'export default'.
|
|
5438
|
+
inlineCode2 = inlineCode2.replace(/\bdefault\b\s*(?=\}|,|\n)/g, 'default: undefined');
|
|
5439
|
+
// Remove obvious leftover angle generic markers
|
|
5440
|
+
inlineCode2 = inlineCode2.replace(/<unknown>/g, '');
|
|
5441
|
+
// Fix accidental '}=> {' sequences
|
|
5442
|
+
inlineCode2 = inlineCode2.replace(/}\s*=>\s*\{/g, '');
|
|
5443
|
+
// No-op: removed prior broken normalization. Handlers are fixed in the dedicated passes below.
|
|
5444
|
+
}
|
|
5445
|
+
catch { }
|
|
5446
|
+
// Removed redundant render closure heal that could inject an extra '}' before component script.
|
|
5447
|
+
// Rewrite any remaining imports (e.g., relative app paths) to HTTP ESM endpoints
|
|
5448
|
+
try {
|
|
5449
|
+
inlineCode2 = rewriteImports(inlineCode2, base, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, getServerOrigin(server));
|
|
5450
|
+
}
|
|
5451
|
+
catch { }
|
|
5452
|
+
// Final TS strip on the whole assembled module (safety net)
|
|
5453
|
+
try {
|
|
5454
|
+
const tsFinal = await babelCore.transformAsync(inlineCode2, {
|
|
5455
|
+
plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
|
|
5456
|
+
ast: false,
|
|
5457
|
+
sourceType: 'module',
|
|
5458
|
+
configFile: false,
|
|
5459
|
+
babelrc: false,
|
|
5460
|
+
});
|
|
5461
|
+
if (tsFinal?.code)
|
|
5462
|
+
inlineCode2 = tsFinal.code;
|
|
5463
|
+
}
|
|
5464
|
+
catch { }
|
|
5465
|
+
// Heal Vue v-model update handlers that lost the ": else" branch during transforms:
|
|
5466
|
+
// "onUpdate:modelValue": _cache[N] || (_cache[N] = $event => _isRef(name) ? name.value = $event)
|
|
5467
|
+
// → add else branch to keep syntax valid: : (name = $event)
|
|
5468
|
+
try {
|
|
5469
|
+
// Fix missing else branch on v-model handlers: support dotted expressions (e.g., $setup.acceptTerms)
|
|
5470
|
+
const reMissingElse = /\"onUpdate:modelValue\"\s*:\s*_cache\[(\d+)\]\s*\|\|\s*\(_cache\[\1\]\s*=\s*\$event\s*=>\s*_isRef\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\)\s*\?\s*\2\.value\s*=\s*\$event\s*\)/g;
|
|
5471
|
+
inlineCode2 = inlineCode2.replace(reMissingElse, (_m, idx, expr) => {
|
|
5472
|
+
return `\"onUpdate:modelValue\": _cache[${idx}] || (_cache[${idx}] = $event => (_isRef(${expr}) ? (${expr}.value = $event) : (${expr} = $event)))`;
|
|
5473
|
+
});
|
|
5474
|
+
// Repair malformed handlers without an arrow (introduced by previous transforms):
|
|
5475
|
+
// Convert pattern assigning to $event without an arrow into a proper arrow using the same target expression.
|
|
5476
|
+
const reMalformed = /\"onUpdate:modelValue\"\s*:\s*_cache\[(\d+)\]\s*\|\|\s*\(_cache\[\1\]\s*=\s*[^=]*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\)[^=]*=\s*\$event\s*\)\s*\)/g;
|
|
5477
|
+
inlineCode2 = inlineCode2.replace(reMalformed, (_m, idx, expr) => {
|
|
5478
|
+
return `\"onUpdate:modelValue\": _cache[${idx}] || (_cache[${idx}] = $event => (_isRef(${expr}) ? (${expr}.value = $event) : (${expr} = $event)))`;
|
|
5479
|
+
});
|
|
5480
|
+
}
|
|
5481
|
+
catch { }
|
|
5482
|
+
// Structural heal: ensure balanced braces before the first import statement
|
|
5483
|
+
try {
|
|
5484
|
+
const idx = inlineCode2.search(/^[\t ]*import\b/m);
|
|
5485
|
+
if (idx > 0) {
|
|
5486
|
+
const prefix = inlineCode2.slice(0, idx);
|
|
5487
|
+
let open = 0, close = 0;
|
|
5488
|
+
let inS = false, inD = false, inT = false, inLC = false, inBC = false;
|
|
5489
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
5490
|
+
const ch = prefix[i], nx = prefix[i + 1];
|
|
5491
|
+
if (inLC) {
|
|
5492
|
+
if (ch === '\n')
|
|
5493
|
+
inLC = false;
|
|
5494
|
+
continue;
|
|
5495
|
+
}
|
|
5496
|
+
if (inBC) {
|
|
5497
|
+
if (ch === '*' && nx === '/') {
|
|
5498
|
+
inBC = false;
|
|
5499
|
+
i++;
|
|
5500
|
+
}
|
|
5501
|
+
continue;
|
|
5502
|
+
}
|
|
5503
|
+
if (inS) {
|
|
5504
|
+
if (ch === '\\') {
|
|
5505
|
+
i++;
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
5508
|
+
if (ch === "'")
|
|
5509
|
+
inS = false;
|
|
5510
|
+
continue;
|
|
5511
|
+
}
|
|
5512
|
+
if (inD) {
|
|
5513
|
+
if (ch === '\\') {
|
|
5514
|
+
i++;
|
|
5515
|
+
continue;
|
|
5516
|
+
}
|
|
5517
|
+
if (ch === '"')
|
|
5518
|
+
inD = false;
|
|
5519
|
+
continue;
|
|
5520
|
+
}
|
|
5521
|
+
if (inT) {
|
|
5522
|
+
if (ch === '\\') {
|
|
5523
|
+
i++;
|
|
5524
|
+
continue;
|
|
5525
|
+
}
|
|
5526
|
+
if (ch === '`')
|
|
5527
|
+
inT = false;
|
|
5528
|
+
continue;
|
|
5529
|
+
}
|
|
5530
|
+
if (ch === '/' && nx === '/') {
|
|
5531
|
+
inLC = true;
|
|
5532
|
+
i++;
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
if (ch === '/' && nx === '*') {
|
|
5536
|
+
inBC = true;
|
|
5537
|
+
i++;
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
5540
|
+
if (ch === "'") {
|
|
5541
|
+
inS = true;
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
5544
|
+
if (ch === '"') {
|
|
5545
|
+
inD = true;
|
|
5546
|
+
continue;
|
|
5547
|
+
}
|
|
5548
|
+
if (ch === '`') {
|
|
5549
|
+
inT = true;
|
|
5550
|
+
continue;
|
|
5551
|
+
}
|
|
5552
|
+
if (ch === '{')
|
|
5553
|
+
open++;
|
|
5554
|
+
else if (ch === '}')
|
|
5555
|
+
close++;
|
|
5556
|
+
}
|
|
5557
|
+
const missing = open - close;
|
|
5558
|
+
if (missing > 0) {
|
|
5559
|
+
inlineCode2 = inlineCode2.slice(0, idx) + '}'.repeat(missing) + '\n' + inlineCode2.slice(idx);
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
catch { }
|
|
5564
|
+
// Final TS strip on the whole assembled module (safety net)
|
|
5565
|
+
try {
|
|
5566
|
+
const tsFinal = await babelCore.transformAsync(inlineCode2, {
|
|
5567
|
+
plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
|
|
5568
|
+
ast: false,
|
|
5569
|
+
sourceType: 'module',
|
|
5570
|
+
configFile: false,
|
|
5571
|
+
babelrc: false,
|
|
5572
|
+
});
|
|
5573
|
+
if (tsFinal?.code)
|
|
5574
|
+
inlineCode2 = tsFinal.code;
|
|
5575
|
+
}
|
|
5576
|
+
catch { }
|
|
5577
|
+
inlineCode2 = ensureVariableDynamicImportHelper(inlineCode2);
|
|
5578
|
+
inlineCode2 = ensureGuardPlainDynamicImports(inlineCode2, origin);
|
|
5579
|
+
inlineCode2 = REQUIRE_GUARD_SNIPPET + inlineCode2;
|
|
5580
|
+
// If no render materialized, return a clear error module for deterministic failure
|
|
5581
|
+
try {
|
|
5582
|
+
const lacksRender = !/__ns_render\b/.test(inlineCode2) && !/__ns_sfc__\.render\s*=/.test(inlineCode2);
|
|
5583
|
+
if (lacksRender) {
|
|
5584
|
+
const err = `throw new Error(\"[sfc-asm] ${base}: no render generated by assembler\");\nexport default {};`;
|
|
5585
|
+
res.statusCode = 200;
|
|
5586
|
+
res.end(err);
|
|
5587
|
+
return;
|
|
5588
|
+
}
|
|
5589
|
+
}
|
|
5590
|
+
catch { }
|
|
5591
|
+
// Cosmetic and parser-friendly: ensure a newline after the canonical banner
|
|
5592
|
+
try {
|
|
5593
|
+
inlineCode2 = inlineCode2.replace(/(\/\/ \[sfc-asm\]\[canonical\])(?!\n)/, '$1\n');
|
|
5594
|
+
}
|
|
5595
|
+
catch { }
|
|
5596
|
+
// Bust device cache for runtime bridge so helpers are always current for this graph version
|
|
5597
|
+
try {
|
|
5598
|
+
const origin = getServerOrigin(server);
|
|
5599
|
+
inlineCode2 = ensureVersionedRtImports(inlineCode2, origin, Number(ver));
|
|
5600
|
+
inlineCode2 = ACTIVE_STRATEGY.ensureVersionedImports(inlineCode2, origin, Number(ver));
|
|
5601
|
+
inlineCode2 = ensureVersionedCoreImports(inlineCode2, origin, Number(ver));
|
|
5602
|
+
}
|
|
5603
|
+
catch { }
|
|
5604
|
+
// Normalize imports/helpers via AST to ensure _defineComponent and other helpers are bound once
|
|
5605
|
+
try {
|
|
5606
|
+
inlineCode2 = astNormalizeModuleImportsAndHelpers(inlineCode2);
|
|
5607
|
+
}
|
|
5608
|
+
catch { }
|
|
5609
|
+
// Guarantee a concrete component object exists before exporting default.
|
|
5610
|
+
try {
|
|
5611
|
+
// Detect an existing declaration of __ns_sfc__ even if it's appended after a semicolon on the same line
|
|
5612
|
+
// e.g., "import ...;let __ns_sfc__;" (no newline). Accept start-of-string, newline, or semicolon as anchors.
|
|
5613
|
+
const hasDecl = /(^|[\n;])\s*(?:const|let|var)\s+__ns_sfc__\b/.test(inlineCode2);
|
|
5614
|
+
if (!hasDecl) {
|
|
5615
|
+
inlineCode2 = inlineCode2.replace(/(\/\/ \[sfc-asm\]\[canonical\]\n)/, `$1let __ns_sfc__ = {};\n`);
|
|
5616
|
+
}
|
|
5617
|
+
// Heal empty declarations (e.g., "let __ns_sfc__;" → initialize to {}), also when preceded by a semicolon
|
|
5618
|
+
inlineCode2 = inlineCode2.replace(/(^|[\n;])\s*let\s+__ns_sfc__\s*;?/g, '$1let __ns_sfc__ = {};');
|
|
5619
|
+
inlineCode2 = inlineCode2.replace(/(^|[\n;])\s*var\s+__ns_sfc__\s*;?/g, '$1var __ns_sfc__ = {};');
|
|
5620
|
+
}
|
|
5621
|
+
catch { }
|
|
5622
|
+
if (!/export\s+default\s+__ns_sfc__/.test(inlineCode2) && /__ns_sfc__/.test(inlineCode2))
|
|
5623
|
+
inlineCode2 += '\nexport default __ns_sfc__';
|
|
5624
|
+
res.statusCode = 200;
|
|
5625
|
+
res.end(inlineCode2);
|
|
5626
|
+
return;
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
catch { }
|
|
5631
|
+
// Do not use compiled ?vue or variant fallbacks; assembler must succeed or emit an error
|
|
5632
|
+
// Prefer compiling template from source via compiler-sfc; fallback to variant extraction
|
|
5633
|
+
let inlineOk = false;
|
|
5634
|
+
let helperBindings = '';
|
|
5635
|
+
let renderDecl = '';
|
|
5636
|
+
let inlineBlock = undefined;
|
|
5637
|
+
try {
|
|
5638
|
+
const root = server.config?.root || process.cwd();
|
|
5639
|
+
const abs = path.join(root, base.replace(/^\//, ''));
|
|
5640
|
+
let sfcSrc = '';
|
|
5641
|
+
try {
|
|
5642
|
+
sfcSrc = readFileSync(abs, 'utf-8');
|
|
5643
|
+
}
|
|
5644
|
+
catch { }
|
|
5645
|
+
if (sfcSrc) {
|
|
5646
|
+
const { descriptor } = parse(sfcSrc, { filename: abs });
|
|
5647
|
+
const tpl = descriptor.template?.content || '';
|
|
5648
|
+
if (tpl) {
|
|
5649
|
+
const id = createHash('md5').update(abs).digest('hex').slice(0, 8);
|
|
5650
|
+
const ct = compileTemplate({
|
|
5651
|
+
source: tpl,
|
|
5652
|
+
id,
|
|
5653
|
+
filename: abs,
|
|
5654
|
+
isProd: false,
|
|
5655
|
+
ssr: false,
|
|
5656
|
+
compilerOptions: {
|
|
5657
|
+
isCustomElement: (tag) => NS_NATIVE_TAGS.has(tag),
|
|
5658
|
+
},
|
|
5659
|
+
});
|
|
5660
|
+
let compiled = (ct && (ct.code || '')) || '';
|
|
5661
|
+
if (compiled) {
|
|
5662
|
+
// Prefer a full inline template block preserving hoists
|
|
5663
|
+
inlineBlock = buildInlineTemplateBlock(compiled) || undefined;
|
|
5664
|
+
if (inlineBlock) {
|
|
5665
|
+
inlineOk = true;
|
|
5666
|
+
}
|
|
5667
|
+
else {
|
|
5668
|
+
const extracted = extractTemplateRender(compiled);
|
|
5669
|
+
inlineOk = extracted.ok;
|
|
5670
|
+
helperBindings = extracted.helperBindings;
|
|
5671
|
+
renderDecl = extracted.renderDecl;
|
|
5672
|
+
inlineBlock = extracted.inlineBlock;
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
3331
5676
|
}
|
|
3332
5677
|
}
|
|
5678
|
+
catch { }
|
|
5679
|
+
// If compiler-sfc path didn't succeed, attempt variant extraction once
|
|
5680
|
+
if (!inlineOk) {
|
|
5681
|
+
const extracted = extractTemplateRender(templateCode);
|
|
5682
|
+
inlineOk = extracted.ok;
|
|
5683
|
+
helperBindings = extracted.helperBindings;
|
|
5684
|
+
renderDecl = extracted.renderDecl;
|
|
5685
|
+
inlineBlock = extracted.inlineBlock;
|
|
5686
|
+
}
|
|
5687
|
+
let asm;
|
|
5688
|
+
if (inlineOk) {
|
|
5689
|
+
if (inlineBlock && inlineBlock.trim()) {
|
|
5690
|
+
asm = [`// [sfc-asm] ${base} (inlined template body)`, `export * from ${JSON.stringify(scriptUrl)};`, `import * as __script from ${JSON.stringify(scriptUrl)};`, inlineBlock, `const __ns_sfc__ = (__script && __script.default) ? __script.default : {};`, `try { if (typeof __ns_render === 'function' && !__ns_sfc__.render) __ns_sfc__.render = __ns_render; } catch {}`, `export default __ns_sfc__;`].join('\n');
|
|
5691
|
+
}
|
|
5692
|
+
else {
|
|
5693
|
+
asm = [`// [sfc-asm] ${base} (inlined template)`, `export * from ${JSON.stringify(scriptUrl)};`, `import * as __script from ${JSON.stringify(scriptUrl)};`, helperBindings, renderDecl, `const __ns_sfc__ = (__script && __script.default) ? __script.default : {};`, `try { if (typeof __ns_render === 'function' && !__ns_sfc__.render) __ns_sfc__.render = __ns_render; } catch {}`, `export default __ns_sfc__;`].filter(Boolean).join('\n');
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
else {
|
|
5697
|
+
// Deterministic error path when template extraction failed
|
|
5698
|
+
res.statusCode = 500;
|
|
5699
|
+
res.end(`throw new Error('[sfc-asm] ${base}: template extraction failed');\nexport default {};`);
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
// Run full device processing so helper aliasing and globals are consistent in this path too
|
|
5703
|
+
let code = REQUIRE_GUARD_SNIPPET + asm;
|
|
5704
|
+
code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(base), base);
|
|
5705
|
+
try {
|
|
5706
|
+
code = ensureVersionedCoreImports(code, getServerOrigin(server), Number(ver));
|
|
5707
|
+
}
|
|
5708
|
+
catch { }
|
|
5709
|
+
code = rewriteImports(code, base, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, getServerOrigin(server));
|
|
5710
|
+
try {
|
|
5711
|
+
code = ensureDestructureCoreImports(code);
|
|
5712
|
+
}
|
|
5713
|
+
catch { }
|
|
5714
|
+
code = ensureVariableDynamicImportHelper(code);
|
|
5715
|
+
code = ensureGuardPlainDynamicImports(code, origin);
|
|
5716
|
+
try {
|
|
5717
|
+
const origin = getServerOrigin(server);
|
|
5718
|
+
code = ensureVersionedRtImports(code, origin, Number(ver));
|
|
5719
|
+
code = ACTIVE_STRATEGY.ensureVersionedImports(code, origin, Number(ver));
|
|
5720
|
+
code = ensureVersionedCoreImports(code, origin, Number(ver));
|
|
5721
|
+
}
|
|
5722
|
+
catch { }
|
|
5723
|
+
// Inline-template body path already runs processCodeForDevice (AST + sanitizers); no additional _defineComponent fix needed
|
|
3333
5724
|
res.statusCode = 200;
|
|
3334
5725
|
res.end(code);
|
|
3335
5726
|
}
|
|
3336
5727
|
catch (e) {
|
|
3337
|
-
|
|
5728
|
+
res.statusCode = 500;
|
|
5729
|
+
res.end('export {}\n');
|
|
3338
5730
|
}
|
|
3339
5731
|
});
|
|
3340
|
-
registerTxnHandler(server, {
|
|
3341
|
-
resolveTxnIds: (version, fallbackChangedIds) => {
|
|
3342
|
-
const ids = txnBatches.get(version) || [];
|
|
3343
|
-
if (ids.length) {
|
|
3344
|
-
return ids;
|
|
3345
|
-
}
|
|
3346
|
-
return fallbackChangedIds.length ? computeTxnOrderForChanged(fallbackChangedIds) : [];
|
|
3347
|
-
},
|
|
3348
|
-
});
|
|
3349
|
-
registerVueSfcHandlers(server, {
|
|
3350
|
-
verbose,
|
|
3351
|
-
requireGuardSnippet: REQUIRE_GUARD_SNIPPET,
|
|
3352
|
-
appVirtualWithSlash: APP_VIRTUAL_WITH_SLASH,
|
|
3353
|
-
sfcFileMap,
|
|
3354
|
-
depFileMap,
|
|
3355
|
-
getGraphVersion: () => Number(graphVersion || 0),
|
|
3356
|
-
getStrategy: () => ACTIVE_STRATEGY,
|
|
3357
|
-
getServerOrigin,
|
|
3358
|
-
cleanCode,
|
|
3359
|
-
processCodeForDevice,
|
|
3360
|
-
rewriteImports,
|
|
3361
|
-
ensureVariableDynamicImportHelper,
|
|
3362
|
-
ensureGuardPlainDynamicImports,
|
|
3363
|
-
ensureVersionedRtImports,
|
|
3364
|
-
ensureVersionedCoreImports,
|
|
3365
|
-
ensureDestructureCoreImports,
|
|
3366
|
-
extractExportMetadata,
|
|
3367
|
-
});
|
|
3368
5732
|
wss.on('connection', async (ws) => {
|
|
3369
5733
|
if (verbose)
|
|
3370
5734
|
console.log('[hmr-ws] Client connected (dynamic fetch mode)');
|
|
@@ -3690,6 +6054,103 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3690
6054
|
if (isRuntimeGraphExcludedPath(file)) {
|
|
3691
6055
|
return;
|
|
3692
6056
|
}
|
|
6057
|
+
// Always-on update timing — see HMR_CORE_REALM_DETERMINISTIC_PLAN.md
|
|
6058
|
+
// ("Track 2 — round one, 2026-04: HMR update metrics"). Captures
|
|
6059
|
+
// the four phases (await, framework, broadcast, total) plus
|
|
6060
|
+
// invalidated module count and recipient count. Emitted at the
|
|
6061
|
+
// end of this function via `emitHmrUpdateSummary()`. Single line,
|
|
6062
|
+
// always-on so a 6-second `.ts` save is immediately visible
|
|
6063
|
+
// without flipping verbose.
|
|
6064
|
+
const updateRoot = server.config.root || process.cwd();
|
|
6065
|
+
const updateRel = (() => {
|
|
6066
|
+
try {
|
|
6067
|
+
return '/' + path.posix.normalize(path.relative(updateRoot, file)).split(path.sep).join('/');
|
|
6068
|
+
}
|
|
6069
|
+
catch {
|
|
6070
|
+
return file;
|
|
6071
|
+
}
|
|
6072
|
+
})();
|
|
6073
|
+
const updateMetrics = {
|
|
6074
|
+
file: updateRel,
|
|
6075
|
+
kind: classifyHmrUpdateKind(file),
|
|
6076
|
+
t0: Date.now(),
|
|
6077
|
+
tAfterAwait: 0,
|
|
6078
|
+
tAfterFramework: 0,
|
|
6079
|
+
tEnd: 0,
|
|
6080
|
+
invalidated: 0,
|
|
6081
|
+
recipients: 0,
|
|
6082
|
+
// Round-eight diagnostic — populated by the angular branch when
|
|
6083
|
+
// the changed file is `.ts`, otherwise remains undefined and is
|
|
6084
|
+
// omitted from the summary line entirely. See
|
|
6085
|
+
// HMR_CORE_REALM_DETERMINISTIC_PLAN.md ("Round-eight — surface
|
|
6086
|
+
// narrowing decision").
|
|
6087
|
+
narrowed: undefined,
|
|
6088
|
+
emitted: false,
|
|
6089
|
+
};
|
|
6090
|
+
// alpha.62 follow-up — broadcast a "pending" notification at
|
|
6091
|
+
// the very start of handleHotUpdate so the client can show
|
|
6092
|
+
// the HMR-applying overlay BEFORE we spend time on graph
|
|
6093
|
+
// updates / transforms / dependency analysis (typically
|
|
6094
|
+
// 7–200ms on a warm cache). Without this, the overlay only
|
|
6095
|
+
// appears at `ns:angular-update` broadcast time and the
|
|
6096
|
+
// user perceives a "delayed" reaction to their save.
|
|
6097
|
+
//
|
|
6098
|
+
// Fire-and-forget: a failed pending broadcast must never
|
|
6099
|
+
// hold up the actual update. The client treats receipt of
|
|
6100
|
+
// `ns:angular-update` (or `ns:css-updates`) as authoritative;
|
|
6101
|
+
// the pending message is purely a UX hint.
|
|
6102
|
+
try {
|
|
6103
|
+
const pendingPayload = JSON.stringify(createHmrPendingMessage({
|
|
6104
|
+
origin: getServerOrigin(server),
|
|
6105
|
+
path: updateMetrics.file,
|
|
6106
|
+
kind: updateMetrics.kind,
|
|
6107
|
+
timestamp: updateMetrics.t0,
|
|
6108
|
+
}));
|
|
6109
|
+
wss.clients.forEach((client) => {
|
|
6110
|
+
if (isSocketClientOpen(client)) {
|
|
6111
|
+
try {
|
|
6112
|
+
client.send(pendingPayload);
|
|
6113
|
+
}
|
|
6114
|
+
catch { }
|
|
6115
|
+
}
|
|
6116
|
+
});
|
|
6117
|
+
}
|
|
6118
|
+
catch { }
|
|
6119
|
+
const emitHmrUpdateSummary = () => {
|
|
6120
|
+
if (updateMetrics.emitted)
|
|
6121
|
+
return;
|
|
6122
|
+
updateMetrics.emitted = true;
|
|
6123
|
+
updateMetrics.tEnd = Date.now();
|
|
6124
|
+
try {
|
|
6125
|
+
const awaitMs = (updateMetrics.tAfterAwait || updateMetrics.t0) - updateMetrics.t0;
|
|
6126
|
+
const frameworkMs = (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0) - (updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
6127
|
+
const broadcastMs = updateMetrics.tEnd - (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
6128
|
+
const totalMs = updateMetrics.tEnd - updateMetrics.t0;
|
|
6129
|
+
console.info(formatHmrUpdateSummary({
|
|
6130
|
+
file: updateMetrics.file,
|
|
6131
|
+
kind: updateMetrics.kind,
|
|
6132
|
+
awaitMs,
|
|
6133
|
+
frameworkMs,
|
|
6134
|
+
broadcastMs,
|
|
6135
|
+
totalMs,
|
|
6136
|
+
invalidated: updateMetrics.invalidated,
|
|
6137
|
+
recipients: updateMetrics.recipients,
|
|
6138
|
+
narrowed: updateMetrics.narrowed,
|
|
6139
|
+
}));
|
|
6140
|
+
}
|
|
6141
|
+
catch { }
|
|
6142
|
+
};
|
|
6143
|
+
// Track 1.3: the first /ns/m request kicks off populateInitialGraph
|
|
6144
|
+
// in the background. If an HMR update races in before that walk
|
|
6145
|
+
// completes, we'd lose transitive-importer data. Await completion
|
|
6146
|
+
// here so the delta computation below always sees a populated graph.
|
|
6147
|
+
if (graphInitialPopulationPromise) {
|
|
6148
|
+
try {
|
|
6149
|
+
await graphInitialPopulationPromise;
|
|
6150
|
+
}
|
|
6151
|
+
catch { }
|
|
6152
|
+
}
|
|
6153
|
+
updateMetrics.tAfterAwait = Date.now();
|
|
3693
6154
|
// Graph update for this file change (wrapped to avoid aborting rest of handler)
|
|
3694
6155
|
try {
|
|
3695
6156
|
const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
|
|
@@ -3739,6 +6200,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3739
6200
|
console.log(`[hmr-ws] Hot update for: ${file}`);
|
|
3740
6201
|
// Handle CSS updates
|
|
3741
6202
|
if (file.endsWith('.css')) {
|
|
6203
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
3742
6204
|
try {
|
|
3743
6205
|
let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
3744
6206
|
const origin = getServerOrigin(server);
|
|
@@ -3757,12 +6219,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3757
6219
|
wss.clients.forEach((client) => {
|
|
3758
6220
|
if (isSocketClientOpen(client)) {
|
|
3759
6221
|
client.send(JSON.stringify(msg));
|
|
6222
|
+
updateMetrics.recipients += 1;
|
|
3760
6223
|
}
|
|
3761
6224
|
});
|
|
3762
6225
|
}
|
|
3763
6226
|
catch (error) {
|
|
3764
6227
|
console.warn('[hmr-ws] CSS update failed:', error);
|
|
3765
6228
|
}
|
|
6229
|
+
emitHmrUpdateSummary();
|
|
3766
6230
|
return;
|
|
3767
6231
|
}
|
|
3768
6232
|
// Framework-specific hot update handling
|
|
@@ -3778,6 +6242,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3778
6242
|
});
|
|
3779
6243
|
if (!(isHtml || isTs))
|
|
3780
6244
|
return;
|
|
6245
|
+
updateMetrics.invalidated += angularHotUpdateRoots.length;
|
|
3781
6246
|
if (angularHotUpdateRoots.length) {
|
|
3782
6247
|
for (const mod of angularHotUpdateRoots) {
|
|
3783
6248
|
try {
|
|
@@ -3794,13 +6259,80 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3794
6259
|
}
|
|
3795
6260
|
}
|
|
3796
6261
|
const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
|
|
3797
|
-
|
|
6262
|
+
// Round-six narrowing: read the source for `.ts/.tsx/.js/.jsx`
|
|
6263
|
+
// edits so `shouldInvalidateAngularTransitiveImporters` can
|
|
6264
|
+
// distinguish leaf modules (constants/utils) from real
|
|
6265
|
+
// Angular files. If `ctx.read()` throws (file deleted, race
|
|
6266
|
+
// against the watcher), `angularChangedSource` stays
|
|
6267
|
+
// undefined and we fall back to the conservative
|
|
6268
|
+
// pre-round-six "always invalidate transitively" behavior.
|
|
6269
|
+
let angularChangedSource;
|
|
6270
|
+
if (isTs) {
|
|
3798
6271
|
try {
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
6272
|
+
angularChangedSource = await ctx.read();
|
|
6273
|
+
}
|
|
6274
|
+
catch {
|
|
6275
|
+
angularChangedSource = undefined;
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
const angularNeedsTransitive = shouldInvalidateAngularTransitiveImporters({
|
|
6279
|
+
flavor: ACTIVE_STRATEGY.flavor,
|
|
6280
|
+
file,
|
|
6281
|
+
source: angularChangedSource,
|
|
6282
|
+
});
|
|
6283
|
+
// Round-eight diagnostic. We surface the narrowing decision on
|
|
6284
|
+
// every `.ts` Angular hot update (HTML routes always invalidate
|
|
6285
|
+
// transitively today and aren't subject to Round-Seven, so we
|
|
6286
|
+
// leave them as `undefined` — the field is omitted from the
|
|
6287
|
+
// summary line). The boolean is the inverse of
|
|
6288
|
+
// `angularNeedsTransitive` because "needs transitive" is the
|
|
6289
|
+
// pre-Round-Seven (broad) behavior.
|
|
6290
|
+
if (isTs) {
|
|
6291
|
+
updateMetrics.narrowed = !angularNeedsTransitive;
|
|
6292
|
+
}
|
|
6293
|
+
// alpha.59 — Stable URL + Explicit Invalidation:
|
|
6294
|
+
//
|
|
6295
|
+
// Compute the transitive importer closure ONCE here and reuse it
|
|
6296
|
+
// for (a) `server.moduleGraph.invalidateModule` (so Vite's
|
|
6297
|
+
// transform pipeline re-runs on next request), (b) the shared
|
|
6298
|
+
// transform-request cache, and (c) the runtime eviction set we
|
|
6299
|
+
// broadcast in `ns:angular-update`. Pre-alpha.59 code computed
|
|
6300
|
+
// this list twice in adjacent try blocks; consolidating it
|
|
6301
|
+
// removes a redundant graph walk and guarantees the three
|
|
6302
|
+
// consumers see the exact same set of importers (otherwise a
|
|
6303
|
+
// late module-graph mutation between calls could leave an
|
|
6304
|
+
// asymmetric narrowed/broad mix).
|
|
6305
|
+
//
|
|
6306
|
+
// alpha.59.1 — separate Vite-transform narrowing from runtime
|
|
6307
|
+
// eviction. `angularNeedsTransitive` answers the question "does
|
|
6308
|
+
// the changed file's symbol shape change such that importers
|
|
6309
|
+
// must be re-transformed by Vite?". The runtime, however, has
|
|
6310
|
+
// a stricter requirement: ESM live bindings only refresh if
|
|
6311
|
+
// the importing module re-evaluates inside V8. A constants
|
|
6312
|
+
// file with no Angular decorator does NOT need a Vite
|
|
6313
|
+
// re-transform of its importers (their compiled JS is
|
|
6314
|
+
// identical), but its importers still hold stale bindings to
|
|
6315
|
+
// the OLD constants Module record. After eviction + re-import
|
|
6316
|
+
// of `main.ts`, V8 sees the cached importers, returns them
|
|
6317
|
+
// unchanged, and they continue to read the OLD values. The
|
|
6318
|
+
// user-visible symptom: HMR completes successfully, logs are
|
|
6319
|
+
// clean, but the simulator does not reflect the change.
|
|
6320
|
+
//
|
|
6321
|
+
// The fix: ALWAYS compute the transitive importer closure for
|
|
6322
|
+
// runtime eviction. Only skip Vite's `moduleGraph.invalidate`
|
|
6323
|
+
// + transform-cache purge when `angularNeedsTransitive` is
|
|
6324
|
+
// false — those are the genuine narrowing wins (saves
|
|
6325
|
+
// re-transform work on the server). The eviction set always
|
|
6326
|
+
// includes importers so V8 re-fetches and re-binds them.
|
|
6327
|
+
let transitiveImporters = [];
|
|
6328
|
+
try {
|
|
6329
|
+
transitiveImporters = collectAngularTransitiveImportersForInvalidation({
|
|
6330
|
+
modules: angularTransitiveInvalidationRoots,
|
|
6331
|
+
isExcluded: (id) => id.includes('/node_modules/'),
|
|
6332
|
+
maxDepth: 16,
|
|
6333
|
+
});
|
|
6334
|
+
if (angularNeedsTransitive) {
|
|
6335
|
+
updateMetrics.invalidated += transitiveImporters.length;
|
|
3804
6336
|
for (const mod of transitiveImporters) {
|
|
3805
6337
|
try {
|
|
3806
6338
|
server.moduleGraph.invalidateModule(mod);
|
|
@@ -3815,24 +6347,40 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3815
6347
|
console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
|
|
3816
6348
|
}
|
|
3817
6349
|
}
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
6350
|
+
else if (isTs && typeof angularChangedSource === 'string') {
|
|
6351
|
+
// Round-eight: was previously gated on `verbose`. Surfacing this
|
|
6352
|
+
// log unconditionally lets the user immediately confirm whether
|
|
6353
|
+
// Round-Seven's narrowing fired for a given `.ts` edit (the
|
|
6354
|
+
// summary line below still emits `narrowed=yes`/`no`, but
|
|
6355
|
+
// having both makes the decision easier to spot in noisy logs
|
|
6356
|
+
// and lets the user diff scenarios without flipping
|
|
6357
|
+
// `NS_HMR_VERBOSE=true`).
|
|
6358
|
+
//
|
|
6359
|
+
// alpha.59.1 — narrowing now means "skip Vite re-transform"
|
|
6360
|
+
// (the importers still get evicted from the V8 module
|
|
6361
|
+
// registry so live bindings refresh). The log call-out is
|
|
6362
|
+
// preserved but extended with the importer count to make the
|
|
6363
|
+
// distinction visible.
|
|
6364
|
+
console.log(`[hmr-ws][angular] narrowed transitive invalidation (no @Component/@Directive/@Pipe/@Injectable/@NgModule): ${updateRel} — Vite transform skipped, runtime eviction includes ${transitiveImporters.length} importer(s)`);
|
|
3821
6365
|
}
|
|
3822
6366
|
}
|
|
6367
|
+
catch (error) {
|
|
6368
|
+
if (verbose)
|
|
6369
|
+
console.warn('[hmr-ws][angular] transitive importer collection failed', error);
|
|
6370
|
+
}
|
|
3823
6371
|
try {
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
6372
|
+
// alpha.59.1 — purge shared transform cache for the changed
|
|
6373
|
+
// file + hot-update roots unconditionally (their transform
|
|
6374
|
+
// output IS different now). Transitive importers are only
|
|
6375
|
+
// purged when narrowing decides their output may have
|
|
6376
|
+
// changed; otherwise their cached transforms are still
|
|
6377
|
+
// valid (compiled JS is identical even though the runtime
|
|
6378
|
+
// must re-evaluate them to refresh ESM bindings).
|
|
3831
6379
|
const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
|
|
3832
6380
|
file,
|
|
3833
6381
|
isTs,
|
|
3834
6382
|
hotUpdateRoots: angularHotUpdateRoots,
|
|
3835
|
-
transitiveImporters,
|
|
6383
|
+
transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
|
|
3836
6384
|
projectRoot: server.config.root || process.cwd(),
|
|
3837
6385
|
}));
|
|
3838
6386
|
if (transformCacheInvalidationUrls.size) {
|
|
@@ -3846,17 +6394,74 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3846
6394
|
if (verbose)
|
|
3847
6395
|
console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
|
|
3848
6396
|
}
|
|
6397
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
3849
6398
|
try {
|
|
3850
6399
|
const root = server.config.root || process.cwd();
|
|
3851
6400
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
3852
6401
|
rememberAngularReloadSuppression(root, file);
|
|
3853
6402
|
const origin = getServerOrigin(server);
|
|
6403
|
+
const bootstrapEntryRel = getBootstrapEntryRelPath();
|
|
6404
|
+
// alpha.59 — Stable URL + Explicit Invalidation:
|
|
6405
|
+
//
|
|
6406
|
+
// `evictPaths` is the canonical list of `/ns/m/<rel>` URLs
|
|
6407
|
+
// the runtime must drop from `g_moduleRegistry` before
|
|
6408
|
+
// re-importing `importerEntry`. Pre-alpha.59 the server
|
|
6409
|
+
// signaled invalidation by bumping a global `graphVersion`
|
|
6410
|
+
// counter and embedding it in every URL — but V8 keys the
|
|
6411
|
+
// module registry by full URL, so a v1 → v2 bump
|
|
6412
|
+
// effectively flushed the entire dependency graph from
|
|
6413
|
+
// the cache and forced the runtime to re-fetch + re-eval
|
|
6414
|
+
// every transitively-imported module on each save (~3s
|
|
6415
|
+
// HMR cycles, dominated by Vite's single-threaded transform
|
|
6416
|
+
// pipeline). The new model:
|
|
6417
|
+
//
|
|
6418
|
+
// 1. URLs are stable: `/ns/m/<rel>` everywhere, no `vN`.
|
|
6419
|
+
// 2. The server walks the inverse-dependency closure and
|
|
6420
|
+
// sends only the modules that actually need to be
|
|
6421
|
+
// re-evaluated (typically O(1) for component edits,
|
|
6422
|
+
// or the changed file + entry for narrowed edits).
|
|
6423
|
+
// 3. The client calls `__nsInvalidateModules(evictPaths)`
|
|
6424
|
+
// and re-imports `importerEntry`, which causes V8 to
|
|
6425
|
+
// refetch ONLY those modules. Everything else stays
|
|
6426
|
+
// hot in the registry.
|
|
6427
|
+
//
|
|
6428
|
+
// Invariants enforced by `collectAngularEvictionUrls`:
|
|
6429
|
+
// - Always includes the changed file (so the new source
|
|
6430
|
+
// is fetched).
|
|
6431
|
+
// - Always includes `importerEntry` (so re-import
|
|
6432
|
+
// re-evaluates).
|
|
6433
|
+
// - Excludes node_modules (vendor packages are stable).
|
|
6434
|
+
// - Excludes virtual / runtime-graph-excluded ids.
|
|
6435
|
+
// - Origin-prefixed: `http://host:port/ns/m/<rel>`.
|
|
6436
|
+
let evictPaths = [];
|
|
6437
|
+
try {
|
|
6438
|
+
evictPaths = collectAngularEvictionUrls({
|
|
6439
|
+
file,
|
|
6440
|
+
hotUpdateRoots: angularHotUpdateRoots,
|
|
6441
|
+
transitiveImporters,
|
|
6442
|
+
projectRoot: root,
|
|
6443
|
+
origin,
|
|
6444
|
+
bootstrapEntry: bootstrapEntryRel,
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
catch (error) {
|
|
6448
|
+
if (verbose)
|
|
6449
|
+
console.warn('[hmr-ws][angular] eviction set computation failed', error);
|
|
6450
|
+
}
|
|
6451
|
+
if (verbose) {
|
|
6452
|
+
console.log('[hmr-ws][angular] eviction set', {
|
|
6453
|
+
count: evictPaths.length,
|
|
6454
|
+
importerEntry: bootstrapEntryRel,
|
|
6455
|
+
});
|
|
6456
|
+
}
|
|
3854
6457
|
const msg = {
|
|
3855
6458
|
type: 'ns:angular-update',
|
|
3856
6459
|
origin,
|
|
3857
6460
|
path: rel,
|
|
3858
6461
|
version: graphVersion,
|
|
3859
6462
|
timestamp: Date.now(),
|
|
6463
|
+
evictPaths,
|
|
6464
|
+
importerEntry: bootstrapEntryRel,
|
|
3860
6465
|
};
|
|
3861
6466
|
if (verbose) {
|
|
3862
6467
|
console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
|
|
@@ -3868,12 +6473,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3868
6473
|
wss.clients.forEach((client) => {
|
|
3869
6474
|
if (isSocketClientOpen(client)) {
|
|
3870
6475
|
client.send(JSON.stringify(msg));
|
|
6476
|
+
updateMetrics.recipients += 1;
|
|
3871
6477
|
}
|
|
3872
6478
|
});
|
|
3873
6479
|
}
|
|
3874
6480
|
catch (error) {
|
|
3875
6481
|
console.warn('[hmr-ws][angular] update failed:', error);
|
|
3876
6482
|
}
|
|
6483
|
+
emitHmrUpdateSummary();
|
|
3877
6484
|
if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
|
|
3878
6485
|
return [];
|
|
3879
6486
|
}
|
|
@@ -3881,6 +6488,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3881
6488
|
}
|
|
3882
6489
|
// TypeScript flavor: emit generic graph delta for app XML/TS/style changes
|
|
3883
6490
|
if (ACTIVE_STRATEGY.flavor === 'typescript') {
|
|
6491
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
3884
6492
|
try {
|
|
3885
6493
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
3886
6494
|
if (verbose)
|
|
@@ -3894,6 +6502,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3894
6502
|
if (verbose)
|
|
3895
6503
|
console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
|
|
3896
6504
|
}
|
|
6505
|
+
emitHmrUpdateSummary();
|
|
3897
6506
|
return;
|
|
3898
6507
|
}
|
|
3899
6508
|
// Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
|
|
@@ -3907,6 +6516,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3907
6516
|
const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
|
|
3908
6517
|
if (!isSolidFile)
|
|
3909
6518
|
return;
|
|
6519
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
3910
6520
|
try {
|
|
3911
6521
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
3912
6522
|
if (verbose)
|
|
@@ -3931,6 +6541,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3931
6541
|
if (verbose)
|
|
3932
6542
|
console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
|
|
3933
6543
|
}
|
|
6544
|
+
emitHmrUpdateSummary();
|
|
3934
6545
|
return;
|
|
3935
6546
|
}
|
|
3936
6547
|
// Handle .vue file updates
|
|
@@ -4216,6 +6827,10 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
|
|
|
4216
6827
|
console.warn('[hmr-ws] HMR update failed:', error);
|
|
4217
6828
|
console.error(error);
|
|
4218
6829
|
}
|
|
6830
|
+
// Vue path emits update summary at the end of the function so
|
|
6831
|
+
// every framework branch gets exactly one log line. Idempotent
|
|
6832
|
+
// — if any branch already emitted, this is a no-op.
|
|
6833
|
+
emitHmrUpdateSummary();
|
|
4219
6834
|
// CRITICAL: Return empty array to prevent Vite's default HMR
|
|
4220
6835
|
return [];
|
|
4221
6836
|
},
|