create-octane-xplat 0.16.0 → 0.17.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.
package/README.md CHANGED
@@ -99,7 +99,7 @@ your app as you replace the starter screen.
99
99
 
100
100
  Platforms added later also receive their test setup. The library template
101
101
  includes its component tests and publishing checks.
102
- See [checking an app](../../docs/verify/testing.md#add-automated-tests) for the
102
+ See [checking an app](../../docs/testing/testing.md#add-automated-tests) for the
103
103
  commands, file conventions, and checks that still need a browser or phone.
104
104
 
105
105
  ## Keep compatible versions
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "Release compatibility set, shared by create-octane-xplat and xplat doctor/upgrade. Edit external pins here, then run pnpm sync:compatibility. Framework package names are generated from the lockstep release set. Optional packages are managed only when declared; library consumer peer ranges remain app-owned.",
3
3
  "schemaVersion": 1,
4
- "frameworkVersion": "0.16.0",
4
+ "frameworkVersion": "0.17.0",
5
5
  "tools": {
6
6
  "nodeMinimum": "22.18.0",
7
7
  "pnpm": "11.24.0"
@@ -29,6 +29,7 @@
29
29
  "@octane-xplat/image-crop",
30
30
  "@octane-xplat/intersection-observer",
31
31
  "@octane-xplat/jsx",
32
+ "@octane-xplat/keyboard",
32
33
  "@octane-xplat/lexical",
33
34
  "@octane-xplat/lib",
34
35
  "@octane-xplat/lingui",
@@ -65,7 +66,7 @@
65
66
  ],
66
67
  "packages": {
67
68
  "octane": {
68
- "version": "0.11.0",
69
+ "version": "0.14.0",
69
70
  "group": "runtime",
70
71
  "singleton": true
71
72
  },
@@ -108,7 +109,7 @@
108
109
  "group": "bundler"
109
110
  },
110
111
  "@nativescript/vite": {
111
- "version": "8.0.17",
112
+ "version": "8.0.20",
112
113
  "group": "bundler"
113
114
  },
114
115
  "nativescript": {
@@ -160,15 +161,15 @@
160
161
  "group": "plugin"
161
162
  },
162
163
  "@nativescript-community/ui-image": {
163
- "version": "5.0.17",
164
+ "version": "5.0.18",
164
165
  "group": "plugin"
165
166
  },
166
167
  "@valor/nativescript-websockets": {
167
168
  "version": "2.0.3",
168
169
  "group": "plugin"
169
170
  },
170
- "@triniwiz/nativescript-masonkit": {
171
- "version": "1.0.0-beta.110",
171
+ "@octane-xplat/nativescript-masonkit": {
172
+ "version": "1.0.0-beta.126",
172
173
  "group": "runtime"
173
174
  },
174
175
  "jsdom": {
package/dist/scaffold.js CHANGED
@@ -170,29 +170,6 @@ function composeManifest(ids) {
170
170
  }
171
171
  var PATCH_ENTRY = /^\s+"?(@?[\w./-]+@[\w.-]+)"?:\s+\S+\s*$/;
172
172
  var patchPkg = (key) => key.replace(/@[^@]+$/, "");
173
- function parsePatchEntries(lines) {
174
- const entries = [];
175
- let inPatches = false;
176
- let pending = [];
177
- for (const line of lines) {
178
- if (/^\S/.test(line)) {
179
- inPatches = line.startsWith("patchedDependencies");
180
- pending = [];
181
- continue;
182
- }
183
- if (!inPatches) continue;
184
- const m = line.match(PATCH_ENTRY);
185
- if (m) {
186
- entries.push({
187
- name: patchPkg(m[1]),
188
- comments: pending,
189
- line
190
- });
191
- pending = [];
192
- } else pending.push(line);
193
- }
194
- return entries;
195
- }
196
173
  /** The pnpm-workspace.yaml a given target set produces. */
197
174
  function composeWorkspaceYaml(ids) {
198
175
  const resolved = new Set(resolveTargets(ids));
@@ -237,7 +214,7 @@ function composeWorkspaceYaml(ids) {
237
214
  return String(doc);
238
215
  }
239
216
  /**
240
- * Add the patch entries that `enabledIds` owns to the app's
217
+ * Add the patch entries that `enabledIds` owns to the owning workspace's
241
218
  * pnpm-workspace.yaml. Entries already present (by package name, any
242
219
  * version) are kept. Returns true if the file changed.
243
220
  */
@@ -245,31 +222,36 @@ function ensurePatchEntries(appRoot, enabledIds) {
245
222
  const needed = /* @__PURE__ */ new Set();
246
223
  for (const id of enabledIds) for (const name of targets[id].patches ?? []) needed.add(name);
247
224
  if (!needed.size) return false;
248
- const yamlPath = join(appRoot, "pnpm-workspace.yaml");
225
+ let workspace = resolve(appRoot);
226
+ while (!existsSync(join(workspace, "pnpm-workspace.yaml")) && dirname(workspace) !== workspace) workspace = dirname(workspace);
227
+ if (!existsSync(join(workspace, "pnpm-workspace.yaml"))) workspace = appRoot;
228
+ const yamlPath = join(workspace, "pnpm-workspace.yaml");
249
229
  const appText = existsSync(yamlPath) ? readFileSync(yamlPath, "utf8") : "";
250
- const present = new Set(parsePatchEntries(appText.split("\n")).map((e) => e.name));
251
- const missing = [...needed].filter((name) => !present.has(name));
252
- if (!missing.length) return false;
253
- const blocks = parsePatchEntries(readFileSync(join(TEMPLATE_DIR, "pnpm-workspace.yaml"), "utf8").split("\n")).filter((e) => missing.includes(e.name)).map((e) => [...e.comments, e.line].join("\n"));
254
230
  if (!appText.trim()) {
255
231
  writeFileSync(yamlPath, composeWorkspaceYaml(enabledIds));
256
232
  return true;
257
233
  }
258
- let next = appText;
259
- const lines = next.split("\n");
260
- const sectionStart = lines.findIndex((l) => l.startsWith("patchedDependencies"));
261
- if (sectionStart === -1) next = `${appText.trimEnd()}\npatchedDependencies:\n${blocks.join("\n")}\n`;
262
- else {
263
- let insertAt = lines.length;
264
- for (let i = sectionStart + 1; i < lines.length; i++) if (/^\S/.test(lines[i])) {
265
- insertAt = i;
266
- break;
234
+ const doc = parseDocument(appText);
235
+ if (doc.errors.length) throw new Error(`${yamlPath}: ${doc.errors[0].message}`);
236
+ const present = new Set(Object.keys(doc.toJS()?.patchedDependencies ?? {}).map(patchPkg));
237
+ const template = parseDocument(readFileSync(join(TEMPLATE_DIR, "pnpm-workspace.yaml"), "utf8")).toJS();
238
+ let changed = false;
239
+ for (const [key, path] of Object.entries(template.patchedDependencies)) {
240
+ const name = patchPkg(key);
241
+ if (needed.has(name) && !present.has(name)) {
242
+ doc.setIn(["patchedDependencies", key], path);
243
+ changed = true;
267
244
  }
268
- lines.splice(insertAt, 0, ...blocks.join("\n").split("\n"));
269
- next = lines.join("\n");
270
245
  }
271
- writeFileSync(yamlPath, next.endsWith("\n") ? next : next + "\n");
272
- return true;
246
+ for (const [name, spec] of Object.entries(template.configDependencies)) {
247
+ const prefix = `node_modules/.pnpm-config/${name}/`;
248
+ if (Object.values(doc.toJS()?.patchedDependencies ?? {}).some((path) => typeof path === "string" && path.startsWith(prefix)) && !doc.hasIn(["configDependencies", name])) {
249
+ doc.setIn(["configDependencies", name], spec);
250
+ changed = true;
251
+ }
252
+ }
253
+ if (changed) writeFileSync(yamlPath, String(doc));
254
+ return changed;
273
255
  }
274
256
  /**
275
257
  * The tsconfig.json a given target set produces. One program covers every
@@ -73,8 +73,8 @@
73
73
  "peerDependencies": {
74
74
  "@nativescript-community/octane": ">=0.2.1 <1",
75
75
  "@nativescript/core": ">=9.1.0 <10",
76
- "@octane-xplat/jsx": "^0.16.0",
77
- "@octane-xplat/ui": "^0.16.0",
76
+ "@octane-xplat/jsx": "^0.17.0",
77
+ "@octane-xplat/ui": "^0.17.0",
78
78
  "octane": ">=0.6.3 <1"
79
79
  },
80
80
  "peerDependenciesMeta": {
@@ -9,13 +9,13 @@ catalogs:
9
9
  "@nativescript-community/octane": 0.2.5
10
10
  "@nativescript/core": 9.1.3
11
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
12
+ "@octane-xplat/cli": 0.17.0
13
+ "@octane-xplat/jsx": 0.17.0
14
+ "@octane-xplat/ui": 0.17.0
15
15
  "@octanejs/vite-plugin": 0.1.61
16
16
  "@tsrx/oxc": 0.20.0
17
17
  "@tsrx/typescript-plugin": 0.4.11
18
- octane: 0.11.0
18
+ octane: 0.14.0
19
19
  tsrx-typegen: 0.2.2
20
20
  typescript: 5.9.3
21
21
  vite: 8.3.0
@@ -25,23 +25,23 @@ overrides:
25
25
  "@nativescript-community/vite-octane>esbuild": 0.27.7
26
26
  vite>esbuild: 0.27.7
27
27
  configDependencies:
28
- "@octane-xplat/patches": 0.16.0
28
+ "@octane-xplat/patches": 0.17.0
29
29
  allowUnusedPatches: true
30
30
  # Framework patches are supplied by the @octane-xplat/patches config dependency and
31
31
  # applied via pnpm patchedDependencies. Managed upstream by @octane-xplat/cli (`xplat
32
32
  # patches apply` refreshes, `xplat patches check` verifies); do not edit by hand.
33
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
34
+ # Published 0.14.0 still requires renderer-owned Children.toArray/map with explicit
35
+ # universal-native re-exports for older runtimes, universal signal-read tracking and local
36
+ # hooks, root event-error forwarding, the complete universal imperative-ref lifecycle, the
37
+ # Descriptor declaring-owner reader fallback, and the signal-owner/enclosingOwner chain so
38
+ # child reads reuse instance declarations. Rebased against the registry 0.14.0 tarball,
39
+ # including local signal-hook registration in the split runtime/hooks modules. Provider
40
+ # null/undefined descendant cleanup is now upstream and no longer patched. Marks generic
41
+ # binding bags from bagOf so fragment commit rehomes their DOM parent references,
42
+ # preventing stale fragment parents, namespace recursion, and invalid insertion during
43
+ # typeahead updates.
44
+ octane@0.14.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.14.0.patch
45
45
  # Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
46
46
  # flex alignment, so those hunks are removed. Retains the unshipped parent-padding
47
47
  # measure-spec fix and Android explicit-size flex alignment, iOS continuous corner
@@ -55,58 +55,53 @@ patchedDependencies:
55
55
  # _populateRules. Adds atlayer() covering block and statement forms. Applies to a
56
56
  # transitive dep — no app declares it directly.
57
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
58
+ # Remaining dev-server fixes on @nativescript/vite 8.0.20: collect re-export specifiers in
59
+ # the .vite/deps rewrite; route outside-roots /@fs paths through /ns/m; probe non-active
60
+ # platform and native suffixes; redirect bare octane to octane/universal/native in
61
+ # deps-bundle esbuild passes; add .mobile to tsconfig-paths and generated checker
62
+ # suffixes; scan the app-local pnpm virtual store for served node_modules URLs; ignore
63
+ # per-package turbo.json as workspace markers; resolve # package-imports specifiers in
64
+ # served-module rewrites; preserve, compile, and hot-update virtual stylesheets
65
+ # (NativeScript#11551); and inspect ESM export declarations/specifiers for native
66
+ # default-import validation instead of matching comments, strings, or a narrow export
67
+ # syntax (NativeScript#11566). Shared-source graph and hot-context IDs now use the same
68
+ # app/workspace/@fs paths as served imports, preserving distinct roots
69
+ # (NativeScript#11569); extensionless workspace requests probe filesystem-anchored
70
+ # extension/platform candidates so hot reimports can fetch shared modules
71
+ # (NativeScript#11570). Linked-checkout discovery and candidate anchoring shipped in
72
+ # 8.0.20 (NativeScript#11559), so those local hunks are removed.
73
+ "@nativescript/vite@8.0.20": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.20.patch
91
74
  # Retains the workspace's NativeScript driver fixes when upgrading to 0.2.5 for portal
92
75
  # support: a prop dropped from an update's complete prop bag — or set to
93
76
  # `undefined`/`null` — is a removal that restores the documented default
94
77
  # (Property/CssProperty setters get `unsetValue`, plain fields the captured pre-prop
95
78
  # 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.
79
+ # Synced with PR #14: registered setters are identified before resetting (ordinary fields
80
+ # never receive the sentinel), inline removal refreshes matching class CSS through the
81
+ # isolated core 9.1 `_cssState.onChange` hook, CSS variables use public removal, strings
82
+ # use the core CSS parser, and replacement views capture fresh defaults. Also advertises
83
+ # the implemented visibility capability, carries Octane callback ref types, and preserves
84
+ # authored Grid row/col/span metadata. Hosts custom-layout containers (e.g. MasonKit
85
+ # ViewBase) that own child lists via insertChild/removeChild/getChildrenCount without
86
+ # subclassing LayoutBase. Additionally, the masonry cutover: masonview resolves to
87
+ # MasonKit View and scrollview to MasonKit Scroll as the only layout containers, with zero
88
+ # layout props — every former prop name warns-and-drops and all CSS semantics arrive
89
+ # through the style bag; foreign native leaves mirror resolved class and inline layout
90
+ # styles through Style.fromView, observe longhand changes and native recreation, and
91
+ # explicitly clear removed placement — removal restores the CSS cascade, so a later
92
+ # sibling attachment cannot resurrect removed tracks. Container alignment uses the core
93
+ # stretch default without a local style override, allowing class-authored centering.
94
+ # Inline style resets reapply the matching CSS cascade. Re-exports Octane's ClassValue
95
+ # unchanged and stringifies truthy numeric class values while omitting 0 and NaN, matching
96
+ # Octane 0.11.
105
97
  "@nativescript-community/octane@0.2.5": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.5.patch
106
98
  # octaneServerStrategy's SCRIPT_FILE_RE only covers [jt]sx? — .tsrx edits never emit the
107
99
  # deferred ns:hmr-delta broadcast, so component HMR silently never reaches the device.
108
100
  # Also counts open /ns-hmr sockets so the update summary stops reporting recipients=0 for
109
- # delivered updates.
101
+ # delivered updates. Includes shared script sources in transaction ordering while
102
+ # excluding dependencies, tests and declarations. External-source edits use the absolute
103
+ # source for graph identity and a Vite /@fs transform URL instead of treating
104
+ # parent-relative paths as app URLs.
110
105
  "@nativescript-community/vite-octane@0.2.1": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__vite-octane@0.2.1.patch
111
106
  # Enables `resolveHiddenExtensions` on the language plugin's typescript facet so
112
107
  # extensionless specifiers (e.g. './Text') probe `.tsrx` after moduleSuffixes — required
@@ -130,27 +125,21 @@ patchedDependencies:
130
125
  # boot). Same fix as linkifyjs: try/catch new RegExp with an ASCII-range fallback — the
131
126
  # matcher only feeds autolink, which native doesn't use.
132
127
  "@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
128
  # @nativescript/doctor's Android runtime check reads the app's raw devDependencies
146
129
  # specifier and npm dist-tag resolves anything that isn't a semver range — a pnpm
147
130
  # `catalog:` pin throws 'The determined Android runtime version ... is not valid' and
148
131
  # kills `ns build android` before toolchain checks. Resolve catalog specifiers through the
149
132
  # installed package's manifest, reusing the existing tgz-branch mechanism.
150
133
  "@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
134
+ # shaders@4.0.0 inlines TypeGPU 0.12.3 into dist/core/std-ClftfWpF.js, which declares
135
+ # /\p{XID_Start}/ and /\p{XID_Continue}/ regex literals at module scope (WGSL identifier
136
+ # validation, external prop-chain matching, minify separator check). NativeScript's
137
+ # embedded V8 on iOS (@nativescript/ios 9.1.0) and Android (@nativescript/android 9.1.1,
138
+ # V8 14.9.207.39) is built without ICU — every \p{…} escape is a SyntaxError, even in a
139
+ # never-called function, so a literal fails the whole module at parse time and any bundled
140
+ # app importing `shaders/js` dies on boot. Rewriting to \p{ID_Start}/\p{L} would not help
141
+ # — all property escapes are rejected. The patch builds the regexes via try/catch new
142
+ # RegExp with ASCII/non-ASCII-range fallbacks — byte-identical .source where ICU exists.
143
+ # dist/js/bundle.js, react/bundle.js, and cli.js carry the same escapes but are
144
+ # unreachable from `shaders/js` and left unpatched.
145
+ shaders@4.0.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/shaders@4.0.0.patch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-octane-xplat",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Scaffold Octane Xplat apps and cross-platform libraries",
5
5
  "keywords": [
6
6
  "component-library",
package/scaffold.ts CHANGED
@@ -252,35 +252,6 @@ const PATCH_ENTRY = /^\s+"?(@?[\w./-]+@[\w.-]+)"?:\s+\S+\s*$/
252
252
 
253
253
  const patchPkg = (key) => key.replace(/@[^@]+$/, '')
254
254
 
255
- // Split a pnpm-workspace.yaml into per-package patch blocks (leading comment
256
- // lines included) so compose can drop entries and apply can transplant them.
257
- function parsePatchEntries(lines) {
258
- const entries: any[] = []
259
- let inPatches = false
260
- let pending: string[] = []
261
- for (const line of lines) {
262
- if (/^\S/.test(line)) {
263
- inPatches = line.startsWith('patchedDependencies')
264
- pending = []
265
- continue
266
- }
267
-
268
- if (!inPatches) {
269
- continue
270
- }
271
-
272
- const m = line.match(PATCH_ENTRY)
273
- if (m) {
274
- entries.push({ name: patchPkg(m[1]), comments: pending, line })
275
- pending = []
276
- } else {
277
- pending.push(line)
278
- }
279
- }
280
-
281
- return entries
282
- }
283
-
284
255
  /** The pnpm-workspace.yaml a given target set produces. */
285
256
  export function composeWorkspaceYaml(ids) {
286
257
  const resolved = new Set(resolveTargets(ids))
@@ -341,7 +312,7 @@ export function composeWorkspaceYaml(ids) {
341
312
  }
342
313
 
343
314
  /**
344
- * Add the patch entries that `enabledIds` owns to the app's
315
+ * Add the patch entries that `enabledIds` owns to the owning workspace's
345
316
  * pnpm-workspace.yaml. Entries already present (by package name, any
346
317
  * version) are kept. Returns true if the file changed.
347
318
  */
@@ -357,45 +328,57 @@ function ensurePatchEntries(appRoot, enabledIds) {
357
328
  return false
358
329
  }
359
330
 
360
- const yamlPath = join(appRoot, 'pnpm-workspace.yaml')
361
- const appText = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : ''
362
- const present = new Set(parsePatchEntries(appText.split('\n')).map((e) => e.name))
363
- const missing = [...needed].filter((name) => !present.has(name))
364
- if (!missing.length) {
365
- return false
331
+ // Match catalog ownership: adding a target must not split an existing
332
+ // monorepo into a new, nested workspace.
333
+ let workspace = resolve(appRoot)
334
+ while (!existsSync(join(workspace, 'pnpm-workspace.yaml')) && dirname(workspace) !== workspace) {
335
+ workspace = dirname(workspace)
366
336
  }
367
-
368
- const tplText = readFileSync(join(TEMPLATE_DIR, 'pnpm-workspace.yaml'), 'utf8')
369
- const blocks = parsePatchEntries(tplText.split('\n'))
370
- .filter((e) => missing.includes(e.name))
371
- .map((e) => [...e.comments, e.line].join('\n'))
372
-
337
+ if (!existsSync(join(workspace, 'pnpm-workspace.yaml'))) {
338
+ workspace = appRoot
339
+ }
340
+ const yamlPath = join(workspace, 'pnpm-workspace.yaml')
341
+ const appText = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : ''
373
342
  if (!appText.trim()) {
374
343
  writeFileSync(yamlPath, composeWorkspaceYaml(enabledIds))
375
344
  return true
376
345
  }
377
346
 
378
- let next = appText
379
- const lines = next.split('\n')
380
- const sectionStart = lines.findIndex((l) => l.startsWith('patchedDependencies'))
381
- if (sectionStart === -1) {
382
- next = `${appText.trimEnd()}\npatchedDependencies:\n${blocks.join('\n')}\n`
383
- } else {
384
- // Insert before the next top-level key (or at EOF).
385
- let insertAt = lines.length
386
- for (let i = sectionStart + 1; i < lines.length; i++) {
387
- if (/^\S/.test(lines[i])) {
388
- insertAt = i
389
- break
390
- }
347
+ const doc = parseDocument(appText)
348
+ if (doc.errors.length) {
349
+ throw new Error(`${yamlPath}: ${doc.errors[0].message}`)
350
+ }
351
+ const present = new Set(Object.keys(doc.toJS()?.patchedDependencies ?? {}).map(patchPkg))
352
+ const template = parseDocument(
353
+ readFileSync(join(TEMPLATE_DIR, 'pnpm-workspace.yaml'), 'utf8'),
354
+ ).toJS()
355
+ let changed = false
356
+ for (const [key, path] of Object.entries(template.patchedDependencies)) {
357
+ const name = patchPkg(key)
358
+ if (needed.has(name) && !present.has(name)) {
359
+ doc.setIn(['patchedDependencies', key], path)
360
+ changed = true
391
361
  }
362
+ }
363
+
364
+ // Template paths are relative to the owning workspace. pnpm installs the
365
+ // config package there before resolving its patch files on the first install.
366
+ for (const [name, spec] of Object.entries(template.configDependencies)) {
367
+ const prefix = `node_modules/.pnpm-config/${name}/`
368
+ const usesPackage = Object.values(doc.toJS()?.patchedDependencies ?? {}).some(
369
+ (path) => typeof path === 'string' && path.startsWith(prefix),
370
+ )
392
371
 
393
- lines.splice(insertAt, 0, ...blocks.join('\n').split('\n'))
394
- next = lines.join('\n')
372
+ if (usesPackage && !doc.hasIn(['configDependencies', name])) {
373
+ doc.setIn(['configDependencies', name], spec)
374
+ changed = true
375
+ }
395
376
  }
396
377
 
397
- writeFileSync(yamlPath, next.endsWith('\n') ? next : next + '\n')
398
- return true
378
+ if (changed) {
379
+ writeFileSync(yamlPath, String(doc))
380
+ }
381
+ return changed
399
382
  }
400
383
 
401
384
  /**
@@ -151,7 +151,7 @@ export function BrowserGreeting() { return <p>Hello</p> }
151
151
  - `references/components.md` — the component vocabulary plus per-platform
152
152
  divergences and name traps.
153
153
  - `references/styling.md` — tokens, the portable CSS subset, dark mode.
154
- - `references/navigation.md` — route table, links, params, stacks.
154
+ - `references/navigation.md` — file routes, links, params, stacks, modals.
155
155
  - `references/platform.md` — device services and writing your own leaf.
156
156
  - Current limits and platform boundaries:
157
157
  https://octane-xplat.goddardai.org/known-limits
@@ -31,6 +31,12 @@ export function Welcome() {
31
31
  | Shells | `Screen`, `Tabs` |
32
32
  | Theme and measurement | `useColorScheme`, `useSafeAreaInsets`, `useMeasure` |
33
33
 
34
+ Native pages start full-screen: app content extends under the status bar
35
+ and home indicator, and `SafeArea` is the only source of safe-area padding —
36
+ wrap screen content that must stay clear of the system bars. A custom
37
+ NativeScript page needs `prepareFullScreenPage(page)` from
38
+ `@octane-xplat/ui/nativescript` before its content renders.
39
+
34
40
  For images, prefer the `Image` from `@octane-xplat/image` — it has the exact
35
41
  same props as `@octane-xplat/ui`'s `Image` but loads through a real image
36
42
  engine on native (Glide on Android, SDWebImage on iOS) with a sized memory +
@@ -1,87 +1,148 @@
1
1
  # Navigation
2
2
 
3
- Register screens once during app startup. A route names a screen and a stack;
4
- web turns it into a URL, while native presents the screen in the app's navigation.
5
- Put components in `.tsrx` files.
3
+ Routes are files under `src/app`, not registered lists. A `[param]` segment
4
+ becomes a typed `:param`. Write screens in `.tsrx` files and type callbacks
5
+ with the generated `routes.gen.contracts` module:
6
6
 
7
7
  ```tsrx
8
+ // src/app/items/[id].tsrx
8
9
  import { Text } from '@octane-xplat/ui'
9
- import { defineRoutes, registerRoutes } from '@octane-xplat/ui/routing'
10
+ import type { RouteProps, RouteLoaderArgs } from '../../routes.gen.contracts'
10
11
 
11
- function Home() { return <Text>Home</Text> }
12
- function Settings() { return <Text>Settings</Text> }
12
+ export function loader({ params }: RouteLoaderArgs<'/items/:id'>) {
13
+ return { title: 'Item ' + params.id }
14
+ }
13
15
 
14
- registerRoutes(defineRoutes([
15
- { path: '/', screen: Home },
16
- { path: '/settings', screen: Settings },
17
- ]))
16
+ export function screen(props: RouteProps<'/items/:id'>) {
17
+ return <Text>{props.data.title}</Text>
18
+ }
18
19
  ```
19
20
 
20
- After registering `settings`, use `NavLink` for a screen link or `pushRoute`
21
- in an event handler. `Link` accepts a URL, including an external website.
21
+ Regenerate the catalog after adding or renaming route files (the dev and
22
+ build commands also run it):
23
+
24
+ ```sh
25
+ pnpm exec xplat routes --dir src/app
26
+ ```
27
+
28
+ Import the generated client once in your entry module and place `RouteOutlet`
29
+ around the content that forms the home screen — the starter's `App` keeps its
30
+ own content and wraps it:
22
31
 
23
32
  ```tsrx
24
- import { HStack, Link, NavLink, Pressable, Text } from '@octane-xplat/ui'
25
- import { pushRoute } from '@octane-xplat/ui/routing'
33
+ // src/App.tsrx
34
+ import './routes.gen'
35
+ import { NavLink, RouteOutlet, Text } from '@octane-xplat/ui'
36
+
37
+ export function App() {
38
+ return <RouteOutlet>
39
+ <Text>My collection</Text>
40
+ <NavLink to="/items/:id" params={{ id: 'coat' }}><Text>Open coat</Text></NavLink>
41
+ </RouteOutlet>
42
+ }
43
+ ```
44
+
45
+ Use `NavLink` for a screen link or `useRouter()` in an event handler.
46
+ Destinations are objects: a declared `to` pattern plus `params`. `Link`
47
+ accepts a URL, including an external website.
48
+
49
+ ```tsrx
50
+ // src/SettingsLinks.tsrx
51
+ import { HStack, Link, Pressable, Text } from '@octane-xplat/ui'
52
+ import { useRouter } from '@octane-xplat/ui/routing'
26
53
 
27
54
  export function SettingsLinks() {
28
- const settings = { stack: 'root', name: 'settings', params: {} }
55
+ const navigation = useRouter()
29
56
  return <HStack>
30
- <NavLink route={settings}><Text>Settings</Text></NavLink>
31
- <Pressable onPress={() => pushRoute(settings)}><Text>Open settings</Text></Pressable>
57
+ <Pressable onPress={() => navigation.push({ to: '/items/:id', params: { id: '42' } })}>
58
+ <Text>Open item</Text>
59
+ </Pressable>
32
60
  <Link href="https://octane-xplat.goddardai.org"><Text>Documentation</Text></Link>
33
61
  </HStack>
34
62
  }
35
63
  ```
36
64
 
37
- Use `useRoute` to read the current screen and `useCanGoBack` to decide whether
38
- to offer a back button. Pass the same stack name to `popRoute`.
65
+ `router.push` returns a result, not void: check `status === 'committed'`,
66
+ then `await result.finished` only when animation settlement matters.
67
+ `router.back()` pops one entry, `router.dismiss()` closes a modal,
68
+ `useCanGoBack` gates a Back control, and `useRouterState()` reads the current
69
+ route's declared `to` pattern. `router.navigateUrl(url)` admits an incoming
70
+ absolute or relative URL.
39
71
 
40
72
  ```tsrx
73
+ // src/NavigationStatus.tsrx
41
74
  import { Pressable, Text } from '@octane-xplat/ui'
42
- import { useRoute, useCanGoBack, popRoute } from '@octane-xplat/ui/routing'
75
+ import { useCanGoBack, useRouter, useRouterState } from '@octane-xplat/ui/routing'
43
76
 
44
77
  export function NavigationStatus() {
45
- const route = useRoute('root')
46
- const canGoBack = useCanGoBack('root')
78
+ const navigation = useRouter()
79
+ const route = useRouterState().route
80
+ const canGoBack = useCanGoBack()
47
81
  return <>
48
- <Text>{route?.name ?? 'Home'}</Text>
49
- <Pressable disabled={!canGoBack} onPress={() => popRoute('root')}>
82
+ <Text>{route?.to ?? 'Home'}</Text>
83
+ <Pressable disabled={!canGoBack} onPress={() => navigation.back()}>
50
84
  <Text>Back</Text>
51
85
  </Pressable>
52
86
  </>
53
87
  }
54
88
  ```
55
89
 
56
- Prefer string IDs in route params: generated route helpers use string params,
57
- and IDs make URLs shareable. The low-level router JSON-encodes object params
58
- on web with a warning; do not rely on it to carry application state. With a
59
- registered `/projects/:id` screen, navigate using the project ID:
90
+ A guard runs in `beforeLoad` before the attempted route commits: return context
91
+ values, `redirect({ to, params, search })`, or throw `RouteForbidden`. Type the
92
+ args with the route's generated `RouteGuardArgs`:
60
93
 
61
- ```ts
62
- import { pushRoute } from '@octane-xplat/ui/routing'
94
+ ```tsrx
95
+ // src/app/items/[id].tsrx
96
+ import { redirect } from '@octane-xplat/ui/routing'
97
+ import type { RouteGuardArgs } from '../../routes.gen.contracts'
63
98
 
64
- pushRoute({ stack: 'root', name: 'projects/:id', params: { id: '42' } })
99
+ export function beforeLoad({ params }: RouteGuardArgs<'/items/:id'>) {
100
+ if (params.id === 'old-coat') redirect({ to: '/items/:id', params: { id: 'coat' } })
101
+ return { checked: true }
102
+ }
65
103
  ```
66
104
 
67
- Named tab stacks keep each tab's navigation separate. `Tabs` renders the tab's
68
- initial content; pushing a route into that stack opens it inside the tab pane.
69
- The screen below assumes `settings` is already registered.
105
+ Prefer string IDs in params: generated contracts type params as strings and
106
+ IDs make URLs shareable. Search is validated per route with `validateSearch`
107
+ (return `as const`); screens update it with `useUpdateSearch()` inside a
108
+ component via the route's `getRouteApi` accessor.
109
+
110
+ ```ts
111
+ // src/app/items/[id].tsrx — search schema for the same route file
112
+ export function validateSearch(raw: { tab?: unknown }) {
113
+ const tab = raw.tab ?? 'details'
114
+ if (tab !== 'details' && tab !== 'activity') {
115
+ throw new Error('Invalid tab')
116
+ }
117
+ return { tab } as const
118
+ }
119
+ ```
70
120
 
71
121
  ```tsrx
72
- import { Tabs, Text, Pressable } from '@octane-xplat/ui'
73
- import { pushRoute } from '@octane-xplat/ui/routing'
74
-
75
- export function AppTabs() {
76
- return <Tabs tabs={[
77
- { title: 'Home', stack: 'home', render: () => <Text>Home</Text> },
78
- { title: 'Account', stack: 'account', render: () =>
79
- <Pressable onPress={() => pushRoute({ stack: 'account', name: 'settings', params: {} })}>
80
- <Text>Settings</Text>
81
- </Pressable> },
82
- ]} />
122
+ // src/ActivityFilter.tsrx
123
+ import { Button } from '@octane-xplat/ui'
124
+ import { getRouteApi } from '@octane-xplat/ui/routing'
125
+
126
+ const item = getRouteApi('/items/:id')
127
+
128
+ export function ActivityFilter() {
129
+ const updateSearch = item.useUpdateSearch()
130
+ return <Button label="Show activity" clickAction={() => {
131
+ void updateSearch(previous => ({ ...previous, tab: 'activity' }))
132
+ }} />
83
133
  }
84
134
  ```
85
135
 
86
- For loaders, deep links, route config, and generated route types, see the
87
- [navigation guide](https://octane-xplat.goddardai.org/navigation).
136
+ Named stacks keep each tab's history separate: declare `"stacks": ["catalog",
137
+ "account"]` in `xplat.routes.json`, render `Tabs`, and address a stack with
138
+ `stack:` in the destination or `router.forStack(name)`:
139
+
140
+ ```ts
141
+ await router.forStack('catalog').push({ to: '/items/:id', params: { id: 'coat' } })
142
+ ```
143
+
144
+ `presentation: 'modal'` on a destination (or a `+modal` route-file suffix)
145
+ opens an owned modal flow the host dismisses as one surface.
146
+
147
+ For deep links, transitions, file-route details, and generated contracts, see
148
+ the [navigation guide](https://octane-xplat.goddardai.org/navigation).
@@ -53,7 +53,7 @@ pnpm test:android -- --device DEVICE_SERIAL
53
53
  Only commands for enabled platforms are added. Web tests use jsdom; phone
54
54
  tests need a selected simulator or device. Report targets actually run and
55
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).
56
+ [testing guide](https://octane-xplat.goddardai.org/testing#add-automated-tests).
57
57
 
58
58
  ## Stable automation targets
59
59
 
@@ -73,11 +73,11 @@ than array positions. Preserve `id` values, useful accessibility labels and
73
73
  roles. Do not put IDs on every decorative child or expose personal data.
74
74
  Inputs target their editable host; overlays target mounted content. Verify
75
75
  forwarding when composing another component; inherited types alone do not
76
- promise platform support. See [qualified hosts and mappings](https://octane-xplat.goddardai.org/verify/test-identifiers).
76
+ promise platform support. See [qualified hosts and mappings](https://octane-xplat.goddardai.org/test-identifiers).
77
77
 
78
78
  Argent is optional AI-agent device/browser control through MCP, with no app
79
79
  runtime dependency. Use [official setup](https://docs.swmansion.com/argent/docs/fundamentals/installation/)
80
- and the [qualified local recipe](https://octane-xplat.goddardai.org/verify/argent)
80
+ and the [qualified local recipe](https://octane-xplat.goddardai.org/argent)
81
81
  (version 0.27.0). Prefer stable IDs for reliable targeting while keeping
82
82
  spoken accessibility semantics. Discovery and replay have different native
83
83
  prerequisites. Chromium prefers DOM `id` over `data-testid` when both exist.
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
6
  <title>octane-xplat app</title>
7
7
  </head>
8
8
  <body>
@@ -26,7 +26,7 @@
26
26
  "@octane-xplat/jsx": "catalog:xplat",
27
27
  "@octane-xplat/platform": "catalog:xplat",
28
28
  "@octane-xplat/ui": "catalog:xplat",
29
- "@triniwiz/nativescript-masonkit": "catalog:xplat",
29
+ "@octane-xplat/nativescript-masonkit": "catalog:xplat",
30
30
  "@valor/nativescript-websockets": "catalog:xplat",
31
31
  "octane": "catalog:xplat"
32
32
  },
@@ -21,17 +21,17 @@ allowUnusedPatches: true
21
21
  # applied via pnpm patchedDependencies. Managed upstream by @octane-xplat/cli (`xplat
22
22
  # patches apply` refreshes, `xplat patches check` verifies); do not edit by hand.
23
23
  patchedDependencies:
24
- # Published 0.11.0 still ships none of the retained fixes: it adds the document guard
25
- # context around installDefaultSignalOwner and reflows the compile.js hook-classification
26
- # sites, but every patched feature remains absent upstream. Retains renderer-owned
27
- # Children.toArray/map with explicit universal-native re-exports for older runtimes,
28
- # universal signal-read tracking and local hooks, root event-error forwarding, the
29
- # complete universal imperative-ref lifecycle, the Descriptor declaring-owner reader
30
- # fallback, and the signal-owner/enclosingOwner chain so child reads reuse instance
31
- # declarations. Verified against the registry 0.11.0 tarball; all hunks re-anchored to the
32
- # rewritten compile.js and document-owner call sites. Disposes DOM provider descendants
33
- # when children become null or undefined, releasing their refs and effects.
34
- octane@0.11.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.11.0.patch
24
+ # Published 0.14.0 still requires renderer-owned Children.toArray/map with explicit
25
+ # universal-native re-exports for older runtimes, universal signal-read tracking and local
26
+ # hooks, root event-error forwarding, the complete universal imperative-ref lifecycle, the
27
+ # Descriptor declaring-owner reader fallback, and the signal-owner/enclosingOwner chain so
28
+ # child reads reuse instance declarations. Rebased against the registry 0.14.0 tarball,
29
+ # including local signal-hook registration in the split runtime/hooks modules. Provider
30
+ # null/undefined descendant cleanup is now upstream and no longer patched. Marks generic
31
+ # binding bags from bagOf so fragment commit rehomes their DOM parent references,
32
+ # preventing stale fragment parents, namespace recursion, and invalid insertion during
33
+ # typeahead updates.
34
+ octane@0.14.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.14.0.patch
35
35
  # Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
36
36
  # flex alignment, so those hunks are removed. Retains the unshipped parent-padding
37
37
  # measure-spec fix and Android explicit-size flex alignment, iOS continuous corner
@@ -45,58 +45,53 @@ patchedDependencies:
45
45
  # _populateRules. Adds atlayer() covering block and statement forms. Applies to a
46
46
  # transitive dep — no app declares it directly.
47
47
  css@3.0.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/css@3.0.0.patch
48
- # Dev-server fixes still missing after @nativescript/vite 8.0.17 added percent decoding
49
- # for path-style /ns/m and Vue /ns/sfc requests (NativeScript#11483): collect re-export
50
- # specifiers in the .vite/deps rewrite, route /@fs paths outside both roots through /ns/m
51
- # (linked checkouts bypass the device transform upstream), and probe the non-active
52
- # platform + `native` suffix tags in module candidates. The deps-bundle esbuild passes
53
- # ignore vite resolve.alias, so a dep importing bare `octane` bundles the DOM runtime
54
- # beside the universal one (hooks crash on boot, octane-xplat#3) — an onResolve plugin
55
- # redirects `octane` → `octane/universal/native`. tsconfig-paths and the generated
56
- # build-time checker moduleSuffixes also need the `.mobile` tier for iOS/Android/visionOS
57
- # so `.mobile` leaves resolve under mapped imports and `ns build`. Served `/node_modules/`
58
- # tails now also scan pnpm's `.pnpm` store and `link:`/`file:`/`portal:` dep roots — a
59
- # linked package's transitive deps (e.g. @iconify/utils under @octane-xplat/icons) resolve
60
- # nowhere else and 404 the dev session. findMonorepoWorkspaceRoot no longer treats a lone
61
- # turbo.json as a workspace root — Turborepo's per-package task configs marked apps/* as
62
- # roots, masking the real pnpm root so served `/node_modules/` tails never reached the
63
- # `.pnpm` store scan and non-hoisted transitive deps like source-map-js 404'd dev boot.
64
- # Linked dependency fallbacks also discover each linked package's enclosing workspace and
65
- # reuse the cached, bounded workspace node_modules scan so transitive workspace siblings
66
- # (auth to secure-storage to a private native plugin) resolve without the consumer
67
- # supplying that workspace root. Transform candidates now keep root-relative URLs only
68
- # when they identify the discovered file; nested linked-dependency and pnpm-store matches
69
- # inside the app root use their actual filesystem paths, preserving query suffixes. The
70
- # /ns/m import rewriter now resolves `#`-prefixed package `imports` specifiers through the
71
- # importer's own package.json — raw-read node_modules files (e.g. octane's
72
- # dist/signals/graph.js importing `#octane/dist/signal-actions/graph`) otherwise reach the
73
- # device verbatim and the HTTP loader throws `Cannot find module`, deadlocking dev boot
74
- # before any boot recording can bundle the dep. NativeScript#11551: preserve user virtual
75
- # stylesheet imports while retaining browser-helper filtering, route virtual CSS through
76
- # the native inline compiler/registration path before the generic virtual-module shortcut,
77
- # and relay Vite virtual stylesheet updates as tagged native CSS updates through the same
78
- # adapted-text endpoint. Empty sheets clear previous rules; CSS compilation failures
79
- # return HTTP 500 instead of raw CSS.
80
- "@nativescript/vite@8.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.17.patch
48
+ # Remaining dev-server fixes on @nativescript/vite 8.0.20: collect re-export specifiers in
49
+ # the .vite/deps rewrite; route outside-roots /@fs paths through /ns/m; probe non-active
50
+ # platform and native suffixes; redirect bare octane to octane/universal/native in
51
+ # deps-bundle esbuild passes; add .mobile to tsconfig-paths and generated checker
52
+ # suffixes; scan the app-local pnpm virtual store for served node_modules URLs; ignore
53
+ # per-package turbo.json as workspace markers; resolve # package-imports specifiers in
54
+ # served-module rewrites; preserve, compile, and hot-update virtual stylesheets
55
+ # (NativeScript#11551); and inspect ESM export declarations/specifiers for native
56
+ # default-import validation instead of matching comments, strings, or a narrow export
57
+ # syntax (NativeScript#11566). Shared-source graph and hot-context IDs now use the same
58
+ # app/workspace/@fs paths as served imports, preserving distinct roots
59
+ # (NativeScript#11569); extensionless workspace requests probe filesystem-anchored
60
+ # extension/platform candidates so hot reimports can fetch shared modules
61
+ # (NativeScript#11570). Linked-checkout discovery and candidate anchoring shipped in
62
+ # 8.0.20 (NativeScript#11559), so those local hunks are removed.
63
+ "@nativescript/vite@8.0.20": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.20.patch
81
64
  # Retains the workspace's NativeScript driver fixes when upgrading to 0.2.5 for portal
82
65
  # support: a prop dropped from an update's complete prop bag — or set to
83
66
  # `undefined`/`null` — is a removal that restores the documented default
84
67
  # (Property/CssProperty setters get `unsetValue`, plain fields the captured pre-prop
85
68
  # value, `style` keys diff per key); a never-applied `undefined` still skips the write.
86
- # Also advertises the implemented visibility capability, carries Octane callback ref
87
- # types, and preserves authored Grid row/col/span metadata. Hosts custom-layout containers
88
- # (e.g. MasonKit ViewBase) that own child lists via
89
- # insertChild/removeChild/getChildrenCount without subclassing LayoutBase. Additionally,
90
- # the masonry cutover: masonview resolves to MasonKit View and scrollview to MasonKit
91
- # Scroll as the only layout containers, with zero layout props — every former prop name
92
- # warns-and-drops and all CSS semantics arrive through the style bag; style keys on a
93
- # mason child route to its leaf (stashed pre-attach, replayed at attach) via
94
- # Style.fromView.
69
+ # Synced with PR #14: registered setters are identified before resetting (ordinary fields
70
+ # never receive the sentinel), inline removal refreshes matching class CSS through the
71
+ # isolated core 9.1 `_cssState.onChange` hook, CSS variables use public removal, strings
72
+ # use the core CSS parser, and replacement views capture fresh defaults. Also advertises
73
+ # the implemented visibility capability, carries Octane callback ref types, and preserves
74
+ # authored Grid row/col/span metadata. Hosts custom-layout containers (e.g. MasonKit
75
+ # ViewBase) that own child lists via insertChild/removeChild/getChildrenCount without
76
+ # subclassing LayoutBase. Additionally, the masonry cutover: masonview resolves to
77
+ # MasonKit View and scrollview to MasonKit Scroll as the only layout containers, with zero
78
+ # layout props — every former prop name warns-and-drops and all CSS semantics arrive
79
+ # through the style bag; foreign native leaves mirror resolved class and inline layout
80
+ # styles through Style.fromView, observe longhand changes and native recreation, and
81
+ # explicitly clear removed placement — removal restores the CSS cascade, so a later
82
+ # sibling attachment cannot resurrect removed tracks. Container alignment uses the core
83
+ # stretch default without a local style override, allowing class-authored centering.
84
+ # Inline style resets reapply the matching CSS cascade. Re-exports Octane's ClassValue
85
+ # unchanged and stringifies truthy numeric class values while omitting 0 and NaN, matching
86
+ # Octane 0.11.
95
87
  "@nativescript-community/octane@0.2.5": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.5.patch
96
88
  # octaneServerStrategy's SCRIPT_FILE_RE only covers [jt]sx? — .tsrx edits never emit the
97
89
  # deferred ns:hmr-delta broadcast, so component HMR silently never reaches the device.
98
90
  # Also counts open /ns-hmr sockets so the update summary stops reporting recipients=0 for
99
- # delivered updates.
91
+ # delivered updates. Includes shared script sources in transaction ordering while
92
+ # excluding dependencies, tests and declarations. External-source edits use the absolute
93
+ # source for graph identity and a Vite /@fs transform URL instead of treating
94
+ # parent-relative paths as app URLs.
100
95
  "@nativescript-community/vite-octane@0.2.1": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__vite-octane@0.2.1.patch
101
96
  # Enables `resolveHiddenExtensions` on the language plugin's typescript facet so
102
97
  # extensionless specifiers (e.g. './Text') probe `.tsrx` after moduleSuffixes — required
@@ -120,32 +115,26 @@ patchedDependencies:
120
115
  # boot). Same fix as linkifyjs: try/catch new RegExp with an ASCII-range fallback — the
121
116
  # matcher only feeds autolink, which native doesn't use.
122
117
  "@lexical/link@0.51.0": node_modules/.pnpm-config/@octane-xplat/patches/patches/@lexical__link@0.51.0.patch
123
- # ui-image 5.x regressed two iOS prefetch behaviors the 4.x line shipped (still present in
124
- # 4.6.20): prefetchToCacheType ignores its cacheType argument — SDWebImagePrefetcher's
125
- # default context stores to disk+memory either way, so a 'disk' prefetch also decodes and
126
- # warms memory, diverging from Android's bytes-only Glide downloadOnly — and
127
- # getContextFromOptions drops PrefetchOptions.headers because it never sets
128
- # SDWebImageContextDownloadRequestModifier, so prefetch({headers}) silently sends no
129
- # headers on iOS while Android's CustomGlideUrl forwards them. Ports upstream's own 4.x
130
- # implementation — SDWebImageContextStoreCacheType on the prefetch context plus the
131
- # request-modifier branch in getContextFromOptions — and additionally sets
132
- # SDWebImageContextOriginalStoreCacheType so a memory prefetch does not spill original
133
- # bytes to disk.
134
- "@nativescript-community/ui-image@5.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__ui-image@5.0.17.patch
135
118
  # @nativescript/doctor's Android runtime check reads the app's raw devDependencies
136
119
  # specifier and npm dist-tag resolves anything that isn't a semver range — a pnpm
137
120
  # `catalog:` pin throws 'The determined Android runtime version ... is not valid' and
138
121
  # kills `ns build android` before toolchain checks. Resolve catalog specifiers through the
139
122
  # installed package's manifest, reusing the existing tgz-branch mechanism.
140
123
  "@nativescript/doctor@2.0.18": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__doctor@2.0.18.patch
141
- # list/index.ios.js assigns `ObjCProtocols = [MasonListDelegate]` at module scope.
142
- # MasonListDelegate is declared in Mason-Swift.h, which the app's metadata generation does
143
- # not surface as a runtime global — importing masonkit on iOS throws `MasonListDelegate is
144
- # not defined` during bundle eval and kills the app at boot. Guard the binding; actual
145
- # List delegate conformance when the protocol is absent is a separate gap.
146
- "@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
124
+ # shaders@4.0.0 inlines TypeGPU 0.12.3 into dist/core/std-ClftfWpF.js, which declares
125
+ # /\p{XID_Start}/ and /\p{XID_Continue}/ regex literals at module scope (WGSL identifier
126
+ # validation, external prop-chain matching, minify separator check). NativeScript's
127
+ # embedded V8 on iOS (@nativescript/ios 9.1.0) and Android (@nativescript/android 9.1.1,
128
+ # V8 14.9.207.39) is built without ICU — every \p{…} escape is a SyntaxError, even in a
129
+ # never-called function, so a literal fails the whole module at parse time and any bundled
130
+ # app importing `shaders/js` dies on boot. Rewriting to \p{ID_Start}/\p{L} would not help
131
+ # — all property escapes are rejected. The patch builds the regexes via try/catch new
132
+ # RegExp with ASCII/non-ASCII-range fallbacks — byte-identical .source where ICU exists.
133
+ # dist/js/bundle.js, react/bundle.js, and cli.js carry the same escapes but are
134
+ # unreachable from `shaders/js` and left unpatched.
135
+ shaders@4.0.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/shaders@4.0.0.patch
147
136
  configDependencies:
148
- "@octane-xplat/patches": 0.16.0
137
+ "@octane-xplat/patches": 0.17.0
149
138
  catalogs:
150
139
  xplat:
151
140
  "@nativescript-community/octane": 0.2.5
@@ -155,19 +144,19 @@ catalogs:
155
144
  "@nativescript/ios": 9.1.0
156
145
  "@nativescript/types": 9.1.4
157
146
  "@nativescript/unit-test-runner": 5.0.0
158
- "@nativescript/vite": 8.0.17
159
- "@octane-xplat/cli": 0.16.0
160
- "@octane-xplat/jsx": 0.16.0
161
- "@octane-xplat/platform": 0.16.0
162
- "@octane-xplat/ui": 0.16.0
147
+ "@nativescript/vite": 8.0.20
148
+ "@octane-xplat/cli": 0.17.0
149
+ "@octane-xplat/jsx": 0.17.0
150
+ "@octane-xplat/nativescript-masonkit": 1.0.0-beta.126
151
+ "@octane-xplat/platform": 0.17.0
152
+ "@octane-xplat/ui": 0.17.0
163
153
  "@octanejs/vite-plugin": 0.1.61
164
- "@triniwiz/nativescript-masonkit": 1.0.0-beta.110
165
154
  "@tsrx/oxc": 0.20.0
166
155
  "@tsrx/typescript-plugin": 0.4.11
167
156
  "@valor/nativescript-websockets": 2.0.3
168
157
  jsdom: 30.1.1
169
158
  nativescript: 9.1.1
170
- octane: 0.11.0
159
+ octane: 0.14.0
171
160
  typescript: 5.9.3
172
161
  vite: 8.3.0
173
162
  vitest: 4.1.11
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'octane';
2
- import { View, HStack, Text, Pressable, ScrollableArea, useColorScheme } from '@octane-xplat/ui';
2
+ import { View, HStack, Text, Pressable, ScrollableArea, SafeArea, useColorScheme } from '@octane-xplat/ui';
3
3
  import { storage } from '@octane-xplat/platform';
4
4
 
5
5
  const FEATURES = [
@@ -23,26 +23,28 @@ export function App() @{
23
23
  const scheme = useColorScheme();
24
24
  const dark = override ?? scheme === 'dark';
25
25
 
26
- <View className={['app', dark ? 'dark' : 'light']}>
27
- <Text className="title">octane-xplat</Text>
28
- <Text className="muted">one codebase — web · iOS · android</Text>
26
+ <SafeArea>
27
+ <View className={['app', dark ? 'dark' : 'light']}>
28
+ <Text className="title">octane-xplat</Text>
29
+ <Text className="muted">one codebase — web · iOS · android</Text>
29
30
 
30
- <HStack className="counter">
31
- <Pressable testID="counter.decrement" accessibilityLabel="Decrease count" className="btn" onPress={() => setCount(count - 1)}><Text>−</Text></Pressable>
32
- <Text testID="counter.value" className="count">{count}</Text>
33
- <Pressable testID="counter.increment" accessibilityLabel="Increase count" className="btn" onPress={() => setCount(count + 1)}><Text>+</Text></Pressable>
34
- </HStack>
31
+ <HStack className="counter">
32
+ <Pressable testID="counter.decrement" accessibilityLabel="Decrease count" className="btn" onPress={() => setCount(count - 1)}><Text>−</Text></Pressable>
33
+ <Text testID="counter.value" className="count">{count}</Text>
34
+ <Pressable testID="counter.increment" accessibilityLabel="Increase count" className="btn" onPress={() => setCount(count + 1)}><Text>+</Text></Pressable>
35
+ </HStack>
35
36
 
36
- <ScrollableArea>
37
- {FEATURES.map((f) => <Text className="chip">{f}</Text>)}
38
- </ScrollableArea>
37
+ <ScrollableArea>
38
+ {FEATURES.map((f) => <Text className="chip">{f}</Text>)}
39
+ </ScrollableArea>
39
40
 
40
- <Pressable testID="settings.toggle-theme" className="link" onPress={() => {
41
- const next = !dark;
42
- storage.setString('theme-preference', next ? 'dark' : 'light');
43
- setOverride(next);
44
- }}>
45
- <Text>{dark ? 'light' : 'dark'} mode (system: {scheme})</Text>
46
- </Pressable>
47
- </View>
41
+ <Pressable testID="settings.toggle-theme" className="link" onPress={() => {
42
+ const next = !dark;
43
+ storage.setString('theme-preference', next ? 'dark' : 'light');
44
+ setOverride(next);
45
+ }}>
46
+ <Text>{dark ? 'light' : 'dark'} mode (system: {scheme})</Text>
47
+ </Pressable>
48
+ </View>
49
+ </SafeArea>
48
50
  }
@@ -2,18 +2,18 @@
2
2
  // different shell: a Frame hosting a Page that renders the universal root.
3
3
  import { Application, Frame, Page } from '@nativescript/core'
4
4
  import { renderNativeScriptApp } from '@nativescript-community/octane'
5
+ import { prepareFullScreenPage } from '@octane-xplat/ui/nativescript'
5
6
  import { App } from './App.tsrx'
6
7
  import '@octane-xplat/ui/theme/tokens.css'
7
8
  import './style.css'
8
9
 
9
10
  const entry = {
10
11
  create: () => {
11
- // Root is a Frame so pushRoute() can push Pages later — a Frame
12
- // window root doubles as the 'root' stack with no registration;
13
- // named stacks come from TabSpec.stack or registerStack. Route
14
- // pushes also need registerScreens({name: Component}) at boot.
12
+ // A Frame hosts the root Page. RouteOutlet binds canonical routing
13
+ // to this Frame when the app adds generated routes.
15
14
  const frame = new Frame()
16
- const page = new Page()
15
+ // Full-screen page: SafeArea owns safe-area padding.
16
+ const page = prepareFullScreenPage(new Page())
17
17
  page.actionBarHidden = true
18
18
  renderNativeScriptApp(page, App)
19
19
  frame.navigate({ create: () => page })