Skip to content
37 changes: 37 additions & 0 deletions content/guide/pathways.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: Two pathways
description: NativeScript is your development runtime, not your production runtime. Write SwiftUI and develop it live, or write TypeScript and compile the release to native code.
contributors:
- NathanWalker
---

NativeScript has always run your app on a JavaScript runtime with direct access to every platform API. It now offers
two pathways that keep that development experience and ship **no JavaScript runtime at all**:

| | 1. SwiftUI, live | 2. TypeScript, compiled |
| --- | --- | --- |
| **You write** | SwiftUI, in an Xcode project | TypeScript with `@nativescript/core` and the framework you like (Angular, Vue, React, Svelte, Solid, plain TypeScript) |
| **While developing** | NativeScript runs your SwiftUI views live: each save is on screen in about 0.15–0.5 s, state kept | `ns run`: the JavaScript runtime with HMR, Chrome DevTools and the whole npm ecosystem, exactly as today |
| **What ships** | Your Swift, built by Xcode as usual | Your app compiled to Swift (iOS) and Kotlin (Android) by `ns build --compiled` |
| **Platforms** | iOS; Android live development, rendered by Jetpack Compose | iOS and Android |
| **Status** | [Preview](/guide/swiftui-live) | [Preview](/guide/publishing/compiled) |

Both rest on the same idea: **develop live, ship native.** The development runtime gives you instant feedback; the
release is the native code a platform developer would have shipped.

## Which one fits

- **You know SwiftUI, or you are starting an iOS-first app in Swift**: take [SwiftUI, live](/guide/swiftui-live).
You keep Xcode, Swift and your project as they are; NativeScript only shortens the edit-to-screen loop.
- **You know web development, or you already have a NativeScript app**: take
[TypeScript, compiled](/guide/publishing/compiled). Nothing changes while you develop. The release build is
compiled instead of bundled.

The two meet at the release: both ship native code with no engine inside, and both are checked the same way,
pixel by pixel against the build they replace.

## The JavaScript runtime is not going anywhere

A release on the JavaScript runtime (`ns build --release`) remains the default and is fully supported. Compiled
releases are opt-in, per build or per platform, and you can switch back with `--no-compiled` at any time. See
[Publishing](/guide/publishing/).
140 changes: 140 additions & 0 deletions content/guide/publishing/compiled-differences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: "Compiled releases: supported subset and known differences"
description: What a compiled release refuses to build, where it still behaves differently from the JavaScript release, and the versions it is tested with.
contributors:
- NathanWalker
---

<!-- Sourced from the compiler's adversarial review (October 2026) and its preflight rules (packages/compiler/src/preflight.ts). Update an entry when its fix lands. -->

A [compiled release](/guide/publishing/compiled) runs your TypeScript as Swift and Kotlin, against a runtime that
reproduces JavaScript's semantics. This page lists where that stops: what the build refuses, and the differences
known today between a compiled release and the JavaScript release of the same app. Each entry names the platform
it affects. Check every compiled release against its JavaScript release before you ship it
([Checking a compiled release](/guide/publishing/compiled#checking-a-compiled-release)).

## What the build refuses

Before anything is compiled, the compiler checks the app (and the plugin code the app reaches) and stops on each of
these with the file, the line and what to write instead. All of them are reported at once.

| Construct | Why | Instead |
| --- | --- | --- |
| `new Proxy(…)` | No traps without a JavaScript engine | A class with explicit getters and setters, or a `Map` for dynamic keys |
| `Reflect.*` | Not provided by the runtime | The operation itself: `obj[key]`, `key in obj`, `Object.keys(obj)`, `new Cls(...args)` |
| `eval(…)`, `new Function(…)` | There is no engine to evaluate code | A lookup of known functions; or keep this app on the JavaScript release |
| `FinalizationRegistry` | Finalizers do not run | Release the resource explicitly (`dispose()`, `unloaded`, `disposeNativeView`) |
| `SharedArrayBuffer`, `Atomics` | No shared memory between workers | An `ArrayBuffer`, with copies posted between workers |
| `with (obj) { … }` | | Name the object explicitly |
| `import(expr)`, `require(expr)` with a computed specifier | Every module is linked when the app is built | Import each module by a literal path, and choose among those at run time |
| `arguments.callee`, `__proto__` | | A named function; `Object.getPrototypeOf` |
| A bundler constant nothing defines (`declare const __API_URL__`, `process.env.X`) | It would read as undefined at run time | Define it in the bundler config: Vite's `define` and webpack's `DefinePlugin` values are read and compiled in |

The build also stops, before compiling, when:

- **The app's `@nativescript/core` is not the core the compiler's kit was generated from.** The kit is core's own
code, compiled; the native widgets it drives come from your installed core, so the two must be the same release.
Install the core version the message names, or the `@nativescript/compiler` released with your core.
`release: { allowCoreMismatch: true }` builds anyway, with a warning.
- **A framework package is outside the versions its support is tested with** (table below).
`release: { allowUntestedFrameworks: true }` builds anyway, with a warning.
- **The toolchain is missing or too old:** Xcode 26 or newer for iOS; JDK 17 or newer and the Android SDK for
Android.

Other unsupported language and framework constructs are refused by the compiler as it meets them, with the file and
line; see [What stops a build](/guide/publishing/compiled#what-stops-a-build).

## Tested framework versions

| Framework | Packages and versions |
| --- | --- |
| Vue | `nativescript-vue` 3.1 and later 3.x |
| Angular | `@nativescript/angular` and `@angular/core` 22.x |
| React | `react-nativescript` 5.x with `react` 18.2 or later 18.x |
| Solid | `@nativescript-community/solid-js` and `solid-js` 2.0 (from 2.0.0-rc.0) |
| Svelte | `svelte-native` 1.x with `svelte` 4.2+, or `svelte-native` 5 (from 5.0.0-alpha.0) with `svelte` 5.50+ |
| Octane | `@nativescript-community/octane` 0.2.4 and later 0.2.x |

Framework features each front end does not support yet stop the build with a message. The main ones:

- **Angular:** pipes other than `async`; a pipe inside `*ngFor`/`@for`; `OnPush` under zone.js; more than one
`@Component` in a file. Templates are parsed with the compiler's own `@angular/compiler` 22.
- **React:** a component body may hold function declarations, variable declarations and the `return`; a bare
statement such as `useEffect(() => …, [])` on its own line is refused. JSX spread attributes are refused.
- **Vue:** `<style module>`, and Options API keys beyond the common ones.

## Known differences at run time

These compile and run, and behave differently from the JavaScript release. They are being fixed; until then,
avoid relying on them.

### Values and types

- **`null` and `undefined` are one value in a typed optional** (both platforms). With `b?: string | null`, an
explicit `null` reads as `undefined`, disappears from `JSON.stringify`, and a destructuring default applies to it.
Code that tells `=== null` from `=== undefined` on such a field takes the wrong branch.
- **Fields of an `Error` subclass read through `any` or `unknown` are undefined** (both): `catch (e: any) { e.code }`.
On Android, also after `instanceof`.
- **A value of the wrong type in a typed slot can stop the app** instead of throwing a `TypeError` (iOS): an
untyped value (`any`, parsed JSON) assigned where a class or interface is declared, a downcast to the wrong
subclass, a definitely-assigned field (`x!: T`) read before it is set, an out-of-union value in an exhaustive
`switch`. Validate parsed data before typing it.
- **Number to integer conversions of NaN, Infinity or very large values can stop the app** (iOS), where JavaScript
gives 0 or wraps.
- **`valueOf()` is never called**: arithmetic on objects concatenates or gives NaN (both).
- **`typeof SomeClass` is `"object"`** (both), and `typeof new String('x')` is `"string"`.
- **Subclass fields are initialized before the base constructor runs** (both), so a base constructor that calls an
overridden method sees the subclass's fields already set.
- **`var` in a `for` loop gets a new binding per iteration** (both), as `let` does.

### Strings and regular expressions

- **`\d`, `\w`, `\b` and `.` follow Unicode rules** (iOS): `\d` matches Arabic-Indic digits, `\w` matches accented
letters.
- **A capture group that did not take part reads `''`**, not `undefined` (iOS).
- **`replace` with a string pattern ignores `$&`, `` $` ``, `$'` and `$$`** (iOS).
- **Cutting a string between the two halves of a surrogate pair gives U+FFFD** (iOS), so an emoji split and rejoined
is corrupted.
- **`str[i]` past the end is `''`**, not `undefined` (both).
- **`===` on strings compares by Unicode canonical equivalence** (iOS): `'é'` precomposed equals `'e'` + accent,
while `Map` and `Set` keys keep them apart.
- **`localeCompare` ignores its locale and options** (both). `lastIndexOf(x, fromIndex)` ignores `fromIndex`.

### Arrays, dates, JSON

- **Holes in a `number[]` read as NaN** (iOS). **`flat(depth)` ignores the depth** (iOS).
- **`Date.parse` accepts out-of-range ISO fields** and rolls them over (iOS).
- **`JSON.parse` ignores a reviver** (both).

### Functions, listeners and memory

- **A function read twice is not `===` to itself** (iOS), so `off(handler)` and `removeEventListener` do not remove
a listener, and the same handler added twice runs twice. Keep the subscription handle where an API returns one.
- **Closures stored on an object, and parent and child views, hold each other strongly** (iOS): a page navigated
away from may stay in memory.
- **`WeakMap` and `WeakSet` lack ephemeron semantics** (both): a value that reaches its key keeps both alive.
- **`WeakRef` timing differs**: on iOS a target can be freed within the job that created it; on Android a
`WeakRef` is a soft reference and survives ordinary collections.
- **Script entered from a native background thread is not serialized** with the main thread (both).

### Errors and crashes

- **Uncaught errors are printed, and `Application.on('uncaughtError')` does not fire** (both); the
`uncaughtErrorPolicy` setting is not read.
- **`error.stack`** lists the native frames the error was made in, not JavaScript frames: on iOS
`symbol (App+0xoffset)`, which the archive's dSYM resolves to your TypeScript lines; on Android
`class.method(File.kt:line)`, which `ns-native-retrace` maps to your TypeScript lines.
- **Deep recursion crashes** (iOS) instead of throwing a `RangeError`.

### Android release builds (R8)

Members reached by name at run time (a method called on an `any` value, `typeof x.getFoo`, `'getFoo' in x`) are
kept from R8's renaming by a scan of the compiled code. A name computed at run time (`x[name]()` with `name` built
from strings) is not found by the scan; keep such members with a ProGuard rule in
`App_Resources/Android/app.gradle`, and check the minified release on a device.

By default every class and member of the kit is kept whole, which leaves most of the kit's code to R8 untouched.
`android: { release: { narrowKitKeeps: true } }` keeps only what the kit's reflection needs (class names,
constructors, the names of the members that remain) and lets R8 remove the rest: on Recipes (Vue) the kit's dex goes
from 3.3 to 2.0 MB and the APK from 2.44 to 1.92 MB, with both screens matching the JavaScript release. It is opt-in
until it has been verified on larger apps; under it, a kit member reached only through a computed name is removed.
Loading
Loading