Repository navigation
fix(vite): unvendored plugin-patterned deps rewrite to __nsVendorRequire stubs instead of the /ns/m HTTP fallback #11458
Description
Issue Description
In dev sessions (ns run HMR over /ns/m), an npm package that depends on a NativeScript runtime plugin via a regular dependencies entry fails when the app does not also declare the plugin. The wrapper package's import { Img } from '@nativescript-community/ui-image' is rewritten into a synchronous __nsVendorRequire("@nativescript-community/ui-image"); the vendor registry has no entry (the plugin isn't vendored), so the device gets the __nsMissing proxy stub and downstream calls fail with things like X is not a constructor.
This hits hard under pnpm's isolated linker (pnpm workspaces, nodeLinker: isolated), where transitive dependencies aren't resolvable from the project root — but the same failure exists anywhere a plugin-patterned package is absent from the vendor manifest (NS_VENDOR_EXCLUDE, flavor vendor.exclude, local file:/workspace: sources that ship a plugin dep).
ns prepare handles this fine — NodeModulesDependenciesBuilder BFS-traverses each package's dependencies via require.resolve from the resolved parent dir, so the plugin's platforms/ios/Podfile and platforms/android/include.gradle merge correctly without the app declaring the plugin. Only the Vite dev server loses the JS side.
Root cause
collectVendorModules intentionally does not collect transitive runtime deps (see the comment in hmr/shared/vendor/manifest-collect.ts), expecting "non-vendor packages" to "route through HTTP to the Vite dev server." That fallback exists — GET /ns/m/node_modules/@nativescript-community/ui-image (bare, unvendored, transitive) does return real transformed code. But two call sites assume plugin-patterned specifiers are always vendored and never let them reach the fallback:
resolveVendorRouting(hmr/server/websocket-module-specifiers.ts): theisLikelyNativeScriptRuntimePluginSpecifierfast paths return{ route: 'vendor' }before the manifest-membership check below them.ensureNativeScriptModuleBindings(hmr/server/websocket-module-bindings.ts):canonicalis force-assigned fromisLikelyNativeScriptPluginSpecifier(specifier)— pattern match only, no manifest check — so the import becomes a__nsVendorRequirebinding instead of staying an HTTP import.
Repro
app/package.json: "@scope/gif": "workspace:*" # or published dep
packages/gif/package.json: "dependencies": { "@nativescript-community/ui-image": "^4.6.20" }
packages/gif/src/index.native.ts: import { Img } from '@nativescript-community/ui-image'
pnpm, nodeLinker: isolated. ns run ios → fetch GET /ns/m/node_modules/@scope/gif/src/index.native.ts → served code contains __nsVendorRequire("@nativescript-community/ui-image") → registry miss → Img is a soft-stub Proxy.
Proposed fix
Gate the plugin-patterned vendor routes on vendor-manifest membership:
- In
resolveVendorRouting, hoistgetVendorManifest()before the fast paths and requiremanifest.modules[pkgName]; unvendored plugin deps returnnull→ callers emit the per-module/ns/m/node_modules/...HTTP form, which resolves them via Vite's importer-aware resolution (works from.pnpmrealpaths too). - In
ensureNativeScriptModuleBindings, dropcanonicalwhen it isn't in the manifest, and only apply theisLikelyNativeScriptPluginSpecifierforce-canonical branch whenresolveVendorSpecifier(specifier)hits.
Verified locally in a pnpm-isolated workspace: with the gate applied, the wrapper leaf's served code emits import { Img } from "http://…/ns/m/node_modules/.pnpm/@[email protected]/node_modules/@nativescript-community/ui-image/index.ios.js", which returns 200 with transformed code.
Happy to send the PR.
Activity