create-octane-xplat 0.14.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.
- package/README.md +78 -1
- package/compatibility.json +199 -0
- package/compatibility.ts +109 -0
- package/dist/compatibility.js +66 -0
- package/dist/index.js +132 -0
- package/dist/scaffold.js +439 -0
- package/index.ts +198 -0
- package/library-template/AGENTS.md +18 -0
- package/{LICENSE → library-template/LICENSE} +6 -6
- package/library-template/README.md +84 -0
- package/library-template/gitignore +5 -0
- package/library-template/package.json +92 -0
- package/library-template/pnpm-workspace.yaml +156 -0
- package/library-template/src/Badge.tsx +21 -0
- package/library-template/src/Badge.web.tsx +21 -0
- package/library-template/src/index.ts +3 -0
- package/library-template/src/index.web.ts +3 -0
- package/library-template/src/normalizeBadgeLabel.ts +12 -0
- package/library-template/src/props.ts +14 -0
- package/library-template/tests/normalizeBadgeLabel.test.ts +6 -0
- package/library-template/tests/packed-consumer.mjs +156 -0
- package/library-template/tsconfig.envs.json +17 -0
- package/library-template/tsconfig.json +27 -0
- package/library-template/tsconfig.types.json +11 -0
- package/library-template/tsrx-typegen.json +16 -0
- package/package.json +27 -8
- package/{scaffold.mjs → scaffold.ts} +149 -47
- package/template/.agents/skills/xplat/SKILL.md +11 -3
- package/template/.agents/skills/xplat/references/components.md +2 -2
- package/template/.agents/skills/xplat/references/navigation.md +9 -5
- package/template/.agents/skills/xplat/references/platform.md +2 -2
- package/template/AGENTS.md +47 -0
- package/template/App_Resources/Android/app.gradle +1 -1
- package/template/package.json +35 -32
- package/template/pnpm-workspace.yaml +73 -11
- package/template/src/app.spec.tsrx +80 -2
- package/template/src/app.web.test.tsx +20 -0
- package/template/src/failure-reporting.spec.ts +11 -0
- package/template/src/main.web.tsrx +1 -0
- package/template/src/native-runtime.spec.ts +18 -0
- package/template/src/test-setup.web.ts +17 -0
- package/template/tsconfig.json +32 -8
- package/template/vite.config.ts +2 -2
- package/template/vitest.web.config.mts +13 -0
- package/index.mjs +0 -129
- package/template/.oxlintrc.json +0 -25
- package/template/tsconfig.native.json +0 -18
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-octane-xplat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -20,21 +21,39 @@
|
|
|
20
21
|
"directory": "packages/create"
|
|
21
22
|
},
|
|
22
23
|
"bin": {
|
|
23
|
-
"create-octane-xplat": "./index.
|
|
24
|
+
"create-octane-xplat": "./dist/index.js"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
|
-
"index.
|
|
27
|
-
"scaffold.
|
|
28
|
-
"
|
|
27
|
+
"index.ts",
|
|
28
|
+
"scaffold.ts",
|
|
29
|
+
"compatibility.ts",
|
|
30
|
+
"compatibility.json",
|
|
31
|
+
"dist",
|
|
32
|
+
"template",
|
|
33
|
+
"library-template"
|
|
29
34
|
],
|
|
30
35
|
"type": "module",
|
|
31
36
|
"exports": {
|
|
32
|
-
"./scaffold": "./scaffold.
|
|
37
|
+
"./scaffold": "./dist/scaffold.js",
|
|
38
|
+
"./compatibility": {
|
|
39
|
+
"types": "./compatibility.ts",
|
|
40
|
+
"default": "./dist/compatibility.js"
|
|
41
|
+
},
|
|
42
|
+
"./compatibility.json": "./compatibility.json"
|
|
33
43
|
},
|
|
34
44
|
"publishConfig": {
|
|
35
45
|
"access": "public"
|
|
36
46
|
},
|
|
37
47
|
"dependencies": {
|
|
38
|
-
"@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"
|
|
39
58
|
}
|
|
40
59
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// Scaffold manifest + compose/apply shared by create-octane-xplat and the
|
|
2
|
-
// CLI's `xplat add`.
|
|
3
|
-
//
|
|
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`. compatibility.json owns version pins; the generated
|
|
3
|
+
// templates own script bodies and the dependency set for each starter.
|
|
7
4
|
import {
|
|
8
5
|
cpSync,
|
|
9
6
|
existsSync,
|
|
@@ -14,27 +11,40 @@ import {
|
|
|
14
11
|
writeFileSync,
|
|
15
12
|
} from 'node:fs'
|
|
16
13
|
|
|
17
|
-
import { dirname, join } from 'node:path'
|
|
14
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
18
15
|
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { parseDocument } from 'yaml'
|
|
17
|
+
import { ensureCompatibilityCatalog } from './compatibility.ts'
|
|
19
18
|
|
|
20
|
-
|
|
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')
|
|
21
24
|
|
|
22
25
|
/** Scaffoldable targets. `files` entries are template-relative paths —
|
|
23
26
|
* directories copy recursively. `deps`/`devDeps`/`scripts` name keys whose
|
|
24
27
|
* values come from template/package.json. `typecheck` contributes one clause
|
|
25
28
|
* to the generated typecheck script. */
|
|
26
|
-
export const targets = {
|
|
29
|
+
export const targets: Record<string, any> = {
|
|
27
30
|
web: {
|
|
28
31
|
label: 'Web (Vite)',
|
|
29
|
-
files: [
|
|
30
|
-
|
|
31
|
-
|
|
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'],
|
|
32
42
|
},
|
|
33
43
|
ios: {
|
|
34
44
|
label: 'iOS (NativeScript)',
|
|
35
45
|
requires: ['native-shared'],
|
|
36
46
|
files: ['App_Resources/iOS'],
|
|
37
|
-
scripts: ['dev:ios', '
|
|
47
|
+
scripts: ['dev:ios', 'build:ios', 'test:ios'],
|
|
38
48
|
devDeps: ['@nativescript/ios'],
|
|
39
49
|
note: 'needs macOS + Xcode — `pnpm xplat doctor` checks the toolchain',
|
|
40
50
|
},
|
|
@@ -42,7 +52,7 @@ export const targets = {
|
|
|
42
52
|
label: 'Android (NativeScript)',
|
|
43
53
|
requires: ['native-shared'],
|
|
44
54
|
files: ['App_Resources/Android'],
|
|
45
|
-
scripts: ['dev:android', '
|
|
55
|
+
scripts: ['dev:android', 'build:android', 'test:android'],
|
|
46
56
|
devDeps: ['@nativescript/android'],
|
|
47
57
|
note: 'needs Android SDK + a compatible JDK — `pnpm xplat doctor` checks the toolchain',
|
|
48
58
|
},
|
|
@@ -64,23 +74,17 @@ export const targets = {
|
|
|
64
74
|
files: [
|
|
65
75
|
'nativescript.config.ts',
|
|
66
76
|
'vite.config.native.mts',
|
|
67
|
-
'tsconfig.native.json',
|
|
68
77
|
'references.d.ts',
|
|
69
78
|
'src/main.ts',
|
|
70
|
-
|
|
71
|
-
// --env.unitTesting and runs every src/**/*.spec file on a
|
|
72
|
-
// simulator/emulator through the upstream runner.
|
|
79
|
+
'vitest.config.mts',
|
|
73
80
|
'src/test.ts',
|
|
74
81
|
'src/app.spec.tsrx',
|
|
75
|
-
'
|
|
82
|
+
'src/native-runtime.spec.ts',
|
|
83
|
+
'src/failure-reporting.spec.ts',
|
|
76
84
|
],
|
|
77
85
|
deps: [
|
|
78
|
-
'@nativescript-community/gesturehandler',
|
|
79
86
|
'@nativescript-community/octane',
|
|
80
|
-
'@nativescript-community/ui-drawer',
|
|
81
87
|
'@nativescript/core',
|
|
82
|
-
'@octane-xplat/effects',
|
|
83
|
-
'@octane-xplat/image',
|
|
84
88
|
'@valor/nativescript-websockets',
|
|
85
89
|
],
|
|
86
90
|
devDeps: [
|
|
@@ -89,9 +93,8 @@ export const targets = {
|
|
|
89
93
|
'@nativescript/unit-test-runner',
|
|
90
94
|
'@nativescript/vite',
|
|
91
95
|
'nativescript',
|
|
92
|
-
'vitest',
|
|
93
96
|
],
|
|
94
|
-
typecheck: '
|
|
97
|
+
typecheck: 'xplat typecheck',
|
|
95
98
|
},
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -100,15 +103,15 @@ 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 = '
|
|
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) {
|
|
111
|
-
const resolved = []
|
|
114
|
+
const resolved: string[] = []
|
|
112
115
|
const visit = (id) => {
|
|
113
116
|
const t = targets[id]
|
|
114
117
|
if (!t) {
|
|
@@ -147,7 +150,7 @@ function ownerOf(path) {
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
function walk(dir, prefix = '') {
|
|
150
|
-
const out = []
|
|
153
|
+
const out: string[] = []
|
|
151
154
|
for (const name of readdirSync(dir)) {
|
|
152
155
|
if (name === 'node_modules') {
|
|
153
156
|
continue
|
|
@@ -165,7 +168,7 @@ function walk(dir, prefix = '') {
|
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
function pickKeys(source, names, label) {
|
|
168
|
-
const out = {}
|
|
171
|
+
const out: Record<string, string> = {}
|
|
169
172
|
for (const name of names) {
|
|
170
173
|
if (source[name] === undefined) {
|
|
171
174
|
throw new Error(`template/package.json has no ${label} entry "${name}"`)
|
|
@@ -184,11 +187,11 @@ function pickKeys(source, names, label) {
|
|
|
184
187
|
export function composeManifest(ids) {
|
|
185
188
|
const resolved = resolveTargets(ids)
|
|
186
189
|
const tpl = templateManifest()
|
|
187
|
-
const manifest = { ...tpl }
|
|
190
|
+
const manifest: Record<string, any> = { ...tpl }
|
|
188
191
|
|
|
189
192
|
for (const section of ['dependencies', 'devDependencies']) {
|
|
190
193
|
const key = section === 'dependencies' ? 'deps' : 'devDeps'
|
|
191
|
-
const wanted = new Set()
|
|
194
|
+
const wanted = new Set<string>()
|
|
192
195
|
for (const id of resolved) {
|
|
193
196
|
for (const name of targets[id][key] ?? []) {
|
|
194
197
|
wanted.add(name)
|
|
@@ -216,14 +219,14 @@ export function composeManifest(ids) {
|
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
const scripts = pickKeys(tpl.scripts ?? {}, [...wantedScripts].sort(), 'script')
|
|
219
|
-
const typecheck = [BASE_TYPECHECK]
|
|
222
|
+
const typecheck: string[] = [BASE_TYPECHECK]
|
|
220
223
|
for (const id of resolved) {
|
|
221
224
|
if (targets[id].typecheck) {
|
|
222
225
|
typecheck.push(targets[id].typecheck)
|
|
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 ?? {})) {
|
|
@@ -252,9 +255,9 @@ const patchPkg = (key) => key.replace(/@[^@]+$/, '')
|
|
|
252
255
|
// Split a pnpm-workspace.yaml into per-package patch blocks (leading comment
|
|
253
256
|
// lines included) so compose can drop entries and apply can transplant them.
|
|
254
257
|
function parsePatchEntries(lines) {
|
|
255
|
-
const entries = []
|
|
258
|
+
const entries: any[] = []
|
|
256
259
|
let inPatches = false
|
|
257
|
-
let pending = []
|
|
260
|
+
let pending: string[] = []
|
|
258
261
|
for (const line of lines) {
|
|
259
262
|
if (/^\S/.test(line)) {
|
|
260
263
|
inPatches = line.startsWith('patchedDependencies')
|
|
@@ -281,7 +284,7 @@ function parsePatchEntries(lines) {
|
|
|
281
284
|
/** The pnpm-workspace.yaml a given target set produces. */
|
|
282
285
|
export function composeWorkspaceYaml(ids) {
|
|
283
286
|
const resolved = new Set(resolveTargets(ids))
|
|
284
|
-
const drop = new Set()
|
|
287
|
+
const drop = new Set<string>()
|
|
285
288
|
for (const [id, t] of Object.entries(targets)) {
|
|
286
289
|
if (!resolved.has(id)) {
|
|
287
290
|
for (const name of t.patches ?? []) {
|
|
@@ -291,9 +294,9 @@ export function composeWorkspaceYaml(ids) {
|
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
const lines = readFileSync(join(TEMPLATE_DIR, 'pnpm-workspace.yaml'), 'utf8').split('\n')
|
|
294
|
-
const out = []
|
|
297
|
+
const out: string[] = []
|
|
295
298
|
let inPatches = false
|
|
296
|
-
let pending = []
|
|
299
|
+
let pending: string[] = []
|
|
297
300
|
for (const line of lines) {
|
|
298
301
|
if (/^\S/.test(line)) {
|
|
299
302
|
out.push(...pending, line)
|
|
@@ -323,7 +326,18 @@ export function composeWorkspaceYaml(ids) {
|
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
out.push(...pending)
|
|
326
|
-
|
|
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)
|
|
327
341
|
}
|
|
328
342
|
|
|
329
343
|
/**
|
|
@@ -384,13 +398,33 @@ function ensurePatchEntries(appRoot, enabledIds) {
|
|
|
384
398
|
return true
|
|
385
399
|
}
|
|
386
400
|
|
|
387
|
-
/**
|
|
401
|
+
/**
|
|
402
|
+
* The tsconfig.json a given target set produces. One program covers every
|
|
403
|
+
* platform suffix (moduleSuffixes is the fixed precedence order); a web-only
|
|
404
|
+
* scaffold just drops the native ambient root and entrypoint.
|
|
405
|
+
*/
|
|
406
|
+
export function composeTsconfig(ids) {
|
|
407
|
+
const resolved = new Set(resolveTargets(ids))
|
|
408
|
+
const config = JSON.parse(readFileSync(join(TEMPLATE_DIR, 'tsconfig.json'), 'utf8'))
|
|
409
|
+
if (!resolved.has('native-shared')) {
|
|
410
|
+
delete config.files
|
|
411
|
+
config.compilerOptions.types = (config.compilerOptions.types ?? []).filter(
|
|
412
|
+
(t) => t !== '@nativescript/types',
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
config.exclude = [...new Set([...(config.exclude ?? []), 'src/main.ts'])]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return JSON.stringify(config, null, '\t') + '\n'
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Write a scaffold with tests for `ids` into `dir` (must not exist or be empty). */
|
|
388
422
|
export function composeTargets(ids, dir) {
|
|
389
423
|
const resolved = new Set(resolveTargets(ids))
|
|
390
424
|
mkdirSync(dir, { recursive: true })
|
|
391
425
|
|
|
392
426
|
for (const rel of walk(TEMPLATE_DIR)) {
|
|
393
|
-
if (rel === 'package.json' || rel === 'pnpm-workspace.yaml') {
|
|
427
|
+
if (rel === 'package.json' || rel === 'pnpm-workspace.yaml' || rel === 'tsconfig.json') {
|
|
394
428
|
continue // generated below
|
|
395
429
|
}
|
|
396
430
|
|
|
@@ -406,9 +440,43 @@ export function composeTargets(ids, dir) {
|
|
|
406
440
|
|
|
407
441
|
writeFileSync(join(dir, 'package.json'), JSON.stringify(composeManifest(ids), null, '\t') + '\n')
|
|
408
442
|
writeFileSync(join(dir, 'pnpm-workspace.yaml'), composeWorkspaceYaml(ids))
|
|
443
|
+
writeFileSync(join(dir, 'tsconfig.json'), composeTsconfig(ids))
|
|
409
444
|
return [...resolved]
|
|
410
445
|
}
|
|
411
446
|
|
|
447
|
+
/** Write the cross-platform library starter into `dir`. */
|
|
448
|
+
export function composeLibrary(dir, packageName = basename(resolve(dir))) {
|
|
449
|
+
if (
|
|
450
|
+
packageName.length > 214 ||
|
|
451
|
+
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName)
|
|
452
|
+
) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`invalid npm package name "${packageName}" — use lowercase letters, numbers, dots, underscores, or hyphens`,
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
mkdirSync(dir, { recursive: true })
|
|
459
|
+
for (const rel of walk(LIBRARY_TEMPLATE_DIR)) {
|
|
460
|
+
if (rel === 'package.json') {
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const dest = join(dir, rel === 'gitignore' ? '.gitignore' : rel)
|
|
465
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
466
|
+
const source = join(LIBRARY_TEMPLATE_DIR, rel)
|
|
467
|
+
if (/\.(?:md|ts|tsx|mjs|json)$/.test(rel)) {
|
|
468
|
+
const content = readFileSync(source, 'utf8').replaceAll('octane-xplat-library', packageName)
|
|
469
|
+
writeFileSync(dest, content)
|
|
470
|
+
} else {
|
|
471
|
+
cpSync(source, dest, { recursive: true })
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const manifest = JSON.parse(readFileSync(join(LIBRARY_TEMPLATE_DIR, 'package.json'), 'utf8'))
|
|
476
|
+
manifest.name = packageName
|
|
477
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest, null, '\t') + '\n')
|
|
478
|
+
}
|
|
479
|
+
|
|
412
480
|
const readJson = (file) => {
|
|
413
481
|
try {
|
|
414
482
|
return JSON.parse(readFileSync(file, 'utf8'))
|
|
@@ -431,7 +499,7 @@ export function targetEnabled(appRoot, id) {
|
|
|
431
499
|
|
|
432
500
|
// ios/android: declared runtime devDep is the canonical signal — App_Resources
|
|
433
501
|
// alone doesn't reach the bundler config.
|
|
434
|
-
const manifest = readJson(join(appRoot, 'package.json')) ?? {}
|
|
502
|
+
const manifest: Record<string, any> = readJson(join(appRoot, 'package.json')) ?? {}
|
|
435
503
|
const declared = { ...manifest.dependencies, ...manifest.devDependencies }
|
|
436
504
|
const runtime = { ios: '@nativescript/ios', android: '@nativescript/android' }[id]
|
|
437
505
|
return runtime ? typeof declared[runtime] === 'string' : false
|
|
@@ -451,6 +519,33 @@ function copyMissing(rel, appRoot, report) {
|
|
|
451
519
|
report.copied.push(rel)
|
|
452
520
|
}
|
|
453
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Fold a native-capable app's sources into the one tsconfig program: the
|
|
524
|
+
* manifest (tsconfig.envs.json) is gone — one program covers every platform
|
|
525
|
+
* suffix — so enabling native means wiring the ambient declaration file and
|
|
526
|
+
* the entrypoint into the app's own config.
|
|
527
|
+
*/
|
|
528
|
+
function enableNativeInTsconfig(appRoot) {
|
|
529
|
+
const path = join(appRoot, 'tsconfig.json')
|
|
530
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
|
|
531
|
+
const config = readJson(path)
|
|
532
|
+
if (!config || typeof config !== 'object') {
|
|
533
|
+
return false
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const indent = text.match(/\n(\s+)"/)?.[1] ?? '\t'
|
|
537
|
+
config.files = [...new Set(['references.d.ts', ...(config.files ?? [])])]
|
|
538
|
+
config.exclude = (config.exclude ?? []).filter((e) => e !== 'src/main.ts')
|
|
539
|
+
|
|
540
|
+
const types = config.compilerOptions?.types
|
|
541
|
+
if (config.compilerOptions && Array.isArray(types) && !types.includes('@nativescript/types')) {
|
|
542
|
+
config.compilerOptions.types = [...types, '@nativescript/types']
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
writeFileSync(path, JSON.stringify(config, null, indent) + '\n')
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
|
|
454
549
|
/**
|
|
455
550
|
* Enable `id` (plus unmet `requires`) inside an existing app at `appRoot`.
|
|
456
551
|
* Additive only: existing files, scripts, and dep entries are skipped, never
|
|
@@ -459,9 +554,9 @@ function copyMissing(rel, appRoot, report) {
|
|
|
459
554
|
export function applyTarget(appRoot, id) {
|
|
460
555
|
const manifestPath = join(appRoot, 'package.json')
|
|
461
556
|
const raw = readFileSync(manifestPath, 'utf8')
|
|
462
|
-
const manifest = JSON.parse(raw)
|
|
557
|
+
const manifest: Record<string, any> = JSON.parse(raw)
|
|
463
558
|
const tpl = templateManifest()
|
|
464
|
-
const report = {
|
|
559
|
+
const report: Record<string, any> = {
|
|
465
560
|
enabled: [],
|
|
466
561
|
already: [],
|
|
467
562
|
copied: [],
|
|
@@ -515,8 +610,15 @@ export function applyTarget(appRoot, id) {
|
|
|
515
610
|
}
|
|
516
611
|
}
|
|
517
612
|
|
|
518
|
-
if (tid === 'native-shared'
|
|
519
|
-
manifest.main
|
|
613
|
+
if (tid === 'native-shared') {
|
|
614
|
+
if (manifest.main === undefined) {
|
|
615
|
+
manifest.main = tpl.main
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Native sources join the app's single typecheck program — pull the
|
|
619
|
+
// ambient declaration entry into the roots and un-exclude the
|
|
620
|
+
// NativeScript entry that a web-only scaffold skips.
|
|
621
|
+
report.tsconfigUpdated = enableNativeInTsconfig(appRoot)
|
|
520
622
|
}
|
|
521
623
|
|
|
522
624
|
touched = true
|
|
@@ -529,12 +631,12 @@ export function applyTarget(appRoot, id) {
|
|
|
529
631
|
if (ensurePatchEntries(appRoot, [...report.enabled, ...report.already])) {
|
|
530
632
|
report.patchesAdded = true
|
|
531
633
|
}
|
|
634
|
+
ensureCompatibilityCatalog(appRoot, manifest)
|
|
532
635
|
|
|
533
636
|
if (touched) {
|
|
534
637
|
// Match the file's existing indent so diffs stay minimal.
|
|
535
638
|
const indent = raw.match(/\n(\s+)"/)?.[1] ?? '\t'
|
|
536
639
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, indent) + '\n')
|
|
537
640
|
}
|
|
538
|
-
|
|
539
641
|
return report
|
|
540
642
|
}
|
|
@@ -119,22 +119,30 @@ 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 @
|
|
122
|
+
start with `/** @jsxImportSource @octane-xplat/jsx */` on
|
|
123
123
|
line 1 — nothing may precede it:
|
|
124
124
|
|
|
125
125
|
```tsrx
|
|
126
|
-
/** @jsxImportSource @
|
|
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 |
|
|
135
143
|
| ----------------------------------- | ------------------------------------------------------------------------ |
|
|
136
144
|
| `pnpm lint` | vocabulary / DOM-global / style violations — its output is authoritative |
|
|
137
|
-
| `pnpm typecheck` |
|
|
145
|
+
| `pnpm typecheck` | `tsconfig.json` + every environment declared in `tsconfig.envs.json` |
|
|
138
146
|
| `pnpm dev` | web behavior |
|
|
139
147
|
| `pnpm dev:ios` / `pnpm dev:android` | the platform where behavior actually differs |
|
|
140
148
|
|
|
@@ -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 @
|
|
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 @
|
|
157
|
+
/** @jsxImportSource @octane-xplat/jsx */
|
|
158
158
|
import { useState } from 'octane'
|
|
159
159
|
import { MaterialSwitch } from '@octane-xplat/ui/android'
|
|
160
160
|
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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={[
|
|
@@ -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
|
|
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 @
|
|
70
|
+
/** @jsxImportSource @octane-xplat/jsx */
|
|
71
71
|
import { Application } from '@nativescript/core'
|
|
72
72
|
import { Pressable, Text } from '@octane-xplat/ui'
|
|
73
73
|
|
package/template/AGENTS.md
CHANGED
|
@@ -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
|