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
@@ -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,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
+ })
@@ -6,13 +6,37 @@
6
6
  "moduleResolution": "bundler",
7
7
  "jsx": "preserve",
8
8
  "jsxImportSource": "octane",
9
- "moduleSuffixes": [".web", ""],
10
- "customConditions": ["web"],
11
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
12
- "types": ["vite/client"],
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 { 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: [...octane(), xplatBoundary('web'), xplatDevBridge()],
11
11
  define: xplatNodeEnvDefine(mode),
12
12
  server: { port: 5200 },
13
13
  resolve: {
@@ -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
- }