⚡ Sangsakala Proxy 🏠 Home
Skip to content

@nativescript/vite: extensionless shared-workspace module requests return 404 #11570

Open

Description

@aleclarson

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions