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/template/package.json
CHANGED
|
@@ -8,44 +8,47 @@
|
|
|
8
8
|
"dev": "vite",
|
|
9
9
|
"dev:ios": "ns run ios",
|
|
10
10
|
"dev:android": "ns run android",
|
|
11
|
-
"test:ios": "ns test ios",
|
|
12
|
-
"test:android": "ns test android",
|
|
13
11
|
"build": "vite build",
|
|
14
12
|
"build:ios": "ns build ios",
|
|
15
13
|
"build:android": "ns build android",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
14
|
+
"test": "vitest run --config vitest.web.config.mts",
|
|
15
|
+
"test:web": "vitest run --config vitest.web.config.mts",
|
|
16
|
+
"test:ios": "ns test ios",
|
|
17
|
+
"test:android": "ns test android",
|
|
18
|
+
"lint": "xplat lint",
|
|
19
|
+
"lint:fix": "xplat lint --fix",
|
|
20
|
+
"typecheck": "xplat typecheck",
|
|
21
|
+
"format": "xplat fmt"
|
|
19
22
|
},
|
|
20
23
|
"dependencies": {
|
|
21
|
-
"@nativescript-community/
|
|
22
|
-
"@nativescript
|
|
23
|
-
"@
|
|
24
|
-
"@
|
|
25
|
-
"@octane-xplat/
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"
|
|
29
|
-
"@valor/nativescript-websockets": "2.0.3",
|
|
30
|
-
"octane": "0.11.0"
|
|
24
|
+
"@nativescript-community/octane": "catalog:xplat",
|
|
25
|
+
"@nativescript/core": "catalog:xplat",
|
|
26
|
+
"@octane-xplat/jsx": "catalog:xplat",
|
|
27
|
+
"@octane-xplat/platform": "catalog:xplat",
|
|
28
|
+
"@octane-xplat/ui": "catalog:xplat",
|
|
29
|
+
"@triniwiz/nativescript-masonkit": "catalog:xplat",
|
|
30
|
+
"@valor/nativescript-websockets": "catalog:xplat",
|
|
31
|
+
"octane": "catalog:xplat"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@nativescript-community/vite-octane": "
|
|
34
|
-
"@nativescript/android": "
|
|
35
|
-
"@nativescript/ios": "
|
|
36
|
-
"@nativescript/types": "
|
|
37
|
-
"@nativescript/unit-test-runner": "
|
|
38
|
-
"@nativescript/vite": "
|
|
39
|
-
"@octane-xplat/cli": "
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@tsrx/
|
|
43
|
-
"
|
|
44
|
-
"nativescript": "
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"vitest": "4.1.11"
|
|
34
|
+
"@nativescript-community/vite-octane": "catalog:xplat",
|
|
35
|
+
"@nativescript/android": "catalog:xplat",
|
|
36
|
+
"@nativescript/ios": "catalog:xplat",
|
|
37
|
+
"@nativescript/types": "catalog:xplat",
|
|
38
|
+
"@nativescript/unit-test-runner": "catalog:xplat",
|
|
39
|
+
"@nativescript/vite": "catalog:xplat",
|
|
40
|
+
"@octane-xplat/cli": "catalog:xplat",
|
|
41
|
+
"@octanejs/vite-plugin": "catalog:xplat",
|
|
42
|
+
"@tsrx/oxc": "catalog:xplat",
|
|
43
|
+
"@tsrx/typescript-plugin": "catalog:xplat",
|
|
44
|
+
"jsdom": "catalog:xplat",
|
|
45
|
+
"nativescript": "catalog:xplat",
|
|
46
|
+
"typescript": "catalog:xplat",
|
|
47
|
+
"vite": "catalog:xplat",
|
|
48
|
+
"vitest": "catalog:xplat"
|
|
49
49
|
},
|
|
50
|
-
"packageManager": "pnpm@11.24.0"
|
|
50
|
+
"packageManager": "pnpm@11.24.0",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.18.0"
|
|
53
|
+
}
|
|
51
54
|
}
|
|
@@ -29,7 +29,8 @@ patchedDependencies:
|
|
|
29
29
|
# complete universal imperative-ref lifecycle, the Descriptor declaring-owner reader
|
|
30
30
|
# fallback, and the signal-owner/enclosingOwner chain so child reads reuse instance
|
|
31
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.
|
|
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.
|
|
33
34
|
octane@0.11.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.11.0.patch
|
|
34
35
|
# Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
|
|
35
36
|
# flex alignment, so those hunks are removed. Retains the unshipped parent-padding
|
|
@@ -60,18 +61,38 @@ patchedDependencies:
|
|
|
60
61
|
# turbo.json as a workspace root — Turborepo's per-package task configs marked apps/* as
|
|
61
62
|
# roots, masking the real pnpm root so served `/node_modules/` tails never reached the
|
|
62
63
|
# `.pnpm` store scan and non-hoisted transitive deps like source-map-js 404'd dev boot.
|
|
63
|
-
#
|
|
64
|
-
# the
|
|
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
|
|
65
72
|
# dist/signals/graph.js importing `#octane/dist/signal-actions/graph`) otherwise reach the
|
|
66
73
|
# device verbatim and the HTTP loader throws `Cannot find module`, deadlocking dev boot
|
|
67
|
-
# before any boot recording can bundle the dep.
|
|
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.
|
|
68
80
|
"@nativescript/vite@8.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.17.patch
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
81
|
+
# Retains the workspace's NativeScript driver fixes when upgrading to 0.2.5 for portal
|
|
82
|
+
# support: a prop dropped from an update's complete prop bag — or set to
|
|
83
|
+
# `undefined`/`null` — is a removal that restores the documented default
|
|
84
|
+
# (Property/CssProperty setters get `unsetValue`, plain fields the captured pre-prop
|
|
85
|
+
# 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.
|
|
95
|
+
"@nativescript-community/octane@0.2.5": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.5.patch
|
|
75
96
|
# octaneServerStrategy's SCRIPT_FILE_RE only covers [jt]sx? — .tsrx edits never emit the
|
|
76
97
|
# deferred ns:hmr-delta broadcast, so component HMR silently never reaches the device.
|
|
77
98
|
# Also counts open /ns-hmr sockets so the update summary stops reporting recipients=0 for
|
|
@@ -111,5 +132,46 @@ patchedDependencies:
|
|
|
111
132
|
# SDWebImageContextOriginalStoreCacheType so a memory prefetch does not spill original
|
|
112
133
|
# bytes to disk.
|
|
113
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
|
+
# @nativescript/doctor's Android runtime check reads the app's raw devDependencies
|
|
136
|
+
# specifier and npm dist-tag resolves anything that isn't a semver range — a pnpm
|
|
137
|
+
# `catalog:` pin throws 'The determined Android runtime version ... is not valid' and
|
|
138
|
+
# kills `ns build android` before toolchain checks. Resolve catalog specifiers through the
|
|
139
|
+
# installed package's manifest, reusing the existing tgz-branch mechanism.
|
|
140
|
+
"@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
|
|
114
147
|
configDependencies:
|
|
115
|
-
"@octane-xplat/patches": 0.
|
|
148
|
+
"@octane-xplat/patches": 0.16.0
|
|
149
|
+
catalogs:
|
|
150
|
+
xplat:
|
|
151
|
+
"@nativescript-community/octane": 0.2.5
|
|
152
|
+
"@nativescript-community/vite-octane": 0.2.1
|
|
153
|
+
"@nativescript/android": 9.1.1
|
|
154
|
+
"@nativescript/core": 9.1.3
|
|
155
|
+
"@nativescript/ios": 9.1.0
|
|
156
|
+
"@nativescript/types": 9.1.4
|
|
157
|
+
"@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
|
|
163
|
+
"@octanejs/vite-plugin": 0.1.61
|
|
164
|
+
"@triniwiz/nativescript-masonkit": 1.0.0-beta.110
|
|
165
|
+
"@tsrx/oxc": 0.20.0
|
|
166
|
+
"@tsrx/typescript-plugin": 0.4.11
|
|
167
|
+
"@valor/nativescript-websockets": 2.0.3
|
|
168
|
+
jsdom: 30.1.1
|
|
169
|
+
nativescript: 9.1.1
|
|
170
|
+
octane: 0.11.0
|
|
171
|
+
typescript: 5.9.3
|
|
172
|
+
vite: 8.3.0
|
|
173
|
+
vitest: 4.1.11
|
|
174
|
+
overrides:
|
|
175
|
+
"@nativescript/vite>esbuild": 0.27.7
|
|
176
|
+
"@nativescript-community/vite-octane>esbuild": 0.27.7
|
|
177
|
+
vite>esbuild: 0.27.7
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Starter spec — `pnpm test:ios`
|
|
1
|
+
// Starter spec — `pnpm test:ios` / `pnpm test:android` run in the real mobile runtime
|
|
2
2
|
// (same renderer, native views, and services your app uses).
|
|
3
3
|
//
|
|
4
4
|
// mountXplat() renders a component onto the test host page and unmounts it
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// gesture observers as real touches — that proves your wiring, not OS
|
|
7
7
|
// hit-testing (device journeys like that belong to Maestro).
|
|
8
8
|
import { describe, expect, it } from 'vitest'
|
|
9
|
-
import { useEffect, useState } from 'octane'
|
|
9
|
+
import { useEffect, useState, useSyncExternalStore } from 'octane'
|
|
10
10
|
import { Pressable, Text, TextInput, View } from '@octane-xplat/ui'
|
|
11
11
|
import {
|
|
12
12
|
type TextInputView,
|
|
@@ -130,3 +130,81 @@ describe('starter app on device', () => {
|
|
|
130
130
|
})
|
|
131
131
|
})
|
|
132
132
|
})
|
|
133
|
+
|
|
134
|
+
// Observable cleanup contracts: every effect subscribe must pair with a
|
|
135
|
+
// cleanup at unmount, delayed work must stop firing, and a re-mount must not
|
|
136
|
+
// leave the previous root subscribed.
|
|
137
|
+
let activeEffects = 0
|
|
138
|
+
let ticks = 0
|
|
139
|
+
|
|
140
|
+
function IntervalCounter() @{
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
activeEffects += 1
|
|
143
|
+
const timer = setInterval(() => {
|
|
144
|
+
ticks += 1
|
|
145
|
+
}, 15)
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
activeEffects -= 1
|
|
149
|
+
clearInterval(timer)
|
|
150
|
+
}
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
153
|
+
<Text>interval</Text>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const listeners = new Set<() => void>()
|
|
157
|
+
|
|
158
|
+
function StoreReader() @{
|
|
159
|
+
const count = useSyncExternalStore(
|
|
160
|
+
(callback) => {
|
|
161
|
+
listeners.add(callback)
|
|
162
|
+
return () => {
|
|
163
|
+
listeners.delete(callback)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
() => listeners.size,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
<Text>{'subs: ' + count}</Text>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('mount/unmount cleanup', () => {
|
|
173
|
+
it('runs effect cleanups and stops delayed work at unmount', async () => {
|
|
174
|
+
ticks = 0
|
|
175
|
+
for (let i = 0; i < 3; i++) {
|
|
176
|
+
const { unmount } = await mountXplat(IntervalCounter)
|
|
177
|
+
expect(activeEffects, 'each mount subscribes exactly once').toBe(1)
|
|
178
|
+
await unmount()
|
|
179
|
+
expect(activeEffects, 'unmount runs the effect cleanup').toBe(0)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The cleared interval must not fire after the last unmount.
|
|
183
|
+
const settled = ticks
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 60))
|
|
185
|
+
expect(ticks, 'interval work continued after unmount').toBe(settled)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('releases external-store subscriptions at unmount', async () => {
|
|
189
|
+
for (let i = 0; i < 3; i++) {
|
|
190
|
+
const { unmount } = await mountXplat(StoreReader)
|
|
191
|
+
expect(listeners.size, 'each mount subscribes exactly once').toBe(1)
|
|
192
|
+
await unmount()
|
|
193
|
+
expect(listeners.size, 'unmount releases the subscription').toBe(0)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('re-mounting replaces the previous root instead of leaking it', async () => {
|
|
198
|
+
await mountXplat(IntervalCounter)
|
|
199
|
+
expect(activeEffects).toBe(1)
|
|
200
|
+
// mountXplat drops the previous root — its cleanup must already have run.
|
|
201
|
+
await mountXplat(IntervalCounter)
|
|
202
|
+
expect(activeEffects, 'previous root stayed subscribed after re-mount').toBe(1)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('finishes automatic cleanup before the next case', () => {
|
|
206
|
+
expect(activeEffects).toBe(0)
|
|
207
|
+
expect(listeners.size).toBe(0)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @jsxImportSource @octane-xplat/jsx/dom */
|
|
2
|
+
import { expect, it, vi } from 'vitest'
|
|
3
|
+
import { createRoot } from 'octane'
|
|
4
|
+
import { App } from './App.tsrx'
|
|
5
|
+
|
|
6
|
+
it('updates the counter when its button is pressed', async () => {
|
|
7
|
+
const container = document.createElement('div')
|
|
8
|
+
document.body.append(container)
|
|
9
|
+
const root = createRoot(container)
|
|
10
|
+
try {
|
|
11
|
+
root.render(<App />)
|
|
12
|
+
const value = () => container.querySelector('[data-testid="counter.value"]')?.textContent
|
|
13
|
+
await vi.waitFor(() => expect(value()).toBe('0'))
|
|
14
|
+
container.querySelector<HTMLElement>('[data-testid="counter.increment"]')!.click()
|
|
15
|
+
await vi.waitFor(() => expect(value()).toBe('1'))
|
|
16
|
+
} finally {
|
|
17
|
+
root.unmount()
|
|
18
|
+
container.remove()
|
|
19
|
+
}
|
|
20
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// An intentionally failing assertion, marked expected-to-fail: it proves
|
|
4
|
+
// assertion failures on device cross the WebSocket bridge and surface as
|
|
5
|
+
// real Vitest failures (it.fails inverts the outcome, keeping the committed
|
|
6
|
+
// suite green while pinning the failure-reporting path).
|
|
7
|
+
describe('failure reporting', () => {
|
|
8
|
+
it.fails('a failed assertion on device is reported as a failure', () => {
|
|
9
|
+
expect('octane-xplat').toBe('a different string entirely')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { Device, isAndroid, isIOS, knownFolders } from '@nativescript/core'
|
|
3
|
+
|
|
4
|
+
// This executes NativeScript's filesystem bridge in the actual mobile host.
|
|
5
|
+
// Only this test's uniquely named temporary file is written and removed.
|
|
6
|
+
describe('native runtime', () => {
|
|
7
|
+
it('round-trips a file through the native bridge', () => {
|
|
8
|
+
expect(isAndroid || isIOS).toBe(true)
|
|
9
|
+
expect(Device.os).toBe(isAndroid ? 'Android' : 'iOS')
|
|
10
|
+
const file = knownFolders.temp().getFile(`xplat-native-test-${Date.now()}.txt`)
|
|
11
|
+
try {
|
|
12
|
+
file.writeTextSync('octane-xplat')
|
|
13
|
+
expect(file.readTextSync()).toBe('octane-xplat')
|
|
14
|
+
} finally {
|
|
15
|
+
file.removeSync()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// jsdom has no system color-scheme producer. Keep component tests in light mode.
|
|
4
|
+
globalThis.matchMedia ??= (query: string) => ({
|
|
5
|
+
media: query,
|
|
6
|
+
matches: false,
|
|
7
|
+
onchange: null,
|
|
8
|
+
addListener() {},
|
|
9
|
+
removeListener() {},
|
|
10
|
+
addEventListener() {},
|
|
11
|
+
removeEventListener() {},
|
|
12
|
+
dispatchEvent: () => true,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// Node 26 also exposes a host localStorage getter. Use jsdom's window storage.
|
|
16
|
+
declare const jsdom: { window: Window }
|
|
17
|
+
vi.stubGlobal('localStorage', jsdom.window.localStorage)
|
package/template/tsconfig.json
CHANGED
|
@@ -5,14 +5,38 @@
|
|
|
5
5
|
"module": "ESNext",
|
|
6
6
|
"moduleResolution": "bundler",
|
|
7
7
|
"jsx": "preserve",
|
|
8
|
-
"jsxImportSource": "octane",
|
|
9
|
-
"moduleSuffixes": [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
"jsxImportSource": "@octane-xplat/jsx",
|
|
9
|
+
"moduleSuffixes": [
|
|
10
|
+
"",
|
|
11
|
+
".web",
|
|
12
|
+
".ios",
|
|
13
|
+
".android",
|
|
14
|
+
".mobile",
|
|
15
|
+
".native",
|
|
16
|
+
".macos",
|
|
17
|
+
".linux",
|
|
18
|
+
".windows"
|
|
19
|
+
],
|
|
20
|
+
"lib": [
|
|
21
|
+
"ESNext",
|
|
22
|
+
"DOM",
|
|
23
|
+
"DOM.Iterable"
|
|
24
|
+
],
|
|
25
|
+
"types": [
|
|
26
|
+
"vite/client",
|
|
27
|
+
"@nativescript/types"
|
|
28
|
+
],
|
|
13
29
|
"noEmit": true,
|
|
14
|
-
"skipLibCheck": true
|
|
30
|
+
"skipLibCheck": true,
|
|
31
|
+
"allowImportingTsExtensions": true
|
|
15
32
|
},
|
|
16
|
-
"include": [
|
|
17
|
-
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*"
|
|
35
|
+
],
|
|
36
|
+
"exclude": [
|
|
37
|
+
"node_modules"
|
|
38
|
+
],
|
|
39
|
+
"files": [
|
|
40
|
+
"references.d.ts"
|
|
41
|
+
]
|
|
18
42
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
2
|
import { octane } from '@octanejs/vite-plugin'
|
|
3
|
-
import { xplatBoundary, xplatNodeEnvDefine } from '@octane-xplat/cli/vite'
|
|
3
|
+
import { xplatCss, xplatBoundary, xplatDevBridge, xplatNodeEnvDefine } from '@octane-xplat/cli/vite'
|
|
4
4
|
|
|
5
5
|
// The web build. For native (iOS/Android), the same source runs through
|
|
6
6
|
// @nativescript-community/vite-octane with `conditions: ['native']`, the
|
|
7
7
|
// .ios/.android/.mobile suffix chain, and the unsuffixed native default — see
|
|
8
8
|
// the octane-xplat repo's apps/mobile/vite.config.mts.
|
|
9
9
|
export default defineConfig(({ mode }) => ({
|
|
10
|
-
plugins: [...octane(), xplatBoundary('web')],
|
|
10
|
+
plugins: [xplatCss('web'), ...octane(), xplatBoundary('web'), xplatDevBridge()],
|
|
11
11
|
define: xplatNodeEnvDefine(mode),
|
|
12
12
|
server: { port: 5200 },
|
|
13
13
|
resolve: {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig, mergeConfig } from 'vitest/config'
|
|
2
|
+
import appConfig from './vite.config.ts'
|
|
3
|
+
|
|
4
|
+
// Reuse the app's compiler, web platform resolution and aliases.
|
|
5
|
+
export default defineConfig(async (env) =>
|
|
6
|
+
mergeConfig(await (typeof appConfig === 'function' ? appConfig(env) : appConfig), {
|
|
7
|
+
test: {
|
|
8
|
+
environment: 'jsdom',
|
|
9
|
+
include: ['src/**/*.web.test.{ts,tsx,tsrx}'],
|
|
10
|
+
setupFiles: ['src/test-setup.web.ts'],
|
|
11
|
+
},
|
|
12
|
+
}),
|
|
13
|
+
)
|
package/index.mjs
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
-
import { resolve } from 'node:path'
|
|
4
|
-
import { spawnSync } from 'node:child_process'
|
|
5
|
-
import {
|
|
6
|
-
composeTargets,
|
|
7
|
-
defaultTargets,
|
|
8
|
-
resolveTargets,
|
|
9
|
-
selectableTargets,
|
|
10
|
-
targets,
|
|
11
|
-
} from './scaffold.mjs'
|
|
12
|
-
|
|
13
|
-
const argv = process.argv.slice(2)
|
|
14
|
-
const noInstall = argv.includes('--no-install')
|
|
15
|
-
const targetsIndex = argv.findIndex((arg) => arg === '--targets' || arg.startsWith('--targets='))
|
|
16
|
-
const targetsValueIndex =
|
|
17
|
-
targetsIndex !== -1 && argv[targetsIndex] === '--targets' ? targetsIndex + 1 : -1
|
|
18
|
-
|
|
19
|
-
const targetsArg =
|
|
20
|
-
targetsIndex === -1
|
|
21
|
-
? undefined
|
|
22
|
-
: targetsValueIndex !== -1
|
|
23
|
-
? argv[targetsValueIndex]
|
|
24
|
-
: argv[targetsIndex].slice('--targets='.length)
|
|
25
|
-
|
|
26
|
-
if (targetsIndex !== -1 && (targetsArg === undefined || targetsArg.startsWith('-'))) {
|
|
27
|
-
console.error(`✗ --targets needs a comma list: ${selectableTargets.join(', ')}`)
|
|
28
|
-
process.exit(1)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const positional = argv.filter((arg, i) => !arg.startsWith('-') && i !== targetsValueIndex)
|
|
32
|
-
const dir = resolve(positional[0] ?? 'octane-xplat-app')
|
|
33
|
-
|
|
34
|
-
if (existsSync(dir) && readdirSync(dir).length > 0) {
|
|
35
|
-
console.error(`✗ ${dir} exists and is not empty`)
|
|
36
|
-
process.exit(1)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const parseTargets = (value) => {
|
|
40
|
-
const ids = value
|
|
41
|
-
.split(',')
|
|
42
|
-
.map((s) => s.trim())
|
|
43
|
-
.filter(Boolean)
|
|
44
|
-
|
|
45
|
-
if (ids.length === 0) {
|
|
46
|
-
throw new Error(`--targets needs a comma list: ${selectableTargets.join(', ')}`)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (const id of ids) {
|
|
50
|
-
if (!selectableTargets.includes(id)) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
`unknown target "${id}" — expected a comma list of: ${selectableTargets.join(', ')}`,
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return resolveTargets(ids)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let ids
|
|
61
|
-
if (targetsArg !== undefined) {
|
|
62
|
-
try {
|
|
63
|
-
ids = parseTargets(targetsArg)
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error(`✗ ${error.message}`)
|
|
66
|
-
process.exit(1)
|
|
67
|
-
}
|
|
68
|
-
} else if (process.stdout.isTTY) {
|
|
69
|
-
// Lazy: the extracted tarball has no installed deps — only the interactive
|
|
70
|
-
// path needs @clack/prompts (verify-consumer runs the bin dep-free).
|
|
71
|
-
const p = await import('@clack/prompts')
|
|
72
|
-
p.intro('create-octane-xplat')
|
|
73
|
-
const picked = await p.multiselect({
|
|
74
|
-
message: 'Target platforms',
|
|
75
|
-
options: selectableTargets.map((id) => ({
|
|
76
|
-
value: id,
|
|
77
|
-
label: targets[id].label,
|
|
78
|
-
hint: targets[id].note,
|
|
79
|
-
})),
|
|
80
|
-
initialValues: [...defaultTargets],
|
|
81
|
-
required: true,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
if (p.isCancel(picked)) {
|
|
85
|
-
p.cancel('Cancelled')
|
|
86
|
-
process.exit(0)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
ids = picked
|
|
90
|
-
} else {
|
|
91
|
-
ids = defaultTargets
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
composeTargets(ids, dir)
|
|
95
|
-
|
|
96
|
-
console.log(
|
|
97
|
-
`\n✓ scaffolded ${dir} (${[...new Set(resolveTargets(ids))].filter((id) => !targets[id].hidden).join(', ')})`,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
if (noInstall) {
|
|
101
|
-
console.log('\nSkipped install (--no-install). To run manually:')
|
|
102
|
-
printNextSteps(dir, ids)
|
|
103
|
-
process.exit(0)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const install = spawnSync('pnpm', ['install'], { cwd: dir, stdio: 'inherit' })
|
|
107
|
-
if (install.status !== 0) {
|
|
108
|
-
console.log(`\nInstall didn't finish (pnpm missing?). To run manually:`)
|
|
109
|
-
printNextSteps(dir, ids)
|
|
110
|
-
process.exit(install.status ?? 1)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (ids.includes('web')) {
|
|
114
|
-
console.log('\n✓ installed — starting dev server (Ctrl+C to stop)\n')
|
|
115
|
-
spawnSync('pnpm', ['dev'], { cwd: dir, stdio: 'inherit' })
|
|
116
|
-
} else {
|
|
117
|
-
console.log('\n✓ installed. To run:')
|
|
118
|
-
printNextSteps(dir, ids)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function printNextSteps(dir, ids) {
|
|
122
|
-
console.log(` cd ${dir}\n pnpm install`)
|
|
123
|
-
if (ids.includes('web')) {
|
|
124
|
-
console.log(' pnpm dev')
|
|
125
|
-
} else {
|
|
126
|
-
// No web entry — `xplat dev` discovers whichever native targets exist.
|
|
127
|
-
console.log(' pnpm xplat dev')
|
|
128
|
-
}
|
|
129
|
-
}
|
package/template/.oxlintrc.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"jsPlugins": ["@octane-xplat/lint/oxlint-plugin"],
|
|
3
|
-
"rules": {
|
|
4
|
-
"curly": "error",
|
|
5
|
-
"xplat/spacing": "error",
|
|
6
|
-
"xplat/no-dom-globals": "error",
|
|
7
|
-
"xplat/no-web-only-api": "error",
|
|
8
|
-
"xplat/element-vocabulary": "error",
|
|
9
|
-
"xplat/no-nativescript-import": "error",
|
|
10
|
-
"xplat/no-ns-deep-import": "error",
|
|
11
|
-
"xplat/no-native-dead-style": "error",
|
|
12
|
-
"xplat/no-line-height-unitless": "error",
|
|
13
|
-
"xplat/no-style-string": "error",
|
|
14
|
-
"xplat/no-unknown-class": "warn",
|
|
15
|
-
"xplat/no-ts-imports-tsrx": "error",
|
|
16
|
-
"xplat/signal-dollar-suffix": "error",
|
|
17
|
-
"xplat/require-signals-runtime": "warn",
|
|
18
|
-
"xplat/no-view-onpress": "error",
|
|
19
|
-
"xplat/no-console-debug": "error",
|
|
20
|
-
"xplat/hooks-in-plain-ts": "warn",
|
|
21
|
-
"xplat/native-pragma-first-line": "error",
|
|
22
|
-
"xplat/no-jsx-text-breakers": "error",
|
|
23
|
-
"xplat/no-root-logical-render": "error"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"strict": true,
|
|
4
|
-
"target": "ESNext",
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"jsx": "preserve",
|
|
8
|
-
"jsxImportSource": "@nativescript-community/octane",
|
|
9
|
-
"moduleSuffixes": [".ios", ".android", ".mobile", ""],
|
|
10
|
-
"customConditions": ["native"],
|
|
11
|
-
"lib": ["ESNext"],
|
|
12
|
-
"types": ["@nativescript/types"],
|
|
13
|
-
"noEmit": true,
|
|
14
|
-
"skipLibCheck": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*", "references.d.ts"],
|
|
17
|
-
"exclude": ["node_modules", "platforms", "**/*.web.*"]
|
|
18
|
-
}
|