create-octane-xplat 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +57 -0
  2. package/compatibility.json +199 -0
  3. package/compatibility.ts +109 -0
  4. package/dist/compatibility.js +66 -0
  5. package/dist/index.js +132 -0
  6. package/dist/scaffold.js +439 -0
  7. package/{index.mjs → index.ts} +3 -4
  8. package/library-template/AGENTS.md +3 -1
  9. package/library-template/README.md +20 -0
  10. package/library-template/package.json +22 -19
  11. package/library-template/pnpm-workspace.yaml +150 -11
  12. package/library-template/src/Badge.tsx +1 -1
  13. package/library-template/src/Badge.web.tsx +1 -1
  14. package/library-template/tests/packed-consumer.mjs +2 -1
  15. package/library-template/tsconfig.envs.json +3 -0
  16. package/library-template/tsconfig.json +1 -1
  17. package/library-template/tsrx-typegen.json +1 -1
  18. package/package.json +23 -6
  19. package/{scaffold.mjs → scaffold.ts} +51 -40
  20. package/template/.agents/skills/xplat/SKILL.md +10 -2
  21. package/template/.agents/skills/xplat/references/components.md +2 -2
  22. package/template/.agents/skills/xplat/references/platform.md +2 -2
  23. package/template/AGENTS.md +47 -0
  24. package/template/App_Resources/Android/app.gradle +1 -1
  25. package/template/package.json +33 -31
  26. package/template/pnpm-workspace.yaml +64 -29
  27. package/template/src/app.web.test.tsx +20 -0
  28. package/template/src/main.web.tsrx +1 -0
  29. package/template/src/test-setup.web.ts +17 -0
  30. package/template/tsconfig.json +1 -1
  31. package/template/vite.config.ts +2 -2
  32. package/template/vitest.web.config.mts +13 -0
  33. package/library-template/.oxlintrc.json +0 -24
  34. package/template/.oxlintrc.json +0 -24
@@ -4,14 +4,153 @@ allowBuilds:
4
4
  esbuild: true
5
5
  '@parcel/watcher': true
6
6
  '@nativescript/core': false
7
- # Shared version pins — bump these here, every dependency field reads them.
8
- catalog:
9
- octane: 0.11.0
10
- '@nativescript-community/octane': 0.2.4
11
- '@nativescript/core': 9.1.3
12
- '@nativescript/types': 9.1.4
13
- typescript: 5.9.3
14
- vite: 8.3.0
15
- '@octanejs/vite-plugin': 0.1.61
16
- '@tsrx/typescript-plugin': 0.4.11
17
- vitest: 4.1.11
7
+ catalogs:
8
+ xplat:
9
+ "@nativescript-community/octane": 0.2.5
10
+ "@nativescript/core": 9.1.3
11
+ "@nativescript/types": 9.1.4
12
+ "@octane-xplat/cli": 0.16.0
13
+ "@octane-xplat/jsx": 0.16.0
14
+ "@octane-xplat/ui": 0.16.0
15
+ "@octanejs/vite-plugin": 0.1.61
16
+ "@tsrx/oxc": 0.20.0
17
+ "@tsrx/typescript-plugin": 0.4.11
18
+ octane: 0.11.0
19
+ tsrx-typegen: 0.2.2
20
+ typescript: 5.9.3
21
+ vite: 8.3.0
22
+ vitest: 4.1.11
23
+ overrides:
24
+ "@nativescript/vite>esbuild": 0.27.7
25
+ "@nativescript-community/vite-octane>esbuild": 0.27.7
26
+ vite>esbuild: 0.27.7
27
+ configDependencies:
28
+ "@octane-xplat/patches": 0.16.0
29
+ allowUnusedPatches: true
30
+ # Framework patches are supplied by the @octane-xplat/patches config dependency and
31
+ # applied via pnpm patchedDependencies. Managed upstream by @octane-xplat/cli (`xplat
32
+ # patches apply` refreshes, `xplat patches check` verifies); do not edit by hand.
33
+ patchedDependencies:
34
+ # Published 0.11.0 still ships none of the retained fixes: it adds the document guard
35
+ # context around installDefaultSignalOwner and reflows the compile.js hook-classification
36
+ # sites, but every patched feature remains absent upstream. Retains renderer-owned
37
+ # Children.toArray/map with explicit universal-native re-exports for older runtimes,
38
+ # universal signal-read tracking and local hooks, root event-error forwarding, the
39
+ # complete universal imperative-ref lifecycle, the Descriptor declaring-owner reader
40
+ # fallback, and the signal-owner/enclosingOwner chain so child reads reuse instance
41
+ # declarations. Verified against the registry 0.11.0 tarball; all hunks re-anchored to the
42
+ # rewritten compile.js and document-owner call sites. Disposes DOM provider descendants
43
+ # when children become null or undefined, releasing their refs and effects.
44
+ octane@0.11.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.11.0.patch
45
+ # Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
46
+ # flex alignment, so those hunks are removed. Retains the unshipped parent-padding
47
+ # measure-spec fix and Android explicit-size flex alignment, iOS continuous corner
48
+ # clipping, CSS @layer cascade support, custom Android variable-font weight mapping, and
49
+ # percentage translate parsing, native view, and animation support. Verified against the
50
+ # registry 9.1.3 tarball; it has no Windows implementation, so the separate Windows
51
+ # preview patch remains.
52
+ "@nativescript/core@9.1.3": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__core@9.1.3.patch
53
+ # @nativescript/vite serializes bundled CSS and dev app.css through reworkcss `css`@3.0.0,
54
+ # which mis-parses @layer into garbage selectors — rules die before reaching
55
+ # _populateRules. Adds atlayer() covering block and statement forms. Applies to a
56
+ # transitive dep — no app declares it directly.
57
+ css@3.0.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/css@3.0.0.patch
58
+ # Dev-server fixes still missing after @nativescript/vite 8.0.17 added percent decoding
59
+ # for path-style /ns/m and Vue /ns/sfc requests (NativeScript#11483): collect re-export
60
+ # specifiers in the .vite/deps rewrite, route /@fs paths outside both roots through /ns/m
61
+ # (linked checkouts bypass the device transform upstream), and probe the non-active
62
+ # platform + `native` suffix tags in module candidates. The deps-bundle esbuild passes
63
+ # ignore vite resolve.alias, so a dep importing bare `octane` bundles the DOM runtime
64
+ # beside the universal one (hooks crash on boot, octane-xplat#3) — an onResolve plugin
65
+ # redirects `octane` → `octane/universal/native`. tsconfig-paths and the generated
66
+ # build-time checker moduleSuffixes also need the `.mobile` tier for iOS/Android/visionOS
67
+ # so `.mobile` leaves resolve under mapped imports and `ns build`. Served `/node_modules/`
68
+ # tails now also scan pnpm's `.pnpm` store and `link:`/`file:`/`portal:` dep roots — a
69
+ # linked package's transitive deps (e.g. @iconify/utils under @octane-xplat/icons) resolve
70
+ # nowhere else and 404 the dev session. findMonorepoWorkspaceRoot no longer treats a lone
71
+ # turbo.json as a workspace root — Turborepo's per-package task configs marked apps/* as
72
+ # roots, masking the real pnpm root so served `/node_modules/` tails never reached the
73
+ # `.pnpm` store scan and non-hoisted transitive deps like source-map-js 404'd dev boot.
74
+ # Linked dependency fallbacks also discover each linked package's enclosing workspace and
75
+ # reuse the cached, bounded workspace node_modules scan so transitive workspace siblings
76
+ # (auth to secure-storage to a private native plugin) resolve without the consumer
77
+ # supplying that workspace root. Transform candidates now keep root-relative URLs only
78
+ # when they identify the discovered file; nested linked-dependency and pnpm-store matches
79
+ # inside the app root use their actual filesystem paths, preserving query suffixes. The
80
+ # /ns/m import rewriter now resolves `#`-prefixed package `imports` specifiers through the
81
+ # importer's own package.json — raw-read node_modules files (e.g. octane's
82
+ # dist/signals/graph.js importing `#octane/dist/signal-actions/graph`) otherwise reach the
83
+ # device verbatim and the HTTP loader throws `Cannot find module`, deadlocking dev boot
84
+ # before any boot recording can bundle the dep. NativeScript#11551: preserve user virtual
85
+ # stylesheet imports while retaining browser-helper filtering, route virtual CSS through
86
+ # the native inline compiler/registration path before the generic virtual-module shortcut,
87
+ # and relay Vite virtual stylesheet updates as tagged native CSS updates through the same
88
+ # adapted-text endpoint. Empty sheets clear previous rules; CSS compilation failures
89
+ # return HTTP 500 instead of raw CSS.
90
+ "@nativescript/vite@8.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.17.patch
91
+ # Retains the workspace's NativeScript driver fixes when upgrading to 0.2.5 for portal
92
+ # support: a prop dropped from an update's complete prop bag — or set to
93
+ # `undefined`/`null` — is a removal that restores the documented default
94
+ # (Property/CssProperty setters get `unsetValue`, plain fields the captured pre-prop
95
+ # value, `style` keys diff per key); a never-applied `undefined` still skips the write.
96
+ # Also advertises the implemented visibility capability, carries Octane callback ref
97
+ # types, and preserves authored Grid row/col/span metadata. Hosts custom-layout containers
98
+ # (e.g. MasonKit ViewBase) that own child lists via
99
+ # insertChild/removeChild/getChildrenCount without subclassing LayoutBase. Additionally,
100
+ # the masonry cutover: masonview resolves to MasonKit View and scrollview to MasonKit
101
+ # Scroll as the only layout containers, with zero layout props — every former prop name
102
+ # warns-and-drops and all CSS semantics arrive through the style bag; style keys on a
103
+ # mason child route to its leaf (stashed pre-attach, replayed at attach) via
104
+ # Style.fromView.
105
+ "@nativescript-community/octane@0.2.5": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.5.patch
106
+ # octaneServerStrategy's SCRIPT_FILE_RE only covers [jt]sx? — .tsrx edits never emit the
107
+ # deferred ns:hmr-delta broadcast, so component HMR silently never reaches the device.
108
+ # Also counts open /ns-hmr sockets so the update summary stops reporting recipients=0 for
109
+ # delivered updates.
110
+ "@nativescript-community/vite-octane@0.2.1": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__vite-octane@0.2.1.patch
111
+ # Enables `resolveHiddenExtensions` on the language plugin's typescript facet so
112
+ # extensionless specifiers (e.g. './Text') probe `.tsrx` after moduleSuffixes — required
113
+ # for a single extensionless shared barrel to resolve per-platform leaves under tsrx-tsc
114
+ # and the IDE plugin.
115
+ "@tsrx/typescript-plugin@0.4.11": node_modules/.pnpm-config/@octane-xplat/patches/patches/@tsrx__typescript-plugin@0.4.11.patch
116
+ # linkifyjs declares /\p{L}/u and /\p{Emoji}/u as regex literals at module scope;
117
+ # NativeScript's embedded V8 builds without ICU, so the literals throw SyntaxError at
118
+ # parse time and poison the whole bundle (any bundled app importing @tiptap/extension-link
119
+ # dies on boot). Rewrites them as try/catch new RegExp builds with ASCII-range fallbacks —
120
+ # same behavior where ICU exists, graceful degradation where it doesn't.
121
+ linkifyjs@4.3.3: node_modules/.pnpm-config/@octane-xplat/patches/patches/linkifyjs@4.3.3.patch
122
+ # useEditor.ts reads bare process.env.NODE_ENV — no node global exists in the harness
123
+ # typecheck program. Adds a module-local process declaration for typecheck. (The
124
+ # './Context' self-import half of the 0.0.51 patch shipped upstream in 0.0.52 via
125
+ # octanejs/octane#1502.)
126
+ "@octanejs/tiptap@0.0.52": node_modules/.pnpm-config/@octane-xplat/patches/patches/@octanejs__tiptap@0.0.52.patch
127
+ # @lexical/link declares a /\p{L}\p{N}/u URL matcher as a regex literal at module scope;
128
+ # NativeScript's embedded V8 builds without ICU, so the literal throws SyntaxError at
129
+ # parse time and poisons the whole bundle (any bundled app importing @lexical/link dies on
130
+ # boot). Same fix as linkifyjs: try/catch new RegExp with an ASCII-range fallback — the
131
+ # matcher only feeds autolink, which native doesn't use.
132
+ "@lexical/link@0.51.0": node_modules/.pnpm-config/@octane-xplat/patches/patches/@lexical__link@0.51.0.patch
133
+ # ui-image 5.x regressed two iOS prefetch behaviors the 4.x line shipped (still present in
134
+ # 4.6.20): prefetchToCacheType ignores its cacheType argument — SDWebImagePrefetcher's
135
+ # default context stores to disk+memory either way, so a 'disk' prefetch also decodes and
136
+ # warms memory, diverging from Android's bytes-only Glide downloadOnly — and
137
+ # getContextFromOptions drops PrefetchOptions.headers because it never sets
138
+ # SDWebImageContextDownloadRequestModifier, so prefetch({headers}) silently sends no
139
+ # headers on iOS while Android's CustomGlideUrl forwards them. Ports upstream's own 4.x
140
+ # implementation — SDWebImageContextStoreCacheType on the prefetch context plus the
141
+ # request-modifier branch in getContextFromOptions — and additionally sets
142
+ # SDWebImageContextOriginalStoreCacheType so a memory prefetch does not spill original
143
+ # bytes to disk.
144
+ "@nativescript-community/ui-image@5.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__ui-image@5.0.17.patch
145
+ # @nativescript/doctor's Android runtime check reads the app's raw devDependencies
146
+ # specifier and npm dist-tag resolves anything that isn't a semver range — a pnpm
147
+ # `catalog:` pin throws 'The determined Android runtime version ... is not valid' and
148
+ # kills `ns build android` before toolchain checks. Resolve catalog specifiers through the
149
+ # installed package's manifest, reusing the existing tgz-branch mechanism.
150
+ "@nativescript/doctor@2.0.18": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__doctor@2.0.18.patch
151
+ # list/index.ios.js assigns `ObjCProtocols = [MasonListDelegate]` at module scope.
152
+ # MasonListDelegate is declared in Mason-Swift.h, which the app's metadata generation does
153
+ # not surface as a runtime global — importing masonkit on iOS throws `MasonListDelegate is
154
+ # not defined` during bundle eval and kills the app at boot. Guard the binding; actual
155
+ # List delegate conformance when the protocol is absent is a separate gap.
156
+ "@triniwiz/nativescript-masonkit@1.0.0-beta.110": node_modules/.pnpm-config/@octane-xplat/patches/patches/@triniwiz__nativescript-masonkit@1.0.0-beta.110.patch
@@ -1,4 +1,4 @@
1
- /** @jsxImportSource @nativescript-community/octane */
1
+ /** @jsxImportSource @octane-xplat/jsx */
2
2
  import { Text, View } from '@octane-xplat/ui'
3
3
  import { normalizeBadgeLabel } from './normalizeBadgeLabel'
4
4
  import type { BadgeProps } from './props'
@@ -1,4 +1,4 @@
1
- /** @jsxImportSource octane */
1
+ /** @jsxImportSource @octane-xplat/jsx/dom */
2
2
  import { Text, View } from '@octane-xplat/ui'
3
3
  import { normalizeBadgeLabel } from './normalizeBadgeLabel'
4
4
  import type { BadgeProps } from './props'
@@ -67,6 +67,7 @@ try {
67
67
  installDirectory(packed, sourceManifest.name, consumer)
68
68
  for (const dependency of [
69
69
  'octane',
70
+ '@octane-xplat/jsx',
70
71
  '@octane-xplat/ui',
71
72
  ...(target === 'native'
72
73
  ? ['@nativescript-community/octane', '@nativescript/core', '@nativescript/types']
@@ -105,7 +106,7 @@ void element; void missing; void invalid
105
106
  module: mode === 'bundler' ? 'esnext' : 'nodenext',
106
107
  moduleResolution: mode,
107
108
  jsx: 'react-jsx',
108
- jsxImportSource: target === 'native' ? '@nativescript-community/octane' : 'octane',
109
+ jsxImportSource: target === 'native' ? '@octane-xplat/jsx' : '@octane-xplat/jsx/dom',
109
110
  customConditions: [target],
110
111
  moduleSuffixes: target === 'web' ? ['.web', ''] : ['.mobile', ''],
111
112
  },
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsxImportSource": "@octane-xplat/jsx"
5
+ },
3
6
  "environments": {
4
7
  "web": {
5
8
  "include": ["src/**/*.{ts,tsx,tsrx}"]
@@ -5,7 +5,7 @@
5
5
  "module": "esnext",
6
6
  "moduleResolution": "bundler",
7
7
  "jsx": "react-jsx",
8
- "jsxImportSource": "octane",
8
+ "jsxImportSource": "@octane-xplat/jsx",
9
9
  "moduleSuffixes": [
10
10
  "",
11
11
  ".web",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "targets": {
3
3
  "octane": {
4
- "renderer": "octane",
4
+ "renderer": "@octane-xplat/jsx",
5
5
  "project": "tsconfig.types.json",
6
6
  "outDir": "types",
7
7
  "entrypoints": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-octane-xplat",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Scaffold Octane Xplat apps and cross-platform libraries",
5
5
  "keywords": [
6
6
  "component-library",
@@ -21,22 +21,39 @@
21
21
  "directory": "packages/create"
22
22
  },
23
23
  "bin": {
24
- "create-octane-xplat": "./index.mjs"
24
+ "create-octane-xplat": "./dist/index.js"
25
25
  },
26
26
  "files": [
27
- "index.mjs",
28
- "scaffold.mjs",
27
+ "index.ts",
28
+ "scaffold.ts",
29
+ "compatibility.ts",
30
+ "compatibility.json",
31
+ "dist",
29
32
  "template",
30
33
  "library-template"
31
34
  ],
32
35
  "type": "module",
33
36
  "exports": {
34
- "./scaffold": "./scaffold.mjs"
37
+ "./scaffold": "./dist/scaffold.js",
38
+ "./compatibility": {
39
+ "types": "./compatibility.ts",
40
+ "default": "./dist/compatibility.js"
41
+ },
42
+ "./compatibility.json": "./compatibility.json"
35
43
  },
36
44
  "publishConfig": {
37
45
  "access": "public"
38
46
  },
39
47
  "dependencies": {
40
- "@clack/prompts": "1.8.1"
48
+ "@clack/prompts": "1.8.1",
49
+ "yaml": "2.9.1"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "24.10.1",
53
+ "vite": "8.3.0"
54
+ },
55
+ "scripts": {
56
+ "build": "vite build",
57
+ "typecheck": "tsc -p tsconfig.json"
41
58
  }
42
59
  }
@@ -1,7 +1,6 @@
1
1
  // Scaffold manifest + compose/apply shared by create-octane-xplat and the
2
- // CLI's `xplat add`. template/package.json is the canonical store for app dep
3
- // versions and script bodies; library-template/package.json owns the library
4
- // starter. The release bump script updates both manifests.
2
+ // CLI's `xplat add`. compatibility.json owns version pins; the generated
3
+ // templates own script bodies and the dependency set for each starter.
5
4
  import {
6
5
  cpSync,
7
6
  existsSync,
@@ -14,29 +13,38 @@ import {
14
13
 
15
14
  import { basename, dirname, join, resolve } from 'node:path'
16
15
  import { fileURLToPath } from 'node:url'
16
+ import { parseDocument } from 'yaml'
17
+ import { ensureCompatibilityCatalog } from './compatibility.ts'
17
18
 
18
- export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'template')
19
- export const LIBRARY_TEMPLATE_DIR = join(
20
- dirname(fileURLToPath(import.meta.url)),
21
- 'library-template',
22
- )
19
+ const moduleDir = dirname(fileURLToPath(import.meta.url))
20
+ // dist/ emit sits one level deeper than the root sources it mirrors.
21
+ const pkgRoot = basename(moduleDir) === 'dist' ? join(moduleDir, '..') : moduleDir
22
+ export const TEMPLATE_DIR = join(pkgRoot, 'template')
23
+ export const LIBRARY_TEMPLATE_DIR = join(pkgRoot, 'library-template')
23
24
 
24
25
  /** Scaffoldable targets. `files` entries are template-relative paths —
25
26
  * directories copy recursively. `deps`/`devDeps`/`scripts` name keys whose
26
27
  * values come from template/package.json. `typecheck` contributes one clause
27
28
  * to the generated typecheck script. */
28
- export const targets = {
29
+ export const targets: Record<string, any> = {
29
30
  web: {
30
31
  label: 'Web (Vite)',
31
- files: ['index.html', 'vite.config.ts', 'src/main.web.tsrx'],
32
- scripts: ['dev', 'build'],
33
- devDeps: ['@octanejs/vite-plugin'],
32
+ files: [
33
+ 'index.html',
34
+ 'vite.config.ts',
35
+ 'src/main.web.tsrx',
36
+ 'vitest.web.config.mts',
37
+ 'src/app.web.test.tsx',
38
+ 'src/test-setup.web.ts',
39
+ ],
40
+ scripts: ['dev', 'build', 'test', 'test:web'],
41
+ devDeps: ['@octanejs/vite-plugin', 'jsdom'],
34
42
  },
35
43
  ios: {
36
44
  label: 'iOS (NativeScript)',
37
45
  requires: ['native-shared'],
38
46
  files: ['App_Resources/iOS'],
39
- scripts: ['dev:ios', 'test:ios', 'build:ios'],
47
+ scripts: ['dev:ios', 'build:ios', 'test:ios'],
40
48
  devDeps: ['@nativescript/ios'],
41
49
  note: 'needs macOS + Xcode — `pnpm xplat doctor` checks the toolchain',
42
50
  },
@@ -44,7 +52,7 @@ export const targets = {
44
52
  label: 'Android (NativeScript)',
45
53
  requires: ['native-shared'],
46
54
  files: ['App_Resources/Android'],
47
- scripts: ['dev:android', 'test:android', 'build:android'],
55
+ scripts: ['dev:android', 'build:android', 'test:android'],
48
56
  devDeps: ['@nativescript/android'],
49
57
  note: 'needs Android SDK + a compatible JDK — `pnpm xplat doctor` checks the toolchain',
50
58
  },
@@ -68,22 +76,15 @@ export const targets = {
68
76
  'vite.config.native.mts',
69
77
  'references.d.ts',
70
78
  'src/main.ts',
71
- // On-device Vitest lane — `ns test` builds src/test.ts under
72
- // --env.unitTesting and runs every src/**/*.spec file on a
73
- // simulator/emulator through the upstream runner.
79
+ 'vitest.config.mts',
74
80
  'src/test.ts',
75
81
  'src/app.spec.tsrx',
76
82
  'src/native-runtime.spec.ts',
77
83
  'src/failure-reporting.spec.ts',
78
- 'vitest.config.mts',
79
84
  ],
80
85
  deps: [
81
- '@nativescript-community/gesturehandler',
82
86
  '@nativescript-community/octane',
83
- '@nativescript-community/ui-drawer',
84
87
  '@nativescript/core',
85
- '@octane-xplat/effects',
86
- '@octane-xplat/image',
87
88
  '@valor/nativescript-websockets',
88
89
  ],
89
90
  devDeps: [
@@ -92,7 +93,6 @@ export const targets = {
92
93
  '@nativescript/unit-test-runner',
93
94
  '@nativescript/vite',
94
95
  'nativescript',
95
- 'vitest',
96
96
  ],
97
97
  typecheck: 'xplat typecheck',
98
98
  },
@@ -111,7 +111,7 @@ const BASE_SCRIPTS = ['xplat', 'lint', 'lint:fix', 'format']
111
111
 
112
112
  /** Expand `requires` edges, dependencies first. Throws on unknown ids. */
113
113
  export function resolveTargets(ids) {
114
- const resolved = []
114
+ const resolved: string[] = []
115
115
  const visit = (id) => {
116
116
  const t = targets[id]
117
117
  if (!t) {
@@ -150,7 +150,7 @@ function ownerOf(path) {
150
150
  }
151
151
 
152
152
  function walk(dir, prefix = '') {
153
- const out = []
153
+ const out: string[] = []
154
154
  for (const name of readdirSync(dir)) {
155
155
  if (name === 'node_modules') {
156
156
  continue
@@ -168,7 +168,7 @@ function walk(dir, prefix = '') {
168
168
  }
169
169
 
170
170
  function pickKeys(source, names, label) {
171
- const out = {}
171
+ const out: Record<string, string> = {}
172
172
  for (const name of names) {
173
173
  if (source[name] === undefined) {
174
174
  throw new Error(`template/package.json has no ${label} entry "${name}"`)
@@ -187,11 +187,11 @@ function pickKeys(source, names, label) {
187
187
  export function composeManifest(ids) {
188
188
  const resolved = resolveTargets(ids)
189
189
  const tpl = templateManifest()
190
- const manifest = { ...tpl }
190
+ const manifest: Record<string, any> = { ...tpl }
191
191
 
192
192
  for (const section of ['dependencies', 'devDependencies']) {
193
193
  const key = section === 'dependencies' ? 'deps' : 'devDeps'
194
- const wanted = new Set()
194
+ const wanted = new Set<string>()
195
195
  for (const id of resolved) {
196
196
  for (const name of targets[id][key] ?? []) {
197
197
  wanted.add(name)
@@ -219,7 +219,7 @@ export function composeManifest(ids) {
219
219
  }
220
220
 
221
221
  const scripts = pickKeys(tpl.scripts ?? {}, [...wantedScripts].sort(), 'script')
222
- const typecheck = [BASE_TYPECHECK]
222
+ const typecheck: string[] = [BASE_TYPECHECK]
223
223
  for (const id of resolved) {
224
224
  if (targets[id].typecheck) {
225
225
  typecheck.push(targets[id].typecheck)
@@ -255,9 +255,9 @@ const patchPkg = (key) => key.replace(/@[^@]+$/, '')
255
255
  // Split a pnpm-workspace.yaml into per-package patch blocks (leading comment
256
256
  // lines included) so compose can drop entries and apply can transplant them.
257
257
  function parsePatchEntries(lines) {
258
- const entries = []
258
+ const entries: any[] = []
259
259
  let inPatches = false
260
- let pending = []
260
+ let pending: string[] = []
261
261
  for (const line of lines) {
262
262
  if (/^\S/.test(line)) {
263
263
  inPatches = line.startsWith('patchedDependencies')
@@ -284,7 +284,7 @@ function parsePatchEntries(lines) {
284
284
  /** The pnpm-workspace.yaml a given target set produces. */
285
285
  export function composeWorkspaceYaml(ids) {
286
286
  const resolved = new Set(resolveTargets(ids))
287
- const drop = new Set()
287
+ const drop = new Set<string>()
288
288
  for (const [id, t] of Object.entries(targets)) {
289
289
  if (!resolved.has(id)) {
290
290
  for (const name of t.patches ?? []) {
@@ -294,9 +294,9 @@ export function composeWorkspaceYaml(ids) {
294
294
  }
295
295
 
296
296
  const lines = readFileSync(join(TEMPLATE_DIR, 'pnpm-workspace.yaml'), 'utf8').split('\n')
297
- const out = []
297
+ const out: string[] = []
298
298
  let inPatches = false
299
- let pending = []
299
+ let pending: string[] = []
300
300
  for (const line of lines) {
301
301
  if (/^\S/.test(line)) {
302
302
  out.push(...pending, line)
@@ -326,7 +326,18 @@ export function composeWorkspaceYaml(ids) {
326
326
  }
327
327
 
328
328
  out.push(...pending)
329
- return out.join('\n')
329
+ const doc = parseDocument(out.join('\n'))
330
+ const manifest = composeManifest(ids)
331
+ const used = new Set([
332
+ ...Object.keys(manifest.dependencies),
333
+ ...Object.keys(manifest.devDependencies),
334
+ ])
335
+ for (const name of Object.keys(doc.toJS().catalogs?.xplat ?? {})) {
336
+ if (!used.has(name)) {
337
+ doc.deleteIn(['catalogs', 'xplat', name])
338
+ }
339
+ }
340
+ return String(doc)
330
341
  }
331
342
 
332
343
  /**
@@ -407,7 +418,7 @@ export function composeTsconfig(ids) {
407
418
  return JSON.stringify(config, null, '\t') + '\n'
408
419
  }
409
420
 
410
- /** Write a scaffold for `ids` into `dir` (must not exist or be empty). */
421
+ /** Write a scaffold with tests for `ids` into `dir` (must not exist or be empty). */
411
422
  export function composeTargets(ids, dir) {
412
423
  const resolved = new Set(resolveTargets(ids))
413
424
  mkdirSync(dir, { recursive: true })
@@ -488,7 +499,7 @@ export function targetEnabled(appRoot, id) {
488
499
 
489
500
  // ios/android: declared runtime devDep is the canonical signal — App_Resources
490
501
  // alone doesn't reach the bundler config.
491
- const manifest = readJson(join(appRoot, 'package.json')) ?? {}
502
+ const manifest: Record<string, any> = readJson(join(appRoot, 'package.json')) ?? {}
492
503
  const declared = { ...manifest.dependencies, ...manifest.devDependencies }
493
504
  const runtime = { ios: '@nativescript/ios', android: '@nativescript/android' }[id]
494
505
  return runtime ? typeof declared[runtime] === 'string' : false
@@ -543,9 +554,9 @@ function enableNativeInTsconfig(appRoot) {
543
554
  export function applyTarget(appRoot, id) {
544
555
  const manifestPath = join(appRoot, 'package.json')
545
556
  const raw = readFileSync(manifestPath, 'utf8')
546
- const manifest = JSON.parse(raw)
557
+ const manifest: Record<string, any> = JSON.parse(raw)
547
558
  const tpl = templateManifest()
548
- const report = {
559
+ const report: Record<string, any> = {
549
560
  enabled: [],
550
561
  already: [],
551
562
  copied: [],
@@ -620,12 +631,12 @@ export function applyTarget(appRoot, id) {
620
631
  if (ensurePatchEntries(appRoot, [...report.enabled, ...report.already])) {
621
632
  report.patchesAdded = true
622
633
  }
634
+ ensureCompatibilityCatalog(appRoot, manifest)
623
635
 
624
636
  if (touched) {
625
637
  // Match the file's existing indent so diffs stay minimal.
626
638
  const indent = raw.match(/\n(\s+)"/)?.[1] ?? '\t'
627
639
  writeFileSync(manifestPath, JSON.stringify(manifest, null, indent) + '\n')
628
640
  }
629
-
630
641
  return report
631
642
  }
@@ -119,16 +119,24 @@ export function NameField() {
119
119
  ```
120
120
 
121
121
  - `.mobile.tsrx`/`.ios.tsrx`/`.android.tsrx` files and unsuffixed native-default files containing JSX must
122
- start with `/** @jsxImportSource @nativescript-community/octane */` on
122
+ start with `/** @jsxImportSource @octane-xplat/jsx */` on
123
123
  line 1 — nothing may precede it:
124
124
 
125
125
  ```tsrx
126
- /** @jsxImportSource @nativescript-community/octane */
126
+ /** @jsxImportSource @octane-xplat/jsx */
127
127
  import { Text } from '@octane-xplat/ui'
128
128
 
129
129
  export function NativeGreeting() { return <Text>Hello</Text> }
130
130
  ```
131
131
 
132
+ - Browser JSX leaves select DOM types explicitly on line 1. These pragmas
133
+ select types; the build keeps the existing web and native renderers:
134
+
135
+ ```tsx
136
+ /** @jsxImportSource @octane-xplat/jsx/dom */
137
+ export function BrowserGreeting() { return <p>Hello</p> }
138
+ ```
139
+
132
140
  ## Verify
133
141
 
134
142
  | Command | Catches |
@@ -141,7 +141,7 @@ Choose platform widgets when you want the OS's own controls. Import them only
141
141
  in the matching platform file. This iOS example belongs in `Preference.ios.tsrx`:
142
142
 
143
143
  ```tsrx
144
- /** @jsxImportSource @nativescript-community/octane */
144
+ /** @jsxImportSource @octane-xplat/jsx */
145
145
  import { useState } from 'octane'
146
146
  import { UISwitch } from '@octane-xplat/ui/ios'
147
147
 
@@ -154,7 +154,7 @@ export function Preference() {
154
154
  The Android equivalent belongs in `Preference.android.tsrx`:
155
155
 
156
156
  ```tsrx
157
- /** @jsxImportSource @nativescript-community/octane */
157
+ /** @jsxImportSource @octane-xplat/jsx */
158
158
  import { useState } from 'octane'
159
159
  import { MaterialSwitch } from '@octane-xplat/ui/android'
160
160
 
@@ -61,13 +61,13 @@ import { Scanner } from './parts/Scanner'
61
61
  export function ScanScreen() { return <Scanner /> }
62
62
  ```
63
63
 
64
- Native JSX files start with the NativeScript pragma on line 1. Import native
64
+ Native JSX files start with the shared JSX facade pragma on line 1. Import native
65
65
  APIs from `@nativescript/core`, and read app lifecycle state inside the call
66
66
  rather than during module initialization. This example belongs in
67
67
  `Status.mobile.tsrx`:
68
68
 
69
69
  ```tsrx
70
- /** @jsxImportSource @nativescript-community/octane */
70
+ /** @jsxImportSource @octane-xplat/jsx */
71
71
  import { Application } from '@nativescript/core'
72
72
  import { Pressable, Text } from '@octane-xplat/ui'
73
73
 
@@ -8,6 +8,53 @@ Before writing code, read `.agents/skills/xplat/SKILL.md` (or invoke the
8
8
  `xplat` skill). Its invariants are enforced by `pnpm lint` — violations
9
9
  usually still compile but silently break on one platform.
10
10
 
11
+ ## Framework versions
12
+
13
+ Keep framework/compiler/native tool pins in the supplied `catalog:xplat`.
14
+ Upgrade the version set and patches together, then run the app's checks.
15
+ Within a multi-package workspace, upgrade covers all packages. Preserve
16
+ app-specific dependencies and reconcile custom-patch conflicts before retrying:
17
+
18
+ ```sh
19
+ pnpm dlx @octane-xplat/cli@latest upgrade --check
20
+ pnpm dlx @octane-xplat/cli@latest upgrade
21
+ pnpm xplat doctor
22
+ pnpm typecheck
23
+ ```
24
+
25
+ ## JSX types
26
+
27
+ The app's `tsconfig.json` selects shared native JSX types. Browser JSX leaves
28
+ select DOM types on their first line; keep runtime imports from `octane`:
29
+
30
+ ```tsx
31
+ /** @jsxImportSource @octane-xplat/jsx/dom */
32
+ export function BrowserMessage() {
33
+ return <p>Ready</p>
34
+ }
35
+ ```
36
+
37
+ Native leaves can explicitly select the same default with
38
+ `/** @jsxImportSource @octane-xplat/jsx */`. Do not replace it with a renderer
39
+ pragma. The facade supplies types; Vite still selects the platform renderer.
40
+
41
+ ## App testing
42
+
43
+ The starter includes managed runners, sample tests, configuration, and scripts
44
+ for its enabled platforms. Adapt the samples to the app and run the relevant
45
+ commands:
46
+
47
+ ```sh
48
+ pnpm test:web
49
+ pnpm test:ios -- --device DEVICE_UDID
50
+ pnpm test:android -- --device DEVICE_SERIAL
51
+ ```
52
+
53
+ Only commands for enabled platforms are added. Web tests use jsdom; phone
54
+ tests need a selected simulator or device. Report targets actually run and
55
+ distinguish document checks from browser layout or OS input checks. See the
56
+ [testing guide](https://octane-xplat.goddardai.org/verify/testing#add-automated-tests).
57
+
11
58
  ## Stable automation targets
12
59
 
13
60
  ```tsx
@@ -4,7 +4,7 @@ dependencies {
4
4
  }
5
5
 
6
6
  android {
7
- compileSdkVersion 35
7
+ compileSdkVersion 36
8
8
  buildToolsVersion "35"
9
9
  // ndkVersion ""
10
10