create-octane-xplat 0.13.0 → 0.15.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 +21 -1
  2. package/index.mjs +109 -39
  3. package/library-template/.oxlintrc.json +24 -0
  4. package/library-template/AGENTS.md +16 -0
  5. package/{LICENSE → library-template/LICENSE} +6 -6
  6. package/library-template/README.md +64 -0
  7. package/library-template/gitignore +5 -0
  8. package/library-template/package.json +89 -0
  9. package/library-template/pnpm-workspace.yaml +17 -0
  10. package/library-template/src/Badge.tsx +21 -0
  11. package/library-template/src/Badge.web.tsx +21 -0
  12. package/library-template/src/index.ts +3 -0
  13. package/library-template/src/index.web.ts +3 -0
  14. package/library-template/src/normalizeBadgeLabel.ts +12 -0
  15. package/library-template/src/props.ts +14 -0
  16. package/library-template/tests/normalizeBadgeLabel.test.ts +6 -0
  17. package/library-template/tests/packed-consumer.mjs +155 -0
  18. package/library-template/tsconfig.envs.json +14 -0
  19. package/library-template/tsconfig.json +27 -0
  20. package/library-template/tsconfig.types.json +11 -0
  21. package/library-template/tsrx-typegen.json +16 -0
  22. package/package.json +5 -3
  23. package/scaffold.mjs +105 -14
  24. package/template/.agents/skills/xplat/SKILL.md +1 -1
  25. package/template/.agents/skills/xplat/references/navigation.md +9 -5
  26. package/template/.oxlintrc.json +0 -1
  27. package/template/package.json +19 -18
  28. package/template/pnpm-workspace.yaml +34 -7
  29. package/template/src/app.spec.tsrx +80 -2
  30. package/template/src/failure-reporting.spec.ts +11 -0
  31. package/template/src/native-runtime.spec.ts +18 -0
  32. package/template/tsconfig.json +31 -7
  33. package/template/vite.config.ts +2 -2
  34. package/template/tsconfig.native.json +0 -18
@@ -0,0 +1,155 @@
1
+ import assert from 'node:assert/strict'
2
+ import { spawnSync } from 'node:child_process'
3
+ import {
4
+ cpSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ writeFileSync,
12
+ } from 'node:fs'
13
+
14
+ import { tmpdir } from 'node:os'
15
+ import { dirname, join, resolve } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
19
+ const sourceManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
20
+ const temporary = mkdtempSync(join(tmpdir(), 'octane-library-consumer-'))
21
+
22
+ function run(command, args, cwd) {
23
+ const result = spawnSync(command, args, { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
24
+ if (result.stdout) {
25
+ process.stdout.write(result.stdout)
26
+ }
27
+
28
+ if (result.stderr) {
29
+ process.stderr.write(result.stderr)
30
+ }
31
+
32
+ if (result.error) {
33
+ throw result.error
34
+ }
35
+
36
+ assert.equal(result.status, 0, `${command} ${args.join(' ')} failed`)
37
+ }
38
+
39
+ function installDirectory(root, name, destination) {
40
+ const segments = name.split('/')
41
+ const path = join(destination, 'node_modules', ...segments)
42
+ mkdirSync(dirname(path), { recursive: true })
43
+ cpSync(root, path, { recursive: true })
44
+ }
45
+
46
+ try {
47
+ const pack = join(temporary, 'pack')
48
+ const extracted = join(temporary, 'extracted')
49
+ mkdirSync(pack)
50
+ mkdirSync(extracted)
51
+ run('pnpm', ['pack', '--pack-destination', pack], packageRoot)
52
+ const tarball = readdirSync(pack).find((name) => name.endsWith('.tgz'))
53
+ assert.ok(tarball)
54
+ run('tar', ['-xzf', join(pack, tarball), '-C', extracted], temporary)
55
+ const packed = join(extracted, 'package')
56
+ const manifest = JSON.parse(readFileSync(join(packed, 'package.json'), 'utf8'))
57
+ assert.ok(manifest.exports['.'].web.default.startsWith('./src/'))
58
+ assert.ok(manifest.exports['.'].native.default.startsWith('./src/'))
59
+ assert.ok(manifest.exports['.'].web.types)
60
+ assert.ok(manifest.exports['.'].native.types)
61
+
62
+ for (const target of ['web', 'native']) {
63
+ for (const mode of ['bundler', 'nodenext']) {
64
+ const consumer = join(temporary, `${target}-${mode}`)
65
+ mkdirSync(consumer)
66
+ mkdirSync(join(consumer, 'node_modules'), { recursive: true })
67
+ installDirectory(packed, sourceManifest.name, consumer)
68
+ for (const dependency of [
69
+ 'octane',
70
+ '@octane-xplat/ui',
71
+ ...(target === 'native'
72
+ ? ['@nativescript-community/octane', '@nativescript/core', '@nativescript/types']
73
+ : []),
74
+ ]) {
75
+ const source = join(packageRoot, 'node_modules', ...dependency.split('/'))
76
+ const destination = join(consumer, 'node_modules', ...dependency.split('/'))
77
+ mkdirSync(dirname(destination), { recursive: true })
78
+ symlinkSync(source, destination, 'dir')
79
+ }
80
+
81
+ writeFileSync(join(consumer, 'package.json'), '{"type":"module"}')
82
+ writeFileSync(
83
+ join(consumer, 'consumer.tsx'),
84
+ `import { Badge, type BadgeProps } from '${sourceManifest.name}'
85
+ const props: BadgeProps = { label: 'Ready' }
86
+ const element = <Badge {...props} />
87
+ // @ts-expect-error label is required
88
+ const missing = <Badge />
89
+ // @ts-expect-error label must be a string
90
+ const invalid = <Badge label={1} />
91
+ void element; void missing; void invalid
92
+ `,
93
+ )
94
+
95
+ writeFileSync(
96
+ join(consumer, 'tsconfig.json'),
97
+ JSON.stringify(
98
+ {
99
+ compilerOptions: {
100
+ strict: true,
101
+ noEmit: true,
102
+ skipLibCheck: true,
103
+ target: 'esnext',
104
+ types: [],
105
+ module: mode === 'bundler' ? 'esnext' : 'nodenext',
106
+ moduleResolution: mode,
107
+ jsx: 'react-jsx',
108
+ jsxImportSource: target === 'native' ? '@nativescript-community/octane' : 'octane',
109
+ customConditions: [target],
110
+ moduleSuffixes: target === 'web' ? ['.web', ''] : ['.mobile', ''],
111
+ },
112
+ files: ['consumer.tsx'],
113
+ },
114
+ null,
115
+ '\t',
116
+ ),
117
+ )
118
+
119
+ run(
120
+ process.execPath,
121
+ [join(packageRoot, 'node_modules/typescript/bin/tsc'), '-p', 'tsconfig.json'],
122
+ consumer,
123
+ )
124
+
125
+ const listed = spawnSync(
126
+ process.execPath,
127
+ [
128
+ join(packageRoot, 'node_modules/typescript/bin/tsc'),
129
+ '-p',
130
+ 'tsconfig.json',
131
+ '--listFilesOnly',
132
+ ],
133
+ { cwd: consumer, encoding: 'utf8' },
134
+ )
135
+
136
+ assert.equal(listed.status, 0, listed.stderr)
137
+ const implementation = listed.stdout
138
+ .split('\n')
139
+ .filter(
140
+ (file) =>
141
+ file.includes(`/node_modules/${sourceManifest.name}/src/`) && !file.endsWith('.d.ts'),
142
+ )
143
+
144
+ assert.deepEqual(
145
+ implementation,
146
+ [],
147
+ 'consumer types use declarations without package implementation',
148
+ )
149
+ }
150
+ }
151
+
152
+ console.log('packed consumer: web/native declarations pass Bundler and NodeNext')
153
+ } finally {
154
+ rmSync(temporary, { recursive: true, force: true })
155
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "environments": {
4
+ "web": {
5
+ "include": ["src/**/*.{ts,tsx,tsrx}"]
6
+ },
7
+ "native": {
8
+ "include": ["src/**/*.{ts,tsx,tsrx}"],
9
+ "compilerOptions": {
10
+ "types": ["@nativescript/types"]
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "es2022",
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "octane",
9
+ "moduleSuffixes": [
10
+ "",
11
+ ".web",
12
+ ".ios",
13
+ ".android",
14
+ ".mobile",
15
+ ".native",
16
+ ".macos",
17
+ ".linux",
18
+ ".windows"
19
+ ],
20
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
21
+ "types": ["@nativescript/types"],
22
+ "noEmit": true,
23
+ "skipLibCheck": true
24
+ },
25
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.tsrx"],
26
+ "exclude": ["dist", "types", "node_modules"]
27
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "emitDeclarationOnly": true,
6
+ "noEmit": false,
7
+ "noEmitOnError": true,
8
+ "rootDir": "src",
9
+ "outDir": "types"
10
+ }
11
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "targets": {
3
+ "octane": {
4
+ "renderer": "octane",
5
+ "project": "tsconfig.types.json",
6
+ "outDir": "types",
7
+ "entrypoints": {
8
+ ".": {
9
+ "source": "src/index.ts",
10
+ "runtime": ["./src/index.ts", "./src/index.web.ts"],
11
+ "types": ["./types/index.d.ts", "./types/index.web.d.ts"]
12
+ }
13
+ }
14
+ }
15
+ }
16
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "create-octane-xplat",
3
- "version": "0.13.0",
4
- "description": "Scaffold a minimal @octane-xplat/ui app — one Octane codebase for web + iOS + Android",
3
+ "version": "0.15.0",
4
+ "description": "Scaffold Octane Xplat apps and cross-platform libraries",
5
5
  "keywords": [
6
+ "component-library",
6
7
  "create",
7
8
  "cross-platform",
8
9
  "init",
@@ -25,7 +26,8 @@
25
26
  "files": [
26
27
  "index.mjs",
27
28
  "scaffold.mjs",
28
- "template"
29
+ "template",
30
+ "library-template"
29
31
  ],
30
32
  "type": "module",
31
33
  "exports": {
package/scaffold.mjs CHANGED
@@ -1,9 +1,7 @@
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 dep
3
- // versions and script bodies — targets name which keys they own; unclaimed
4
- // files, deps, and scripts are part of the base scaffold. Keeping versions in
5
- // template/package.json means scripts/bump-versions.mjs keeps working
6
- // unchanged at release time.
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.
7
5
  import {
8
6
  cpSync,
9
7
  existsSync,
@@ -14,10 +12,14 @@ import {
14
12
  writeFileSync,
15
13
  } from 'node:fs'
16
14
 
17
- import { dirname, join } from 'node:path'
15
+ import { basename, dirname, join, resolve } from 'node:path'
18
16
  import { fileURLToPath } from 'node:url'
19
17
 
20
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
+ )
21
23
 
22
24
  /** Scaffoldable targets. `files` entries are template-relative paths —
23
25
  * directories copy recursively. `deps`/`devDeps`/`scripts` name keys whose
@@ -64,7 +66,6 @@ export const targets = {
64
66
  files: [
65
67
  'nativescript.config.ts',
66
68
  'vite.config.native.mts',
67
- 'tsconfig.native.json',
68
69
  'references.d.ts',
69
70
  'src/main.ts',
70
71
  // On-device Vitest lane — `ns test` builds src/test.ts under
@@ -72,6 +73,8 @@ export const targets = {
72
73
  // simulator/emulator through the upstream runner.
73
74
  'src/test.ts',
74
75
  'src/app.spec.tsrx',
76
+ 'src/native-runtime.spec.ts',
77
+ 'src/failure-reporting.spec.ts',
75
78
  'vitest.config.mts',
76
79
  ],
77
80
  deps: [
@@ -91,7 +94,7 @@ export const targets = {
91
94
  'nativescript',
92
95
  'vitest',
93
96
  ],
94
- typecheck: 'tsrx-tsc --noEmit -p tsconfig.native.json',
97
+ typecheck: 'xplat typecheck',
95
98
  },
96
99
  }
97
100
 
@@ -100,11 +103,11 @@ export const selectableTargets = Object.keys(targets).filter((id) => !targets[id
100
103
  /** `pnpm create` without --targets and `xplat add`'s prompt default. */
101
104
  export const defaultTargets = ['web', 'ios', 'android']
102
105
 
103
- const BASE_TYPECHECK = 'tsrx-tsc --noEmit'
106
+ const BASE_TYPECHECK = 'xplat typecheck'
104
107
 
105
108
  // package.json keys every scaffolded app keeps regardless of target set —
106
109
  // the targets' `scripts` lists own the rest.
107
- const BASE_SCRIPTS = ['xplat', 'lint', 'lint:fix']
110
+ const BASE_SCRIPTS = ['xplat', 'lint', 'lint:fix', 'format']
108
111
 
109
112
  /** Expand `requires` edges, dependencies first. Throws on unknown ids. */
110
113
  export function resolveTargets(ids) {
@@ -223,7 +226,7 @@ export function composeManifest(ids) {
223
226
  }
224
227
  }
225
228
 
226
- scripts.typecheck = typecheck.join(' && ')
229
+ scripts.typecheck = [...new Set(typecheck)].join(' && ')
227
230
  // Keep the template's key order: scripts sit where they were.
228
231
  manifest.scripts = {}
229
232
  for (const name of Object.keys(tpl.scripts ?? {})) {
@@ -384,13 +387,33 @@ function ensurePatchEntries(appRoot, enabledIds) {
384
387
  return true
385
388
  }
386
389
 
390
+ /**
391
+ * The tsconfig.json a given target set produces. One program covers every
392
+ * platform suffix (moduleSuffixes is the fixed precedence order); a web-only
393
+ * scaffold just drops the native ambient root and entrypoint.
394
+ */
395
+ export function composeTsconfig(ids) {
396
+ const resolved = new Set(resolveTargets(ids))
397
+ const config = JSON.parse(readFileSync(join(TEMPLATE_DIR, 'tsconfig.json'), 'utf8'))
398
+ if (!resolved.has('native-shared')) {
399
+ delete config.files
400
+ config.compilerOptions.types = (config.compilerOptions.types ?? []).filter(
401
+ (t) => t !== '@nativescript/types',
402
+ )
403
+
404
+ config.exclude = [...new Set([...(config.exclude ?? []), 'src/main.ts'])]
405
+ }
406
+
407
+ return JSON.stringify(config, null, '\t') + '\n'
408
+ }
409
+
387
410
  /** Write a scaffold for `ids` into `dir` (must not exist or be empty). */
388
411
  export function composeTargets(ids, dir) {
389
412
  const resolved = new Set(resolveTargets(ids))
390
413
  mkdirSync(dir, { recursive: true })
391
414
 
392
415
  for (const rel of walk(TEMPLATE_DIR)) {
393
- if (rel === 'package.json' || rel === 'pnpm-workspace.yaml') {
416
+ if (rel === 'package.json' || rel === 'pnpm-workspace.yaml' || rel === 'tsconfig.json') {
394
417
  continue // generated below
395
418
  }
396
419
 
@@ -406,9 +429,43 @@ export function composeTargets(ids, dir) {
406
429
 
407
430
  writeFileSync(join(dir, 'package.json'), JSON.stringify(composeManifest(ids), null, '\t') + '\n')
408
431
  writeFileSync(join(dir, 'pnpm-workspace.yaml'), composeWorkspaceYaml(ids))
432
+ writeFileSync(join(dir, 'tsconfig.json'), composeTsconfig(ids))
409
433
  return [...resolved]
410
434
  }
411
435
 
436
+ /** Write the cross-platform library starter into `dir`. */
437
+ export function composeLibrary(dir, packageName = basename(resolve(dir))) {
438
+ if (
439
+ packageName.length > 214 ||
440
+ !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName)
441
+ ) {
442
+ throw new Error(
443
+ `invalid npm package name "${packageName}" — use lowercase letters, numbers, dots, underscores, or hyphens`,
444
+ )
445
+ }
446
+
447
+ mkdirSync(dir, { recursive: true })
448
+ for (const rel of walk(LIBRARY_TEMPLATE_DIR)) {
449
+ if (rel === 'package.json') {
450
+ continue
451
+ }
452
+
453
+ const dest = join(dir, rel === 'gitignore' ? '.gitignore' : rel)
454
+ mkdirSync(dirname(dest), { recursive: true })
455
+ const source = join(LIBRARY_TEMPLATE_DIR, rel)
456
+ if (/\.(?:md|ts|tsx|mjs|json)$/.test(rel)) {
457
+ const content = readFileSync(source, 'utf8').replaceAll('octane-xplat-library', packageName)
458
+ writeFileSync(dest, content)
459
+ } else {
460
+ cpSync(source, dest, { recursive: true })
461
+ }
462
+ }
463
+
464
+ const manifest = JSON.parse(readFileSync(join(LIBRARY_TEMPLATE_DIR, 'package.json'), 'utf8'))
465
+ manifest.name = packageName
466
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest, null, '\t') + '\n')
467
+ }
468
+
412
469
  const readJson = (file) => {
413
470
  try {
414
471
  return JSON.parse(readFileSync(file, 'utf8'))
@@ -451,6 +508,33 @@ function copyMissing(rel, appRoot, report) {
451
508
  report.copied.push(rel)
452
509
  }
453
510
 
511
+ /**
512
+ * Fold a native-capable app's sources into the one tsconfig program: the
513
+ * manifest (tsconfig.envs.json) is gone — one program covers every platform
514
+ * suffix — so enabling native means wiring the ambient declaration file and
515
+ * the entrypoint into the app's own config.
516
+ */
517
+ function enableNativeInTsconfig(appRoot) {
518
+ const path = join(appRoot, 'tsconfig.json')
519
+ const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
520
+ const config = readJson(path)
521
+ if (!config || typeof config !== 'object') {
522
+ return false
523
+ }
524
+
525
+ const indent = text.match(/\n(\s+)"/)?.[1] ?? '\t'
526
+ config.files = [...new Set(['references.d.ts', ...(config.files ?? [])])]
527
+ config.exclude = (config.exclude ?? []).filter((e) => e !== 'src/main.ts')
528
+
529
+ const types = config.compilerOptions?.types
530
+ if (config.compilerOptions && Array.isArray(types) && !types.includes('@nativescript/types')) {
531
+ config.compilerOptions.types = [...types, '@nativescript/types']
532
+ }
533
+
534
+ writeFileSync(path, JSON.stringify(config, null, indent) + '\n')
535
+ return true
536
+ }
537
+
454
538
  /**
455
539
  * Enable `id` (plus unmet `requires`) inside an existing app at `appRoot`.
456
540
  * Additive only: existing files, scripts, and dep entries are skipped, never
@@ -515,8 +599,15 @@ export function applyTarget(appRoot, id) {
515
599
  }
516
600
  }
517
601
 
518
- if (tid === 'native-shared' && manifest.main === undefined) {
519
- manifest.main = tpl.main
602
+ if (tid === 'native-shared') {
603
+ if (manifest.main === undefined) {
604
+ manifest.main = tpl.main
605
+ }
606
+
607
+ // Native sources join the app's single typecheck program — pull the
608
+ // ambient declaration entry into the roots and un-exclude the
609
+ // NativeScript entry that a web-only scaffold skips.
610
+ report.tsconfigUpdated = enableNativeInTsconfig(appRoot)
520
611
  }
521
612
 
522
613
  touched = true
@@ -134,7 +134,7 @@ export function NativeGreeting() { return <Text>Hello</Text> }
134
134
  | Command | Catches |
135
135
  | ----------------------------------- | ------------------------------------------------------------------------ |
136
136
  | `pnpm lint` | vocabulary / DOM-global / style violations — its output is authoritative |
137
- | `pnpm typecheck` | both web and native TS configs |
137
+ | `pnpm typecheck` | `tsconfig.json` + every environment declared in `tsconfig.envs.json` |
138
138
  | `pnpm dev` | web behavior |
139
139
  | `pnpm dev:ios` / `pnpm dev:android` | the platform where behavior actually differs |
140
140
 
@@ -5,7 +5,8 @@ web turns it into a URL, while native presents the screen in the app's navigatio
5
5
  Put components in `.tsrx` files.
6
6
 
7
7
  ```tsrx
8
- import { defineRoutes, registerRoutes, Text } from '@octane-xplat/ui'
8
+ import { Text } from '@octane-xplat/ui'
9
+ import { defineRoutes, registerRoutes } from '@octane-xplat/ui/routing'
9
10
 
10
11
  function Home() { return <Text>Home</Text> }
11
12
  function Settings() { return <Text>Settings</Text> }
@@ -20,7 +21,8 @@ After registering `settings`, use `NavLink` for a screen link or `pushRoute`
20
21
  in an event handler. `Link` accepts a URL, including an external website.
21
22
 
22
23
  ```tsrx
23
- import { HStack, Link, NavLink, Pressable, Text, pushRoute } from '@octane-xplat/ui'
24
+ import { HStack, Link, NavLink, Pressable, Text } from '@octane-xplat/ui'
25
+ import { pushRoute } from '@octane-xplat/ui/routing'
24
26
 
25
27
  export function SettingsLinks() {
26
28
  const settings = { stack: 'root', name: 'settings', params: {} }
@@ -36,7 +38,8 @@ Use `useRoute` to read the current screen and `useCanGoBack` to decide whether
36
38
  to offer a back button. Pass the same stack name to `popRoute`.
37
39
 
38
40
  ```tsrx
39
- import { Pressable, Text, useRoute, useCanGoBack, popRoute } from '@octane-xplat/ui'
41
+ import { Pressable, Text } from '@octane-xplat/ui'
42
+ import { useRoute, useCanGoBack, popRoute } from '@octane-xplat/ui/routing'
40
43
 
41
44
  export function NavigationStatus() {
42
45
  const route = useRoute('root')
@@ -56,7 +59,7 @@ on web with a warning; do not rely on it to carry application state. With a
56
59
  registered `/projects/:id` screen, navigate using the project ID:
57
60
 
58
61
  ```ts
59
- import { pushRoute } from '@octane-xplat/ui'
62
+ import { pushRoute } from '@octane-xplat/ui/routing'
60
63
 
61
64
  pushRoute({ stack: 'root', name: 'projects/:id', params: { id: '42' } })
62
65
  ```
@@ -66,7 +69,8 @@ initial content; pushing a route into that stack opens it inside the tab pane.
66
69
  The screen below assumes `settings` is already registered.
67
70
 
68
71
  ```tsrx
69
- import { Tabs, Text, Pressable, pushRoute } from '@octane-xplat/ui'
72
+ import { Tabs, Text, Pressable } from '@octane-xplat/ui'
73
+ import { pushRoute } from '@octane-xplat/ui/routing'
70
74
 
71
75
  export function AppTabs() {
72
76
  return <Tabs tabs={[
@@ -2,7 +2,6 @@
2
2
  "jsPlugins": ["@octane-xplat/lint/oxlint-plugin"],
3
3
  "rules": {
4
4
  "curly": "error",
5
- "xplat/spacing": "error",
6
5
  "xplat/no-dom-globals": "error",
7
6
  "xplat/no-web-only-api": "error",
8
7
  "xplat/element-vocabulary": "error",
@@ -15,37 +15,38 @@
15
15
  "build:android": "ns build android",
16
16
  "lint": "xplat-lint",
17
17
  "lint:fix": "xplat-lint --fix",
18
- "typecheck": "tsrx-tsc --noEmit && tsrx-tsc --noEmit -p tsconfig.native.json"
18
+ "typecheck": "xplat typecheck",
19
+ "format": "xplat fmt"
19
20
  },
20
21
  "dependencies": {
21
22
  "@nativescript-community/gesturehandler": "2.0.45",
22
- "@nativescript-community/octane": "0.2.4",
23
+ "@nativescript-community/octane": "catalog:",
23
24
  "@nativescript-community/ui-drawer": "0.1.32",
24
- "@nativescript/core": "9.1.3",
25
- "@octane-xplat/effects": "^0.13.0",
26
- "@octane-xplat/image": "^0.13.0",
27
- "@octane-xplat/platform": "^0.13.0",
28
- "@octane-xplat/ui": "^0.13.0",
25
+ "@nativescript/core": "catalog:",
26
+ "@octane-xplat/effects": "^0.15.0",
27
+ "@octane-xplat/image": "^0.15.0",
28
+ "@octane-xplat/platform": "^0.15.0",
29
+ "@octane-xplat/ui": "^0.15.0",
29
30
  "@valor/nativescript-websockets": "2.0.3",
30
- "octane": "0.11.0"
31
+ "octane": "catalog:"
31
32
  },
32
33
  "devDependencies": {
33
- "@nativescript-community/vite-octane": "0.2.1",
34
+ "@nativescript-community/vite-octane": "catalog:",
34
35
  "@nativescript/android": "9.1.1",
35
36
  "@nativescript/ios": "9.1.0",
36
- "@nativescript/types": "9.1.4",
37
- "@nativescript/unit-test-runner": "5.0.0",
37
+ "@nativescript/types": "catalog:",
38
+ "@nativescript/unit-test-runner": "catalog:",
38
39
  "@nativescript/vite": "8.0.17",
39
- "@octane-xplat/cli": "^0.13.0",
40
- "@octane-xplat/lint": "^0.13.0",
41
- "@octanejs/vite-plugin": "0.1.61",
40
+ "@octane-xplat/cli": "^0.15.0",
41
+ "@octane-xplat/lint": "^0.15.0",
42
+ "@octanejs/vite-plugin": "catalog:",
42
43
  "@tsrx/oxc": "0.20.0",
43
- "@tsrx/typescript-plugin": "0.4.11",
44
+ "@tsrx/typescript-plugin": "catalog:",
44
45
  "nativescript": "9.1.1",
45
46
  "oxlint": "^1.74.0",
46
- "typescript": "5.9.3",
47
- "vite": "8.3.0",
48
- "vitest": "4.1.11"
47
+ "typescript": "catalog:",
48
+ "vite": "catalog:",
49
+ "vitest": "catalog:"
49
50
  },
50
51
  "packageManager": "pnpm@11.24.0"
51
52
  }
@@ -14,6 +14,19 @@ allowBuilds:
14
14
  '@parcel/watcher': true
15
15
  nativescript: true
16
16
  '@nativescript/core': false
17
+ # Shared version pins — bump these here, every dependency field reads them.
18
+ catalog:
19
+ octane: 0.11.0
20
+ '@nativescript-community/octane': 0.2.4
21
+ '@nativescript-community/vite-octane': 0.2.1
22
+ '@nativescript/core': 9.1.3
23
+ '@nativescript/types': 9.1.4
24
+ '@nativescript/unit-test-runner': 5.0.0
25
+ typescript: 5.9.3
26
+ vite: 8.3.0
27
+ '@octanejs/vite-plugin': 0.1.61
28
+ '@tsrx/typescript-plugin': 0.4.11
29
+ vitest: 4.1.11
17
30
  # The patch set covers optional leaf deps (tiptap, linkifyjs, …) the starter
18
31
  # doesn't install — tolerate entries that apply to nothing.
19
32
  allowUnusedPatches: true
@@ -29,7 +42,8 @@ patchedDependencies:
29
42
  # complete universal imperative-ref lifecycle, the Descriptor declaring-owner reader
30
43
  # fallback, and the signal-owner/enclosingOwner chain so child reads reuse instance
31
44
  # declarations. Verified against the registry 0.11.0 tarball; all hunks re-anchored to the
32
- # rewritten compile.js and document-owner call sites.
45
+ # rewritten compile.js and document-owner call sites. Disposes DOM provider descendants
46
+ # when children become null or undefined, releasing their refs and effects.
33
47
  octane@0.11.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.11.0.patch
34
48
  # Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
35
49
  # flex alignment, so those hunks are removed. Retains the unshipped parent-padding
@@ -66,12 +80,25 @@ patchedDependencies:
66
80
  # device verbatim and the HTTP loader throws `Cannot find module`, deadlocking dev boot
67
81
  # before any boot recording can bundle the dep.
68
82
  "@nativescript/vite@8.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.17.patch
69
- # Driver setProp skips `undefined` writes — NativeScript native setters coerce it (e.g.
70
- # editable → ios.userInteractionEnabled = NO), leaving dead UI that still has its JS
71
- # listeners. Aligns intrinsic ref types with Octane callback cleanup and nested arrays
72
- # with optional entries. Advertises the implemented visibility capability so committed
73
- # suspense bodies can be retained while pending.
83
+ # Driver treats a prop dropped from an update's complete prop bag — or set to
84
+ # `undefined`/`null` — as a removal and restores the documented default:
85
+ # Property/CssProperty setters get `unsetValue`, plain fields restore the captured
86
+ # pre-prop value, and applied `style` keys diff per key. A never-applied `undefined` still
87
+ # skips the write since NativeScript native setters coerce it (e.g. editable →
88
+ # ios.userInteractionEnabled = NO). Aligns intrinsic ref types with Octane callback
89
+ # cleanup and nested arrays with optional entries. Advertises the implemented visibility
90
+ # capability so committed suspense bodies can be retained while pending. Preserves
91
+ # authored Grid row/col/span metadata independently of NativeScript defaults and computed
92
+ # auto-placement, notifying Grid when placement props change.
74
93
  "@nativescript-community/octane@0.2.4": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.4.patch
94
+ # Retains the workspace's NativeScript driver fixes when upgrading to 0.2.5 for portal
95
+ # support: a prop dropped from an update's complete prop bag — or set to
96
+ # `undefined`/`null` — is a removal that restores the documented default
97
+ # (Property/CssProperty setters get `unsetValue`, plain fields the captured pre-prop
98
+ # value, `style` keys diff per key); a never-applied `undefined` still skips the write.
99
+ # Also advertises the implemented visibility capability, carries Octane callback ref
100
+ # types, and preserves authored Grid row/col/span metadata.
101
+ "@nativescript-community/octane@0.2.5": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.5.patch
75
102
  # octaneServerStrategy's SCRIPT_FILE_RE only covers [jt]sx? — .tsrx edits never emit the
76
103
  # deferred ns:hmr-delta broadcast, so component HMR silently never reaches the device.
77
104
  # Also counts open /ns-hmr sockets so the update summary stops reporting recipients=0 for
@@ -112,4 +139,4 @@ patchedDependencies:
112
139
  # bytes to disk.
113
140
  "@nativescript-community/ui-image@5.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__ui-image@5.0.17.patch
114
141
  configDependencies:
115
- "@octane-xplat/patches": 0.13.0
142
+ "@octane-xplat/patches": 0.15.0