Repository navigation
@nativescript/vite: link check rejects valid aliased default exports #11566
Description
@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/vite8.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 checkThe 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.
Activity