From 7a00133b618beb0335301c95e35da35161dd87bf Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Nov 2026 01:14:28 -0300 Subject: [PATCH 1/2] docs: drop `return global.__native(this)` from Android subclass examples With TypeScript 2.0 and newer the compiled constructor already returns the native instance from `_super.call(this)`, so `global.__native` is a no-op. Remove it from the examples, drop constructors that only called `super()`, and add a short note that existing calls are harmless. --- content/guide/android-marshalling.md | 9 ------- ...ses-and-implementing-interfaces-android.md | 27 +++---------------- content/public/assets/agentic/NATIVESCRIPT.md | 7 ++--- 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/content/guide/android-marshalling.md b/content/guide/android-marshalling.md index d5bde33a..e4e311a3 100644 --- a/content/guide/android-marshalling.md +++ b/content/guide/android-marshalling.md @@ -628,11 +628,6 @@ let MyVersatileCopyWriter = java.lang.Object.extend({ ```ts @Interfaces([com.a.b.Printer, com.a.b.Copier, com.a.b.Writer]) /* the interfaces that will be inherited by the resulting MyVersatileCopyWriter class */ class MyVersatileCopyWriter extends java.lang.Object { - constructor() { - super(); - return global.__native(this); - } - print() { ... } copy() { ... } write() { ... } @@ -653,10 +648,6 @@ Java/Kotlin method overloads are handled by the developer by explicitly checking ```ts class MyVersatileCopyWriter extends ... { - constructor() { - super(); - return global.__native(this); - } ... print() { let content = ""; diff --git a/content/guide/extending-classes-and-implementing-interfaces-android.md b/content/guide/extending-classes-and-implementing-interfaces-android.md index 5d9f8626..2c8ee590 100644 --- a/content/guide/extending-classes-and-implementing-interfaces-android.md +++ b/content/guide/extending-classes-and-implementing-interfaces-android.md @@ -53,9 +53,6 @@ class MyButton extends android.widget.Button { constructor() { super() constructorCalled = true - - // necessary when extending TypeScript constructors - return global.__native(this) } setEnabled(enabled: boolean): void { @@ -88,6 +85,10 @@ const button = new MyButton(context) In the above example, the `setEnabled` function's use of `this` keyword points to the JavaScript object that proxies the extended native instance. The `this.super` usage provides access to the base class method implementation. ::: +:::tip Note +Older examples end the constructor with `return global.__native(this)`. This is not needed with TypeScript 2.0 and newer. The runtime keeps `global.__native` only for backward compatibility, so existing calls are harmless and can be removed. +::: + ### Creating an anonymous Java class which extends from the base Java `java.lang.Object` class ::: code-group @@ -95,12 +96,6 @@ In the above example, the `setEnabled` function's use of `this` keyword points t ```ts [via TypeScript] @NativeClass class MyClass extends java.lang.Object { - constructor() { - super() - // necessary when extending TypeScript constructors - return global.__native(this) - } - toString(): string { // override Object's toString } @@ -135,13 +130,6 @@ To create a named Java class which extends from the `java.lang.Object` class: @NativeClass @JavaProxy('org.nativescript.example.MyClass') class MyClass extends java.lang.Object { - // constructor - constructor() { - super() - // necessary when extending TypeScript constructors - return global.__native(this) - } - toString(): string { // override Object's toString } @@ -545,13 +533,6 @@ class ClickListener extends java.lang.Object implements android.view.View.OnClickListener { - constructor() { - super() - - // necessary when extending TypeScript constructors - return global.__native(this) - } - onClick(view: android.view.View): void { // Perform action on click } diff --git a/content/public/assets/agentic/NATIVESCRIPT.md b/content/public/assets/agentic/NATIVESCRIPT.md index c18540e5..fe43fc3e 100644 --- a/content/public/assets/agentic/NATIVESCRIPT.md +++ b/content/public/assets/agentic/NATIVESCRIPT.md @@ -167,17 +167,14 @@ Rogue timers cause memory leaks and unexpected behavior. ```typescript @NativeClass() class MyButton extends android.widget.Button { - constructor() { - super() - return global.__native(this) - } - setEnabled(enabled: boolean): void { this.super.setEnabled(enabled) } } ``` +Do not add `return global.__native(this)` to constructors. It is a legacy pattern that is not needed with TypeScript 2.0 and newer; a constructor only needs `super(...)`, and can be omitted if it does nothing else. + ### iOS (using @NativeClass decorator) ```typescript From 836791fbc2f4e0ca398cd0033d2a5b36bb5e975f Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Nov 2026 01:21:06 -0300 Subject: [PATCH 2/2] docs: format the Vite configuration pages with prettier --- .../configuration/vite-framework-flavors.md | 219 +++++++++++------- content/configuration/vite.md | 9 +- 2 files changed, 144 insertions(+), 84 deletions(-) diff --git a/content/configuration/vite-framework-flavors.md b/content/configuration/vite-framework-flavors.md index bf2915a0..b94cfac4 100644 --- a/content/configuration/vite-framework-flavors.md +++ b/content/configuration/vite-framework-flavors.md @@ -12,20 +12,20 @@ Flavors are not limited to the built-ins. A framework — or a renderer, a route ## What a flavor provides ```ts -import { registerFrameworkFlavor } from '@nativescript/vite/framework'; +import { registerFrameworkFlavor } from '@nativescript/vite/framework' registerFrameworkFlavor({ - flavor: 'octane', // the name, everywhere - server: octaneServerStrategy, // runs in the dev server + flavor: 'octane', // the name, everywhere + server: octaneServerStrategy, // runs in the dev server client: '@nativescript-community/vite-octane/client', // fetched and evaluated by the device -}); +}) ``` -| Part | Runs where | Responsibility | -| --- | --- | --- | -| **Config helper** | Vite process | Registers the flavor, wraps `baseConfig({ mode, flavor })`, adds the framework's Vite plugin(s). | -| **Server strategy** | Vite process | Owns `handleHotUpdate` for the flavor's files: update the module graph, purge transform caches, broadcast the delta. | -| **Client strategy** | Device | Decides what a freshly re-imported module *means* for the live app: fire accept callbacks, propagate to importers, reload the graph. | +| Part | Runs where | Responsibility | +| ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| **Config helper** | Vite process | Registers the flavor, wraps `baseConfig({ mode, flavor })`, adds the framework's Vite plugin(s). | +| **Server strategy** | Vite process | Owns `handleHotUpdate` for the flavor's files: update the module graph, purge transform caches, broadcast the delta. | +| **Client strategy** | Device | Decides what a freshly re-imported module _means_ for the live app: fire accept callbacks, propagate to importers, reload the graph. | The shared pieces stay shared. `@nativescript/vite` serves the app over HTTP ESM, speaks the WebSocket protocol, evicts and re-imports modules through `ns:module`, drives the on-device overlay, and tracks workers. A strategy never re-implements those. @@ -49,12 +49,18 @@ Two rules shape the layout: "name": "@nativescript-community/vite-octane", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" }, - "./client": { "import": "./dist/client/strategy.js", "types": "./dist/client/strategy.d.ts" } + "./client": { + "import": "./dist/client/strategy.js", + "types": "./dist/client/strategy.d.ts" + } }, "nativescript": { "vite": { "flavor": "octane", - "config": { "import": "octaneConfig", "from": "@nativescript-community/vite-octane" } + "config": { + "import": "octaneConfig", + "from": "@nativescript-community/vite-octane" + } } }, "peerDependencies": { "@nativescript/vite": ">=8.0.0", "vite": "^8.0.0" } @@ -64,36 +70,51 @@ Two rules shape the layout: With that in place, `npx nativescript-vite init` in an app that depends on the package generates: ```ts -import { defineConfig } from 'vite'; -import { octaneConfig } from '@nativescript-community/vite-octane'; +import { defineConfig } from 'vite' +import { octaneConfig } from '@nativescript-community/vite-octane' -export default defineConfig(({ mode }) => octaneConfig({ mode })); +export default defineConfig(({ mode }) => octaneConfig({ mode })) ``` ## 1. The config helper ```ts // src/index.ts -import { mergeConfig, type UserConfig } from 'vite'; -import { baseConfig, getTypeCheckPlugins, registerFrameworkFlavor, type TypeCheckControlOptions } from '@nativescript/vite/framework'; -import { octane, type OctanePluginOptions } from '@octanejs/vite-plugin'; -import { octaneServerStrategy } from './server/strategy.js'; +import { mergeConfig, type UserConfig } from 'vite' +import { + baseConfig, + getTypeCheckPlugins, + registerFrameworkFlavor, + type TypeCheckControlOptions, +} from '@nativescript/vite/framework' +import { octane, type OctanePluginOptions } from '@octanejs/vite-plugin' +import { octaneServerStrategy } from './server/strategy.js' -registerFrameworkFlavor({ flavor: 'octane', server: octaneServerStrategy, client: '@nativescript-community/vite-octane/client' }); +registerFrameworkFlavor({ + flavor: 'octane', + server: octaneServerStrategy, + client: '@nativescript-community/vite-octane/client', +}) export interface OctaneConfigOptions extends TypeCheckControlOptions { - octane?: OctanePluginOptions; + octane?: OctanePluginOptions } -export const octaneConfig = ({ mode }: { mode: string }, options: OctaneConfigOptions = {}): UserConfig => +export const octaneConfig = ( + { mode }: { mode: string }, + options: OctaneConfigOptions = {}, +): UserConfig => mergeConfig(baseConfig({ mode, flavor: 'octane' }), { - plugins: [...getTypeCheckPlugins('typescript', options.typeCheck), ...octane(options.octane)], - }); + plugins: [ + ...getTypeCheckPlugins('typescript', options.typeCheck), + ...octane(options.octane), + ], + }) ``` Register at module scope, before `baseConfig` can run. `baseConfig` installs the HMR plugins for the declared flavor and looks the server strategy up by name; it also seeds the device bundle with the client module's path (`__NS_CLIENT_STRATEGY_URL__`) so the on-device loader can fetch it without knowing your package. -`getTypeCheckPlugins` takes the *kind* of type-check, not the flavor name: `'typescript'` for a `.ts`/`.tsx` project, `'vue'` for `vue-tsc`. +`getTypeCheckPlugins` takes the _kind_ of type-check, not the flavor name: `'typescript'` for a `.ts`/`.tsx` project, `'vue'` for `vue-tsc`. ## 2. The server strategy @@ -101,45 +122,67 @@ Most frameworks need nothing new on the server. `typescriptServerStrategy` is th ```ts // src/server/strategy.ts -import * as path from 'node:path'; -import { purgeTransformCachesForHotUpdate, runHotUpdatePrologue, typescriptServerStrategy, type FrameworkServerStrategy } from '@nativescript/vite/framework'; +import * as path from 'node:path' +import { + purgeTransformCachesForHotUpdate, + runHotUpdatePrologue, + typescriptServerStrategy, + type FrameworkServerStrategy, +} from '@nativescript/vite/framework' -const SCRIPT_FILE_RE = /\.(?:[mc]?[jt]sx?)$/i; +const SCRIPT_FILE_RE = /\.(?:[mc]?[jt]sx?)$/i export const octaneServerStrategy: FrameworkServerStrategy = { ...typescriptServerStrategy, flavor: 'octane', deferDeltaBroadcast: true, async handleHotUpdate(ctx, deps) { - const state = await runHotUpdatePrologue(ctx, deps); - if (!state) return; - const { root, metrics, emitSummary } = state; - const { moduleGraph, verbose, sharedTransformRequest } = deps; - const { file, server } = ctx; - if (!SCRIPT_FILE_RE.test(file)) return emitSummary(); - metrics.tAfterFramework = Date.now(); - - const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/'); - const id = moduleGraph.normalizeGraphId(rel); - if (!moduleGraph.get(id)) moduleGraph.upsert(rel, `/* hmr ${Date.now()} */`, [], { emitDeltaOnInsert: true }); + const state = await runHotUpdatePrologue(ctx, deps) + if (!state) return + const { root, metrics, emitSummary } = state + const { moduleGraph, verbose, sharedTransformRequest } = deps + const { file, server } = ctx + if (!SCRIPT_FILE_RE.test(file)) return emitSummary() + metrics.tAfterFramework = Date.now() + + const rel = + '/' + + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/') + const id = moduleGraph.normalizeGraphId(rel) + if (!moduleGraph.get(id)) + moduleGraph.upsert(rel, `/* hmr ${Date.now()} */`, [], { + emitDeltaOnInsert: true, + }) // The device applies an edit by re-fetching the module. Purge BEFORE the // delta goes out, or the re-fetch is served from the previous save. - purgeTransformCachesForHotUpdate({ file, server, sharedTransformRequest, verbose, label: 'octane' }); - const fresh = await sharedTransformRequest(rel, 30000); + purgeTransformCachesForHotUpdate({ + file, + server, + sharedTransformRequest, + verbose, + label: 'octane', + }) + const fresh = await sharedTransformRequest(rel, 30000) if (fresh?.code) { - const mod = server.moduleGraph.getModuleById(file); - const deps = mod ? Array.from(mod.importedModules).map((m) => (m.id || '').replace(/\?.*$/, '')).filter(Boolean) : moduleGraph.get(id)?.deps ?? []; - moduleGraph.upsert(id, fresh.code, deps as string[], { broadcastDelta: false }); + const mod = server.moduleGraph.getModuleById(file) + const deps = mod + ? Array.from(mod.importedModules) + .map((m) => (m.id || '').replace(/\?.*$/, '')) + .filter(Boolean) + : (moduleGraph.get(id)?.deps ?? []) + moduleGraph.upsert(id, fresh.code, deps as string[], { + broadcastDelta: false, + }) } - const gm = moduleGraph.get(id); - if (gm) moduleGraph.emitDelta([gm], []); - emitSummary(); + const gm = moduleGraph.get(id) + if (gm) moduleGraph.emitDelta([gm], []) + emitSummary() }, -}; +} ``` -`deferDeltaBroadcast: true` is the contract that makes the purge-then-broadcast order hold: the shared prologue records the change but leaves the broadcast to you. Read the module's dependency edges *after* the re-transform — import analysis runs inside it — so a newly added import reaches the client graph on the save that introduced it. +`deferDeltaBroadcast: true` is the contract that makes the purge-then-broadcast order hold: the shared prologue records the change but leaves the broadcast to you. Read the module's dependency edges _after_ the re-transform — import analysis runs inside it — so a newly added import reaches the client graph on the save that introduced it. Other optional members of `FrameworkServerStrategy` cover the less common needs: `transformNodeModule` (patch a vendor module before it is served), `rewriteServedModule`, `registerRoutes` (framework-owned dev endpoints), `importMapEntries`, `volatilePatterns`, `handleClientCustomEvent` (a `hot.send` from the device). The interface is documented inline in `hmr/server/framework-strategy.ts`. @@ -149,34 +192,44 @@ This is where a framework's HMR semantics live. The shared queue does, for every ```ts // src/client/strategy.ts — served to the device as-is -import type { FrameworkClientStrategy } from '@nativescript/vite/hmr/client/framework.js'; -import { getNsHotRegistry, graph, setUpdateStage } from '@nativescript/vite/hmr/client/framework.js'; +import type { FrameworkClientStrategy } from '@nativescript/vite/hmr/client/framework.js' +import { + getNsHotRegistry, + graph, + setUpdateStage, +} from '@nativescript/vite/hmr/client/framework.js' export const octaneClientStrategy: FrameworkClientStrategy = { flavor: 'octane', drivesQueueOverlayStages: true, install() {}, - beforeBatchEvict(drained) { /* snapshot accept callbacks, run dispose */ }, - afterModuleReimport(id, namespace) { /* fire accept with the fresh namespace */ }, - async refreshAfterBatch(drained, ctx) { /* propagate what nothing accepted; finish the overlay */ }, -}; -export default octaneClientStrategy; + beforeBatchEvict(drained) { + /* snapshot accept callbacks, run dispose */ + }, + afterModuleReimport(id, namespace) { + /* fire accept with the fresh namespace */ + }, + async refreshAfterBatch(drained, ctx) { + /* propagate what nothing accepted; finish the overlay */ + }, +} +export default octaneClientStrategy ``` Export it as `default`, `clientStrategy`, or `ClientStrategy`. ### Hooks, in the order they run -| Hook | When | Typical use | -| --- | --- | --- | -| `install()` | once, when the client resolves the strategy | dev shims, event listeners | -| `shouldQueueReimport(id)` | per changed id, before anything is fetched | return `false` for a module that must not evaluate in this realm (a worker script, a type-only module) or that nothing can accept | -| `applyUnqueuedChanges(ids)` | once, for the ids declined above | fire dependency acceptors; request a graph reload | -| `beforeBatchEvict(drained)` | once, before eviction | the last moment the outgoing module instances are reachable: read their `hot.accept` callbacks, drain `hot.dispose` | -| `afterModuleReimport(id, namespace)` | per re-imported module | invoke the captured accept callbacks with the fresh namespace | -| `refreshAfterBatch(drained, ctx)` | once, after the drain | walk the reverse graph for modules nothing accepted; `ctx.setUpdateOverlayStage('complete', …)` | -| `handleGraphResync(changedIds)` | a full graph with drifted hashes (dev-server restart) | return `true` after requesting one ordered reload instead of the piecemeal default | -| `handleHotUpdateMessage(msg, ctx)` | any protocol message the shared dispatcher did not consume | framework-specific messages a server strategy broadcasts | +| Hook | When | Typical use | +| ------------------------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `install()` | once, when the client resolves the strategy | dev shims, event listeners | +| `shouldQueueReimport(id)` | per changed id, before anything is fetched | return `false` for a module that must not evaluate in this realm (a worker script, a type-only module) or that nothing can accept | +| `applyUnqueuedChanges(ids)` | once, for the ids declined above | fire dependency acceptors; request a graph reload | +| `beforeBatchEvict(drained)` | once, before eviction | the last moment the outgoing module instances are reachable: read their `hot.accept` callbacks, drain `hot.dispose` | +| `afterModuleReimport(id, namespace)` | per re-imported module | invoke the captured accept callbacks with the fresh namespace | +| `refreshAfterBatch(drained, ctx)` | once, after the drain | walk the reverse graph for modules nothing accepted; `ctx.setUpdateOverlayStage('complete', …)` | +| `handleGraphResync(changedIds)` | a full graph with drifted hashes (dev-server restart) | return `true` after requesting one ordered reload instead of the piecemeal default | +| `handleHotUpdateMessage(msg, ctx)` | any protocol message the shared dispatcher did not consume | framework-specific messages a server strategy broadcasts | `ctx.graph` (and the `graph` export) is the live mirror of the server's module graph: `Map`. It is what makes "who imports the changed module" answerable on the device. @@ -184,15 +237,15 @@ Export it as `default`, `clientStrategy`, or `ClientStrategy`. `getNsHotRegistry()` is the process-wide `import.meta.hot` implementation — every served app module gets a context from it, keyed by canonical id (`/src/app`, extensionless). The members a strategy uses: -| Member | Purpose | -| --- | --- | -| `getAcceptCallbacks(key)` | the self-accept callbacks registered by the key's **current** evaluation (a copy); non-empty = self-accepting boundary | -| `getDepAcceptors(depKey)` / `acceptsDep(owner, dep)` | the dependency form — `hot.accept('./worker', cb)` — resolved relative to the owner; lets an update reach a module that never imported the changed file | -| `runDispose(keys)` / `runPrune(keys)` | drain `hot.dispose` / `hot.prune` | -| `hasDeclined(keys)` | any `hot.decline()` | -| `createHotContext(id).data` | persists across re-evaluations of the same key | -| `dispatchHotEvent(event, payload)` / `createHotContext(id).on(event, cb)` | custom events; the shared client emits `vite:beforeFullReload`, `ns:full-reload-complete`, `ns:full-reload-failed` | -| `requestFullReload(reason)` | the in-process graph reload: drains dispose, evicts every app-owned module (never `@nativescript/core` or vendor), re-imports the entry | +| Member | Purpose | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `getAcceptCallbacks(key)` | the self-accept callbacks registered by the key's **current** evaluation (a copy); non-empty = self-accepting boundary | +| `getDepAcceptors(depKey)` / `acceptsDep(owner, dep)` | the dependency form — `hot.accept('./worker', cb)` — resolved relative to the owner; lets an update reach a module that never imported the changed file | +| `runDispose(keys)` / `runPrune(keys)` | drain `hot.dispose` / `hot.prune` | +| `hasDeclined(keys)` | any `hot.decline()` | +| `createHotContext(id).data` | persists across re-evaluations of the same key | +| `dispatchHotEvent(event, payload)` / `createHotContext(id).on(event, cb)` | custom events; the shared client emits `vite:beforeFullReload`, `ns:full-reload-complete`, `ns:full-reload-failed` | +| `requestFullReload(reason)` | the in-process graph reload: drains dispose, evicts every app-owned module (never `@nativescript/core` or vendor), re-imports the entry | One subtlety worth knowing before you write `afterModuleReimport`: the registry holds the callbacks of the **latest** evaluation (Vite's semantics). If your framework's accept callback closes over a module-local binding — as Octane's does — the callback that can reach the live instances is the **first** evaluation's, and you must keep firing that one. If it consults a global registry keyed by component id (Vue, React Refresh, solid-refresh), the latest is equivalent. The Octane strategy's `anchors` map is the reference for the former. @@ -209,15 +262,15 @@ The client module is served at `/ns/m/node_modules//` — the reg Keep the decision logic pure. Octane's reverse-graph walk is a function `(changedIds, graph, { acceptsSelf, acceptsDep }) → { boundaries, evict, dead }` with a dozen cases that run in milliseconds. The strategy itself is testable against the real hot registry: ```ts -import { getNsHotRegistry } from '@nativescript/vite/hmr/client/framework.js'; -import { octaneClientStrategy } from './strategy.js'; - -const hot = getNsHotRegistry().createHotContext('/src/app.tsx'); -hot.accept(gen1); -octaneClientStrategy.beforeBatchEvict!(['/src/app.tsx']); -getNsHotRegistry().createHotContext('/src/app.tsx').accept(gen2); // the fresh evaluation -octaneClientStrategy.afterModuleReimport!('/src/app.tsx', { App: 'v2' }); -expect(gen1).toHaveBeenCalledWith({ App: 'v2' }); +import { getNsHotRegistry } from '@nativescript/vite/hmr/client/framework.js' +import { octaneClientStrategy } from './strategy.js' + +const hot = getNsHotRegistry().createHotContext('/src/app.tsx') +hot.accept(gen1) +octaneClientStrategy.beforeBatchEvict!(['/src/app.tsx']) +getNsHotRegistry().createHotContext('/src/app.tsx').accept(gen2) // the fresh evaluation +octaneClientStrategy.afterModuleReimport!('/src/app.tsx', { App: 'v2' }) +expect(gen1).toHaveBeenCalledWith({ App: 'v2' }) ``` On a device, five saves cover the matrix: a component, a plain dependency, a worker script, a registry-style module that accepts itself, and the entry. diff --git a/content/configuration/vite.md b/content/configuration/vite.md index ff666005..9845ff67 100644 --- a/content/configuration/vite.md +++ b/content/configuration/vite.md @@ -588,6 +588,7 @@ npm i -D @vitejs/plugin-vue-jsx vue-tsc **Problem:** New type errors during the Vite build: `Conversion of type 'UIView' to type 'View'` in `.android.ts` files, `Cannot find module ''`, or plugin options reported as unknown properties. **Solution:** The build runs `vue-tsc --moduleSuffixes .ios,.native,` (or `.android`) over the whole project. So: + - **Other-platform files get the wrong typings.** `nativeViewProtected` is `UIView` inside an `.android.ts` file. Use `this.nativeView` (typed `any`) for platform casts. - **Some plugin typings stop resolving.** A `typings` field naming a missing file, or `import from '../x'` landing on `x.ios.d.ts`, can break. Fix with an ambient `declare module` or a module augmentation in your `types/` folder. - **`.vue` templates are checked for the first time.** @@ -601,6 +602,7 @@ To defer the check: `vueConfig({ mode }, { typeCheck: 'warn' | 'off' })`, or `NS **Problem:** The app dies at launch with `Error instantiating module …/bundle.mjs`, after `module graph walk missed http://:5173/…`. **Solution:** A dev build has the dev server URL baked in, and the device couldn't reach it. The simulator log (`xcrun simctl spawn log show --predicate 'process == ""'`) shows `Connection refused` when nothing is listening. The Vite server only lives as long as the `ns run` / `ns debug` session: + - Launch from a running `ns run`/`ns debug` session, not by relaunching the app afterwards. - Check the CLI output for `Vite dev server for ios exited with code …`. - Confirm the server is reachable: `curl http://:5173/ns/core/xhr` should return 200. @@ -611,6 +613,7 @@ To defer the check: `vueConfig({ mode }, { typeCheck: 'warn' | 'off' })`, or `NS ### 4. Node-only packages in `dependencies` crash the dev session **Problem:** `[ns-entry] deterministic dev session bootstrap failed`, with a stack through `/ns/deps-bundle.mjs`. Examples: + - `Cannot read properties of undefined (reading 'substr')` in `graceful-fs` ← `fs-extra` ← `patch-package` - `Cannot read properties of undefined (reading 'custom')` in `object-inspect` ← `qs` ← `url` @@ -637,9 +640,13 @@ Then run `npm install`, confirm with `npm ls vue`, and clear `node_modules/.vite **Problem:** `viewClass is not a constructor` when an element renders. Or a lazy `require('../some-module')` in app code yields `undefined` exports. **Solution:** Webpack resolved `require` at build time. Vite leaves it as a runtime call, and a **relative** path can't be resolved on the device, so it returns `{}`. For example, `@nativescript/google-mobile-ads/vue` registers its views with `require('../').BannerAd`. Bare package names like `require('')` do resolve. + - **Plugins:** skip the plugin's Vue install and register its elements yourself: ```ts - registerElement('BannerAd', () => require('@nativescript/google-mobile-ads').BannerAd) + registerElement( + 'BannerAd', + () => require('@nativescript/google-mobile-ads').BannerAd, + ) ``` - **App code:** replace lazy relative requires (often used to break import cycles) with `import('../module')`.