← 输入框|github.com|刷新
Skip to content

@nativescript/vite: link check rejects valid aliased default exports #11566

Closed

Description

@aleclarson

@nativescript/vite: link check rejects valid aliased default exports

The native /ns/m dev module server rejects export { named as default } even though the module has a valid ESM default export. An importer requesting that default is replaced with a throwing module.

Environment and scope

  • @nativescript/vite 8.0.20, without dependency patches
  • Vite 8.3.0; Node 26.12.0; macOS
  • Plain JavaScript; no Coreframe or Octane plugins
  • The fixture mounts the production module-server route directly in a real Vite server. It does not initialize a complete NativeScript app or launch a device.
  • The affected consumer also fails during actual iOS native dev startup when its compiler emits the same export form. Bundled mode works independently.

Expected and observed behavior

Module Node namespace Native importer
export default 17 Has default 17 Accepted
const named = 23; export { named as default } Has default 23 Replaced with a throwing module
export const named = 99 No default Rejected, as expected

Importers use absolute HTTP URLs under /ns/m. The current link check skips root-relative imports, so relative imports will not reproduce this case.

The aliased importer's HTTP response is 200 with:

throw new Error("[link-check] Missing default export in /ns/m/src/aliased.mjs (imported by /src/import-aliased.mjs)");
export {};

The check in packages/vite/hmr/server/websocket-ns-m.ts recognizes direct default syntax and a narrow default re-export form, but misses a named binding exported as default. The same matching expression remains on upstream main by source inspection; current main was not run on a device.

Reproduction

In an empty directory, create the following files, then run:

pnpm install
pnpm check

The check writes the full case matrix to result.json and fails on the aliased case because native link validation disagrees with the actual ESM namespace. The ordinary-default and missing-default cases are controls. The middleware-only fixture may emit a flavor-configuration diagnostic; the request handling and results below are still exercised.

package.json:

{
  "name": "nativescript-default-export-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"

repro.mjs:

import assert from 'node:assert/strict'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { resolve } from 'node:path'
import { createServer } from 'vite'
import { registerNsModuleServerRoute } from './node_modules/@nativescript/vite/hmr/server/websocket-ns-m.js'

process.env.NODE_ENV='development'
process.env.NS_HMR_PREFER_LAN_HOST='0'
const root=fileURLToPath(new URL('.',import.meta.url))
await mkdir(resolve(root,'src'),{recursive:true})
const cases=[
 {name:'direct',source:'export default 17;\n',value:17},
 {name:'aliased',source:'const named = 23; export { named as default };\n',value:23},
 {name:'missing',source:'export const named = 99;\n',value:undefined},
]
for(const entry of cases){
 await writeFile(resolve(root,'src',entry.name+'.mjs'),entry.source)
}
const server=await createServer({
 configFile:false,root,logLevel:'silent',
 server:{host:'127.0.0.1',port:0,strictPort:true,fs:{allow:[root]}},
 plugins:[{
  name:'mount-native-module-route',
  configureServer(server){
   registerNsModuleServerRoute(server,{
    verbose:false,appVirtualWithSlash:'/app/',sfcFileMap:new Map(),depFileMap:new Map(),
    getGraphVersion:()=>0,getStrategy:()=>({flavor:'javascript'}),
    sharedTransformRequest:(url)=>server.transformRequest(url),
    ensureInitialGraphPopulationStarted:()=>{},upsertGraphModule:()=>{},depsBundle:null,
   })
  },
 }],
})
try{
 await server.listen()
 const origin='http://127.0.0.1:'+server.httpServer.address().port
 for(const entry of cases){
  await writeFile(resolve(root,'src','import-'+entry.name+'.mjs'),'import value from "'+origin+'/ns/m/src/'+entry.name+'.mjs"; export const observed = value;\n')
 }
 const results=[]
 for(const entry of cases){
  const namespace=await import(pathToFileURL(resolve(root,'src',entry.name+'.mjs')))
  assert.equal(namespace.default,entry.value)
  const response=await fetch(origin+'/ns/m/src/import-'+entry.name+'.mjs')
  const body=await response.text()
  const rejected=body.includes('[link-check] Missing default export')
  await writeFile(resolve(root,entry.name+'-response.mjs'),body)
  assert.equal(response.status,200)
  results.push({case:entry.name,nodeHasDefault:Object.hasOwn(namespace,'default'),nodeValue:namespace.default??null,nativeLinkCheckRejects:rejected,status:response.status})
 }
 const manifest=JSON.parse(await readFile(resolve(root,'node_modules/@nativescript/vite/package.json'),'utf8'))
 const record={nativeScriptVite:manifest.version,vite:JSON.parse(await readFile(resolve(root,'node_modules/vite/package.json'),'utf8')).version,scope:'Unpatched NativeScript module-server route with real Vite transforms; plain JavaScript and no Coreframe/Octane plugins',results}
 await writeFile(resolve(root,'result.json'),JSON.stringify(record,null,2)+'\n')
 console.log(JSON.stringify(record,null,2))
 for(const result of results){
  assert.equal(result.nativeLinkCheckRejects,!result.nodeHasDefault,result.case+': the native link check must agree with the actual ESM namespace')
 }
}finally{await server.close()}

Requested fix

Recognize actual ESM default exports, including named-to-default aliases, while continuing to reject imports from modules that have no default. This should not require changing valid compiler output or disabling link validation in consuming apps.

The existing parser dependency could inspect export declarations/specifiers. Please cover direct defaults, aliases, genuinely missing defaults, and invalidation after source edits. The fixture above can also verify a fix through the real HTTP route.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions