solid-nativescript-js
v0.0.1
Published
Zero-overhead SolidJS <-> NativeScript bridge. PascalCase elements mapped 1:1 to @nativescript/core classes — no DOM emulation layer.
Maintainers
Readme
solid-nativescript-js
Zero-overhead SolidJS ↔ NativeScript bridge. JSX tags written in their
original PascalCase (<StackLayout>, <ActionBar>, <Label>, ...) are
matched 1:1 to their @nativescript/core class constructors — no
document, no dominative, no undom-ng, and the tag string is never
lowercased anywhere on the path.
import { run, Frame, Page, ActionBar, StackLayout, Label, Button, createSignal } from 'solid-nativescript-js'
const Home = () => {
const [count, setCount] = createSignal(0)
return (
<>
<ActionBar title="App" />
<StackLayout>
<Label text="Clean Code" />
<Button text="Tap me" onTap={() => setCount(c => c + 1)} />
</StackLayout>
</>
)
}
run(() => (
<Frame>
<Page><Home /></Page>
</Frame>
))Why this package exists
The legacy @nativescript-community/solid-js stack renders through
dominative + undom-ng, a full web-DOM emulation layer on top of
NativeScript. This package replaces that stack with a lean universal
reconciler: SolidJS's own createRenderer (from solid-js/universal)
drives diffing and effect-based updates; this package supplies native
operations that talk directly to @nativescript/core views.
Architecture
| Module | Responsibility |
| ----------------- | ------------------------------------------------------------------------------ |
| src/registry.ts | PascalCase tag → core constructor map (+ registerElement for plugin views). |
| src/renderer.ts | createRenderer wiring: createElement/insert/remove/setText on native views. |
| src/props.ts | Attribute binding: splits onTap/on:tap events from props, class, style, ref, android:/ios: prefixes. |
| src/nodes.ts | Per-container-kind child insertion (layouts, content slots, ActionBar, FormattedString, TabView, Frame navigation) + recursive destroyView teardown. |
| src/listeners.ts| Strict event-lifecycle tracking: every .on() the bridge registers is tracked so teardown can always unbind it. Leak audits via activeListenerCount() / getGlobalListenerReport(). |
| src/component.ts+ src/elements.ts | Typed components for every built-in view. TypeScript resolves UPPERCASE JSX tags as values, so each view is exported as a component that instantiates the native class via the registry. Generated from the nativescript-dom/types workspace (scripts/generate-pascal-types.py). |
| src/app.ts | run() / createAppRoot(): root context, view-tree instantiation, anchoring into Application.run, full disposal. |
| src/vite.ts | solidNativeScript() Vite plugin: universal-mode Solid compiler (moduleName: 'solid-nativescript-js') + solid-js aliasing (client dists only; solid-js/web rerouted to this bridge). |
Memory management
NativeScript bridges V8/JSC to Java/Objective-C, so orphaned closures leak across runtimes. The bridge therefore:
- tracks every event listener it binds (
listeners.ts), with optional bind/unbind logging viasetListenerLogging(true); - removes a node ⇒ recursively destroys it: unbinds all tracked listeners,
detaches/destroys children, nulls bridge back-references, empties container
slots and calls
disposeNativeView()so platform GC can reclaim memory; - disposes the Solid root and tears down the whole tree via the disposer
returned by
render()/createAppRoot()(anddisposeCurrentApp()).
Usage
Install
npm i solid-nativescript-js solid-js @nativescript/core
npm i -D @nativescript/vite vite vite-plugin-solid \
@nativescript-dom/core-types @nativescript-dom/solidjs-typesvite.config.mts
import { defineConfig, mergeConfig } from 'vite'
import { baseConfig } from '@nativescript/vite/base'
import { solidNativeScript } from 'solid-nativescript-js/vite'
export default defineConfig(({ mode }) =>
mergeConfig(baseConfig({ mode, flavor: 'solid' }), {
plugins: [solidNativeScript({ mode })],
})
)nativescript.config.ts
export default {
// ...
bundler: 'vite',
bundlerConfigPath: 'vite.config.mts',
} as NativeScriptConfigTypings
Set the JSX type source to the official nativescript-dom typings and import your elements from this package (TypeScript resolves uppercase JSX tags as component values):
// tsconfig.json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@nativescript-dom/solidjs-types",
"types": ["@nativescript-dom/core-types", "@nativescript-dom/solidjs-types"]
}
}Events
onTap={fn}→view.on('tap', fn)(the char afteronis lowercased:onTextChange→textChange).on:tap={fn}binds the raw event name verbatim.- Reassignment unbinds the previous handler; node removal unbinds everything.
Plugin views
import { registerElement } from 'solid-nativescript-js'
import { RadSideDrawer } from '@nativescript-community/ui-drawer'
registerElement('RadSideDrawer', RadSideDrawer, 'content')Development
bun install # deps
bun test # unit/integration suite (bun, mocked @nativescript/core, real solid-js)
bun run typecheck # strict TS
bun run build # emit dist/ (ESM + d.ts)
python3 scripts/generate-pascal-types.py # regenerate src/elements.ts from types/Tests mock @nativescript/core but run the real solid-js universal
renderer, so reconciliation, moves, keyed lists and disposal are covered
end-to-end.
