npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nativescript-community/octane

v0.2.5

Published

Octane on NativeScript

Readme

@nativescript-community/octane

Octane on NativeScript: the universal host driver that turns Octane's host commands into @nativescript/core views, the renderer ABI the Octane compiler targets, and the JSX typings for the NativeScript element vocabulary.

Octane ships octane/universal/native, a host-neutral runtime with no dependency on its DOM build. A renderer for it is a driver that applies host commands to a native view tree plus some compiler metadata; this package is that renderer for NativeScript.

| Entry | What it is | | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | @nativescript-community/octane | renderNativeScriptApp, createNativeScriptRoot, the driver and the element registry, plus the universal runtime: it is the compiler-facing renderer ABI | | @nativescript-community/octane/config | Node-safe renderer metadata for @octanejs/vite-plugin; @nativescript-community/vite-octane applies it | | @nativescript-community/octane/intrinsics | JSX element and attribute types, derived from the installed core view classes | | @nativescript-community/octane/jsx-runtime | What jsxImportSource resolves to |

Install

npm i @nativescript-community/octane octane
npm i -D @nativescript-community/vite-octane @octanejs/vite-plugin @nativescript/vite

@octanejs/vite-plugin declares the octane range it compiles for (0.1.52 pairs with octane 0.2.x), and the app must hold a single copy of octane: the package root re-exports octane/universal/native as the renderer ABI, and hooks only work against the runtime that owns the root.

Setup

vite.config.mts:

import { defineConfig } from 'vite';
import { octaneConfig } from '@nativescript-community/vite-octane';

export default defineConfig(({ mode }) => octaneConfig({ mode }));

octaneConfig compiles src/**/*.tsx and src/**/*.tsrx for this renderer by default. To change the scope, pass the registry yourself:

import { nativeScriptRenderers } from '@nativescript-community/octane/config';

octaneConfig(
  { mode },
  { octane: { renderers: nativeScriptRenderers({ include: 'app/**/*.tsx' }) } },
);

tsconfig.json:

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "@nativescript-community/octane"
  }
}

.tsrx modules

A TSRX component under src/ compiles for this renderer like a .tsx one. Type checking is the difference: the Octane compiler generates the TypeScript that tsrx-tsc and the TSRX language server check, and it names octane as the JSX import source unless the module names one itself, which types <label> and friends as DOM elements. Open every .tsrx module with the renderer's pragma so it is checked against the NativeScript intrinsics:

/** @jsxImportSource @nativescript-community/octane */
export function Greeting({ name }: { name: string }) @{
  <label text={`Hello, ${name}`} />
}

@nativescript/vite runs the build-time type check through tsrx-tsc when @tsrx/typescript-plugin is installed and a module imports .tsrx files, with the same platform file selection as the regular check.

Validation

The stock renderer carries a validation block the compiler enforces on the modules it owns: the DOM globals no NativeScript runtime provides (document, window, navigator, location, history, localStorage, sessionStorage, HTMLElement, Element, Node, DOMParser, MutationObserver, ResizeObserver, IntersectionObserver) and the DOM-side imports (octane/dom-bindings, octane/dom-binding-program, octane/hydration, react-dom, each covering its subpaths). A .tsx that reaches for one fails to compile with the file and line, instead of failing — or silently doing nothing — on device. What core polyfills stays allowed: fetch, XMLHttpRequest, alert, confirm, matchMedia, requestAnimationFrame, crypto, TextEncoder, Blob, FormData, and so on, as does WebSocket, which apps polyfill.

Two limits to know: the check covers the modules a renderer rule matches, and a rule can only select .tsx and .tsrx for this renderer, so a plain .ts helper is not checked; and the check is static, so a computed globalThis['document'] passes. Each list given to nativeScriptRenderers replaces the default one, and false turns the check off:

import {
  nativeScriptRendererValidation,
  nativeScriptRenderers,
} from '@nativescript-community/octane/config';

nativeScriptRenderers({
  validation: {
    forbiddenGlobals: [
      ...nativeScriptRendererValidation.forbiddenGlobals,
      'CustomEvent',
    ],
  },
});
nativeScriptRenderers({ validation: false });

The entry mounts a root into a view the app owns. One root per window keeps a second iPad scene or a CarPlay window on the same component wrapper, so a hot update reaches all of them:

import { Application, Page, type NativeWindow } from '@nativescript/core';
import { renderNativeScriptApp } from '@nativescript-community/octane';
import { App } from './app';

const roots = new Map<
  NativeWindow | undefined,
  ReturnType<typeof renderNativeScriptApp>
>();

function createWindowContent(window: NativeWindow | undefined): Page {
  const page = new Page();
  page.actionBarHidden = true;
  roots.set(window, renderNativeScriptApp(page, App));
  return page;
}

Application.setWindowContentResolver(({ window, isPrimary }) =>
  isPrimary ? undefined : createWindowContent(window),
);
Application.on('windowClose', ({ window }) => {
  roots.get(window)?.unmount();
  roots.delete(window);
});
Application.run({
  create: () => createWindowContent(Application.primaryWindow),
});

Components are ordinary Octane components over lowercase NativeScript tags:

import { useState } from 'octane';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <stacklayout className="p-4">
      <label className="text-xl">Count {count}</label>
      <button text="Add one" onTap={() => setCount(count + 1)} />
    </stacklayout>
  );
}

The element vocabulary

Tag names are the lowercase class names of @nativescript/core: absolutelayout, actionbar, actionitem, activityindicator, button, contentview, datepicker, docklayout, flexboxlayout, formattedstring, frame, gridlayout, htmlview, image, label, liquidglass, listpicker, listview, navigationbutton, page, placeholder, progress, proxyviewcontainer, rootlayout, scrollview, searchbar, segmentedbar, segmentedbaritem, slider, span, stacklayout, switch, tabview, tabviewitem, textfield, textview, timepicker, webview, wraplayout. Unknown or camelCase names are a type error, not a blank screen.

Props are view properties, not attributes: the driver assigns them onto the instance, so anything a view class exposes (row, colSpan, iosOverflowSafeArea, ...) is a prop. className and a string style go through the CSS system; an object style is assigned onto view.style.

Handlers are on + the NativeScript event name (onLoaded, onItemTap), with the web spellings aliased: onTap/onClick/onPress → tap, onDoubleTap, onLongPress, onChange → textChange, onSubmit → returnPress.

Lists

listview recycles a fixed pool of native cells, so its rows are not children of the element. Give it renderItem and the driver owns the cells: each one hosts its own Octane root, bound to items[index] when the table asks for the row and rebound in place — diffed, not remounted — when the cell is recycled for another row, when items changes, or when renderItem changes identity.

<listview
  items={rows}
  renderItem={(row, index) => <Row row={row} selected={index === selected} />}
  onItemTap={(e) => setSelected(e.index)}
/>
  • The value passed is always the current items[index] — a plain array, an ItemsSource, or an ObservableArray — read when the cell is bound, never a snapshot from an earlier wave.
  • A new items array reloads the table (core's items property does that), as does mutating an ObservableArray. NativeScript re-prepares every visible cell on each layout pass; a cell whose row, item and renderItem are unchanged is skipped.
  • A closure over parent state — the selected above — reaches the rows because a changed renderItem re-renders the live cells; memoize it if the parent renders often for reasons the rows do not care about.
  • Hooks inside renderItem belong to the cell, not the row: a recycled cell keeps its state across rows, so key row state by the item.
  • renderItem takes over itemTemplate. onItemLoading still fires after the cell is bound, but must not assign args.view.
  • Sectioned lists and itemTemplateSelector are not managed; use them with plain NativeScript templates.

Plugin views

Register a tag for any ViewBase subclass, and extend the JSX types from a .d.ts in the app:

import { registerElement } from '@nativescript-community/octane';
import { Drawer } from '@nativescript-community/ui-drawer';

registerElement('drawer', Drawer);

declare module '@nativescript-community/octane/intrinsics' {
  interface NativeScriptElements {
    drawer: Attributes<typeof Drawer>;
  }
}

CommonAttributes is the place for attributes a plugin adds to every view (a menu prop, say). Views whose parent wires children through properties rather than addChild (a drawer's mainContent and leftDrawer) take hostSlot="mainContent", and the driver assigns the property instead of inserting.

Registering a tag that is already registered with a different class recreates every live instance of the tag in place, keeping props, listeners, children and position. That is what lets a hot-reloaded element module reach a running app without a remount, so keep registrations in a module that accepts its own updates:

import.meta.hot?.accept();

Driver contract

Details of the host contract that are easy to get wrong, each learned by watching a subtree vanish:

  • Text is a host node. Octane lowers <label>Hi {name}</label> to #text children; the driver folds them into the parent's text and keeps text="..." working alongside.
  • update merges. An update carries the host's dynamic-prop snapshot; attributes that were static in the compiled plan arrive only with create. Replacing would strip a node's static layout props on its first update.
  • A prop write does not echo as its change event. Core raises textChange, checkedChange, selectedIndexChange, ... for a script write exactly as for a user edit. The driver mutes the paired event while it applies a prop, so a controlled <textfield text={value} onChange={...} /> receives onChange only for edits it did not make itself. (Writing an unchanged value is already a no-op in core.)
  • Events during a commit are deferred. Attaching a subtree fires loaded synchronously, inside the batch that registered the listener, before the listener is live. The driver defers those to a microtask and drops a dispatch to a listener that is gone rather than aborting the batch.
  • Detaching is defensive. LayoutBase.removeChild throws for a view that is not attached, and insert detaches unconditionally, so the driver checks parent first.
  • Hidden means collapse. A visibility command maps hidden to NativeScript's collapse, which removes the view from layout as well as from the screen.
  • Portals render into the target view. createPortal(children, view) requires a LayoutBase — typically a rootlayout for overlays — and inserts the children there in declaration order. The content stays owned by its declaring Octane root: it keeps that root's context and re-renders with it. The target view itself may be caller-owned (like getRootLayout()); the driver never reparents or destroys it, and a view created by a different Octane root is rejected. Target references continue to resolve to the current view after element hot replacement, including when a portal closes and reopens. Portals are placement, not context inheritance — a portal into a caller-owned view does not share context with any other root mounted into it.

Peer versions

  • octane >= 0.1.51 (the universal runtime API this driver targets; unchanged through 0.2.2), compiled by @octanejs/vite-plugin >= 0.1.51.
  • @nativescript/core >= 9.1.0.