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.
Files changed (47) hide show
  1. package/README.md +78 -1
  2. package/compatibility.json +199 -0
  3. package/compatibility.ts +109 -0
  4. package/dist/compatibility.js +66 -0
  5. package/dist/index.js +132 -0
  6. package/dist/scaffold.js +439 -0
  7. package/index.ts +198 -0
  8. package/library-template/AGENTS.md +18 -0
  9. package/{LICENSE → library-template/LICENSE} +6 -6
  10. package/library-template/README.md +84 -0
  11. package/library-template/gitignore +5 -0
  12. package/library-template/package.json +92 -0
  13. package/library-template/pnpm-workspace.yaml +156 -0
  14. package/library-template/src/Badge.tsx +21 -0
  15. package/library-template/src/Badge.web.tsx +21 -0
  16. package/library-template/src/index.ts +3 -0
  17. package/library-template/src/index.web.ts +3 -0
  18. package/library-template/src/normalizeBadgeLabel.ts +12 -0
  19. package/library-template/src/props.ts +14 -0
  20. package/library-template/tests/normalizeBadgeLabel.test.ts +6 -0
  21. package/library-template/tests/packed-consumer.mjs +156 -0
  22. package/library-template/tsconfig.envs.json +17 -0
  23. package/library-template/tsconfig.json +27 -0
  24. package/library-template/tsconfig.types.json +11 -0
  25. package/library-template/tsrx-typegen.json +16 -0
  26. package/package.json +27 -8
  27. package/{scaffold.mjs → scaffold.ts} +149 -47
  28. package/template/.agents/skills/xplat/SKILL.md +11 -3
  29. package/template/.agents/skills/xplat/references/components.md +2 -2
  30. package/template/.agents/skills/xplat/references/navigation.md +9 -5
  31. package/template/.agents/skills/xplat/references/platform.md +2 -2
  32. package/template/AGENTS.md +47 -0
  33. package/template/App_Resources/Android/app.gradle +1 -1
  34. package/template/package.json +35 -32
  35. package/template/pnpm-workspace.yaml +73 -11
  36. package/template/src/app.spec.tsrx +80 -2
  37. package/template/src/app.web.test.tsx +20 -0
  38. package/template/src/failure-reporting.spec.ts +11 -0
  39. package/template/src/main.web.tsrx +1 -0
  40. package/template/src/native-runtime.spec.ts +18 -0
  41. package/template/src/test-setup.web.ts +17 -0
  42. package/template/tsconfig.json +32 -8
  43. package/template/vite.config.ts +2 -2
  44. package/template/vitest.web.config.mts +13 -0
  45. package/index.mjs +0 -129
  46. package/template/.oxlintrc.json +0 -25
  47. package/template/tsconfig.native.json +0 -18
@@ -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
- "lint": "xplat-lint",
17
- "lint:fix": "xplat-lint --fix",
18
- "typecheck": "tsrx-tsc --noEmit && tsrx-tsc --noEmit -p tsconfig.native.json"
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/gesturehandler": "2.0.45",
22
- "@nativescript-community/octane": "0.2.4",
23
- "@nativescript-community/ui-drawer": "0.1.32",
24
- "@nativescript/core": "9.1.3",
25
- "@octane-xplat/effects": "^0.14.0",
26
- "@octane-xplat/image": "^0.14.0",
27
- "@octane-xplat/platform": "^0.14.0",
28
- "@octane-xplat/ui": "^0.14.0",
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": "0.2.1",
34
- "@nativescript/android": "9.1.1",
35
- "@nativescript/ios": "9.1.0",
36
- "@nativescript/types": "9.1.4",
37
- "@nativescript/unit-test-runner": "5.0.0",
38
- "@nativescript/vite": "8.0.17",
39
- "@octane-xplat/cli": "^0.14.0",
40
- "@octane-xplat/lint": "^0.14.0",
41
- "@octanejs/vite-plugin": "0.1.61",
42
- "@tsrx/oxc": "0.20.0",
43
- "@tsrx/typescript-plugin": "0.4.11",
44
- "nativescript": "9.1.1",
45
- "oxlint": "^1.74.0",
46
- "typescript": "5.9.3",
47
- "vite": "8.3.0",
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
- # The /ns/m import rewriter now resolves `#`-prefixed package `imports` specifiers through
64
- # the importer's own package.json — raw-read node_modules files (e.g. octane's
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
- # 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.
74
- "@nativescript-community/octane@0.2.4": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.4.patch
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.14.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` runs this inside the real iOS runtime
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
+ })
@@ -1,3 +1,4 @@
1
+ /** @jsxImportSource @octane-xplat/jsx/dom */
1
2
  import { createRoot } from 'octane';
2
3
  import { App } from './App.tsrx';
3
4
  import '@octane-xplat/ui/theme/tokens.css';
@@ -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)
@@ -5,14 +5,38 @@
5
5
  "module": "ESNext",
6
6
  "moduleResolution": "bundler",
7
7
  "jsx": "preserve",
8
- "jsxImportSource": "octane",
9
- "moduleSuffixes": [".web", ""],
10
- "customConditions": ["web"],
11
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
12
- "types": ["vite/client"],
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": ["src/**/*"],
17
- "exclude": ["node_modules", "src/main.ts"]
33
+ "include": [
34
+ "src/**/*"
35
+ ],
36
+ "exclude": [
37
+ "node_modules"
38
+ ],
39
+ "files": [
40
+ "references.d.ts"
41
+ ]
18
42
  }
@@ -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
- }
@@ -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
- }