From d21d415c56711346f2ba282108560f0a64e2bd4d Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Fri, 18 Sep 2026 15:20:37 -0400 Subject: [PATCH] docs(deploy): add a guide for Apple TV through NativeScript tvOS gives third-party apps no browser and no WebView, so the hosted and bundled web app models the other deploy guides describe do not apply. SolidTV runs there through NativeScript and @solidtv/nativescript, and the demo app now does so from its own `src/`, unchanged. This writes that setup down: the project beside `src/`, each config file, the boot file, the Siri Remote key mapping, the Menu contract behind `Config.preventDefaultOnHandledKeys` and the one `onBack` rule an app needs for it, the lifecycle, simulator and device builds, console debugging and the limits (video, SVG, compressed textures). The page says which releases the setup waits on, after @solidtv/solid 1.6.3, @solidtv/renderer 1.9.3 and @solidtv/nativescript 0.1.1, and shows the sibling-checkout links the demo uses until then. Co-Authored-By: Claude Fable 5.1 --- docs/_sidebar.md | 1 + docs/deploy/appletv.md | 456 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 457 insertions(+) create mode 100644 docs/deploy/appletv.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 60764849..38787ed3 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -58,6 +58,7 @@ - [Tizen/Samsung](/deploy/tizen.md) - [WebOS/LG](/deploy/lg.md) - [Android TV](/deploy/androidtv.md) + - [Apple TV](/deploy/appletv.md) - [Legacy Devices](/deploy/legacy.md) - Tooling - [SolidTV Devtools](/tools/solid_devtools.md) diff --git a/docs/deploy/appletv.md b/docs/deploy/appletv.md new file mode 100644 index 00000000..25433db0 --- /dev/null +++ b/docs/deploy/appletv.md @@ -0,0 +1,456 @@ +# Deploying to Apple TV (tvOS) + +tvOS gives third-party apps no browser and no WebView, so the hosted and bundled web app models used for Tizen, webOS and Android TV do not apply. SolidTV runs on Apple TV through [NativeScript](https://nativescript.org/) instead: your JavaScript runs in V8 inside a native app, and the renderer draws into a native WebGL view. There is no HTML page anywhere in the stack. + +The host between NativeScript and SolidTV is [`@solidtv/nativescript`](https://github.com/solid-tv/nativescript). With it, an app written for the browser runs on Apple TV **from the same `src/`**, with a small NativeScript project beside it. The [SolidTV demo app](https://github.com/solid-tv/solid-demo-app/tree/main/nativescript) is the worked example this guide follows: its web entry, `HashRouter`, focus manager, `fetch` calls, relative asset URLs and `loadFonts` all run unchanged. + +## How It Works + +| Piece | Role | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| NativeScript (`tvos` builds) | The runtime: V8 plus the tvOS app shell, built and deployed with the `ns` CLI. | +| `@nativescript/canvas` | A native `Canvas` view with a WebGL context — the surface the renderer draws into. | +| `@nativescript/canvas-polyfill` | Browser-shaped globals the renderer expects: `document`, `XMLHttpRequest`, `Image`, `createImageBitmap`, … | +| `@solidtv/nativescript` | The host: a renderer platform for the Canvas view, shims for the globals the polyfill lacks, a Siri Remote → key event bridge, and app lifecycle bindings. | +| `@solidtv/nativescript/webpack` | `chainSolidTV`, which teaches `@nativescript/webpack` to compile Solid JSX for `@solidtv/solid` and mirror the parts of your Vite config the bundle needs. | + +The build uses **webpack, not Vite**: `@nativescript/vite` has no tvOS platform. Your web build keeps using Vite; only the tvOS bundle goes through webpack. + +> **Version requirements.** Running a browser app unchanged needs the releases **after** `@solidtv/solid` 1.6.3 (`Config.preventDefaultOnHandledKeys`), `@solidtv/renderer` 1.9.3 (`setDefaultPlatform` and the `Platform.resolveSettings` hook) and `@solidtv/nativescript` 0.1.1 (`bindCanvas`, the full shims, `mirrorConsole`). Until those are published, link the three packages from sibling checkouts as the demo does — see [package.json](#packagejson) below. + +## Prerequisites + +Apple TV builds need a Mac. + +- **Xcode 26.4** with the tvOS platform. `xcodebuild -downloadPlatform tvOS` installs the simulator runtime. +- **pnpm 10** and Node 18 or later. +- **Ruby with the `xcodeproj` gem**: `gem install --user-install xcodeproj`. The NativeScript CLI merges xcconfig files with it and fails without it. CocoaPods is not needed. +- **A UTF-8 locale** in the shell that runs `ns`. An interactive terminal usually has one; a script or CI shell needs `LC_ALL=en_US.UTF-8` on every `ns` command, or the `xcodeproj` gem fails reading the xcconfig files. + +You do not install the NativeScript CLI globally — the tvOS build of it is a dev dependency of the project below. + +## Project Layout + +Create a `nativescript/` folder in your SolidTV project, next to `src/`, the same way the other guides use `tizen/`, `lg/` and `androidtv/`. It holds only the host; nothing in `src/` is written for it. + +``` +my-app/ +├── src/ # your app, shared with the web build +├── public/ # fonts and assets, shared with the web build +├── vite.config.js # the web build +└── nativescript/ + ├── app/app.ts # the boot file + ├── App_Resources/ # Info.plist, build.xcconfig, icons + ├── stubs/ # stand-ins for packages with no tvOS build + ├── nativescript.config.ts + ├── webpack.config.js + ├── tsconfig.json + ├── references.d.ts + ├── package.json + └── pnpm-workspace.yaml # makes this folder its own pnpm root +``` + +### package.json + +The NativeScript packages come from the `tvos` dist-tags, and `@nativescript/canvas` must be 3.0.0-alpha.10 or later — the first build with tvOS slices. List your app's own runtime dependencies here too (`solid-js`, the router, anything `src/` imports), since this folder resolves modules on its own. + +```json +{ + "name": "my-app-tvos", + "private": true, + "main": "app/app.ts", + "scripts": { + "tvos": "ns run tvos --emulator --no-hmr", + "build:tvos": "ns build tvos", + "clean": "ns clean" + }, + "dependencies": { + "@nativescript/canvas": "3.0.0-alpha.10", + "@nativescript/canvas-polyfill": "3.0.0-alpha.10", + "@nativescript/core": "9.2.0-tvos.0", + "@solidjs/router": "^0.16.1", + "@solidtv/nativescript": "link:../../nativescript", + "@solidtv/renderer": "link:../../renderer", + "@solidtv/solid": "link:../../solid", + "solid-js": "^1.9.9" + }, + "devDependencies": { + "@nativescript/ios": "9.1.0", + "@nativescript/tvos": "9.1.0", + "@nativescript/types": "9.1.1", + "@nativescript/webpack": "5.0.39-tvos.0", + "nativescript": "9.2.0-tvos.0", + "typescript": "~6.0.0" + }, + "packageManager": "pnpm@10.33.4" +} +``` + +The three `link:` entries point at sibling checkouts of [nativescript](https://github.com/solid-tv/nativescript), [renderer](https://github.com/solid-tv/renderer) and [solid](https://github.com/solid-tv/solid), each built with `pnpm build`. Replace them with version pins once the releases in the note above are published. + +### pnpm-workspace.yaml + +This file makes `nativescript/` its own pnpm root, so installing here never touches your app's lockfile, and it settles three things the NativeScript toolchain depends on. + +```yaml +# Flat node_modules: the NativeScript CLI discovers plugins by walking +# node_modules, and @nativescript/canvas resolves its native headers from there. +nodeLinker: hoisted +onlyBuiltDependencies: + - '@nativescript/core' + - '@nativescript/webpack' + - nativescript +# The alpha polyfill declares '*' on its sibling packages, which resolves to +# the 2.x line (no tvOS slices). Keep every canvas package on the same alpha. +overrides: + '@nativescript/canvas': 3.0.0-alpha.10 + '@nativescript/canvas-media': 3.0.0-alpha.10 + # canvas-svg's xcframework has no tvOS slice and fails the tvOS build. + '@nativescript/canvas-svg': file:./stubs/canvas-svg +``` + +The `canvas-svg` stand-in is a tiny package exporting the names the polyfill imports as empty classes. Copy [`stubs/canvas-svg`](https://github.com/solid-tv/solid-demo-app/tree/main/nativescript/stubs/canvas-svg) from the demo. The cost: SVG images do not decode on tvOS. + +If your root `.gitignore` ignores `pnpm-workspace.yaml` or lockfiles, un-ignore the ones in this folder — they should be tracked. + +### nativescript.config.ts + +```ts +import { NativeScriptConfig } from '@nativescript/core'; + +export default { + id: 'com.example.soliddemo', + projectName: 'SolidTVDemo', + appPath: 'app', + appResourcesPath: 'App_Resources', + bundler: 'webpack', + cli: { + packageManager: 'pnpm', + }, +} as NativeScriptConfig; +``` + +`id` is the bundle identifier. Set `projectName`, or the CLI names the app after the folder (`nativescript`). + +### webpack.config.js + +`chainSolidTV` adds the Solid JSX rule (universal mode, for `@solidtv/solid`), browser export conditions so `solid-js` does not resolve to its server build, and `.js` → `.ts` import resolution. Its options carry over what your Vite config does for `src/`. + +```js +const path = require('path'); +const webpack = require('@nativescript/webpack'); +const { chainSolidTV } = require('@solidtv/nativescript/webpack'); + +// The app is ../src, the web build's source, untouched. +const SRC = path.resolve(__dirname, '../src'); +const PUBLIC = path.resolve(__dirname, '../public'); +const EMPTY = path.resolve(__dirname, 'stubs/empty.cjs'); + +module.exports = (env) => { + webpack.init(env); + webpack.chainWebpack((config) => { + chainSolidTV(config, { + // Only if your Vite build uses the hex color transform. + hexColors: { include: [SRC] }, + alias: { + // Aliases from your vite.config.js. + theme: path.resolve(SRC, 'theme.ts'), + // Packages that cannot run here but are imported by pages that + // still have to bundle. + 'shaka-player': EMPTY, + }, + }); + + // The web build never type-checks (Vite only transpiles), and src/ is + // written against its own tsconfig. Keep `tsc` in the app as the check. + config.plugins.delete('ForkTsCheckerWebpackPlugin'); + + // Fonts and assets the web build serves from public/, at the same + // relative paths, copied into the app bundle. + config.plugin('CopyWebpackPlugin').tap((args) => { + args[0].patterns.push( + { from: path.join(PUBLIC, 'fonts'), to: 'fonts' }, + { from: path.join(PUBLIC, 'assets'), to: 'assets' }, + ); + return args; + }); + }); + return webpack.resolveConfig(); +}; +``` + +`stubs/empty.cjs` is one line, `module.exports = {};`. Use it for any dependency that needs a real DOM — a `