From 26b74ac071df4b599fc3caa1ef14bc067b794a64 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 23 Nov 2025 00:29:11 -0600 Subject: [PATCH 01/14] feat(vite): improve workspace resolutions --- packages/vite/configuration/angular.ts | 2 +- packages/vite/configuration/base.ts | 4 ++-- packages/vite/configuration/react.ts | 2 +- packages/vite/configuration/solid.ts | 2 +- packages/vite/configuration/typescript.ts | 2 +- packages/vite/configuration/vue.ts | 2 +- packages/vite/helpers/main-entry.ts | 21 +++++++++++++++++---- 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/vite/configuration/angular.ts b/packages/vite/configuration/angular.ts index 2dcdbd8181..84f14b790f 100644 --- a/packages/vite/configuration/angular.ts +++ b/packages/vite/configuration/angular.ts @@ -305,7 +305,7 @@ export const angularConfig = ({ mode }): UserConfig => { const enableRollupLinker = process.env.NS_ENABLE_ROLLUP_LINKER === '1' || process.env.NS_ENABLE_ROLLUP_LINKER === 'true' || hmrActive; - return mergeConfig(baseConfig({ mode }), { + return mergeConfig(baseConfig({ mode, flavor: 'angular' }), { plugins: [...plugins, ...(enableRollupLinker ? [angularRollupLinker(process.cwd())] : []), renderChunkLinker, postLinker], // Always alias fesm2022 deep imports to package root so vendor bridge can externalize properly resolve: { diff --git a/packages/vite/configuration/base.ts b/packages/vite/configuration/base.ts index 7905256e6d..54f1f8d6d1 100644 --- a/packages/vite/configuration/base.ts +++ b/packages/vite/configuration/base.ts @@ -331,12 +331,12 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }): // Simplified CommonJS handling - let Vite's optimizeDeps do the heavy lifting commonjs({ include: [/node_modules/], - // Force specific problematic modules to be treated as CommonJS + // Let Rollup/Vite decide default mapping for CommonJS modules. requireReturnsDefault: 'auto', defaultIsModuleExports: 'auto', transformMixedEsModules: true, // Ignore optional dependencies that are meant to fail gracefully - ignore: ['@nativescript/android', '@nativescript/ios'], + ignore: ['@nativescript/android', '@nativescript/ios', '@nativescript/visionos'], }), nsConfigAsJsonPlugin(), NativeScriptPlugin({ platform }), diff --git a/packages/vite/configuration/react.ts b/packages/vite/configuration/react.ts index cd57154f07..70e2cb56fc 100644 --- a/packages/vite/configuration/react.ts +++ b/packages/vite/configuration/react.ts @@ -86,7 +86,7 @@ export default React; ]; export const reactConfig = ({ mode }): UserConfig => { - return mergeConfig(baseConfig({ mode }), { + return mergeConfig(baseConfig({ mode, flavor: 'react' }), { plugins, }); }; diff --git a/packages/vite/configuration/solid.ts b/packages/vite/configuration/solid.ts index 8397296441..2513f00b13 100644 --- a/packages/vite/configuration/solid.ts +++ b/packages/vite/configuration/solid.ts @@ -47,7 +47,7 @@ const plugins = [ ]; export const solidConfig = ({ mode }): UserConfig => { - return mergeConfig(baseConfig({ mode }), { + return mergeConfig(baseConfig({ mode, flavor: 'solid' }), { plugins, }); }; diff --git a/packages/vite/configuration/typescript.ts b/packages/vite/configuration/typescript.ts index bee76cf8fc..9987c562c5 100644 --- a/packages/vite/configuration/typescript.ts +++ b/packages/vite/configuration/typescript.ts @@ -154,7 +154,7 @@ function createXmlLoaderPlugin(): Plugin { } export const typescriptConfig = ({ mode }): UserConfig => { - return mergeConfig(baseConfig({ mode }), { + return mergeConfig(baseConfig({ mode, flavor: 'typescript' }), { plugins: [createXmlLoaderPlugin(), createBundlerContextPlugin()], }); }; diff --git a/packages/vite/configuration/vue.ts b/packages/vite/configuration/vue.ts index 35f980837a..4f7db8780d 100644 --- a/packages/vite/configuration/vue.ts +++ b/packages/vite/configuration/vue.ts @@ -21,7 +21,7 @@ export const vueConfig = ({ mode }): UserConfig => { const isDevMode = targetMode === 'development'; const hmrActive = isDevMode && !!cliFlags.hmr; - return mergeConfig(baseConfig({ mode }), { + return mergeConfig(baseConfig({ mode, flavor: 'vue' }), { plugins: [ { ...alias({ diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index 7e5833ffa3..583d98fca9 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -22,8 +22,21 @@ const mainEntryRelPosix = (() => { const flavor = getProjectFlavor() as string; // Optional polyfills support (non-HMR specific but dev friendly) -const polyfillsPath = getProjectFilePath(getProjectAppRelativePath('polyfills.ts')); -const polyfillsExists = fs.existsSync(polyfillsPath); +// Resolve polyfills relative to the main entry directory so it works both for standalone projects and monorepos/workspaces where the workspace root and app root differ. +// We keep both the absolute filesystem path (for existsSync) and a project-root-relative POSIX path (for the import specifier used in Vite). +const mainEntryDir = path.dirname(mainEntry); +const polyfillsFsPath = path.resolve(mainEntryDir, 'polyfills.ts'); +const polyfillsExists = fs.existsSync(polyfillsFsPath); +const polyfillsImportSpecifier = (() => { + try { + // Normalize to "/..." posix-style (similar to mainEntryRelPosix) + const rel = path.relative(projectRoot, polyfillsFsPath).replace(/\\/g, '/'); + return ('/' + rel).replace(/\/+/g, '/'); + } catch { + // Fallback to a simple relative specifier next to main entry + return './polyfills.ts'; + } +})(); const VIRTUAL_ID = 'virtual:entry-with-polyfills'; const RESOLVED = '\0' + VIRTUAL_ID; @@ -167,9 +180,9 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' // ---- Optional polyfills ---- if (polyfillsExists) { - imports += `import '${polyfillsPath}';\n`; + imports += `import '${polyfillsImportSpecifier}';\n`; if (opts.verbose) { - imports += `console.info('[ns-entry] polyfills imported from', ${JSON.stringify(polyfillsPath)});\n`; + imports += `console.info('[ns-entry] polyfills imported from', ${JSON.stringify(polyfillsImportSpecifier)});\n`; } } else if (opts.verbose) { imports += "console.info('[ns-entry] no polyfills file found');\n"; From 2abf7bc303bd9cb5d9ce9ae6d577bdcca38317b7 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 23 Nov 2025 23:25:37 -0600 Subject: [PATCH 02/14] fix(vite): windows path handling --- packages/vite/hmr/server/vite-plugin.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/vite/hmr/server/vite-plugin.ts b/packages/vite/hmr/server/vite-plugin.ts index 90e325cd5b..67eb82d209 100644 --- a/packages/vite/hmr/server/vite-plugin.ts +++ b/packages/vite/hmr/server/vite-plugin.ts @@ -1,5 +1,6 @@ import type { Plugin, ResolvedConfig } from 'vite'; import { createRequire } from 'node:module'; +import path from 'path'; const require = createRequire(import.meta.url); const VIRTUAL_ID = 'virtual:ns-hmr-client'; @@ -20,7 +21,23 @@ export function nsHmrClientVitePlugin(opts: { platform: string; verbose?: boolea load(id) { if (id !== RESOLVED_ID) return null; - const clientPath = require.resolve('@nativescript/vite/hmr/client/index.js'); + /** + * Use a POSIX-style import specifier for the client entry to avoid + * Windows drive-letter paths (e.g. "D:\\...") accidentally + * becoming bare import ids that Rollup/Vite cannot resolve. + * We still resolve the real filesystem path for correctness, but convert it to a project-relative POSIX path before interpolating it into the generated module. + **/ + const clientFsPath = require.resolve('@nativescript/vite/hmr/client/index.js'); + // Prefer project root when available; otherwise fall back to cwd. + const projectRoot = config?.root || process.cwd(); + let clientImport = clientFsPath; + try { + const rel = path.relative(projectRoot, clientFsPath).replace(/\\/g, '/'); + clientImport = (rel.startsWith('.') ? rel : `/${rel}`).replace(/\/+/g, '/'); + } catch { + // On any failure, keep the original path but normalize to POSIX + clientImport = clientFsPath.replace(/\\/g, '/'); + } // Build ws url from Vite server info let host = process.env.NS_HMR_HOST || (config?.server?.host as any); @@ -38,7 +55,7 @@ export function nsHmrClientVitePlugin(opts: { platform: string; verbose?: boolea // Import client and start it with explicit ws URL const banner = opts.verbose ? `console.log('[ns-hmr-client] starting client -> ${wsUrl} (HTTP loader enabled via __NS_HTTP_ORIGIN__)');` : ''; return ` -import startViteHMR from "${clientPath}"; +import startViteHMR from "${clientImport}"; ${banner} startViteHMR({ wsUrl: ${JSON.stringify(wsUrl)} }); `; From 03f7d266fe09b147daa3e951d9ce851ed075592b Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Mon, 24 Nov 2025 22:42:35 -0600 Subject: [PATCH 03/14] fix: cross platform file paths --- packages/vite/helpers/css-tree.ts | 1 - .../vite/hmr/server/vite-plugin-path.spec.ts | 64 +++++++++++++++++++ packages/vite/hmr/server/vite-plugin.ts | 18 +++++- 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 packages/vite/hmr/server/vite-plugin-path.spec.ts diff --git a/packages/vite/helpers/css-tree.ts b/packages/vite/helpers/css-tree.ts index 271fcb7d50..052987ef59 100644 --- a/packages/vite/helpers/css-tree.ts +++ b/packages/vite/helpers/css-tree.ts @@ -2,7 +2,6 @@ import path from 'path'; import { __dirname } from './project.js'; export const aliasCssTree = [ - // Node.js built-ins and mdn-data polyfills for css-tree { find: 'module', replacement: path.resolve(__dirname, '../polyfills/module.js'), diff --git a/packages/vite/hmr/server/vite-plugin-path.spec.ts b/packages/vite/hmr/server/vite-plugin-path.spec.ts new file mode 100644 index 0000000000..367347a799 --- /dev/null +++ b/packages/vite/hmr/server/vite-plugin-path.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import path from 'path'; +import { pathToFileURL } from 'url'; + +// Reproduce the path logic from vite-plugin.ts in a testable helper +function computeClientImport(options: { projectRoot: string; clientFsPath: string }) { + const { projectRoot, clientFsPath } = options; + let clientImport = clientFsPath; + try { + const rel = path.relative(projectRoot, clientFsPath); + const relPosix = rel.replace(/\\/g, '/'); + + if (path.isAbsolute(rel)) { + clientImport = pathToFileURL(clientFsPath).toString(); + } else { + clientImport = (relPosix.startsWith('.') ? relPosix : `/${relPosix}`).replace(/\/+/g, '/'); + } + } catch { + clientImport = clientFsPath.replace(/\\/g, '/'); + } + return clientImport; +} + +describe('ns-hmr-client vite plugin path handling', () => { + it('keeps a clean project-relative POSIX path on POSIX-like roots', () => { + const projectRoot = '/Users/test/app'; + const clientFsPath = '/Users/test/app/node_modules/@nativescript/vite/hmr/client/index.js'; + + const result = computeClientImport({ projectRoot, clientFsPath }); + + expect(result).toBe('/node_modules/@nativescript/vite/hmr/client/index.js'); + }); + + it('falls back to file URL when relative becomes absolute (Windows-like different drive)', () => { + // Simulate a scenario where projectRoot and clientFsPath are on different drives. + // On real Windows, path.relative('C:/proj', 'D:/lib/file.js') is an absolute path + // starting with the target drive (e.g. 'D:/lib/file.js'). + const projectRoot = 'C:/project/root'; + const clientFsPath = 'D:/ns-vite-demo/node_modules/@nativescript/vite/hmr/client/index.js'; + + const result = computeClientImport({ projectRoot, clientFsPath }); + + // On non-Windows hosts, Node's path.relative may not simulate the + // cross-drive behavior. The important contract is: when the computed + // relative path is absolute, we do NOT generate an import like '/D:/...' + // that would later resolve to 'D:\\D:\\...'. In that case we use a + // file URL; otherwise we leave the relative specifier alone. + if (path.sep === '\\') { + // Windows: expect file URL behavior + expect(result.startsWith('file://')).toBe(true); + } + expect(result.includes('nativescript/vite/hmr/client/index.js')).toBe(true); + }); + + it('handles Windows-style same-drive paths as project-relative POSIX', () => { + const projectRoot = 'D:/ns-vite-demo'; + const clientFsPath = 'D:/ns-vite-demo/node_modules/@nativescript/vite/hmr/client/index.js'; + + const result = computeClientImport({ projectRoot, clientFsPath }); + + // When on the same drive, relative path should be node_modules/... and we normalize to POSIX. + expect(result).toBe('/node_modules/@nativescript/vite/hmr/client/index.js'); + }); +}); diff --git a/packages/vite/hmr/server/vite-plugin.ts b/packages/vite/hmr/server/vite-plugin.ts index 67eb82d209..c7465e43d7 100644 --- a/packages/vite/hmr/server/vite-plugin.ts +++ b/packages/vite/hmr/server/vite-plugin.ts @@ -1,6 +1,7 @@ import type { Plugin, ResolvedConfig } from 'vite'; import { createRequire } from 'node:module'; import path from 'path'; +import { pathToFileURL } from 'url'; const require = createRequire(import.meta.url); const VIRTUAL_ID = 'virtual:ns-hmr-client'; @@ -32,8 +33,21 @@ export function nsHmrClientVitePlugin(opts: { platform: string; verbose?: boolea const projectRoot = config?.root || process.cwd(); let clientImport = clientFsPath; try { - const rel = path.relative(projectRoot, clientFsPath).replace(/\\/g, '/'); - clientImport = (rel.startsWith('.') ? rel : `/${rel}`).replace(/\/+/g, '/'); + // Compute a project-relative POSIX path when possible. When `path.relative` + // returns an absolute path (this can occur on Windows if roots differ or + // when path.relative returns a drive-letter-prefixed path), avoid creating + // a specifier like `/D:/...` which later gets resolved to `D:\D:\...`. + const rel = path.relative(projectRoot, clientFsPath); + const relPosix = rel.replace(/\\/g, '/'); + + // If `rel` is absolute (e.g. starts with a drive letter on Windows), + // use a file:// URL for the import so Vite/Rollup do not prepend the + // project root and cause duplicated drive prefixes. + if (path.isAbsolute(rel)) { + clientImport = pathToFileURL(clientFsPath).toString(); + } else { + clientImport = (relPosix.startsWith('.') ? relPosix : `/${relPosix}`).replace(/\/+/g, '/'); + } } catch { // On any failure, keep the original path but normalize to POSIX clientImport = clientFsPath.replace(/\\/g, '/'); From 40788e379040e9d6af39c32e41e6dbcb1c8ed573 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Thu, 4 Dec 2025 11:17:10 -0800 Subject: [PATCH 04/14] chore: dep cleanup --- apps/automated/package.json | 8 +- apps/toolbox/package.json | 11 +- apps/toolbox/project.json | 2 +- apps/ui/package.json | 10 +- package-lock.json | 542 ++++++++++++++------ package.json | 7 +- packages/vite/bin/cli.cjs | 0 packages/vite/hmr/shared/vendor/manifest.ts | 21 +- packages/webpack5/package.json | 2 +- 9 files changed, 408 insertions(+), 195 deletions(-) mode change 100644 => 100755 packages/vite/bin/cli.cjs diff --git a/apps/automated/package.json b/apps/automated/package.json index a5602ba461..e2f41c27b5 100644 --- a/apps/automated/package.json +++ b/apps/automated/package.json @@ -11,13 +11,13 @@ "nativescript-theme-core": "file:../../node_modules/nativescript-theme-core" }, "devDependencies": { - "@nativescript/android": "alpha", - "@nativescript/ios": "alpha", - "@nativescript/visionos": "~8.9.0", + "@nativescript/android": "~9.0.0", + "@nativescript/ios": "~9.0.0", + "@nativescript/visionos": "~9.0.0", "@nativescript/vite": "file:../../dist/packages/vite", "@nativescript/webpack": "file:../../dist/packages/webpack5", "circular-dependency-plugin": "^5.2.2", - "typescript": "~5.8.0" + "typescript": "~5.9.3" }, "gitHead": "c06800e52ee1a184ea2dffd12a6702aaa43be4e3", "readme": "NativeScript Application" diff --git a/apps/toolbox/package.json b/apps/toolbox/package.json index 7bc8f8db39..998e74fbbb 100644 --- a/apps/toolbox/package.json +++ b/apps/toolbox/package.json @@ -9,14 +9,15 @@ "dependencies": { "@nativescript/core": "file:../../packages/core", "@nativescript/imagepicker": "^4.1.0", - "nativescript-theme-core": "file:../../node_modules/nativescript-theme-core" + "nativescript-theme-core": "file:../../node_modules/nativescript-theme-core", + "@valor/nativescript-websockets": "file:../../node_modules/@valor/nativescript-websockets" }, "devDependencies": { - "@nativescript/android": "alpha", - "@nativescript/ios": "alpha", - "@nativescript/visionos": "~8.9.0", + "@nativescript/android": "~9.0.0", + "@nativescript/ios": "~9.0.0", + "@nativescript/visionos": "~9.0.0", "@nativescript/vite": "file:../../dist/packages/vite", "@nativescript/webpack": "file:../../dist/packages/webpack5", - "typescript": "~5.8.0" + "typescript": "~5.9.3" } } diff --git a/apps/toolbox/project.json b/apps/toolbox/project.json index b0fa652e0f..3163d48e6d 100644 --- a/apps/toolbox/project.json +++ b/apps/toolbox/project.json @@ -24,7 +24,7 @@ "debug": { "executor": "@nativescript/nx:debug", "options": { - "noHmr": true, + "noHmr": false, "uglify": false, "release": false, "forDevice": false, diff --git a/apps/ui/package.json b/apps/ui/package.json index 6b6c66c562..7cf1e6e285 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -8,14 +8,14 @@ }, "dependencies": { "@nativescript/core": "file:../../packages/core", - "nativescript-theme-core": "file:../../node_modules/nativescript-theme-core" + "nativescript-theme-core": "^1.0.4" }, "devDependencies": { - "@nativescript/android": "alpha", - "@nativescript/ios": "alpha", - "@nativescript/visionos": "~8.9.0", + "@nativescript/android": "~9.0.0", + "@nativescript/ios": "~9.0.0", + "@nativescript/visionos": "~9.0.0", "@nativescript/webpack": "file:../../dist/packages/webpack5", - "typescript": "~5.8.0" + "typescript": "~5.9.3" }, "gitHead": "8ab7726d1ee9991706069c1359c552e67ee0d1a4", "readme": "NativeScript Application", diff --git a/package-lock.json b/package-lock.json index b7fc273315..bfe71524e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@valor/nativescript-websockets": "^2.0.2", "nativescript-theme-core": "^1.0.4" }, "devDependencies": { @@ -102,15 +103,172 @@ "vite-plugin-static-copy": "^3.1.4", "vitest": "^3.2.4", "vue-loader": "^15.0.0 <= 15.9.8", - "webpack": "^5.30.0 <= 5.50.0 || ^5.51.2", "webpack-bundle-analyzer": "^4.0.0", "webpack-chain": "^6.0.0", - "webpack-cli": "^4.0.0", - "webpack-merge": "^5.0.0", + "webpack-merge": "^6.0.0", "webpack-virtual-modules": "^0.4.0", "zx": "^8.3.0" } }, + "apps/automated": { + "extraneous": true, + "license": "MIT", + "dependencies": { + "@nativescript/core": "file:../../packages/core", + "nativescript-theme-core": "file:../../node_modules/nativescript-theme-core" + }, + "devDependencies": { + "@nativescript/android": "~9.0.0", + "@nativescript/ios": "~9.0.0", + "@nativescript/visionos": "~9.0.0", + "@nativescript/vite": "file:../../dist/packages/vite", + "@nativescript/webpack": "file:../../dist/packages/webpack5", + "circular-dependency-plugin": "^5.2.2", + "typescript": "~5.9.3" + } + }, + "apps/toolbox": { + "extraneous": true, + "license": "MIT", + "dependencies": { + "@nativescript/core": "file:../../packages/core", + "@nativescript/imagepicker": "^4.1.0", + "@valor/nativescript-websockets": "file:../../node_modules/@valor/nativescript-websockets", + "nativescript-theme-core": "file:../../node_modules/nativescript-theme-core" + }, + "devDependencies": { + "@nativescript/android": "~9.0.0", + "@nativescript/ios": "~9.0.0", + "@nativescript/visionos": "~9.0.0", + "@nativescript/vite": "file:../../dist/packages/vite", + "@nativescript/webpack": "file:../../dist/packages/webpack5", + "typescript": "~5.9.3" + } + }, + "apps/ui": { + "extraneous": true, + "license": "MIT", + "dependencies": { + "@nativescript/core": "file:../../packages/core", + "nativescript-theme-core": "^1.0.4" + }, + "devDependencies": { + "@nativescript/android": "~9.0.0", + "@nativescript/ios": "~9.0.0", + "@nativescript/visionos": "~9.0.0", + "@nativescript/webpack": "file:../../dist/packages/webpack5", + "typescript": "~5.9.3" + } + }, + "dist/packages/vite": { + "name": "@nativescript/vite", + "version": "1.0.2", + "extraneous": true, + "dependencies": { + "@analogjs/vite-plugin-angular": "^2.0.0", + "@angular-devkit/build-angular": "^20.0.0", + "@angular/build": "^20.0.0", + "@babel/core": "^7.28.0", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^5.1.1", + "@vue/compiler-sfc": "^3.4.38", + "esbuild": "^0.25.0", + "minimist": "^1.2.8", + "react-reconciler": "^0.32.0", + "sass": ">=1.70.0 <2", + "vite": "^7.2.0", + "vite-plugin-solid": "^2.11.8", + "vite-plugin-static-copy": "^3.1.0", + "ws": "^8.18.0" + }, + "bin": { + "nativescript-vite": "bin/cli.cjs" + }, + "devDependencies": { + "@types/node": "^24.9.1", + "vitest": "^3.2.4" + } + }, + "dist/packages/webpack5": { + "name": "@nativescript/webpack", + "version": "5.0.27", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "^7.0.0", + "@pmmmwh/react-refresh-webpack-plugin": "~0.6.1", + "@vue/compiler-sfc": "^3.5.17", + "acorn": "^8.0.0", + "acorn-stage3": "^4.0.0", + "ansi-colors": "^4.1.3", + "babel-loader": "^10.0.0", + "cli-highlight": "^2.0.0", + "commander": "^14.0.0", + "copy-webpack-plugin": "^13.0.0", + "css": "^3.0.0", + "css-loader": "^7.0.0", + "dotenv-webpack": "^8.0.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", + "loader-utils": "^2.0.0 || ^3.0.0", + "lodash.get": "^4.0.0", + "micromatch": "^4.0.0", + "postcss": "^8.0.0", + "postcss-import": "^16.0.0", + "postcss-loader": "^8.0.0", + "raw-loader": "^4.0.0", + "react-refresh": "~0.18.0", + "sass": "^1.0.0", + "sass-loader": "^16.0.0", + "sax": "^1.0.0", + "semver": "^7.0.0 || ^6.0.0", + "source-map": "^0.7.0", + "terser-webpack-plugin": "^5.0.0", + "ts-dedent": "^2.0.0", + "ts-loader": "^9.0.0", + "vue-loader": "^17.4.2", + "webpack": "^5.30.0 <= 5.50.0 || ^5.51.2", + "webpack-bundle-analyzer": "^4.0.0", + "webpack-chain": "^6.0.0", + "webpack-cli": "^6.0.0", + "webpack-merge": "^6.0.0", + "webpack-virtual-modules": "^0.4.0" + }, + "bin": { + "nativescript-webpack": "dist/bin/index.js" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@babel/helpers": ">=7.26.10", + "@types/css": "0.0.38", + "@types/jest": "29.5.4", + "@types/loader-utils": "2.0.6", + "@types/lodash.get": "4.4.9", + "@types/micromatch": "4.0.9", + "@types/sax": "1.2.7", + "@types/terser-webpack-plugin": "5.2.0", + "@types/webpack-virtual-modules": "0.4.2", + "jest": "~29.7.0", + "jest-matcher-utils": "~29.7.0", + "nativescript-vue-template-compiler": "2.9.3", + "ts-jest": "29.4.5", + "typescript": "~5.9.3" + }, + "peerDependencies": { + "nativescript-vue-template-compiler": "^2.8.1" + }, + "peerDependenciesMeta": { + "nativescript-vue-template-compiler": { + "optional": true + } + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -10522,6 +10680,10 @@ "win32" ] }, + "node_modules/@valor/nativescript-websockets": { + "version": "2.0.2", + "license": "MIT" + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", @@ -11276,45 +11438,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "envinfo": "^7.7.3" - }, - "peerDependencies": { - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "webpack-cli": "4.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, "node_modules/@xml-tools/parser": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", @@ -12840,9 +12963,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001676", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", - "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true, "funding": [ { @@ -14557,9 +14680,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.50", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz", - "integrity": "sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==", + "version": "1.5.264", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.264.tgz", + "integrity": "sha512-1tEf0nLgltC3iy9wtlYDlQDc5Rg9lEKVjEmIHJ21rI9OcqkvD45K1oyNIRA4rR1z3LgJ7KeGzEBojVcV6m4qjA==", "dev": true, "license": "ISC" }, @@ -14698,19 +14821,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/envinfo": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -15553,16 +15663,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -21916,8 +22016,6 @@ }, "node_modules/nativescript-theme-core": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/nativescript-theme-core/-/nativescript-theme-core-1.0.7.tgz", - "integrity": "sha512-MJi3Lmj00dLylNwY31Zx5uC/vA5ylrODUzeQgkE9JPhmChOKVIJYu56NEEmSDjyphiQwT7f1Yr5gOw7dSFQo7A==", "license": "Apache-2.0" }, "node_modules/nativescript-typedoc-theme": { @@ -22236,9 +22334,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -27832,9 +27930,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, "license": "MIT", "engines": { @@ -28797,107 +28895,25 @@ "node": ">=0.10.0" } }, - "node_modules/webpack-cli": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", - "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "cross-spawn": "^7.0.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "@webpack-cli/migrate": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-cli/node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/webpack-cli/node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.9.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", "engines": { @@ -29659,6 +29675,194 @@ "@types/jsonfile": "*", "@types/node": "*" } + }, + "packages/core": { + "name": "@nativescript/core", + "version": "9.0.5", + "extraneous": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/NativeScript" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NativeScript" + } + ], + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@csstools/css-calc": "~2.1.4", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@nativescript/hook": "~3.0.4", + "acorn": "^8.15.0", + "css-tree": "^3.1.0", + "css-what": "^7.0.0", + "emoji-regex": "^10.2.1", + "source-map": "0.7.6", + "source-map-js": "^1.2.1", + "tslib": "^2.0.0" + } + }, + "packages/devtools": { + "name": "@nativescript/devtools", + "version": "0.0.0", + "extraneous": true, + "devDependencies": { + "@nativescript/core": "../core" + } + }, + "packages/types": { + "name": "@nativescript/types", + "version": "9.0.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@nativescript/types-android": "9.0.0", + "@nativescript/types-ios": "9.0.0" + } + }, + "packages/types-android": { + "name": "@nativescript/types-android", + "version": "9.0.0", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/types-ios": { + "name": "@nativescript/types-ios", + "version": "9.0.0", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/types-minimal": { + "name": "@nativescript/types-minimal", + "version": "9.0.0", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/ui-mobile-base": { + "name": "@nativescript/ui-mobile-base", + "version": "7.0.0", + "extraneous": true, + "license": "Apache-2.0" + }, + "packages/vite": { + "name": "@nativescript/vite", + "version": "1.0.2", + "extraneous": true, + "dependencies": { + "@analogjs/vite-plugin-angular": "^2.0.0", + "@angular-devkit/build-angular": "^20.0.0", + "@angular/build": "^20.0.0", + "@babel/core": "^7.28.0", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@vitejs/plugin-vue": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^5.1.1", + "@vue/compiler-sfc": "^3.4.38", + "esbuild": "^0.25.0", + "minimist": "^1.2.8", + "react-reconciler": "^0.32.0", + "sass": ">=1.70.0 <2", + "vite": "^7.2.0", + "vite-plugin-solid": "^2.11.8", + "vite-plugin-static-copy": "^3.1.0", + "ws": "^8.18.0" + }, + "bin": { + "nativescript-vite": "bin/cli.cjs" + }, + "devDependencies": { + "@types/node": "^24.9.1", + "vitest": "^3.2.4" + } + }, + "packages/webpack5": { + "name": "@nativescript/webpack", + "version": "5.0.27", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "^7.0.0", + "@pmmmwh/react-refresh-webpack-plugin": "~0.6.1", + "@vue/compiler-sfc": "^3.5.17", + "acorn": "^8.0.0", + "acorn-stage3": "^4.0.0", + "ansi-colors": "^4.1.3", + "babel-loader": "^10.0.0", + "cli-highlight": "^2.0.0", + "commander": "^14.0.0", + "copy-webpack-plugin": "^13.0.0", + "css": "^3.0.0", + "css-loader": "^7.0.0", + "dotenv-webpack": "^8.0.0", + "fork-ts-checker-webpack-plugin": "^9.0.0", + "loader-utils": "^2.0.0 || ^3.0.0", + "lodash.get": "^4.0.0", + "micromatch": "^4.0.0", + "postcss": "^8.0.0", + "postcss-import": "^16.0.0", + "postcss-loader": "^8.0.0", + "raw-loader": "^4.0.0", + "react-refresh": "~0.18.0", + "sass": "^1.0.0", + "sass-loader": "^16.0.0", + "sax": "^1.0.0", + "semver": "^7.0.0 || ^6.0.0", + "source-map": "^0.7.0", + "terser-webpack-plugin": "^5.0.0", + "ts-dedent": "^2.0.0", + "ts-loader": "^9.0.0", + "vue-loader": "^17.4.2", + "webpack": "^5.30.0 <= 5.50.0 || ^5.51.2", + "webpack-bundle-analyzer": "^4.0.0", + "webpack-chain": "^6.0.0", + "webpack-cli": "^6.0.0", + "webpack-merge": "^6.0.0", + "webpack-virtual-modules": "^0.4.0" + }, + "bin": { + "nativescript-webpack": "dist/bin/index.js" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@babel/helpers": ">=7.26.10", + "@types/css": "0.0.38", + "@types/jest": "29.5.4", + "@types/loader-utils": "2.0.6", + "@types/lodash.get": "4.4.9", + "@types/micromatch": "4.0.9", + "@types/sax": "1.2.7", + "@types/terser-webpack-plugin": "5.2.0", + "@types/webpack-virtual-modules": "0.4.2", + "jest": "~29.7.0", + "jest-matcher-utils": "~29.7.0", + "nativescript-vue-template-compiler": "2.9.3", + "ts-jest": "29.4.5", + "typescript": "~5.9.3" + }, + "peerDependencies": { + "nativescript-vue-template-compiler": "^2.8.1" + }, + "peerDependenciesMeta": { + "nativescript-vue-template-compiler": { + "optional": true + } + } + }, + "packages/winter-tc": { + "name": "@nativescript/winter-tc", + "version": "0.0.1", + "extraneous": true, + "license": "Apache-2.0" } } } diff --git a/package.json b/package.json index 245d13f5fb..851bb4e6f2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "url": "https://github.com/NativeScript/NativeScript.git" }, "dependencies": { + "@valor/nativescript-websockets": "^2.0.2", "nativescript-theme-core": "^1.0.4" }, "devDependencies": { @@ -109,11 +110,9 @@ "vite-plugin-static-copy": "^3.1.4", "vitest": "^3.2.4", "vue-loader": "^15.0.0 <= 15.9.8", - "webpack": "^5.30.0 <= 5.50.0 || ^5.51.2", - "webpack-bundle-analyzer": "^4.0.0", "webpack-chain": "^6.0.0", - "webpack-cli": "^4.0.0", - "webpack-merge": "^5.0.0", + "webpack-bundle-analyzer": "^4.0.0", + "webpack-merge": "^6.0.0", "webpack-virtual-modules": "^0.4.0", "zx": "^8.3.0" }, diff --git a/packages/vite/bin/cli.cjs b/packages/vite/bin/cli.cjs old mode 100644 new mode 100755 diff --git a/packages/vite/hmr/shared/vendor/manifest.ts b/packages/vite/hmr/shared/vendor/manifest.ts index 364b232024..e4a1865fa0 100644 --- a/packages/vite/hmr/shared/vendor/manifest.ts +++ b/packages/vite/hmr/shared/vendor/manifest.ts @@ -26,6 +26,12 @@ interface VendorBundleResult { entries: string[]; } +// Internal representation of resolved vendor inputs, including any metadata we +// need during esbuild bundling +interface CollectedVendorModules { + entries: string[]; +} + interface VendorManifestPluginOptions { projectRoot: string; platform: string; @@ -98,6 +104,7 @@ const ALWAYS_EXCLUDE = new Set([ 'vue-tsc', 'ws', '@types/node', + 'nativescript-theme-core', ]); const INDEX_ALIAS_SUFFIXES = ['/index', '/index.js', '/index.android.js', '/index.ios.js', '/index.visionos.js']; @@ -266,8 +273,8 @@ export default vendorManifest; async function generateVendorBundle(options: GenerateVendorOptions): Promise { const { projectRoot, platform, mode, flavor } = options; - const entries = collectVendorModules(projectRoot, platform, flavor); - const entryCode = createVendorEntry(entries); + const collected = collectVendorModules(projectRoot, platform, flavor); + const entryCode = createVendorEntry(collected.entries); const plugins: esbuild.Plugin[] = [ // Resolve virtual modules and Angular shims used by the vendor entry. @@ -320,16 +327,16 @@ async function generateVendorBundle(options: GenerateVendorOptions): Promise Date: Sat, 13 Dec 2025 00:04:59 -0800 Subject: [PATCH 05/14] fix(vite): global flags for hmr --- packages/vite/helpers/main-entry.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index 583d98fca9..4ffb2fa9a5 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -67,6 +67,14 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' } if (opts.hmrActive) { + // Seed platform globals on the primary bundle realm using the same + // CLI-derived platform that drives global-defines.ts. This ensures + // HMR-delivered HTTP ESM modules can reliably read platform flags + imports += `globalThis.__DEV__ = ${opts.isDevMode ? 'true' : 'false'};\n`; + imports += `globalThis.__ANDROID__ = ${opts.platform === 'android' ? 'true' : 'false'};\n`; + imports += `globalThis.__IOS__ = ${opts.platform === 'ios' ? 'true' : 'false'};\n`; + imports += `globalThis.__VISIONOS__ = ${opts.platform === 'visionos' ? 'true' : 'false'};\n`; + imports += `globalThis.__APPLE__ = ${opts.platform === 'ios' || opts.platform === 'visionos' ? 'true' : 'false'};\n`; // ---- Vendor manifest bootstrap ---- // Use single self-contained vendor module to avoid extra imports affecting chunking imports += "import vendorManifest, { __nsVendorModuleMap } from '@nativescript/vendor';\n"; From 414ce4398bc7dec8cdf4853430a69499b45c8746 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 13 Dec 2025 00:12:56 -0800 Subject: [PATCH 06/14] chore: log cleanup --- packages/vite/hmr/client/index.ts | 473 +++++------------- packages/vite/hmr/client/utils.ts | 189 +------ .../vite/hmr/frameworks/vue/client/index.ts | 5 +- packages/vite/hmr/server/websocket.ts | 220 ++------ 4 files changed, 196 insertions(+), 691 deletions(-) diff --git a/packages/vite/hmr/client/index.ts b/packages/vite/hmr/client/index.ts index 888d51b228..fced6c15fa 100644 --- a/packages/vite/hmr/client/index.ts +++ b/packages/vite/hmr/client/index.ts @@ -6,7 +6,7 @@ * The HMR client is evaluated via HTTP ESM on device; static imports would create secondary instances. */ -import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore, attachDiagnosticsToFrame, logUiSnapshot } from './utils.js'; +import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore } from './utils.js'; import { handleCssUpdates } from './css-handler.js'; // satisfied by define replacement @@ -22,13 +22,6 @@ const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`; // Policy: by default, let the app's own main entry mount initially; HMR client handles updates/remounts only. // Flip this to true via global __NS_HMR_ALLOW_INITIAL_MOUNT__ if you need the client to perform the first mount. const ALLOW_INITIAL_MOUNT: boolean = !!globalThis.__NS_HMR_ALLOW_INITIAL_MOUNT__; -// When verbose mode is enabled, also enable runtime nav diagnostics so /ns/rt logs are visible -try { - if (VERBOSE) { - globalThis.__NS_DEV_LOGS__ = true; - globalThis.__NS_VERBOSE_RT_NAV__ = true; - } -} catch {} // Ensure core aliases are present on globalThis early so that /ns/rt exports resolve to functions // before any SFCs are evaluated during HTTP-only dev boot. @@ -48,45 +41,11 @@ function ensureCoreAliasesOnGlobalThis() { try { if (P && !g.Page) g.Page = P; } catch {} - // Optional diagnostics: compare with vendor realm if available - if (VERBOSE) { - let vF: any, vA: any, vP: any; - try { - const reg: Map | undefined = g.__nsVendorRegistry; - const vmod = reg?.get ? reg.get('@nativescript/core') : undefined; - const vns = (vmod && (vmod.default || vmod)) || vmod; - vF = vns?.Frame; - vA = vns?.Application; - vP = vns?.Page; - } catch {} - try { - console.log('[hmr-client] core alias status', { - globalHas: { Frame: !!F, Application: !!A, Page: !!P }, - globalMethods: { - FrameTopmost: typeof F?.topmost === 'function', - AppResetRoot: typeof A?.resetRootView === 'function', - }, - sameRef: { - Frame: F && vF ? F === vF : undefined, - Application: A && vA ? A === vA : undefined, - Page: P && vP ? P === vP : undefined, - }, - ctorNames: { - Frame: F?.name || F?.constructor?.name, - Application: A?.name || A?.constructor?.name, - Page: P?.name || P?.constructor?.name, - }, - }); - } catch {} - } } catch {} } // Apply once on module evaluation ensureCoreAliasesOnGlobalThis(); -// Install low-level diagnostics for navigation and root replacement to trace duplicates and state -installDeepDiagnostics(); - /** * Flavor hooks */ @@ -94,190 +53,13 @@ import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSf import { handleAngularHotUpdateMessage, installAngularHmrClientHooks } from '../frameworks/angular/client/index.js'; switch (__NS_TARGET_FLAVOR__) { case 'vue': - if (VERBOSE) { - console.log('[hmr-client] installing nativescript-vue dev shims'); - } installNsVueDevShims(); break; case 'angular': - if (VERBOSE) { - try { - console.log('[hmr-client] Initializing Angular HMR shims'); - } catch {} - } installAngularHmrClientHooks(); break; } -// Global frame diagnostics: instrument Frame.navigate and Frame.topmost to detect -// navigation against non-authoritative frames across the app (helps gray-screen cases) -try { - const g: any = globalThis as any; - const F: any = getCore('Frame') || g.Frame; - if (F && F.prototype && !g.__NS_DEV_GLOBAL_FRAME_PATCHED__) { - const tag = (fr: any) => { - try { - if (!fr) return; - if (!fr.__ns_tag) fr.__ns_tag = Math.random().toString(36).slice(2); - } catch {} - }; - const proto = F.prototype; - const origNav = proto.navigate; - if (typeof origNav === 'function') { - proto.navigate = function __ns_diag_nav(entry: any) { - try { - tag(this); - console.log('[diag][global][frame.navigate]', { - tag: (this as any).__ns_tag, - type: this?.constructor?.name, - hasCreate: !!entry?.create, - clearHistory: !!entry?.clearHistory, - animated: !!entry?.animated, - }); - } catch {} - return origNav.apply(this, arguments as any); - } as any; - } - const origTop = typeof F.topmost === 'function' ? F.topmost.bind(F) : null; - if (origTop) { - F.topmost = function __ns_diag_topmost() { - const fr = origTop(); - try { - tag(fr); - console.log('[diag][global][Frame.topmost]', { - tag: (fr as any)?.__ns_tag, - type: (fr as any)?.constructor?.name, - }); - } catch {} - return fr; - } as any; - } - try { - g.__NS_DEV_GLOBAL_FRAME_PATCHED__ = true; - } catch {} - } -} catch {} - -// --- Diagnostics helpers ---------------------------------------------------- -function summarizeNavEntry(entry: any) { - try { - if (!entry) return { kind: 'empty' }; - if (typeof entry === 'string') return { kind: 'string', moduleName: entry }; - const hasCreate = typeof (entry as any).create === 'function'; - const moduleName = (entry as any).moduleName; - const clearHistory = !!(entry as any).clearHistory; - const animated = (entry as any).animated; - const backstackVisible = (entry as any).backstackVisible; - const contextKeys = Object.keys((entry as any).context || {}); - return { - kind: 'entry', - hasCreate, - moduleName, - clearHistory, - animated, - backstackVisible, - contextKeys, - }; - } catch { - return { kind: 'unknown' }; - } -} - -function classifyResetArg(arg: any) { - try { - const ctorName = String(arg?.constructor?.name || '').replace(/^_+/, ''); - const keys = Object.keys(arg || {}); - const hasCreate = typeof arg?.create === 'function'; - const hasModuleName = typeof arg?.moduleName === 'string'; - const isFrameLike = !!arg && (ctorName === 'Frame' || /^Frame(\$\d+)?$/.test(ctorName) || (typeof arg?.navigate === 'function' && typeof arg?.addChild === 'function')); - const isPageLike = !!arg && (ctorName === 'Page' || /^Page(\$\d+)?$/.test(ctorName) || (typeof arg?.content !== 'undefined' && typeof arg?.addChild === 'function')); - return { - ctorName, - keys, - hasCreate, - hasModuleName, - isFrameLike, - isPageLike, - }; - } catch { - return { ctorName: 'unknown' }; - } -} - -function installDeepDiagnostics() { - if (!VERBOSE) return; - const g: any = globalThis as any; - try { - // Patch Frame.navigate to log calls and a short stack - const F = getCore('Frame') || g.Frame; - if (F?.prototype && !(F.prototype as any).__ns_diag_nav__) { - const orig = F.prototype.navigate; - if (typeof orig === 'function') { - (F.prototype as any).__ns_diag_nav__ = true; - // Simple duplicate navigation suppression in dev: if the same target is navigated twice within a short window, ignore the 2nd. - F.prototype.navigate = function (...args: any[]) { - try { - const entry = args[0]; - const summary = summarizeNavEntry(entry); - const stack = (new Error().stack || '').split('\n').slice(2, 8).join('\n'); - console.log('[diag][Frame.navigate]', { - frameCtor: this?.constructor?.name, - summary, - stack, - }); - try { - const gAny: any = globalThis as any; - const key = JSON.stringify({ - k: 'nav', - m: summary.moduleName || '', - c: !!summary.hasCreate, - ch: !!summary.clearHistory, - a: !!summary.animated, - }); - const now = Date.now(); - const last = gAny.__NS_DIAG_LAST_NAV__; - if (last && last.key === key && now - last.t < 300) { - console.warn('[diag][Frame.navigate] duplicate nav suppressed (dev)', { withinMs: now - last.t, key }); - return; // suppress duplicate - } - gAny.__NS_DIAG_LAST_NAV__ = { key, t: now }; - } catch {} - } catch {} - return orig.apply(this, args as any); - } as any; - } - } - } catch {} - try { - // Wrap Application.resetRootView to log argument classification and stack - const App = getCore('Application') || g.Application; - const proto = App && Object.getPrototypeOf(App); - const orig = (App && App.resetRootView) || (proto && proto.resetRootView); - if (typeof orig === 'function' && !(g as any).__NS_DIAG_RESET_WRAPPED__) { - const wrapped = function __ns_diag_resetRootView(this: any, entry: any) { - try { - const classification = classifyResetArg(entry); - const stack = (new Error().stack || '').split('\n').slice(2, 8).join('\n'); - console.log('[diag][Application.resetRootView]', { - classification, - stack, - }); - } catch {} - return orig.call(this, entry); - } as any; - try { - App.resetRootView = wrapped; - } catch {} - try { - if (proto && typeof proto === 'object') (proto as any).resetRootView = wrapped; - } catch {} - try { - (g as any).__NS_DIAG_RESET_WRAPPED__ = true; - } catch {} - } - } catch {} -} - // Track whether we've mounted an initial app root yet in HTTP-only boot let initialMounted = !!(globalThis as any).__NS_HMR_BOOT_COMPLETE__; // Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations @@ -287,6 +69,7 @@ let tsModuleSet: Set | null = null; let tsMainId: string | null = null; const changedQueue: any[] = []; let processingQueue = false; +let processingPromise: Promise | null = null; // Detect whether the early placeholder root is still active on screen function isPlaceholderActive(): boolean { @@ -759,17 +542,7 @@ function __nsNavigateUsingApp(comp: any, opts: any = {}) { try { (fr as any).navigate(navEntry); } catch {} - try { - attachDiagnosticsToFrame(fr); - } catch {} setRootFrame(fr); - try { - (getRootFrame() as any).__ns_tag ||= Math.random().toString(36).slice(2); - console.log('[diag][root] ROOT_FRAME set (app-nav)', { - tag: getRootFrame()?.__ns_tag, - type: getRootFrame()?.constructor?.name, - }); - } catch {} return fr; }, } as any); @@ -777,24 +550,8 @@ function __nsNavigateUsingApp(comp: any, opts: any = {}) { } throw new Error('Application.resetRootView unavailable'); } - try { - attachDiagnosticsToFrame(frame); - } catch {} const navEntry = { create: () => buildTarget(), ...(opts || {}) } as any; - try { - const summary = summarizeNavEntry(navEntry); - if (VERBOSE) console.log('[app-nav] navigate entry', summary); - } catch {} (frame as any).navigate(navEntry); - try { - const top2 = (g.Frame && g.Frame.topmost && g.Frame.topmost()) || null; - const ctor2 = top2 && top2.constructor && top2.constructor.name; - if (VERBOSE) - console.log('[app-nav] after navigate', { - topCtor: ctor2, - hasTop: !!top2, - }); - } catch {} return undefined; } @@ -803,7 +560,7 @@ try { (globalThis as any).__nsNavigateUsingApp = __nsNavigateUsingApp; } catch {} -async function processQueue() { +async function processQueue(): Promise { if (!(globalThis as any).__NS_HMR_BOOT_COMPLETE__) { if (VERBOSE) console.log('[hmr][gate] deferring HMR eval until boot complete'); setTimeout(() => { @@ -811,64 +568,68 @@ async function processQueue() { processQueue(); } catch {} }, 150); - return; + return Promise.resolve(); } - if (processingQueue) return; + if (processingQueue) return processingPromise || Promise.resolve(); processingQueue = true; - try { - // Simple deterministic drain of the queue. We currently focus on TS flavor - // by re-importing changed modules (to refresh their HTTP ESM copies) and - // then performing a root reset so the UI reflects the new code. - const seen = new Set(); - const drained: string[] = []; - while (changedQueue.length) { - const id = changedQueue.shift(); - if (!id || typeof id !== 'string') continue; - if (seen.has(id)) continue; - seen.add(id); - drained.push(id); - } - if (!drained.length) return; - if (VERBOSE) console.log('[hmr][queue] processing changed ids', drained); - // Evaluate changed modules best-effort; failures shouldn't completely break HMR. - for (const id of drained) { - try { - const spec = normalizeSpec(id); - const url = await requestModuleFromServer(spec); - if (!url) continue; - if (VERBOSE) console.log('[hmr][queue] re-import', { id, spec, url }); - await import(/* @vite-ignore */ url); - } catch (e) { - if (VERBOSE) console.warn('[hmr][queue] re-import failed for', id, e); + processingPromise = (async () => { + try { + // Simple deterministic drain of the queue. We currently focus on TS flavor + // by re-importing changed modules (to refresh their HTTP ESM copies) and + // then performing a root reset so the UI reflects the new code. + const seen = new Set(); + const drained: string[] = []; + while (changedQueue.length) { + const id = changedQueue.shift(); + if (!id || typeof id !== 'string') continue; + if (seen.has(id)) continue; + seen.add(id); + drained.push(id); } - } - // After evaluating the batch, perform flavor-specific UI refresh. - switch (__NS_TARGET_FLAVOR__) { - case 'vue': - // Vue SFCs are handled via the registry update path; nothing to do here. - break; - case 'typescript': { - // For TS apps, always reset back to the conventional app root. - // This preserves the shell (Frame, ActionBar, etc.) that the app's - // own bootstrapping wires up via `Application.run`. + if (!drained.length) return; + if (VERBOSE) console.log('[hmr][queue] processing changed ids', drained); + // Evaluate changed modules best-effort; failures shouldn't completely break HMR. + for (const id of drained) { try { - const g: any = globalThis as any; - const App = getCore('Application') || g.Application; - if (!App || typeof App.resetRootView !== 'function') { - if (VERBOSE) console.warn('[hmr][queue] TS flavor: Application.resetRootView unavailable; skipping UI refresh'); - break; - } - if (VERBOSE) console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes'); - App.resetRootView({ moduleName: 'app-root' } as any); + const spec = normalizeSpec(id); + const url = await requestModuleFromServer(spec); + if (!url) continue; + if (VERBOSE) console.log('[hmr][queue] re-import', { id, spec, url }); + const mod: any = await import(/* @vite-ignore */ url); } catch (e) { - console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e); + if (VERBOSE) console.warn('[hmr][queue] re-import failed for', id, e); } - break; } + // After evaluating the batch, perform flavor-specific UI refresh. + switch (__NS_TARGET_FLAVOR__) { + case 'vue': + // Vue SFCs are handled via the registry update path; nothing to do here. + break; + case 'typescript': { + // For TS apps, always reset back to the conventional app root. + // This preserves the shell (Frame, ActionBar, etc.) that the app's + // own bootstrapping wires up via `Application.run`. + try { + const g: any = globalThis as any; + const App = getCore('Application') || g.Application; + if (!App || typeof App.resetRootView !== 'function') { + if (VERBOSE) console.warn('[hmr][queue] TS flavor: Application.resetRootView unavailable; skipping UI refresh'); + break; + } + if (VERBOSE) console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes'); + App.resetRootView({ moduleName: 'app-root' } as any); + } catch (e) { + console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e); + } + break; + } + } + } finally { + processingQueue = false; + processingPromise = null; } - } finally { - processingQueue = false; - } + })(); + return processingPromise; } let hmrSocket: WebSocket | null = null; // Track server-announced batches for each version so we can import in-order client-side @@ -1017,11 +778,65 @@ async function handleHmrMessage(ev: any) { } catch { return; } - if (VERBOSE) console.log('[hmr-client] msg', msg); + + // Notify optional app-level hook after an HMR batch is applied. + function notifyAppHmrUpdate(kind: 'full-graph' | 'delta', changedIds: string[] | undefined) { + try { + const hook = globalThis.__NS_HMR_ON_UPDATE__; + if (typeof hook === 'function') { + hook({ type: kind, version: getGraphVersion(), changedIds: changedIds || [], raw: msg }); + } + } catch {} + } + if (msg) { if (msg.type === 'ns:hmr-full-graph') { + // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update. + try { + const g: any = globalThis as any; + g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1; + } catch {} + // Capture previous graph snapshot so we can infer which modules changed. + const prevGraph = new Map(graph); setGraphVersion(Number(msg.version || getGraphVersion() || 0)); applyFullGraph(msg); + // In some cases (e.g. server chooses full-graph resync / page reload), we won't + // receive a delta queue to re-import changed TS modules. Without re-import, + // HTTP ESM caching means module bodies (and side effects) won't re-run. + try { + const inferredChanged: string[] = []; + try { + for (const [id, next] of graph.entries()) { + const prev = prevGraph.get(id); + if (!prev || prev.hash !== next.hash) inferredChanged.push(id); + } + // Removed modules are also "changed" but we don't import them. + } catch {} + // Best-effort: only re-import real source modules (avoid .vue registry pipeline and virtual ids) + const toReimport = inferredChanged.filter((id) => { + if (!id || typeof id !== 'string') return false; + if (/^\0|^\/\0/.test(id)) return false; + if (/plugin-vue:export-helper/.test(id)) return false; + if (/\.vue$/i.test(id)) return false; + if (id.endsWith(APP_MAIN_ENTRY_SPEC)) return false; + return true; + }); + if (toReimport.length && VERBOSE) console.log('[hmr][full-graph] inferred changed modules; re-importing', toReimport); + for (const id of toReimport) { + try { + const spec = normalizeSpec(id); + const url = await requestModuleFromServer(spec); + if (!url) continue; + if (VERBOSE) console.log('[hmr][full-graph] re-import', { id, spec, url }); + await import(/* @vite-ignore */ url); + } catch (e) { + if (VERBOSE) console.warn('[hmr][full-graph] re-import failed for', id, e); + } + } + } catch {} + + const fullIds = Array.isArray(msg.modules) ? msg.modules.map((m: any) => m?.id).filter(Boolean) : []; + notifyAppHmrUpdate('full-graph', fullIds); return; } if (msg.type === 'ns:ts-module-registry') { @@ -1048,12 +863,24 @@ async function handleHmrMessage(ev: any) { return; } if (msg.type === 'ns:hmr-delta') { - setGraphVersion(Number(msg.newVersion || getGraphVersion() || 0)); + // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update. + try { + const g: any = globalThis as any; + g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1; + } catch {} try { const ids = Array.isArray(msg.changed) ? msg.changed.map((c: any) => c?.id).filter(Boolean) : []; if (ids.length) txnClientBatches.set(getGraphVersion(), ids); } catch {} applyDelta(msg); + // Ensure queued module re-imports complete before notifying app hooks. + // Otherwise app-level handlers can run against stale module bodies due to HTTP ESM caching. + try { + await processQueue(); + } catch {} + + const deltaIds = Array.isArray(msg.changed) ? msg.changed.map((c: any) => c?.id).filter(Boolean) : []; + notifyAppHmrUpdate('delta', deltaIds); return; } else if (handleAngularHotUpdateMessage(msg, { getCore, verbose: VERBOSE })) { return; @@ -1156,22 +983,6 @@ async function performResetRoot(newComponent: any): Promise { try { ensureCoreAliasesOnGlobalThis(); } catch {} - try { - if (VERBOSE) logUiSnapshot('pre-performResetRoot'); - } catch {} - if (VERBOSE) { - try { - const g: any = globalThis as any; - const vF = getCore('Frame'); - console.log('[hmr-client] alias check before remount', { - globalFrameHasTopmost: typeof g?.Frame?.topmost === 'function', - vendorFrameHasTopmost: typeof vF?.topmost === 'function', - sameFrameRef: vF === g?.Frame, - appHasReset: typeof g?.Application?.resetRootView === 'function', - pageIsCtor: typeof g?.Page === 'function', - }); - } catch {} - } if (VERBOSE) { console.log('[hmr-client] Single-path: replace current root Page'); console.log('[hmr-client] Component details:', { @@ -1361,10 +1172,6 @@ async function performResetRoot(newComponent: any): Promise { const isAuthoritativeFrame = !!existingAppFrame && existingAppFrame !== placeholderFrame; if (!hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof (existingAppFrame as any).navigate === 'function') { try { - if (VERBOSE) console.log('[hmr-client] navigating authoritative app Frame to new Page (no placeholder, smooth swap)'); - try { - attachDiagnosticsToFrame(existingAppFrame); - } catch {} const navEntry = { create: () => preparedRoot, clearHistory: true, @@ -1440,57 +1247,27 @@ async function performResetRoot(newComponent: any): Promise { } as any; try { (fr as any).navigate(navEntry); - if (VERBOSE) console.log('[hmr-client] resetRootView:create navigated Frame'); } catch (e) { console.warn('[hmr-client] resetRootView:create navigate failed', e); } - try { - attachDiagnosticsToFrame(fr); - } catch {} setRootFrame(fr); - try { - (getRootFrame() as any).__ns_tag ||= Math.random().toString(36).slice(2); - console.log('[diag][root] ROOT_FRAME set (new)', { - tag: getRootFrame()?.__ns_tag, - type: getRootFrame()?.constructor?.name, - }); - } catch {} return fr; } else { const fr = preparedRoot; - if (VERBOSE) console.log('[hmr-client] resetRootView:create using provided Frame', { type: fr?.constructor?.name }); - try { - attachDiagnosticsToFrame(fr); - } catch {} setRootFrame(fr); - try { - (getRootFrame() as any).__ns_tag ||= Math.random().toString(36).slice(2); - console.log('[diag][root] ROOT_FRAME set (provided)', { - tag: getRootFrame()?.__ns_tag, - type: getRootFrame()?.constructor?.name, - }); - } catch {} return fr; } }, } as any; - if (VERBOSE) console.log('[hmr-client] invoking Application.resetRootView with entry (always)', { isFrameRoot, hadPlaceholder, isIOS }); // Always use an entry with a create() function to avoid cross‑realm instanceof checks on Android. App2.resetRootView(entry as any); // After authoritative reset, it's safe to detach the early placeholder launch handler try { const restore = (globalThis as any).__NS_DEV_RESTORE_PLACEHOLDER__; if (typeof restore === 'function') { - if (VERBOSE) console.log('[hmr-client] restoring: detach early placeholder launch handler'); restore(); } } catch {} - if (VERBOSE) { - logUiSnapshot('post-resetRootView'); - console.log('[hmr-client] performResetRoot completed', { - elapsedMs: Date.now() - tStart, - }); - } return true; } catch (e) { console.warn('[hmr-client] resetRootView failed', e); diff --git a/packages/vite/hmr/client/utils.ts b/packages/vite/hmr/client/utils.ts index 13bf60f57f..061ccd7b4e 100644 --- a/packages/vite/hmr/client/utils.ts +++ b/packages/vite/hmr/client/utils.ts @@ -153,9 +153,6 @@ export function deriveHttpOrigin(wsUrl: string | undefined) { const url = new URL(wsUrl || 'ws://localhost:5173/ns-hmr'); const http = url.protocol === 'wss:' ? 'https:' : 'http:'; const origin = `${http}//${url.host}`; - if (!/^https?:\/\/[\w\-.:\[\]]+$/.test(origin)) { - console.warn('[hmr-client][origin] invariant failed for', wsUrl, '→', origin); - } return origin; } catch { return 'http://localhost:5173'; @@ -166,7 +163,6 @@ export async function requestModuleFromServer(spec: string): Promise { const isSfcArtifact = (s: string) => /(?:^|\/)sfc-[a-f0-9]{8}\.mjs$/i.test(s) || /\/_ns_hmr\/src\/sfc\//.test(s) || /__NSDOC__\/_ns_hmr\/src\/sfc\//.test(s); // Ignore Vite/virtual helper or empty specs if (/^\/?\0/.test(spec) || /plugin-vue:export-helper/.test(spec)) { - if (__NS_ENV_VERBOSE__) console.log('[hmr-fetch] skipping virtual helper', spec); return Promise.reject(new Error('virtual-helper-skip')); } // Short-circuit device artifact paths (SFC compiled files) – never ask the server for these @@ -181,16 +177,32 @@ export async function requestModuleFromServer(spec: string): Promise { // Let the server send the JSON-wrapped ESM code; we will write it as-is. // We still go through the normal request flow below, but short-circuit index heuristics. } - // Cache hit: return immediately - if (moduleFetchCache.has(spec)) { - if (__NS_ENV_VERBOSE__) console.log('[hmr-fetch] cache hit', spec); - return Promise.resolve(moduleFetchCache.get(spec)!); - } - // Construct HTTP ESM URL for this spec and cache it + // Construct HTTP ESM URL for this spec. + // IMPORTANT: cache-bust via PATH (not query) because the iOS HTTP ESM cache key + // canonicalization strips query params. const origin = httpOriginForVite || deriveHttpOrigin(hmrWsUrl); if (!origin) return Promise.reject(new Error('no-http-origin')); - const url = origin + '/ns/m' + (spec.startsWith('/') ? spec : '/' + spec); - if (__NS_ENV_VERBOSE__) console.log('[hmr-fetch] resolved', spec, '→', url); + const basePath = '/ns/m' + (spec.startsWith('/') ? spec : '/' + spec); + const baseUrl = origin + basePath; + let url = baseUrl; + try { + // Use version+hash to avoid ESM cache returning the old module. + const v = typeof graphVersion === 'number' ? graphVersion : 0; + const h = graph.get(spec)?.hash || ''; + const g: any = globalThis as any; + const n = typeof g?.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0; + // Only add params when we have at least one signal. + if (v || h || n) { + // Prefer nonce when present to guarantee changes apply even if server version/hash are stable. + const tag = n ? `${n}-${v}${h ? `-${h}` : ''}` : h ? `${v}-${h}` : String(v); + // /ns/m/__ns_hmr__// + url = origin + '/ns/m/__ns_hmr__/' + encodeURIComponent(tag) + basePath.slice('/ns/m'.length); + } + } catch {} + const prev = moduleFetchCache.get(spec); + if (prev === url) { + return Promise.resolve(url); + } moduleFetchCache.set(spec, url); return Promise.resolve(url); } @@ -203,94 +215,15 @@ export async function safeDynImport(spec: string): Promise { if (!finalSpec || finalSpec === '@') { finalSpec = (origin ? origin : '') + '/ns/m/__invalid_at__.mjs'; } - if (__NS_ENV_VERBOSE__) { - try { - console.log('[hmr-client][dyn-import]', 'spec=', spec, 'final=', finalSpec); - } catch {} - } // Use native dynamic import // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - dynamic import expression return await import(finalSpec); } catch (e) { - if (__NS_ENV_VERBOSE__) { - console.warn('[hmr-client][dyn-import][error]', spec, e); - } - // Best-effort diagnostics: fetch sanitized and raw sources and print a snippet around the failing frame - try { - await dumpDynImportDiagnostics(spec, e as any); - } catch {} throw e; } } -// Extract a (url, line, column) triple from a V8 stack string -function parseStackFrame(err: any): { - url?: string; - line?: number; - column?: number; -} { - try { - const stack: string = (err && (err.stack || err.message)) || ''; - // Match patterns like: at (http://host/path.js:123:45) OR http://host/path.js:123:45 - const re = /(https?:[^\s)]+):(\d+):(\d+)/; - const m = stack.match(re); - if (m) { - return { - url: m[1], - line: Number(m[2] || '0'), - column: Number(m[3] || '0'), - }; - } - } catch {} - return {}; -} - -async function dumpDynImportDiagnostics(spec: string, err: any) { - try { - const origin = httpOriginForVite || deriveHttpOrigin(hmrWsUrl) || ''; - const target = spec || ''; - const { url: uFromStack, line, column } = parseStackFrame(err); - const url = uFromStack || target; - if (!/^https?:\/\//.test(url)) return; - const addParam = (u: string, k: string, v = '1') => u + (u.includes('?') ? '&' : '?') + `${k}=${v}`; - const urlsToTry = [url]; - if (/(\/ns\/(asm|sfc))/.test(url)) urlsToTry.push(addParam(url, 'raw')); - const results: Array<{ which: string; text?: string; hash?: string }> = []; - for (const u of urlsToTry) { - try { - const res = await fetch(u as any, { method: 'GET' as any }); - const text = await res.text(); - const hash = res.headers?.get?.('X-NS-Source-Hash') || undefined; - results.push({ which: u === url ? 'sanitized' : 'raw', text, hash }); - } catch {} - } - const locInfo = line && line > 0 ? ` at ${url}:${line}:${column || 0}` : ''; - try { - console.warn('[hmr-client][dyn-import][diagnostics]', `error${locInfo}`); - } catch {} - for (const r of results) { - if (!r.text) continue; - const lines = r.text.split('\n'); - let ctx = ''; - if (line && line > 0) { - const start = Math.max(1, line - 3), - end = Math.min(lines.length, line + 3); - for (let i = start; i <= end; i++) { - const mark = i === line ? '>' : ' '; - const ln = String(i).padStart(4, ' '); - ctx += `${mark}${ln}: ${lines[i - 1]}\n`; - } - } else { - ctx = lines.slice(0, 20).join('\n'); - } - try { - console.warn(`[hmr-client][dyn-import][${r.which}]`, r.hash ? `(hash ${r.hash})` : '', '\n' + ctx); - } catch {} - } - } catch {} -} - // Normalize import specifiers for HTTP-only ESM runtime export function normalizeSpec(raw: string): string { if (typeof raw !== 'string') return raw as any; @@ -321,7 +254,6 @@ export function normalizeSpec(raw: string): string { } if (spec === '@') { // Map anomalous '@' sentinel to a safe stub module path. - if (__NS_ENV_VERBOSE__) console.warn('[hmr-normalize] mapping anomalous "@" to stub module'); try { hmrMetrics.invalidAtSpec = (hmrMetrics.invalidAtSpec || 0) + 1; } catch {} @@ -329,76 +261,3 @@ export function normalizeSpec(raw: string): string { } return spec; } - -export function attachDiagnosticsToFrame(frame: any) { - if (!__NS_ENV_VERBOSE__ || !frame) return; - try { - if ((frame as any).__ns_diag_attached__) return; - (frame as any).__ns_diag_attached__ = true; - const safeOn = (v: any, evt: string, cb: Function) => { - try { - v?.on?.(evt as any, cb as any); - } catch {} - }; - const logEvt = (name: string) => (args: any) => { - try { - const page = (args && (args.object?.currentPage || args.object?._currentEntry?.resolvedPage)) || args?.object || null; - const pageCtor = String(page?.constructor?.name || '').replace(/^_+/, ''); - const tag = (page as any)?.__ns_hmr_tag || (page as any)?.__ns_diag_tag; - console.log('[diag][frame]', name, { - frameCtor: frame?.constructor?.name, - pageCtor, - tag, - backstackDepth: (frame as any)?._backStack?.length || 0, - }); - } catch {} - }; - safeOn(frame, 'navigatingTo', logEvt('navigatingTo')); - safeOn(frame, 'navigatedTo', logEvt('navigatedTo')); - safeOn(frame, 'navigatingFrom', logEvt('navigatingFrom')); - safeOn(frame, 'navigatedFrom', logEvt('navigatedFrom')); - safeOn(frame, 'loaded', () => { - try { - console.log('[diag][frame] loaded', { ctor: frame?.constructor?.name }); - } catch {} - }); - safeOn(frame, 'unloaded', () => { - try { - console.log('[diag][frame] unloaded', { - ctor: frame?.constructor?.name, - }); - } catch {} - }); - } catch {} -} - -export function logUiSnapshot(reason: string) { - try { - const g: any = globalThis as any; - const F = getCore('Frame') || g.Frame; - const App = getCore('Application') || g.Application; - const top = F?.topmost?.(); - const rootView = App?.getRootView ? App.getRootView() : undefined; - const page = top?.currentPage || top?._currentEntry?.resolvedPage || null; - const info = { - reason, - placeholderActive: !!(g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__), - activityReady: !!(App?.android && (App.android.foregroundActivity || App.android.startActivity)), - topFrame: top - ? { - ctor: top.constructor?.name, - backstack: (top as any)?._backStack?.length || 0, - } - : null, - rootView: rootView ? { ctor: rootView.constructor?.name } : null, - currentPage: page - ? { - ctor: page.constructor?.name, - tag: (page as any)?.__ns_hmr_tag || (page as any)?.__ns_diag_tag, - title: (page as any)?.title, - } - : null, - }; - console.log('[diag][ui]', info); - } catch {} -} diff --git a/packages/vite/hmr/frameworks/vue/client/index.ts b/packages/vite/hmr/frameworks/vue/client/index.ts index 0614e0bf13..156a83a39f 100644 --- a/packages/vite/hmr/frameworks/vue/client/index.ts +++ b/packages/vite/hmr/frameworks/vue/client/index.ts @@ -1,4 +1,4 @@ -import { attachDiagnosticsToFrame, deriveHttpOrigin, getCore, getCurrentApp, getGraphVersion, getHMRWsUrl, getHttpOriginForVite, normalizeSpec, safeDynImport, safeReadDefault, setCurrentApp } from '../../../client/utils.js'; +import { deriveHttpOrigin, getCore, getCurrentApp, getGraphVersion, getHMRWsUrl, getHttpOriginForVite, normalizeSpec, safeDynImport, safeReadDefault, setCurrentApp } from '../../../client/utils.js'; // satisfied by define replacement declare const __NS_ENV_VERBOSE__: boolean | undefined; @@ -1099,9 +1099,6 @@ export function getRootForVue( // Treat Frame as authoritative root regardless of whether it already has a currentPage. // This avoids producing a Frame inside a wrapper Page which can lead to blank content in complex apps. if (ctorName === 'Frame' || /^Frame(\$\d+)?$/.test(ctorName)) { - try { - attachDiagnosticsToFrame(nativeView); - } catch {} if (__NS_ENV_VERBOSE__) console.log('[hmr-client] [createRoot] root kind=frame (adopting component Frame)'); state.setRootKind('frame'); state.setCachedRoot(nativeView); diff --git a/packages/vite/hmr/server/websocket.ts b/packages/vite/hmr/server/websocket.ts index d5a5f0caf8..1586831d46 100644 --- a/packages/vite/hmr/server/websocket.ts +++ b/packages/vite/hmr/server/websocket.ts @@ -1,22 +1,3 @@ -/* -RAW BYPASS DIAGNOSTICS (added): - Purpose: Fetch original Vite transform output (unsanitized) for differential comparison with sanitized/device-processed output. - Endpoints supporting ?raw=1: - - /ns/asm[/(ver)]?path=/abs/or/@/alias/Comp.vue&raw=1 - Returns either full compiled ?vue output (if available) or concatenated script/template variant transforms. - - /ns/sfc[/(ver)]?path=/abs/or/@/alias/Comp.vue[?vue&type=script|template]&raw=1 - Returns direct transformRequest result (before cleanCode/processCodeForDevice/rewriteImports delegation). - Response markers: - - // [sfc-asm] (raw bypass) - - // [sfc] raw bypass path= - - Hash banner: // [hash:] bytes= raw=1 - - X-NS-Source-Hash header mirrors hash for correlation with runtime compile logs. - Usage Workflow: - 1. Fetch sanitized module normally (without raw=1) and note its hash banner and failing runtime log containing [http-esm][compile][v8-error]. - 2. Fetch same URL with &raw=1 (or ?raw=1 if no existing query) to obtain unsanitized baseline. - 3. Diff raw vs sanitized focusing near reported line/column from v8-error log. - 4. Identify sanitation regex introducing syntax issue; adjust in cleanCode/processCodeForDevice. -*/ import type { Plugin, ViteDevServer, TransformResult } from 'vite'; import { createRequire } from 'node:module'; import { normalizeStrayCoreStringLiterals, fixDanglingCoreFrom, normalizeAnyCoreSpecToBridge } from './core-sanitize.js'; @@ -1163,7 +1144,7 @@ function processCodeForDevice(code: string, isVitePreBundled: boolean): string { 'const __ANDROID__ = globalThis.__ANDROID__ !== undefined ? globalThis.__ANDROID__ : false;', 'const __IOS__ = globalThis.__IOS__ !== undefined ? globalThis.__IOS__ : false;', 'const __VISIONOS__ = globalThis.__VISIONOS__ !== undefined ? globalThis.__VISIONOS__ : false;', - 'const __APPLE__ = globalThis.__APPLE__ !== undefined ? globalThis.__APPLE__ : false;', + 'const __APPLE__ = globalThis.__APPLE__ !== undefined ? globalThis.__APPLE__ : (__IOS__ || __VISIONOS__);', 'const __DEV__ = globalThis.__DEV__ !== undefined ? globalThis.__DEV__ : false;', 'const __COMMONJS__ = globalThis.__COMMONJS__ !== undefined ? globalThis.__COMMONJS__ : false;', 'const __NS_WEBPACK__ = globalThis.__NS_WEBPACK__ !== undefined ? globalThis.__NS_WEBPACK__ : true;', @@ -1249,6 +1230,15 @@ function processCodeForDevice(code: string, isVitePreBundled: boolean): string { // This allows the rewriter to see and canonicalize '/node_modules/.vite/deps/*' specifiers back // to their package ids (e.g., '@nativescript/firebase-core') and generate require-based bindings // so named imports like `{ firebase }` are preserved as const bindings. + // + // Some upstream transforms can emit a multiline form: + // import { x } from + // "/node_modules/.vite/deps/..."; + // If we don't normalize it, later stripping of naked string-only lines can leave + // an invalid `import ... from` statement. + try { + result = result.replace(/(^|\n)([\t ]*import\s+[^;]*?\s+from)\s*\n\s*("\/?node_modules\/\.vite\/deps\/[^"\n]+"\s*;?\s*)/gm, (_m, p1, p2, p3) => `${p1}${p2} ${p3}`); + } catch {} result = ensureNativeScriptModuleBindings(result); // Repair any accidental "import ... = expr" assignments that may have slipped in. @@ -2651,6 +2641,15 @@ function createHmrWebSocketPlugin(opts: { verbose?: boolean }): Plugin { return; } spec = spec.replace(/[?#].*$/, ''); + // Accept path-based HMR cache-busting: /ns/m/__ns_hmr__// + // The iOS HTTP ESM loader canonicalizes cache keys by stripping query params, + // so we must carry the cache-buster in the path. + try { + const m = spec.match(/^\/?__ns_hmr__\/[^\/]+(\/.*)?$/); + if (m) { + spec = m[1] || '/'; + } + } catch {} // Normalize absolute filesystem paths back to project-relative ids (e.g. /src/app.ts) try { const projectRoot = ((server as any).config?.root || process.cwd()) as string; @@ -2728,35 +2727,34 @@ function createHmrWebSocketPlugin(opts: { verbose?: boolean }): Plugin { } catch {} } } - // RAW BYPASS: allow fetching original Vite transform before sanitation for diffing/debugging - if (urlObj.searchParams.get('raw') === '1') { - const raw = transformed?.code || 'export {}\n'; - try { - const h = createHash('sha1').update(raw).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - res.statusCode = 200; - res.end(`// [hash:${h}] bytes=${raw.length} raw=1 m path=${spec}\n` + raw); - } catch { - res.statusCode = 200; - res.end(`// [raw=1] m path=${spec}\n` + raw); - } - return; - } - // Post-transform: inject cache-busting version for all internal /ns/m/* imports to avoid stale module reuse on device + // Post-transform: inject cache-busting version for all internal /ns/m/* imports to avoid stale module reuse on device. + // IMPORTANT: use PATH-based busting (not query) because the iOS HTTP ESM loader strips query params + // when computing module cache keys. try { if (transformed?.code) { const ver = Number((global as any).graphVersion || graphVersion || 0); let code = transformed.code; + const prefix = `/ns/m/__ns_hmr__/v${ver}`; + const rewrite = (p: string) => { + try { + if (!p || typeof p !== 'string') return p; + if (!p.startsWith('/ns/m/')) return p; + if (p.startsWith('/ns/m/__ns_hmr__/')) return p; + return prefix + p.slice('/ns/m'.length); + } catch { + return p; + } + }; // 1) Static imports: import ... from "/ns/m/..." - code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`); + code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`); // 2) Side-effect imports: import "/ns/m/..." - code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`); + code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`); // 3) Dynamic imports: import("/ns/m/...") - code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, `$1$2?v=${ver}$3`); + code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`); // 4) new URL("/ns/m/...", import.meta.url) - code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, `$1$2?v=${ver}$3`); + code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`); // 5) __ns_import(new URL('/ns/m/...', import.meta.url).href) - code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\)\.href)/g, `$1$2?v=${ver}$3`); + code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\)\.href)/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`); transformed.code = code; // TypeScript-specific graph population: when TS flavor is active // and this is an application module under the virtual app root, @@ -2879,15 +2877,10 @@ export const piniaSymbol = p.piniaSymbol; } catch {} } if (!transformed?.code) { - // Enhanced diagnostics: emit a module that throws with context for easier on-device debugging + // Emit a module that throws with context for easier on-device debugging try { const tried = Array.from(new Set(candidates)).slice(0, 12); - let out = `// [ns:m] transform miss path=${spec} tried=${tried.length}\n` + `throw new Error(${JSON.stringify(`[ns/m] transform failed for ${spec} (tried ${tried.length} candidates). Use ?raw=1 to inspect Vite output.`)});\nexport {};\n`; - try { - const h = createHash('sha1').update(out).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - out = `// [hash:${h}] bytes=${out.length}\n` + out; - } catch {} + const out = `// [ns:m] transform miss path=${spec} tried=${tried.length}\n` + `throw new Error(${JSON.stringify(`[ns/m] transform failed for ${spec} (tried ${tried.length} candidates).`)});\nexport {};\n`; res.statusCode = 404; res.end(out); return; @@ -3069,10 +3062,6 @@ export const piniaSymbol = p.piniaSymbol; console.warn('[ns:m][link-check] failed', (eLC as any)?.message || eLC); } catch {} } - try { - const h = createHash('sha1').update(code).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - } catch {} res.statusCode = 200; res.end(code); } catch (e) { @@ -3205,9 +3194,9 @@ export const piniaSymbol = p.piniaSymbol; `export const vShow = (__ensure().vShow);\n` + `export const createApp = (...a) => (__ensure().createApp)(...a);\n` + `export const registerElement = (...a) => (__ensure().registerElement)(...a);\n` + - `export const $navigateTo = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); if (g.__NS_VERBOSE_RT_NAV__ || g.__NS_DEV_LOGS__) console.log('[ns-rt] $navigateTo invoked'); 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; if (g.__NS_VERBOSE_RT_NAV__ || g.__NS_DEV_LOGS__) { console.log('[ns-rt] $navigateTo(single-path)', { via: 'app', rtRealm: __RT_REALM_TAG, hmrRealm, hasTop, topCtor: ctor }); } } 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` + - `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'); if (globalThis && (globalThis.__NS_VERBOSE_RT_NAV__ || globalThis.__NS_DEV_LOGS__)) { console.log('[ns-rt] $navigateBack', { via }); } } 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` + - `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) { try { if (g && (g.__NS_VERBOSE_RT_NAV__ || g.__NS_DEV_LOGS__)) { console.error('[ns-rt] $showModal error', e); } } catch {} } return undefined; }\n` + + `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` + + `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` + + `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` + `export default {\n` + ` defineComponent, resolveComponent, createVNode, createTextVNode, createCommentVNode,\n` + ` Fragment, Teleport, Transition, TransitionGroup, KeepAlive, Suspense, withCtx, openBlock,\n` + @@ -3286,11 +3275,6 @@ export const piniaSymbol = p.piniaSymbol; `const __core = new Proxy({}, { get(_t, p){ if (p === 'default') return __core; if (p === Symbol.toStringTag) return 'Module'; try { const v = g[p]; if (v !== undefined) return v; } catch {} try { const vc = __getVendorCore(); return vc ? vc[p] : undefined; } catch {} return undefined; } });\n` + `// Default export: namespace-like proxy\n` + `export default __core;\n`; - try { - const h = createHash('sha1').update(code).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - code = `// [hash:${h}] bytes=${code.length} core\n` + code; - } catch {} res.statusCode = 200; res.end(code); } catch (e) { @@ -3324,11 +3308,6 @@ export const piniaSymbol = p.piniaSymbol; } catch (e) { content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n'; } - try { - const h = createHash('sha1').update(content).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - content = `// [hash:${h}] bytes=${content.length} entry-rt\n` + content; - } catch {} res.statusCode = 200; res.end(content); } catch (e) { @@ -3394,17 +3373,7 @@ export const piniaSymbol = p.piniaSymbol; ` const startEntry = (__mod && (__mod.default || __mod));\n` + ` 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` + `})();\n`; - try { - const h = createHash('sha1').update(code).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - const banner = `// [hash:${h}] bytes=${code.length} entry\n`; - if (verbose) { - try { - console.log('[hmr-http] reply /ns/entry hash', h, 'bytes', code.length); - } catch {} - } - code = banner + code + `\n//# sourceURL=${origin}/ns/entry`; - } catch {} + code = code + `\n//# sourceURL=${origin}/ns/entry`; res.statusCode = 200; res.end(code); } catch (e) { @@ -3544,12 +3513,7 @@ export const piniaSymbol = p.piniaSymbol; // Emit an erroring module to surface the failure at import site with helpful hints try { const tried = candidates.slice(0, 8); - let 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`; - try { - const h = createHash('sha1').update(out).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - out = `// [hash:${h}] bytes=${out.length}\n` + out; - } catch {} + 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`; res.statusCode = 404; res.end(out); return; @@ -3565,12 +3529,7 @@ export const piniaSymbol = p.piniaSymbol; } catch {} if (!transformed?.code) { try { - let 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`; - try { - const h = createHash('sha1').update(out).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - out = `// [hash:${h}] bytes=${out.length}\n` + out; - } catch {} + 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`; res.statusCode = 404; res.end(out); return; @@ -3590,29 +3549,6 @@ export const piniaSymbol = p.piniaSymbol; return; } - // RAW BYPASS: serve unsanitized transform output (or direct transform of candidate) when ?raw=1 - const rawBypass = urlObj.searchParams.get('raw') === '1'; - if (rawBypass) { - try { - let rawOut = transformed.code || 'export {}\n'; - try { - const h = createHash('sha1').update(rawOut).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - rawOut = `// [hash:${h}] bytes=${rawOut.length} raw=1 sfc kind=${isVariant ? 'variant' : 'full'}\n` + rawOut; - } catch {} - res.statusCode = 200; - res.end(`// [sfc] raw bypass path=${fullSpec.replace(/\n/g, '')}\n` + rawOut); - return; - } catch (eRaw) { - try { - console.warn('[sfc][raw] failed', fullSpec, (eRaw as any)?.message); - } catch {} - res.statusCode = 200; - res.end('// [sfc] raw bypass error\nexport {}\n'); - return; - } - } - let code = transformed.code; // Prepend guard to capture any URL-based require attempts code = REQUIRE_GUARD_SNIPPET + code; @@ -3919,28 +3855,6 @@ export const piniaSymbol = p.piniaSymbol; code += `\nexport default (typeof __ns_sfc__ !== "undefined" ? __ns_sfc__ : (typeof _sfc_main !== "undefined" ? _sfc_main : undefined));`; } } - try { - const h = createHash('sha1').update(code).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - code = `// [hash:${h}] bytes=${code.length}\n` + code; - } catch {} - - // Diagnostic: when serving full SFCs, emit a short snippet and search for common compiled patterns - try { - if (!isVariant && verbose) { - const snippet = code.slice(0, 1024).replace(/\n/g, '\\n'); - const hasExportHelper = /_export_sfc\s*\(/.test(code); - const hasSfcMain = /_sfc_main\b/.test(code); - const hasNsReal = /__ns_real__\b/.test(code); - console.log(`[sfc][serve][diag] ${fullSpec} snippet=${snippet}`); - console.log(`[sfc][serve][diag] patterns exportHelper=${hasExportHelper} sfcMain=${hasSfcMain} mergedVar=${hasNsReal}`); - } - } catch (e) { - try { - console.warn('[sfc][serve][diag] print failed', e); - } catch {} - } - res.statusCode = 200; res.end(sig + code); } catch (e) { @@ -4076,43 +3990,6 @@ export const piniaSymbol = p.piniaSymbol; const scriptUrl = `${origin}/ns/sfc/${ver}${base}?vue&type=script`; const templateCode = templateR?.code || ''; - // RAW BYPASS: return unsanitized compiled SFC or script/template when ?raw=1 for differential debugging - const rawBypass = urlObj.searchParams.get('raw') === '1'; - if (rawBypass) { - try { - let rawOut = ''; - if (fullR?.code) { - rawOut = fullR.code; - } else if (scriptR?.code || templateR?.code) { - // Reconstruct minimal module if only variants available - const parts: string[] = []; - if (scriptR?.code) parts.push('// [raw][script]\n' + scriptR.code); - if (templateR?.code) parts.push('// [raw][template]\n' + templateR.code); - rawOut = parts.join('\n'); - } - if (!rawOut) { - rawOut = 'export {}\n'; - } - try { - const h = createHash('sha1').update(rawOut).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - rawOut = - `// [hash:${h}] bytes=${rawOut.length} raw=1 asm -` + rawOut; - } catch {} - res.statusCode = 200; - res.end(`// [sfc-asm] ${base} (raw bypass)\n` + rawOut); - return; - } catch (eRaw) { - try { - console.warn('[sfc-asm][raw] failed', base, (eRaw as any)?.message); - } catch {} - res.statusCode = 200; - res.end('// [sfc-asm] raw bypass error\nexport {}\n'); - return; - } - } - // INLINE-FIRST assembler: compile SFC source into a self-contained ESM module (enhanced diagnostics) try { const root = (server as any).config?.root || process.cwd(); @@ -4645,11 +4522,6 @@ export const piniaSymbol = p.piniaSymbol; inlineCode2 = inlineCode2.replace(/(^|[\n;])\s*var\s+__ns_sfc__\s*;?/g, '$1var __ns_sfc__ = {};'); } catch {} if (!/export\s+default\s+__ns_sfc__/.test(inlineCode2) && /__ns_sfc__/.test(inlineCode2)) inlineCode2 += '\nexport default __ns_sfc__'; - try { - const h = createHash('sha1').update(inlineCode2).digest('hex'); - res.setHeader('X-NS-Source-Hash', h); - inlineCode2 = `// [hash:${h}] bytes=${inlineCode2.length}\n` + inlineCode2; - } catch {} res.statusCode = 200; res.end(inlineCode2); return; From 49a16c550ef0f65533903890b44ca68c3b55354b Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 13 Dec 2025 08:21:21 -0800 Subject: [PATCH 07/14] feat(vite): onHmrUpdate for custom app hmr update handling --- packages/vite/hmr/shared/runtime/hooks.ts | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/vite/hmr/shared/runtime/hooks.ts diff --git a/packages/vite/hmr/shared/runtime/hooks.ts b/packages/vite/hmr/shared/runtime/hooks.ts new file mode 100644 index 0000000000..77d7061f4d --- /dev/null +++ b/packages/vite/hmr/shared/runtime/hooks.ts @@ -0,0 +1,84 @@ +export type NsHmrUpdatePayload = { + type: 'full-graph' | 'delta'; + version: number; + changedIds: string[]; + // Raw message payload from the HMR WebSocket + raw: any; +}; + +export type NsHmrUpdateHandler = (payload: NsHmrUpdatePayload) => void; + +type NsHmrGlobalState = { + __NS_HMR_ON_UPDATE__?: unknown; + __NS_HMR_ON_UPDATE_DISPATCHER__?: NsHmrUpdateHandler; + __NS_HMR_ON_UPDATE_REGISTRY__?: Map; + __NS_HMR_ON_UPDATE_BASE__?: unknown; +}; + +function getNsHmrGlobal(): NsHmrGlobalState { + return globalThis as any; +} + +function ensureDispatcherInstalled(): { + registry: Map; + dispatcher: NsHmrUpdateHandler; + base: unknown; +} { + const g = getNsHmrGlobal(); + if (!g.__NS_HMR_ON_UPDATE_REGISTRY__) g.__NS_HMR_ON_UPDATE_REGISTRY__ = new Map(); + const registry = g.__NS_HMR_ON_UPDATE_REGISTRY__; + + if (!g.__NS_HMR_ON_UPDATE_DISPATCHER__) { + const base = g.__NS_HMR_ON_UPDATE__; + // If something already owns the hook and it's not our dispatcher, preserve it. + g.__NS_HMR_ON_UPDATE_BASE__ = base; + g.__NS_HMR_ON_UPDATE_DISPATCHER__ = (payload: NsHmrUpdatePayload) => { + // Call registered handlers first (app-level consumers). + try { + for (const handler of registry.values()) { + try { + handler(payload); + } catch {} + } + } catch {} + // Then call any preserved base hook. + try { + const b = (getNsHmrGlobal() as any).__NS_HMR_ON_UPDATE_BASE__; + if (typeof b === 'function') (b as NsHmrUpdateHandler)(payload); + } catch {} + }; + g.__NS_HMR_ON_UPDATE__ = g.__NS_HMR_ON_UPDATE_DISPATCHER__; + } + + return { + registry, + dispatcher: g.__NS_HMR_ON_UPDATE_DISPATCHER__!, + base: g.__NS_HMR_ON_UPDATE_BASE__, + }; +} + +/** + * Register a callback that will be invoked after each HMR batch + * (full graph or delta) is applied on device. + * + * It is safe to call multiple times with the same `id`; the handler + * will be replaced instead of stacking duplicates across module reloads. + */ + +export function onHmrUpdate(handler: NsHmrUpdateHandler, id: string): void { + if (typeof handler !== 'function') return; + if (typeof id !== 'string' || !id) return; + try { + const { registry } = ensureDispatcherInstalled(); + registry.set(id, handler); + } catch {} +} + +/** Remove a previously registered handler (use the same `id` you registered with). */ +export function offHmrUpdate(id: string): void { + if (typeof id !== 'string' || !id) return; + try { + const g = getNsHmrGlobal(); + g.__NS_HMR_ON_UPDATE_REGISTRY__?.delete(id); + } catch {} +} From 60b2f3d985a166223be91768743bcb9f5c61a4d1 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 13 Dec 2025 15:09:00 -0800 Subject: [PATCH 08/14] feat: improve hmr vite server ip connection cases --- packages/vite/helpers/main-entry.ts | 21 +++++++++++- packages/vite/hmr/entry-runtime.ts | 17 +++++++++- packages/vite/hmr/server/vite-plugin.ts | 33 ++++++++++++++++--- .../vite/hmr/shared/runtime/http-only-boot.ts | 21 ++++++++++-- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index 4ffb2fa9a5..d666ff5b69 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -1,5 +1,6 @@ import { getPackageJson, getProjectFilePath, getProjectRootPath } from './project.js'; import fs from 'fs'; +import os from 'os'; import path from 'path'; import { getProjectFlavor } from './flavor.js'; import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from './utils.js'; @@ -226,7 +227,25 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' if (opts.verbose) { imports += `console.info('[ns-entry] including HTTP-only boot', { platform: ${JSON.stringify(opts.platform)}, mainRel: ${JSON.stringify(mainEntryRelPosix)} });\n`; } - const defaultHost = opts.platform === 'android' ? '10.0.2.2' : 'localhost'; + const guessLanHost = (): string | undefined => { + try { + const nets = os.networkInterfaces(); + for (const name of Object.keys(nets)) { + const addrs = nets[name] || []; + for (const a of addrs) { + if (!a) continue; + const family = (a as any).family; + const internal = !!(a as any).internal; + const address = String((a as any).address || ''); + if (internal) continue; + if ((family === 'IPv4' || family === 4) && address && address !== '127.0.0.1') return address; + } + } + } catch {} + return undefined; + }; + // Prefer LAN IP so physical devices work by default; emulator will still be tried as a fallback. + const defaultHost = opts.platform === 'android' ? guessLanHost() || '10.0.2.2' : guessLanHost() || 'localhost'; imports += "import { startHttpOnlyBoot } from '@nativescript/vite/hmr/shared/runtime/http-only-boot.js';\n"; imports += `startHttpOnlyBoot(${JSON.stringify(opts.platform)}, ${JSON.stringify(mainEntryRelPosix)}, ${JSON.stringify((process.env.NS_HMR_HOST || '') as string) || JSON.stringify('')} || ${JSON.stringify(defaultHost)}, __nsVerboseLog);\n`; if (opts.verbose) { diff --git a/packages/vite/hmr/entry-runtime.ts b/packages/vite/hmr/entry-runtime.ts index 790e94c882..4b614d55d6 100644 --- a/packages/vite/hmr/entry-runtime.ts +++ b/packages/vite/hmr/entry-runtime.ts @@ -81,7 +81,20 @@ export default async function startEntry(opts: EntryOpts) { if (VERBOSE) console.info('[ns-entry] entry importing', MAIN_URL); (globalThis as any).__NS_ENTRY_LAST_TARGET__ = MAIN_URL; // used by fetchCodeframe sanitized-vs-raw tag const t_main = Date.now(); - await importHttp(MAIN_URL); + let lastMainErr: any = null; + for (let attempt = 0; attempt < 6; attempt++) { + try { + const url = attempt === 0 ? MAIN_URL : MAIN_URL + '&r=' + String(Date.now()); + await importHttp(url); + lastMainErr = null; + break; + } catch (e_main: any) { + lastMainErr = e_main; + // brief backoff; allows dev server and device network to settle + await new Promise((r) => setTimeout(r, 150 + attempt * 150)); + } + } + if (lastMainErr) throw lastMainErr; TRACE.main = { ok: true, ms: Date.now() - t_main, url: MAIN_URL }; (globalThis as any).__NS_ENTRY_OK__ = true; @@ -122,6 +135,8 @@ export default async function startEntry(opts: EntryOpts) { if (VERBOSE) console.info('[ns-entry][diag] Tip: append ?raw=1 to /ns/m, /ns/sfc, or /ns/asm URLs to compare raw vs sanitized output.'); } catch {} (globalThis as any).__NS_ENTRY_OK__ = false; + // Re-throw so the HTTP bootloader can try other origin candidates. + throw e; } finally { try { TRACE.t1 = Date.now(); diff --git a/packages/vite/hmr/server/vite-plugin.ts b/packages/vite/hmr/server/vite-plugin.ts index c7465e43d7..ea39d46a52 100644 --- a/packages/vite/hmr/server/vite-plugin.ts +++ b/packages/vite/hmr/server/vite-plugin.ts @@ -1,6 +1,7 @@ import type { Plugin, ResolvedConfig } from 'vite'; import { createRequire } from 'node:module'; import path from 'path'; +import os from 'os'; import { pathToFileURL } from 'url'; const require = createRequire(import.meta.url); @@ -10,6 +11,27 @@ const RESOLVED_ID = '\0' + VIRTUAL_ID; export function nsHmrClientVitePlugin(opts: { platform: string; verbose?: boolean }): Plugin { let config: ResolvedConfig | undefined; + const guessLanHost = (): string | undefined => { + try { + const nets = os.networkInterfaces(); + for (const name of Object.keys(nets)) { + const addrs = nets[name] || []; + for (const a of addrs) { + if (!a) continue; + // Node typings vary across versions; keep checks defensive + const family = (a as any).family; + const internal = !!(a as any).internal; + const address = String((a as any).address || ''); + if (internal) continue; + if (family === 'IPv4' || family === 4) { + if (address && address !== '127.0.0.1') return address; + } + } + } + } catch {} + return undefined; + }; + return { name: 'ns-hmr-client', configResolved(c) { @@ -55,11 +77,14 @@ export function nsHmrClientVitePlugin(opts: { platform: string; verbose?: boolea // Build ws url from Vite server info let host = process.env.NS_HMR_HOST || (config?.server?.host as any); - // Android emu special-case - if (opts.platform === 'android' && (host === '0.0.0.0' || !host || host === true)) { - host = '10.0.2.2'; + // If Vite is bound to all interfaces, prefer a LAN IP so physical devices work. + // The HMR client will still try emulator/localhost fallbacks when needed. + const hostStr = typeof host === 'string' ? host : ''; + const isWildcard = host === true || hostStr === '0.0.0.0' || hostStr === '::' || hostStr === ''; + if (isWildcard) { + host = guessLanHost() || (opts.platform === 'android' ? '10.0.2.2' : 'localhost'); } else if (!host) { - host = 'localhost'; + host = opts.platform === 'android' ? guessLanHost() || '10.0.2.2' : 'localhost'; } const port = Number(config?.server?.port || 5173); const secure = !!config?.server?.https; diff --git a/packages/vite/hmr/shared/runtime/http-only-boot.ts b/packages/vite/hmr/shared/runtime/http-only-boot.ts index ec8008b3f5..92f6e46afa 100644 --- a/packages/vite/hmr/shared/runtime/http-only-boot.ts +++ b/packages/vite/hmr/shared/runtime/http-only-boot.ts @@ -46,10 +46,25 @@ export async function startHttpOnlyBoot(platform: 'ios' | 'android' | 'visionos' const buildOrigins = () => { const origins: string[] = []; if ((globalThis as any)['__NS_HTTP_ORIGIN__']) origins.push((globalThis as any)['__NS_HTTP_ORIGIN__']); - const androidExtras = platform === 'android' ? ['10.0.2.2', 'localhost'] : ['localhost']; - const hostCandidates = Array.from(new Set([host, ...androidExtras])); + const hostCandidates: string[] = []; + // Only accept a concrete host string from the default; ignore wildcard bind addresses. + try { + const h = String(host || ''); + if (h && h !== '0.0.0.0' && h !== '::' && h !== 'true') hostCandidates.push(h); + } catch {} + // Always try loopback variants + hostCandidates.push('localhost'); + if (platform === 'android') { + // Physical device via `adb reverse` often works best with 127.0.0.1 + hostCandidates.push('127.0.0.1'); + // Stock Android emulator host loopback + hostCandidates.push('10.0.2.2'); + // Genymotion host loopback + hostCandidates.push('10.0.3.2'); + } + const dedupedHosts = Array.from(new Set(hostCandidates)); for (const p of protoCandidates) { - for (const h of hostCandidates) origins.push(p + '://' + h + ':' + port); + for (const h of dedupedHosts) origins.push(p + '://' + h + ':' + port); } return Array.from(new Set(origins)); }; From 9df273fcf0ae1cc86f2cbdef0cde49302f48690b Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Tue, 16 Dec 2025 19:11:18 -0800 Subject: [PATCH 09/14] feat: improvements for import.meta.hot --- packages/vite/hmr/helpers/ast-normalizer.ts | 9 +++++++++ packages/vite/hmr/server/constants.ts | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/vite/hmr/helpers/ast-normalizer.ts b/packages/vite/hmr/helpers/ast-normalizer.ts index 2adb2859b7..793b0406ed 100644 --- a/packages/vite/hmr/helpers/ast-normalizer.ts +++ b/packages/vite/hmr/helpers/ast-normalizer.ts @@ -103,6 +103,15 @@ export function astNormalizeModuleImportsAndHelpers(code: string): string { // Keep other prebundles as-is; they may be fetchable directly from the dev server } } else if (isViteVirtual(src) || src === '@vite/client' || src === '/@vite/client') { + // Removing the Vite client import should also remove its declared locals from our + // `declared` set; otherwise later bridge rewrites may unnecessarily suffix names + // (e.g. `__vite__createHotContext_1`) while call sites still reference the original. + try { + for (const s of path.node.specifiers || []) { + const local = (s as any)?.local?.name; + if (typeof local === 'string' && local) declared.delete(local); + } + } catch {} path.remove(); return; } diff --git a/packages/vite/hmr/server/constants.ts b/packages/vite/hmr/server/constants.ts index c4d065aa6d..a58b6a5de6 100644 --- a/packages/vite/hmr/server/constants.ts +++ b/packages/vite/hmr/server/constants.ts @@ -19,7 +19,10 @@ export const VUE_FILE_IMPORT = /(?:^|\n)(\s*import\s+[^'";]*?\s+from\s+["'])([^" // Vite/HMR noise cleanup export const VITE_CLIENT_IMPORT = /(?:^|\n)\s*import\s+['"](?:\/@vite\/client|@vite\/client)['"];?/g; -export const IMPORT_META_HOT_ASSIGNMENT = /(?:^|\n)\s*import\.meta\.hot\s*=\s*[^;\n]+;?/g; +// Strip Vite's injected `import.meta.hot = __vite__createHotContext(...)` assignment. +// Important: it may appear mid-line after other tokens/spaces in sanitized HTTP output, +// so we cannot rely on it starting at BOL. +export const IMPORT_META_HOT_ASSIGNMENT = /\bimport\.meta\.hot\s*=\s*[^;\n]+;?/g; export const IMPORT_META_HOT_CALLS = /(?:^|\n)\s*import\.meta\.hot\.[A-Za-z_$][\w$]*\([^)]*\);?\s*/g; // Remove only Vue style virtual imports; keep script/template variants From 8c57189db60b58389c55e1ea757db4bed63305d2 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 21 Dec 2025 12:38:00 -0800 Subject: [PATCH 10/14] chore: oidc workflow improvements --- .github/workflows/secure_nx_release.yml | 36 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/secure_nx_release.yml b/.github/workflows/secure_nx_release.yml index dfd18ea6c8..3bc5509da6 100644 --- a/.github/workflows/secure_nx_release.yml +++ b/.github/workflows/secure_nx_release.yml @@ -282,7 +282,7 @@ jobs: npx nx release publish --tag "${{ steps.ctx.outputs.dist_tag }}" --access public --verbose --dry-run # Tag-triggered publishing: publish the single package referenced by the tag. - - name: nx release publish (tag) + - name: Build and Publish (tag) if: ${{ steps.ctx.outputs.mode == 'tag' && vars.USE_NPM_TOKEN != 'true' }} shell: bash env: @@ -296,19 +296,39 @@ jobs: rm -f "$NPM_CONFIG_USERCONFIG" || true fi - npx nx release publish \ - --projects "${{ steps.taginfo.outputs.project }}" \ - --tag "${{ steps.taginfo.outputs.dist_tag }}" \ - --access public \ - --verbose + project="${{ steps.taginfo.outputs.project }}" + dist_tag="${{ steps.taginfo.outputs.dist_tag }}" + + echo "Building $project..." + npx nx build "$project" --skip-nx-cache + + echo "Publishing from dist/packages/$project..." + cd "dist/packages/$project" + + npm pack + tgz_file=$(ls *.tgz | head -n 1) + npm publish "$tgz_file" --tag "$dist_tag" --access public --verbose - - name: nx release publish (tag, token) + - name: Build and Publish (tag, token) if: ${{ steps.ctx.outputs.mode == 'tag' && vars.USE_NPM_TOKEN == 'true' }} + shell: bash env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} NPM_CONFIG_PROVENANCE: true run: | - npx nx release publish --projects "${{ steps.taginfo.outputs.project }}" --tag "${{ steps.taginfo.outputs.dist_tag }}" --access public --verbose + set -euo pipefail + project="${{ steps.taginfo.outputs.project }}" + dist_tag="${{ steps.taginfo.outputs.dist_tag }}" + + echo "Building $project..." + npx nx build "$project" --skip-nx-cache + + echo "Publishing from dist/packages/$project..." + cd "dist/packages/$project" + + npm pack + tgz_file=$(ls *.tgz | head -n 1) + npm publish "$tgz_file" --tag "$dist_tag" --access public --verbose - name: Summary if: always() From d472881eadbaae6de7a379e4bc621729582fe0a2 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 21 Dec 2025 12:44:51 -0800 Subject: [PATCH 11/14] fix(vite): ensure angular tsconfig prefers .app.json and falls back to no suffix --- packages/vite/configuration/angular.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/vite/configuration/angular.ts b/packages/vite/configuration/angular.ts index 84f14b790f..6e6d6cb328 100644 --- a/packages/vite/configuration/angular.ts +++ b/packages/vite/configuration/angular.ts @@ -1,5 +1,6 @@ import { mergeConfig, type UserConfig, type Plugin } from 'vite'; import path from 'path'; +import fs from 'node:fs'; import { createRequire } from 'node:module'; import angular from '@analogjs/vite-plugin-angular'; import { angularLinkerVitePlugin, angularLinkerVitePluginPost } from '../helpers/angular/angular-linker.js'; @@ -108,6 +109,14 @@ const cliFlags = getCliFlags(); const isDevEnv = process.env.NODE_ENV !== 'production'; const hmrActive = isDevEnv && !!cliFlags.hmr; +const projectRoot = process.cwd(); +const tsConfigAppPath = path.resolve(projectRoot, 'tsconfig.app.json'); +const tsConfigPath = path.resolve(projectRoot, 'tsconfig.json'); +let tsConfig = tsConfigAppPath; +if (!fs.existsSync(tsConfigAppPath) && fs.existsSync(tsConfigPath)) { + tsConfig = tsConfigPath; +} + const plugins = [ // Allow external html template changes to trigger hot reload: Make .ts files depend on their .html templates { @@ -132,6 +141,7 @@ const plugins = [ // angularRollupLinker(process.cwd()), angular({ liveReload: false, // Disable live reload in favor of HMR + tsconfig: tsConfig, }), // Post-phase linker to catch any declarations introduced after other transforms (including project code) angularLinkerVitePluginPost(process.cwd()), From 5083f680f304ddc78f362cd0d18ffdbcd9e80c07 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 21 Dec 2025 22:49:23 -0800 Subject: [PATCH 12/14] fix: init without debug logs by default --- packages/vite/helpers/init.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/helpers/init.ts b/packages/vite/helpers/init.ts index deae3fe2d0..79de7a89e8 100644 --- a/packages/vite/helpers/init.ts +++ b/packages/vite/helpers/init.ts @@ -52,10 +52,10 @@ function ensureScripts(pkg: PackageJson) { pkg.scripts = pkg.scripts ?? {}; pkg.scripts['dev:ios'] = "concurrently -k -n vite,ns 'npm run dev:server:ios' 'wait-on tcp:5173 && npm run ios'"; pkg.scripts['dev:android'] = "concurrently -k -n vite,ns 'npm run dev:server:android' 'wait-on tcp:5173 && npm run android'"; - pkg.scripts['dev:server:ios'] = 'VITE_DEBUG_LOGS=1 vite serve -- --env.ios --env.hmr'; - pkg.scripts['dev:server:android'] = 'VITE_DEBUG_LOGS=1 vite serve -- --env.android --env.hmr'; - pkg.scripts['ios'] = 'VITE_DEBUG_LOGS=1 ns debug ios'; - pkg.scripts['android'] = 'VITE_DEBUG_LOGS=1 ns debug android'; + pkg.scripts['dev:server:ios'] = 'vite serve -- --env.ios --env.hmr'; + pkg.scripts['dev:server:android'] = 'vite serve -- --env.android --env.hmr'; + pkg.scripts['ios'] = 'ns debug ios'; + pkg.scripts['android'] = 'ns debug android'; } function ensureGitignore() { From f5db360d52c4b0cb7cc981b213c28625b19ea3ce Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 7 Jan 2026 10:17:34 -0800 Subject: [PATCH 13/14] fix(vite): dynamic and static import mixed usage (#11042) --- packages/vite/helpers/main-entry.ts | 63 +++++++++++++++++------------ 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index d666ff5b69..064b3bf7ad 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -128,6 +128,8 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' } // ---- Platform-specific always-needed modules ---- + // Track if we need to defer Android activity import (non-HMR only) + let needsAndroidActivityDefer = false; if (opts.platform === 'android') { if (opts.hmrActive) { /** @@ -164,26 +166,10 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' /** * Non-HMR: Defer activity lifecycle wiring until native Application is ready * to avoid "application is null" errors at production boot. + * We set a flag here and emit the actual code after the static Application import + * to avoid mixing dynamic and static imports of @nativescript/core. */ - imports += ` - (function __nsDeferAndroidActivityImport(){ - const load = () => { try { import('@nativescript/core/ui/frame/activity.android.js?ns-keep'); } catch (e) { console.error('[ns-entry] failed to import android activity module', e); } }; - try { - import('@nativescript/core').then(({ Application: __NS_Application }) => { - try { - const hasApp = !!(__NS_Application && __NS_Application.android && __NS_Application.android.nativeApp); - if (hasApp) { - ${opts.verbose ? "console.info('[ns-entry] android activity import: nativeApp present, loading now');" : ''} - load(); - } else { - ${opts.verbose ? "console.info('[ns-entry] android activity import: deferring until launch/nativeApp');" : ''} - try { __NS_Application.on && __NS_Application.on(__NS_Application.launchEvent, load); } catch {} - try { setTimeout(load, 0); } catch {} - } - } catch { try { setTimeout(load, 0); } catch {} } - }).catch(() => { try { setTimeout(load, 0); } catch {} }); - } catch { try { setTimeout(load, 0); } catch {} } - })();\n`; + needsAndroidActivityDefer = true; } } @@ -211,16 +197,43 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' // ---- Global CSS injection (always-needed if file exists) ---- const appCssPath = path.resolve(projectRoot, getProjectAppRelativePath('app.css')); - if (fs.existsSync(appCssPath)) { - imports += `// Import and apply global CSS before app bootstrap\n`; - imports += `import appCssContent from './${appRootDir}/app.css?inline';\n`; + const hasAppCss = fs.existsSync(appCssPath); + + // Import Application statically if needed for CSS or Android activity defer + if (hasAppCss || needsAndroidActivityDefer) { + if (hasAppCss) { + imports += `// Import and apply global CSS before app bootstrap\n`; + imports += `import appCssContent from './${appRootDir}/app.css?inline';\n`; + } imports += `import { Application } from '@nativescript/core';\n`; - imports += `if (appCssContent) { try { Application.addCss(appCssContent); } catch (error) { console.error('Error applying CSS:', error); } }\n`; - if (opts.verbose) { - imports += `console.info('[ns-entry] app.css applied');\n`; + if (hasAppCss) { + imports += `if (appCssContent) { try { Application.addCss(appCssContent); } catch (error) { console.error('Error applying CSS:', error); } }\n`; + if (opts.verbose) { + imports += `console.info('[ns-entry] app.css applied');\n`; + } } } + // ---- Deferred Android activity import (non-HMR only) ---- + // Uses the statically imported Application to avoid mixing dynamic and static imports + if (needsAndroidActivityDefer) { + imports += ` + (function __nsDeferAndroidActivityImport(){ + const load = () => { try { import('@nativescript/core/ui/frame/activity.android.js?ns-keep'); } catch (e) { console.error('[ns-entry] failed to import android activity module', e); } }; + try { + const hasApp = !!(Application && Application.android && Application.android.nativeApp); + if (hasApp) { + ${opts.verbose ? "console.info('[ns-entry] android activity import: nativeApp present, loading now');" : ''} + load(); + } else { + ${opts.verbose ? "console.info('[ns-entry] android activity import: deferring until launch/nativeApp');" : ''} + try { Application.on && Application.on(Application.launchEvent, load); } catch {} + try { setTimeout(load, 0); } catch {} + } + } catch { try { setTimeout(load, 0); } catch {} } + })();\n`; + } + // ---- Application main entry ---- if (opts.hmrActive) { // HTTP-only dev boot: try to import the entire app over HTTP; if not reachable, keep retrying. From d6caa43fd84945f6dae9e1cbd445af100caa6c4a Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 7 Jan 2026 10:18:14 -0800 Subject: [PATCH 14/14] feat(vite): support custom android activites and application classes (#11043) --- packages/vite/configuration/base.ts | 12 +- packages/vite/helpers/app-components.ts | 327 ++++++++++++++++++ packages/vite/helpers/main-entry.ts | 19 + .../vite/helpers/nativeclass-transform.ts | 92 ++++- packages/vite/index.ts | 3 + 5 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 packages/vite/helpers/app-components.ts diff --git a/packages/vite/configuration/base.ts b/packages/vite/configuration/base.ts index 54f1f8d6d1..b9f94f08c4 100644 --- a/packages/vite/configuration/base.ts +++ b/packages/vite/configuration/base.ts @@ -34,6 +34,7 @@ import { createNativeClassTransformerPlugin } from '../helpers/nativeclass-trans import { getThemeCoreGenericAliases, createEnsureHoistedThemeLinkPlugin, createThemeCoreCssFallbackPlugin } from '../helpers/theme-core-plugins.js'; import { createPostCssConfig } from '../helpers/postcss-platform-config.js'; import { getProjectAppPath, getProjectAppRelativePath } from '../helpers/utils.js'; +import { appComponentsPlugin } from '../helpers/app-components.js'; // Load HMR plugins lazily to avoid compiling dev-only sources during library build // This prevents TypeScript from traversing the heavy HMR implementation graph when not needed // function getHMRPluginsSafe(opts: { @@ -342,6 +343,8 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }): NativeScriptPlugin({ platform }), // Ensure globals and Android activity are included early via virtual entry mainEntryPlugin({ platform, isDevMode, verbose, hmrActive }), + // Handle custom Android Activity/Application components (auto-detected or configured) + appComponentsPlugin({ platform, verbose }), dynamicImportPlugin(), // Transform Vite worker URLs to NativeScript format AFTER bundling workerUrlPlugin(), @@ -439,7 +442,14 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }): // Preserve side effects for NativeScript core so classes/functions // aren't tree-shaken out inadvertently. This does NOT cause cross‑chunk duplication; // it only prevents Rollup from dropping modules it considers side‑effect free. - moduleSideEffects: (id) => /node_modules[\\\/]\@nativescript[\\\/]core[\\\/]/.test(id) || null, + // Also preserve side effects for .android and .ios files which may contain + // other decorated classes that register with the native runtime + moduleSideEffects: (id) => { + if (/node_modules[\\\/]\@nativescript[\\\/]core[\\\/]/.test(id)) return true; + // Activity and Application files have side effects (class registration) + if (/\.(android|ios)\.(ts|js)$/.test(id)) return true; + return null; + }, }, input: 'virtual:entry-with-polyfills', output: { diff --git a/packages/vite/helpers/app-components.ts b/packages/vite/helpers/app-components.ts new file mode 100644 index 0000000000..7f7ccdf5e1 --- /dev/null +++ b/packages/vite/helpers/app-components.ts @@ -0,0 +1,327 @@ +import path from 'path'; +import fs from 'fs'; +import type { Plugin, ResolvedConfig } from 'vite'; +import { getProjectRootPath } from './project.js'; +import { getProjectAppRelativePath } from './utils.js'; + +const projectRoot = getProjectRootPath(); + +export interface AppComponentsOptions { + /** + * List of app component paths (relative to project root). + * These are typically custom Android Activity or Application classes. + * Example: ['./app/custom-activity.android.ts', './app/custom-application.android.ts'] + */ + appComponents?: string[]; + platform: 'android' | 'ios' | 'visionos'; + verbose?: boolean; +} + +/** + * Get app components from environment variable or nativescript.config.ts + * Format: comma-separated paths, e.g., "./app/custom-activity.android,./app/custom-application.android" + */ +function getAppComponentsFromEnv(): string[] { + const envValue = process.env.NS_APP_COMPONENTS; + if (!envValue) return []; + return envValue + .split(',') + .map((p) => p.trim()) + .filter(Boolean); +} + +/** + * Resolve an app component path to an absolute path + */ +function resolveComponentPath(componentPath: string): string | null { + // If already absolute, check if exists + if (path.isAbsolute(componentPath)) { + return fs.existsSync(componentPath) ? componentPath : null; + } + + // Remove leading ./ if present for consistency + const cleanPath = componentPath.replace(/^\.\//, ''); + + // Try with and without extensions + const extensions = ['', '.ts', '.js', '.android.ts', '.android.js']; + + for (const ext of extensions) { + const fullPath = path.resolve(projectRoot, cleanPath + ext); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + // Also try in the app directory + const appDir = getProjectAppRelativePath(''); + for (const ext of extensions) { + const fullPath = path.resolve(projectRoot, appDir, cleanPath + ext); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + return null; +} + +/** + * Extract the output name for an app component + * e.g., "./app/custom-activity.android.ts" -> "custom-activity" + * e.g., "./app/custom-application.android.ts" -> "custom-application" + */ +function getComponentOutputName(componentPath: string): string { + const basename = path.basename(componentPath); + // Remove .android.ts, .android.js, .ts, .js extensions + return basename.replace(/\.(android\.)?(ts|js)$/, ''); +} + +/** + * Plugin to handle NativeScript app components (custom Activity/Application classes) + * + * These components need to be bundled as separate entry points because: + * 1. Custom Android Activity classes are loaded by the Android runtime before the main bundle + * 2. Custom Android Application classes are loaded even earlier in the app lifecycle + * + * Usage in vite.config.ts: + * ```ts + * import { defineConfig } from 'vite'; + * import { typescriptConfig, appComponentsPlugin } from '@nativescript/vite'; + * + * export default defineConfig(({ mode }) => { + * const config = typescriptConfig({ mode }); + * config.plugins.push( + * appComponentsPlugin({ + * appComponents: ['./app/custom-activity.android.ts'], + * platform: 'android' + * }) + * ); + * return config; + * }); + * ``` + * + * Or via environment variable: + * NS_APP_COMPONENTS="./app/custom-activity.android,./app/custom-application.android" ns run android + */ +export function appComponentsPlugin(options: AppComponentsOptions): Plugin { + const { platform, verbose = false } = options; + + // Collect app components from all sources + let appComponents: string[] = [...(options.appComponents || []), ...getAppComponentsFromEnv()]; + + // Remove duplicates + appComponents = [...new Set(appComponents)]; + + // Resolve all component paths + const resolvedComponents: Map = new Map(); + + for (const component of appComponents) { + const absolutePath = resolveComponentPath(component); + if (absolutePath) { + const outputName = getComponentOutputName(absolutePath); + resolvedComponents.set(component, { absolutePath, outputName }); + if (verbose) { + console.log(`[app-components] Found: ${component} -> ${outputName}.mjs`); + } + } else if (verbose) { + console.warn(`[app-components] Could not resolve: ${component}`); + } + } + + // Skip if no components found + if (resolvedComponents.size === 0) { + return { + name: 'nativescript-app-components', + apply: 'build', + }; + } + + // Track component output names for entryFileNames + const componentOutputNames = new Set(); + for (const [, { outputName }] of resolvedComponents) { + componentOutputNames.add(outputName); + } + + // Set environment variable so main-entry.ts can inject imports for these components + // This allows the virtual module to know which app components are configured + const componentPaths = Array.from(resolvedComponents.values()).map((c) => c.absolutePath); + process.env.NS_APP_COMPONENTS = componentPaths.join(','); + + // Create a set of output names for quick lookup in resolveId + const outputMjsFiles = new Set(); + const absoluteMjsPaths = new Set(); + for (const [, { outputName }] of resolvedComponents) { + outputMjsFiles.add(`~/${outputName}.mjs`); + outputMjsFiles.add(`./${outputName}.mjs`); + // Also track absolute paths that Vite might resolve ~/foo.mjs to + const appDir = getProjectAppRelativePath(''); + const absoluteMjsPath = path.resolve(projectRoot, appDir, `${outputName}.mjs`); + absoluteMjsPaths.add(absoluteMjsPath); + } + + let config: ResolvedConfig; + + return { + name: 'nativescript-app-components', + apply: 'build', + + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + + // Mark app component output files as external during build + // These are generated as separate entry points and will exist at runtime + resolveId(id) { + // Handle ~/foo.mjs or ./foo.mjs patterns + if (outputMjsFiles.has(id)) { + // Return the id with external flag - this tells Rollup to keep the import as-is + return { id: `./${id.replace(/^~\//, '')}`, external: true }; + } + // Handle absolute paths that Vite resolves ~/foo.mjs to (e.g., /path/to/app/foo.mjs) + if (absoluteMjsPaths.has(id)) { + const basename = path.basename(id); + return { id: `./${basename}`, external: true }; + } + return null; + }, + + // Modify the Vite config to support multiple entry points + config(userConfig) { + if (resolvedComponents.size === 0) return null; + + // We need to modify the output.entryFileNames to handle multiple entries + return { + build: { + rollupOptions: { + output: { + // Use a function to determine entry file names + entryFileNames: (chunkInfo: { name: string }) => { + // App components should output as .mjs files + // This is required because SBG (Static Binding Generator) only parses + // .mjs files as ES modules. If we output as .js, SBG will try to parse + // it as CommonJS and fail on import statements. + if (componentOutputNames.has(chunkInfo.name)) { + return `${chunkInfo.name}.mjs`; + } + // Default: main bundle + return 'bundle.mjs'; + }, + }, + }, + }, + }; + }, + + // Modify rollup options to add additional entry points + options(inputOptions) { + if (resolvedComponents.size === 0) return null; + + // Get current input + const currentInput = inputOptions.input; + const newInput: Record = {}; + + // Preserve existing inputs + if (typeof currentInput === 'string') { + newInput['bundle'] = currentInput; + } else if (Array.isArray(currentInput)) { + currentInput.forEach((input, i) => { + newInput[`entry${i}`] = input; + }); + } else if (currentInput && typeof currentInput === 'object') { + Object.assign(newInput, currentInput); + } + + // Add app component entries - use the actual file path directly + for (const [, { absolutePath, outputName }] of resolvedComponents) { + newInput[outputName] = absolutePath; + } + + if (verbose) { + console.log('[app-components] Build inputs:', newInput); + } + + return { ...inputOptions, input: newInput }; + }, + + // Adjust output file names for app components (fallback in case entryFileNames doesn't work) + generateBundle(options, bundle) { + for (const [fileName, chunk] of Object.entries(bundle)) { + if (chunk.type !== 'chunk') continue; + + // Check if this is an app component entry + if (componentOutputNames.has(chunk.name)) { + // Rename to .mjs (SBG requires .mjs for ES module parsing) + const newFileName = `${chunk.name}.mjs`; + if (fileName !== newFileName) { + chunk.fileName = newFileName; + delete bundle[fileName]; + bundle[newFileName] = chunk; + } + } + } + }, + + // Post-process app component chunks to fix Rollup's internal variable renaming. + // SBG (Static Binding Generator) needs the __extends and __decorate calls to use + // the same class name as the outer variable assignment. + renderChunk(code, chunk) { + // Only process app component chunks + if (!componentOutputNames.has(chunk.name)) { + return null; + } + + // Look for patterns where Rollup renamed the internal class variable + // Pattern: var ClassName = ... __extends(ClassName2, _super); ... return ClassName2; ... + // We need: var ClassName = ... __extends(ClassName, _super); ... return ClassName; ... + + // Use a simpler regex that matches across the various output formats + // This finds: var SomeName = ... __extends(SomeName2, ...) + const varAssignRegex = /var\s+(\w+)\s*=[\s\S]*?__extends\s*\(\s*(\w+)\s*,/g; + + let match; + let modifiedCode = code; + + while ((match = varAssignRegex.exec(code)) !== null) { + const outerName = match[1]; // e.g., "CustomActivity" + const innerName = match[2]; // e.g., "CustomActivity2" + + if (outerName !== innerName && innerName === outerName + '2') { + // Rollup renamed it - fix by replacing all occurrences of the inner name + // Only within this chunk, replace innerName with outerName + // Be careful to only replace as a complete identifier + const innerNameRegex = new RegExp(`\\b${innerName}\\b`, 'g'); + modifiedCode = modifiedCode.replace(innerNameRegex, outerName); + + if (verbose) { + console.log(`[app-components] Fixed Rollup rename: ${innerName} -> ${outerName} in ${chunk.fileName}`); + } + } + } + + if (modifiedCode !== code) { + return { code: modifiedCode, map: null }; + } + + return null; + }, + }; +} + +/** + * Get resolved app components with their output file names + * Used by main-entry.ts to inject imports for custom activities/applications + */ +export function getResolvedAppComponents(platform: string): Array<{ absolutePath: string; outputName: string }> { + // Get components from environment variable (set by appComponentsPlugin during build) + const components = getAppComponentsFromEnv(); + const resolved: Array<{ absolutePath: string; outputName: string }> = []; + + for (const component of components) { + const absolutePath = resolveComponentPath(component); + if (absolutePath) { + const outputName = getComponentOutputName(absolutePath); + resolved.push({ absolutePath, outputName }); + } + } + + return resolved; +} diff --git a/packages/vite/helpers/main-entry.ts b/packages/vite/helpers/main-entry.ts index 064b3bf7ad..58ade823a0 100644 --- a/packages/vite/helpers/main-entry.ts +++ b/packages/vite/helpers/main-entry.ts @@ -4,6 +4,7 @@ import os from 'os'; import path from 'path'; import { getProjectFlavor } from './flavor.js'; import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from './utils.js'; +import { getResolvedAppComponents } from './app-components.js'; // Switched to runtime modules to avoid fragile string injection and enable TS checks const projectRoot = getProjectRootPath(); const appRootDir = getProjectAppPath(); @@ -127,6 +128,24 @@ export function mainEntryPlugin(opts: { platform: 'ios' | 'android' | 'visionos' imports += "import 'virtual:ns-bundler-context';\n"; } + // ---- Custom App Components (Activity/Application) ---- + // These must be loaded early so the JS class is registered before Android instantiates them + if (opts.platform === 'android') { + try { + const appComponents = getResolvedAppComponents('android'); + for (const component of appComponents) { + // The appComponentsPlugin bundles these as separate .mjs entry points + // We must import the output file, not the source, since it's a separate entry + imports += `import "~/${component.outputName}.mjs";\n`; + if (opts.verbose) { + imports += `console.info('[ns-entry] app component loaded: ${component.outputName}');\n`; + } + } + } catch (err) { + console.error('[main-entry] Error resolving app components:', err); + } + } + // ---- Platform-specific always-needed modules ---- // Track if we need to defer Android activity import (non-HMR only) let needsAndroidActivityDefer = false; diff --git a/packages/vite/helpers/nativeclass-transform.ts b/packages/vite/helpers/nativeclass-transform.ts index 371ff9addd..46212c8475 100644 --- a/packages/vite/helpers/nativeclass-transform.ts +++ b/packages/vite/helpers/nativeclass-transform.ts @@ -78,6 +78,16 @@ export function transformNativeClassSource(code: string, fileName: string) { return `${prefix || '\n'}${stacked || ''}/*__NativeClass__*/`; }); + // Also handle cases where @NativeClass() is on its own line with parentheses + // Pattern: @NativeClass() followed by other decorators or class + if (!working.includes('/*__NativeClass__*/') && code.includes('@NativeClass')) { + // Simpler replacement: just replace @NativeClass() or @NativeClass with marker + working = code.replace(/@NativeClass\s*\(\s*\)\s*\n/g, '/*__NativeClass__*/\n'); + if (!working.includes('/*__NativeClass__*/')) { + working = code.replace(/@NativeClass\s*\n/g, '/*__NativeClass__*/\n'); + } + } + // If neither original nor marker is present, skip transform early. if (!working.includes('@NativeClass') && !working.includes('/*__NativeClass__*/')) return null; try { @@ -92,6 +102,8 @@ export function transformNativeClassSource(code: string, fileName: string) { const original = working.slice(fullStart, node.end); const stripped = original.replace(/\/\*__NativeClass__\*\/\s*/g, '').replace(/^\s*@NativeClass(?:\([\s\S]*?\))?\s*$/gm, ''); const hadExport = /^\s*export\s+class\b/.test(stripped); + const className = (node as ts.ClassDeclaration).name?.text; + const down = ts .transpileModule(stripped, { compilerOptions: { @@ -110,11 +122,76 @@ export function transformNativeClassSource(code: string, fileName: string) { return `Object.defineProperty(${obj}, ${key}, {${body}})`; }); let cleaned = down.replace(/export \{\};?\s*$/m, ''); + + // Debug: log the transpileModule output + const debugNativeClass = process.env.NS_DEBUG_NATIVECLASS; + if (debugNativeClass) { + console.log('[NativeClass] fileName:', fileName); + console.log('[NativeClass] className:', className); + console.log('[NativeClass] stripped input (first 300 chars):', stripped.slice(0, 300)); + console.log('[NativeClass] transpileModule output (first 300 chars):', cleaned.slice(0, 300)); + } + + // SBG (Static Binding Generator) expects the __decorate call to be INSIDE the IIFE. + // TypeScript's transpileModule already puts it inside correctly. + // The pattern should be: + // var ClassName = (function(_super) { + // __extends(ClassName, _super); + // function ClassName() { ... } + // // prototype methods... + // ClassName = __decorate([...], ClassName); <-- INSIDE the IIFE + // return ClassName; + // })(BaseClass); + // We do NOT move it outside - the IIFE pattern with __decorate inside is correct. + + // Fix: If the transpiled output is just an IIFE expression (not assigned), + // we need to assign it to a variable so the class is properly registered. + // This happens when TypeScript transpiles a class declaration without assignment context. + if (className) { + // Check if the output is a bare IIFE without assignment + // The transpiled output can look like: + // (/** @class */ (function(_super){...})(Base)); + // or: (function(_super){...})(Base); + // We need to assign it to a variable: var ClassName = ... + const trimmed = cleaned.trim(); + // Pattern: starts with ( and contains IIFE extending a class + // Look for pattern that indicates an unassigned class expression + const startsWithParen = trimmed.startsWith('('); + const hasExtends = /__extends\s*\(/.test(trimmed) || /function\s*\(\s*_super\s*\)/.test(trimmed); + const notAssigned = !/^\s*var\s+\w+\s*=/.test(trimmed) && !/^\s*let\s+\w+\s*=/.test(trimmed) && !/^\s*const\s+\w+\s*=/.test(trimmed); + + if (startsWithParen && hasExtends && notAssigned) { + // The output is a bare class expression, wrap it in assignment + // Simply prepend `var ClassName = ` and remove the trailing semicolon if present + // to avoid `var X = (...);` becoming `var X = (...);` (keep it clean) + let unwrapped = trimmed; + + // Remove trailing semicolon for cleaner output, we'll add it back + if (unwrapped.endsWith(';')) { + unwrapped = unwrapped.slice(0, -1).trimEnd(); + } + + // If wrapped in outer parens that are just for grouping, we can keep them + // The result will be: var CustomActivity = (/** @class */ (function...)); + cleaned = `var ${className} = ${unwrapped};`; + } + } + if (hadExport) { - const name = (node as ts.ClassDeclaration).name?.text; - if (name && !new RegExp(`export\s*{\s*${name}\s*}`, 'm').test(cleaned)) { - cleaned += `\nexport { ${name} };\n`; + if (className && !new RegExp(`export\\s*{\\s*${className}\\s*}`, 'm').test(cleaned)) { + cleaned += `\nexport { ${className} };\n`; } + } else if (className) { + // For non-exported @NativeClass classes (like custom activities), + // we need to ensure they're not tree-shaken by Rollup. + // Register on global to create an unoptimizable side effect. + // Use a pattern that prevents inlining by Rollup. + cleaned += `\n;(function(c) { global.__nativeClasses = global.__nativeClasses || {}; global.__nativeClasses["${className}"] = c; })(${className});\n`; + } + // Ensure the transpiled output starts with a newline to properly separate + // from any preceding code (like import statements) + if (!cleaned.startsWith('\n')) { + cleaned = '\n' + cleaned; } edits.push({ start: fullStart, end: node.end, text: cleaned }); } @@ -148,6 +225,15 @@ export function transformNativeClassSource(code: string, fileName: string) { return true; }) .join('\n'); + + // Debug: Final output + if (process.env.NS_DEBUG_NATIVECLASS === '1') { + console.log('[NativeClass] ===== FINAL OUTPUT ====='); + console.log('[NativeClass] File:', fileName); + console.log('[NativeClass] Output:\n', output); + console.log('[NativeClass] ===== END FINAL OUTPUT ====='); + } + return { code: output, map: null }; } catch { return null; diff --git a/packages/vite/index.ts b/packages/vite/index.ts index 9d116fce27..b72bdc86c6 100644 --- a/packages/vite/index.ts +++ b/packages/vite/index.ts @@ -6,6 +6,9 @@ export * from './configuration/vue.js'; export * from './configuration/javascript.js'; export * from './configuration/typescript.js'; +// App components plugin for custom Android Activity/Application classes +export { appComponentsPlugin, type AppComponentsOptions } from './helpers/app-components.js'; + // Simple CLI entry to support `npx @nativescript/vite init` // This keeps the library export surface intact while allowing a // lightweight command for project bootstrapping.