Repository navigation
Description
@nativescript/vite 8.0.20 serves a workspace-shared TSX module when its device URL includes .tsx, but returns 404 for the extensionless URL. The HMR client uses extensionless script URLs for reimports, so a successful static import does not establish that the same shared module can be hot-reimported.
Verified with the unpatched production NativeScript HTTP route, Vite 8.3.0 and Node 26.12.0. No Coreframe or Octane plugins, device runtime, or images are involved. This is separate from the graph-identity collision in #11569.
Expected and observed
With a native project at <workspace>/native and a module at <workspace>/shared/src/screen.tsx:
| Request | Actual |
|---|---|
/ns/m/shared/src/screen.tsx |
200 |
/ns/m/shared/src/screen |
404 |
Both should serve the same module. server.fs.allow explicitly includes the workspace. The project and temporary directory use real paths to avoid a symlink/filesystem-allow artifact.
Reproduction
Create these files in an empty directory, run pnpm install, then node workspace-fetch.mjs. The assertion intentionally fails after printing the status pair and writing workspace-fetch-result.json.
package.json:
{
"name": "nativescript-hmr-identity-repro",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@11.5.1",
"scripts": {
"check": "node repro.mjs"
},
"devDependencies": {
"@nativescript/vite": "8.0.20",
"vite": "8.3.0"
}
}pnpm-workspace.yaml:
packages:
- .
allowBuilds:
esbuild: true
"@parcel/watcher": true
minimumReleaseAgeExclude:
- "@nativescript/vite@8.0.20"workspace-fetch.mjs:
import assert from 'node:assert/strict'
import { mkdtempSync, realpathSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { createServer } from 'vite'
const ns = pathToFileURL(createRequire(import.meta.url).resolve('@nativescript/vite'))
const { registerNsModuleServerRoute } = await import(new URL('./hmr/server/websocket-ns-m.js', ns))
const { setProjectFlavor } = await import(new URL('./helpers/flavor.js', ns))
setProjectFlavor('javascript')
const workspace = realpathSync(mkdtempSync(join(tmpdir(), 'ns-workspace-fetch-')))
const root = join(workspace, 'native')
mkdirSync(join(root, 'src'), { recursive: true })
mkdirSync(join(workspace, 'shared/src'), { recursive: true })
writeFileSync(join(root, 'package.json'), '{"type":"module"}')
writeFileSync(join(workspace, 'pnpm-workspace.yaml'), 'packages:\n - native\n')
writeFileSync(join(workspace, 'shared/src/screen.tsx'), 'export default "baseline";')
const server = await createServer({
root, configFile: false, logLevel: 'silent', optimizeDeps: { noDiscovery: true },
server: { host: '127.0.0.1', port: 0, watch: null, fs: { allow: [workspace] } },
plugins: [{ name: 'native-route-repro', configureServer(server) {
registerNsModuleServerRoute(server, {
verbose: false, appVirtualWithSlash: '/src/', sfcFileMap: new Map(), depFileMap: new Map(),
getGraphVersion: () => 1, getStrategy: () => ({ flavor: 'javascript' }), depsBundle: null,
sharedTransformRequest: (id) => server.transformRequest(id),
ensureInitialGraphPopulationStarted() {}, upsertGraphModule() {},
})
} }],
})
try {
await server.listen()
const origin = `http://127.0.0.1:${server.httpServer.address().port}`
const results = []
for (const url of ['/ns/m/shared/src/screen.tsx', '/ns/m/shared/src/screen']) {
const response = await fetch(origin + url)
results.push({ url, status: response.status })
await response.text()
}
console.log(JSON.stringify(results))
writeFileSync(new URL('./workspace-fetch-result.json', import.meta.url), JSON.stringify(results, null, 2) + '\n')
assert.ok(results.every(({ status }) => status === 200), 'HMR extensionless fetch must serve the same shared module as its static URL')
} finally {
await server.close()
rmSync(workspace, { recursive: true, force: true })
}Source finding and requested fix
resolveNsMTransformedModule first tries extension/platform candidates as Vite app URLs. Those URLs do not address shared files outside the Vite root. Its later filesystem-root fallback calls resolveFsAnchoredSpec only with the original, extensionless spec; it does not probe the expanded candidates under the workspace root.
Please resolve the same extension/platform candidates under the project/workspace filesystem roots and preserve Vite filesystem access checks. A local exact-version patch doing that passes a real HTTP regression for native-host, shared-workspace and external-source modules, together with graph/hot-context identity checks. This report does not claim device HMR qualification or an upstream-main runtime test.
Activity