npm.io
0.1.2 • Published 23h ago

@solidtv/nativescript

Licence
Apache-2.0
Version
0.1.2
Deps
4
Size
189 kB
Vulns
0
Weekly
0

@solidtv/nativescript

SolidTV on NativeScript: what an app needs to run @solidtv/renderer and @solidtv/solid on Apple TV and iOS without a browser. NativeScript is the runtime host, @nativescript/canvas the WebGL surface, and @nativescript/canvas-polyfill the browser-shaped environment the renderer expects. The plan, its phases and their outcomes are in docs/nativescript-integration-plan.md.

Status: phases 0 to 3 of the plan run on the iPhone and Apple TV simulators (2026-09-17): the demo, the Siri Remote driven by real presses, and the suspend and resume cycle. The same day all of it ran on a physical Apple TV HD too: the exit test passed there, a looping animation survived a minute in the background, and the measurement settled the WebGL decision below. Only the App Store Connect export is open, for want of a paid team. Phase 4 (2026-09-17) runs the public demo app on the Apple TV simulator from its own repository, built from the same source as its web build, with one Back rule added to that source and nothing else: the shims and the platform below carry every seam. See docs/phase4-results.md; the device run there waits on a provisioning profile for the demo's bundle id.

What is here

  • src/, the package:
    • TvPlatform: the renderer Platform for this runtime. Sizes the canvas view's GL surface without touching its layout; keeps the web clock, which the runtime's requestAnimationFrame shares; hands the renderer the Canvas view bindCanvas registered and a target that swallows the append, so an app's own createRenderer() needs neither; and through the renderer's resolveSettings hook corrects the settings the host cannot honour: WebGL2, no image workers, both startup probes skipped, no inspector.
    • bindCanvas(view): registers the Canvas view and makes TvPlatform the renderer's default platform, for an app written for the browser and booted here unchanged.
    • rendererSettings(canvas, screen) and stubTarget: the same as explicit renderer options, for an app that passes them itself as the harness does: 1920x1080 logical, SDF text, the platform's own settings (WebGL2, no image workers, both startup probes skipped, no inspector), and a target that keeps the renderer off document. WebGL2 because the plugin's WebGL1 context has no vertex array objects: 137 GL calls per frame against 32, on an Apple TV HD as on the simulators, at the same 60 fps. Spread the result to override a setting, but not one of the platform's: TvPlatform applies those over whatever the settings say, so changing one takes a subclass that overrides resolveSettings, passed as the platform, which is how the harness runs its WebGL1 comparison.
    • KeyBridge: turns native presses into keydown and keyup events for a focus manager, repeats a held key keyboard style, and is itself the event target: useFocusManager(undefined, bridge). Given a target, it also dispatches every event there: document, for a focus manager that listens where a browser app's does.
    • bindRemote(bridge, Application.ios.window, options?): the Siri Remote into the bridge on tvOS. A recognizer per button on the root view controller's view and four swipe recognizers. A press a keydown listener handled with preventDefault() is the app's; an unhandled Menu goes to the system and leaves the app. Swipes on the touch surface are not supported: they need not work, the swipe recognizers are a courtesy left unverified, and nothing depends on them.
    • bindLifecycle(renderer, bridge, Application): on suspend, releases the bridge's held keys and pauses the renderer at once; on resume, resumes it. The renderer may be a function returning it, for an app that creates its renderer after the host has bound the lifecycle.
    • mirrorConsole(file, options?): every console line into a file in the app's container, written once a second and on suspend and exit, for a device whose console the CLI cannot stream.
    • @solidtv/nativescript/shims: the browser globals the polyfill lacks or installs unusably, so an app written for the browser boots unchanged. location as a file: URL into the app folder, which is what makes a relative asset URL a file in it; a history with a real stack that raises hashchange, for a HashRouter and history.back(); window.dispatchEvent; a document whose listeners work; fixes to XMLHttpRequest for a file-backed json or text response, the runtime's file:///app/ root, a JSON response asked for as a blob (what fetch asks for) and a request with no response type; and console.table. Import it right after the polyfill. The polyfill bugs behind each are listed in docs/phase4-results.md.
  • webpack.cjs, @solidtv/nativescript/webpack: chainSolidTV, the Solid JSX rule, browser export conditions, .js imports resolved to .ts sources, the polyfill's optional-module ignore, and on request the hex-colour transform of @lightningtv/vite-hex-transform as a loader (hexColorLoader.cjs), aliases and import.meta.env members, for a @nativescript/webpack config.
  • harness/: the app that runs the plan's phases on both simulators, with the recorded results. See harness/README.md.
  • docs/: the plan, the phase 2, 3 and 4 briefs (historical), the device brief for the device half of phase 3, and phase4-results.md, the demo app on the Apple TV.

Using it in an app

An app written for the browser, unchanged

The demo app runs on the Apple TV from its own src/, the source of its web build, with a NativeScript project beside it and nothing of the host in it: see solid-demo-app/nativescript. Its boot file, in short:

// app/app.ts
import "@nativescript/canvas-polyfill";
import "@solidtv/nativescript/shims";
import { Application } from "@nativescript/core";
import { Canvas } from "@nativescript/canvas";
import { Config, renderer } from "@solidtv/solid";
import {
  KeyBridge,
  bindCanvas,
  bindLifecycle,
  bindRemote,
} from "@solidtv/nativescript";

// A key press a handler consumed stays with the app; Menu with nothing to
// handle it goes to the system and leaves the app. SolidTV 1.7 or later.
Config.preventDefaultOnHandledKeys = true;

canvas.on("ready", () => {
  // The app's own createRenderer() draws into this view through TvPlatform.
  bindCanvas(canvas);
  // The remote, into the bridge, onto document, where the app's focus
  // manager listens.
  const bridge = new KeyBridge({ target: document });
  bindLifecycle(() => renderer, bridge, Application);
  bindRemote(bridge, Application.ios.window);
  // The app's own entry: it reads the URL, sets Config, creates the
  // renderer and renders its routes, as in the browser.
  import("../../src/index");
});

The app keeps its HashRouter, its focus manager on document, its fetch, its relative asset URLs, its import.meta.env.BASE_URL and its loadFonts. The host's webpack config adds chainSolidTV with the hex-colour loader over the app's source and the app's aliases, copies the app's public fonts and assets into the bundle, and turns the bundle's type check off, as the web build has none.

An app that passes the host its settings
// app.ts
import '@nativescript/canvas-polyfill';
import '@solidtv/nativescript/shims';
import { Application, Screen } from '@nativescript/core';
import { isTvOS } from '@nativescript/core/platform';
import { Canvas } from '@nativescript/canvas';
import { createRenderer, registerDefaultShaders } from '@solidtv/solid';
import { useFocusManager } from '@solidtv/solid/primitives';
import {
  KeyBridge,
  bindLifecycle,
  bindRemote,
  rendererSettings,
  stubTarget,
} from '@solidtv/nativescript';

canvas.on('ready', () => {
  const { renderer, render } = createRenderer(
    rendererSettings(canvas, Screen.mainScreen),
    stubTarget,
  );
  registerDefaultShaders(renderer.stage.shManager);
  const bridge = new KeyBridge();
  bindLifecycle(renderer, bridge, Application);
  if (isTvOS) {
    bindRemote(bridge, Application.ios.window);
  }
  // The renderer's own font loading: the shims make it work on file URLs.
  renderer.stage.loadFont('sdf', {
    fontFamily: 'Ubuntu',
    atlasUrl: 'file://' + appPath + '/assets/Ubuntu-Regular.msdf.png',
    atlasDataUrl: 'file://' + appPath + '/assets/Ubuntu-Regular.msdf.json',
  });
  render(() => <App bridge={bridge} />);
});
// webpack.config.js
const webpack = require("@nativescript/webpack");
const { chainSolidTV } = require("@solidtv/nativescript/webpack");

module.exports = (env) => {
  webpack.init(env);
  webpack.chainWebpack(chainSolidTV);
  return webpack.resolveConfig();
};

The app's tsconfig.json needs "jsx": "preserve" and "jsxImportSource": "@solidtv/solid". The bridge has no input of its own: on tvOS bindRemote feeds it from the Siri Remote; elsewhere the host calls bridge.down(key) and bridge.up(key) (or tap) from whatever reports presses, on-screen buttons in the harness on iOS. A Back handler that navigates calls e.preventDefault(), or Menu leaves the app: SolidTV's focus manager does not call it for a handler that returned true. The harness is the worked example of all of this, including the pixel ratios, the file:// asset URLs and what to log.

Peers: @solidtv/renderer 1.9.4 or later, which adds setDefaultPlatform, the Platform.resolveSettings hook, the platform's own element lookup for the render target, and relative font URLs resolved like image URLs; TvPlatform imports setDefaultPlatform, so the package does not load on 1.9.3. That was the first release with the ./platform export and the Platform.sizeCanvas hook (solid-tv/renderer#199) and the immediate pause, renderer.pause({ immediate: true }), which the lifecycle binding calls (solid-tv/renderer#201). solid-js for the JSX preset, and webpack through @nativescript/webpack. With SolidTV, 1.6.4 or later, which adds Config.preventDefaultOnHandledKeys, the Menu contract, and ignores a second useFocusManager argument that cannot listen; 1.6.3 was the first release where useFocusManager takes the event target (solid-tv/solid#63), and took that second argument for the target.

Developing

pnpm 10 and Node 18 or later. The renderer comes from npm. The harness is its own pnpm root under harness/ and links this package from ..; see its README for the rest.

pnpm install
pnpm build
pnpm test
pnpm lint
Releasing

release-it, the way the renderer and SolidTV release: on main, clean and up to date, with tests and lint green, it asks for the increment, bumps package.json, commits Release x.y.z, tags x.y.z, pushes, publishes to npm (prepack builds first) and creates the GitHub release. It needs an npm login with publish rights on @solidtv and a GitHub token in GITHUB_TOKEN; the gh CLI's token serves:

GITHUB_TOKEN=$(gh auth token) pnpm release

Add --dry-run to see the plan without changing anything.

License

Apache-2.0, see LICENSE.

Keywords