@octane-xplat/workshop 0.0.0-nightly.g2e22171

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/MOBILE.md +143 -0
  3. package/README.md +307 -0
  4. package/dist/node/index.js +50 -0
  5. package/package.json +127 -0
  6. package/src/catalog.ts +58 -0
  7. package/src/contract.ts +257 -0
  8. package/src/controls.ts +159 -0
  9. package/src/discover.mjs +584 -0
  10. package/src/errors.ts +47 -0
  11. package/src/fixture.ts +158 -0
  12. package/src/index.ts +6 -0
  13. package/src/macos/controller.ts +170 -0
  14. package/src/macos/index.macos.ts +2 -0
  15. package/src/macos/shell.macos.mjs +673 -0
  16. package/src/macos/stage.macos.ts +182 -0
  17. package/src/macos-node.mjs +95 -0
  18. package/src/macos-vite.mjs +47 -0
  19. package/src/mobile/controller.ts +134 -0
  20. package/src/mobile/entry.mobile.ts +607 -0
  21. package/src/mobile/index.mobile.ts +3 -0
  22. package/src/mobile/mount.mobile.ts +60 -0
  23. package/src/mobile/tree.mobile.tsrx +17 -0
  24. package/src/mobile-node.mjs +126 -0
  25. package/src/mobile-regex.mjs +60 -0
  26. package/src/node.d.ts +65 -0
  27. package/src/node.mjs +536 -0
  28. package/src/session.ts +520 -0
  29. package/src/vite-mobile.d.ts +21 -0
  30. package/src/vite-mobile.mjs +101 -0
  31. package/src/vite.d.ts +11 -0
  32. package/src/vite.mjs +315 -0
  33. package/src/web/entry.web.ts +270 -0
  34. package/src/web/index.web.ts +7 -0
  35. package/src/web/mount.web.ts +86 -0
  36. package/src/web/shell.css +449 -0
  37. package/src/web/shell.web.tsrx +759 -0
  38. package/src/windows/preflight.mjs +113 -0
  39. package/types/generated/.tsrx-typegen-manifest.json +24 -0
  40. package/types/generated/catalog.d.ts +30 -0
  41. package/types/generated/contract.d.ts +145 -0
  42. package/types/generated/controls.d.ts +28 -0
  43. package/types/generated/errors.d.ts +13 -0
  44. package/types/generated/fixture.d.ts +53 -0
  45. package/types/generated/index.d.ts +6 -0
  46. package/types/generated/macos/controller.d.ts +52 -0
  47. package/types/generated/macos/index.macos.d.ts +2 -0
  48. package/types/generated/macos/stage.macos.d.ts +10 -0
  49. package/types/generated/mobile/controller.d.ts +31 -0
  50. package/types/generated/mobile/entry.mobile.d.ts +22 -0
  51. package/types/generated/mobile/index.mobile.d.ts +3 -0
  52. package/types/generated/mobile/mount.mobile.d.ts +4 -0
  53. package/types/generated/mobile/tree.mobile.d.ts +4 -0
  54. package/types/generated/session.d.ts +84 -0
  55. package/types/generated/web/entry.web.d.ts +53 -0
  56. package/types/generated/web/index.web.d.ts +7 -0
  57. package/types/generated/web/mount.web.d.ts +2 -0
  58. package/types/generated/web/shell.web.d.ts +4 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/MOBILE.md ADDED
@@ -0,0 +1,143 @@
1
+ # Try a component in the mobile workshop
2
+
3
+ The mobile workshop opens your stories in a separate NativeScript development
4
+ entry. Its iOS and Android adapters are implemented and compiled, but **neither
5
+ host is qualified yet**. Actual touch, keyboard, screen-reader, IME, reload and
6
+ cleanup checks are still required before claiming mobile support.
7
+
8
+ ## Prepare your existing app
9
+
10
+ Use an app that already runs Octane Xplat on iOS or Android. Keep its existing
11
+ framework versions, NativeScript resources, styles and plugins. Install the
12
+ workshop as a development dependency, then create a local counter story:
13
+
14
+ ```sh
15
+ pnpm add -D @octane-xplat/workshop
16
+ pnpm exec xplat workshop init
17
+ ```
18
+
19
+ Wrap the result of your existing native Vite configuration with
20
+ `withWorkshopMobile`. This preserves your renderer settings and plugins and
21
+ places the workshop's reload hook before Octane's hook. In a standard scaffold,
22
+ edit `vite.config.native.mts` like this; retain your app-specific options:
23
+
24
+ ```ts
25
+ import { defineConfig } from 'vite'
26
+ import { xplatNative } from '@octane-xplat/cli/vite'
27
+ import { withWorkshopMobile } from '@octane-xplat/workshop/vite-mobile'
28
+
29
+ export default defineConfig(async ({ mode }) => withWorkshopMobile(await xplatNative(mode)))
30
+ ```
31
+
32
+ Your ordinary app entry remains the production entry. The wrapper does nothing
33
+ unless the workshop launcher supplies a mobile target. Keep stories outside
34
+ production imports; check your production bundle rather than relying on their
35
+ filename alone.
36
+
37
+ ```sh
38
+ # Your existing production build remains unchanged.
39
+ pnpm exec ns build android
40
+ ```
41
+
42
+ ## Select one real host
43
+
44
+ Run Doctor inside the app. It reads package/configuration and SDK-directory
45
+ prerequisites without booting a device. `unqualified-backend` means the launch
46
+ setup is present, not that a host acceptance check passed. SDK-directory checks
47
+ are preliminary; the NativeScript launch reports invalid SDK/toolchain setup.
48
+
49
+ ```sh
50
+ pnpm exec xplat workshop doctor --json
51
+ pnpm exec xplat workshop list --target ios
52
+ pnpm exec xplat workshop list --target android
53
+ ```
54
+
55
+ When you are authorized to use an actual host, supply its device ID explicitly.
56
+ iOS requires a macOS/Xcode host; Android requires a configured Android SDK and
57
+ compatible Java installation. The launcher uses the app's installed NativeScript
58
+ CLI and development-session reload loop. Replace the sample IDs below with the
59
+ IDs of your authorized devices; NativeScript may start the selected device.
60
+
61
+ ```sh
62
+ pnpm exec xplat workshop dev --target ios --device YOUR_IOS_DEVICE_ID --story workshop/counter-basic
63
+ pnpm exec xplat workshop dev --target android --device YOUR_ANDROID_DEVICE_ID --story workshop/counter-basic
64
+ ```
65
+
66
+ Select **Workshop / Counter**, then press **Increment**. Open **Controls**, edit
67
+ `step`, and press **Apply step**. Closing the panel returns to the same story.
68
+ Back or hardware Escape closes an open panel, then exits stage-only view, and
69
+ only then leaves the workshop to the host. **Reset** rebuilds its props, store
70
+ and fixture from defaults. Save a story or component to request a fresh
71
+ revision. A failed edit keeps the previous stage marked **STALE**; correcting
72
+ the source requests another revision.
73
+
74
+ ```ts
75
+ // Source edit in the story created by init:
76
+ args: { step: 2 },
77
+ ```
78
+
79
+ ## Providers, styles and story collections
80
+
81
+ The shared authoring contract is still version 1. Use the same story IDs,
82
+ controls, fixture cancellation and cleanup as described in the
83
+ [package guide](README.md). Global providers are configured in the consumer's
84
+ workshop configuration and wrap story-specific providers in declaration order:
85
+
86
+ ```ts
87
+ import { defineWorkshopConfig } from '@octane-xplat/workshop'
88
+ export default defineWorkshopConfig({
89
+ providerModules: ['./src/demo-providers.tsrx'],
90
+ stories: { include: ['src/**/*.stories.tsrx'] },
91
+ })
92
+ ```
93
+
94
+ The workshop preserves the native build configuration, but does not execute your
95
+ production entry's side effects. Import app styles and initialize required
96
+ plugins in a development provider module. Provider and story components need
97
+ the consumer's matching NativeScript compiler rules. Collections outside
98
+ `src/**` or workspace `packages/**` need explicit renderer inclusion:
99
+
100
+ ```ts
101
+ import { defineConfig } from 'vite'
102
+ import { xplatNative } from '@octane-xplat/cli/vite'
103
+ import { withWorkshopMobile } from '@octane-xplat/workshop/vite-mobile'
104
+ export default defineConfig(async ({ mode }) =>
105
+ withWorkshopMobile(
106
+ await xplatNative(mode, {
107
+ rules: [
108
+ { include: 'src/**/*.{ts,tsx,tsrx}', exclude: '**/*.web.*', renderer: 'nativescript' },
109
+ { include: 'stories/**/*.{ts,tsx,tsrx}', exclude: '**/*.web.*', renderer: 'nativescript' },
110
+ ],
111
+ }),
112
+ ),
113
+ )
114
+ ```
115
+
116
+ ## Qualification limits
117
+
118
+ The shell keeps a single attached stage while switching phone, compact and wide
119
+ layouts. Controls use explicit Apply actions to avoid applying unfinished text
120
+ composition. NativeScript buttons, text fields and scroll views provide the shell
121
+ surfaces; accessible names are implemented. Host Back and hardware Escape share
122
+ one dismissal contract: an open stories/controls panel closes first (with a
123
+ focus-return attempt to its opener), stage-only view exits next, and the key
124
+ otherwise stays with the host or story. Android Back is the activity callback;
125
+ hardware Escape uses the unhandled-key-event listener so a story view or IME
126
+ composition that owns the key keeps it. iOS uses a page key command enabled
127
+ only while a dismissal is available, routed to the accessibility escape hook —
128
+ presented story modals keep priority — and requires the iOS 15 key-command API
129
+ (older hosts keep the toolbar and accessibility-escape paths). Their actual
130
+ accessibility, focus return, keyboard visibility, safe areas, IME and
131
+ key-delivery behavior remain unverified on real hosts.
132
+
133
+ The native adapter reuses the pinned portable Storybook composition subset and
134
+ the shared session's preparation/disposal core, then mounts compiler-produced
135
+ Octane components. The mobile Vite wrapper lowers Unicode-property regexes for
136
+ the pinned native runtimes. Actual-host composition and compatibility
137
+ qualification remain open. Browser manager/addon UI is excluded; the web
138
+ adapter and private visual harness are unchanged.
139
+
140
+ Native process crashes require a target restart. Module globals, permissions and
141
+ external application resources are outside the story subtree; authors must
142
+ register owned cleanup or use process-restart isolation. No runtime result,
143
+ visual parity, or six-target completion is claimed by the source/packed checks.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # @octane-xplat/workshop
2
+
3
+ Render an app-owned component on its own development page, edit its inputs,
4
+ and Reset to a fresh example. The current release provides real launch paths
5
+ for three hosts: the **web** browser target, **macOS** through the real AppKit
6
+ renderer on the JavaScriptCore NativeScript host, and **Linux** through the
7
+ real GTK4/WebKitGTK webview host on a Linux machine. Authoring contract
8
+ version: **1**.
9
+
10
+ [iOS/Android launch setup](MOBILE.md) and native adapters are implemented,
11
+ but iOS, Android and Windows remain unqualified backends — `xplat workshop
12
+ doctor` reports them honestly. Keyboard and browser accessibility semantics
13
+ have regression coverage; actual screen-reader announcements and OS IME input
14
+ still require qualification on every host. On macOS, scripted interaction
15
+ dispatches through each view's real recognizer or control action — it is not
16
+ OS-synthesized input or hit-testing. On Linux, the GJS window, GTK input/focus
17
+ and WebKitGTK reload semantics remain actual-Linux-host gates.
18
+
19
+ For Windows prerequisite reporting and the blocked native launch, see
20
+ [Windows workshop setup status](WINDOWS.md). Windows mounting and interaction
21
+ remain incomplete; setup checks do not qualify that host.
22
+
23
+ ## Try it
24
+
25
+ Inside an existing Octane Xplat app, install the development tooling. Keep the
26
+ app's existing matching Octane/compiler versions; the app template already
27
+ provides Octane's compiler peers, including `@tsrx/oxc`.
28
+
29
+ ```sh
30
+ pnpm add -D @octane-xplat/workshop @octane-xplat/cli @octanejs/vite-plugin vite
31
+ pnpm exec xplat workshop init
32
+ pnpm exec xplat workshop dev --target web
33
+ ```
34
+
35
+ Open the printed URL and choose **Workshop / Counter**. Press **Increment**:
36
+ the count increases. Change **step**, leave the field, and press again. **Reset**
37
+ restores the initial count and step.
38
+
39
+ Initialization creates `workshop.config.ts`, a local example story, and an
40
+ optional package script. Existing files and app entries remain intact. Run it
41
+ again to check that the configuration is already present.
42
+
43
+ ```sh
44
+ pnpm exec xplat workshop init
45
+ pnpm exec xplat workshop doctor
46
+ pnpm exec xplat workshop list
47
+ ```
48
+
49
+ ## Declaring stories
50
+
51
+ A story is a named example of your component. Save this in
52
+ `src/counter.stories.tsrx`; `args` supplies its initial inputs, and the controls
53
+ form edits only the declared portable prop values.
54
+
55
+ ```tsx
56
+ import { Pressable, Text } from '@octane-xplat/ui'
57
+ import { useStore, type Store } from '@octane-xplat/ui/data'
58
+ import { defineStory } from '@octane-xplat/workshop'
59
+
60
+ type Props = { step: number; label?: string }
61
+ type State = { count: number }
62
+ type Fixture = { account: string }
63
+
64
+ function Counter(p: Props & { state: Store<State>; fixture: Fixture }) {
65
+ const { count } = useStore(p.state)
66
+ return (
67
+ <Pressable
68
+ accessibilityLabel="Increment"
69
+ onPress={() => p.state.set((s) => ({ count: s.count + p.step }))}
70
+ >
71
+ <Text>
72
+ {p.label ?? p.fixture.account}: {count}
73
+ </Text>
74
+ </Pressable>
75
+ )
76
+ }
77
+
78
+ export const Basic = defineStory<Props, State, Fixture>({
79
+ id: 'counter/basic',
80
+ title: 'Examples / Counter',
81
+ targets: 'all',
82
+ component: Counter,
83
+ args: { step: 1 },
84
+ controls: {
85
+ step: { type: 'number', min: 1, max: 10 },
86
+ label: { type: 'string', allowUnset: true },
87
+ },
88
+ state: () => ({ count: 0 }),
89
+ fixture: () => ({ account: 'Demo' }),
90
+ })
91
+ ```
92
+
93
+ IDs address stories across reloads. Metadata must be literal: discovery reads
94
+ it without evaluating the story or importing an incompatible platform leaf.
95
+ Computed metadata and duplicate IDs produce catalog diagnostics. Use literal
96
+ strings without escape sequences and literal arrays without expression suffixes.
97
+
98
+ ```ts
99
+ // Inside the Basic declaration above:
100
+ id: 'counter/basic',
101
+ title: 'Examples / Counter',
102
+ targets: ['web', 'ios'],
103
+ ```
104
+
105
+ `state` and `fixture` are reserved prop names. The runner supplies a fresh store
106
+ and resolved fixture for each generation; return new nested data from factories.
107
+ The native readers of a store must subscribe with `useStore`, as Counter does.
108
+
109
+ ```ts
110
+ // Inside the Basic declaration:
111
+ state: () => ({ count: 0 }),
112
+ fixture: () => ({ account: 'Demo' }),
113
+ ```
114
+
115
+ ## State-preserving controls vs. full reset
116
+
117
+ Control edits keep the current fixture, providers, store and component-local
118
+ state. **Unset** removes an optional value; required props cannot be unset.
119
+ Empty, non-finite or out-of-range numbers show a field error and keep the last
120
+ valid stage value. **Reset** creates a fresh complete generation.
121
+
122
+ ```ts
123
+ // Inside the Basic declaration:
124
+ controls: {
125
+ step: { type: 'number', min: 1, max: 10 },
126
+ label: { type: 'string', allowUnset: true },
127
+ },
128
+ ```
129
+
130
+ ## Owned fixture resources
131
+
132
+ Register cleanup as soon as a fixture acquires a resource. Preparation has a
133
+ 10-second default budget. Rejection offers Retry/Reset; cancellation prevents
134
+ stale content mounting. Cleanup starts in reverse registration order, attempts
135
+ all entries, and awaits the combined result before another generation. A hung
136
+ or rejected cleanup requires target restart, including failures in late cleanup.
137
+
138
+ ```ts
139
+ // Replace Basic's fixture factory with this local timer example:
140
+ fixture: ({ signal, onCleanup }) => {
141
+ const timer = setInterval(() => {
142
+ if (!signal.aborted) console.log('Demo fixture is active')
143
+ }, 1000)
144
+ onCleanup(() => clearInterval(timer))
145
+ return { account: 'Demo' }
146
+ },
147
+ ```
148
+
149
+ Providers wrap the compiled component; global presets wrap story-specific
150
+ providers. Each owned secondary root needs the same explicit provider data and
151
+ its own registered cleanup. The maintained
152
+ [isolation example](../../apps/workshop-demo/src/stories/isolation.stories.web.tsrx)
153
+ shows an actual secondary Octane root, shared provider store, and disposal.
154
+
155
+ ```tsx
156
+ function DemoProvider(p: { fixture: Fixture; children?: unknown }) {
157
+ return p.children
158
+ }
159
+ // Inside the Basic declaration:
160
+ providers: [DemoProvider],
161
+ ```
162
+
163
+ Stories do not sandbox imported module globals, external stores or OS resources.
164
+ If those cannot recover cleanly, stop and restart the development target.
165
+
166
+ ```sh
167
+ # Stop the running development command with Ctrl+C, then:
168
+ pnpm exec xplat workshop dev --target web --story counter/basic
169
+ ```
170
+
171
+ ## Linux host
172
+
173
+ On a Linux machine the workshop opens inside the real GTK4/WebKitGTK webview,
174
+ not a browser tab. The command starts the same development server, then launches
175
+ the GJS host shipped in `@octane-xplat/cli` at its URL. Closing the window stops
176
+ the server; stopping the command closes the window.
177
+
178
+ ```sh
179
+ pnpm exec xplat workshop dev --target linux
180
+ pnpm exec xplat workshop dev --target linux --story counter/basic
181
+ ```
182
+
183
+ The launch needs `gjs` plus the GTK4, WebKitGTK 6.0 and Adwaita typelibs (on
184
+ Debian/Ubuntu: `gjs libgtk-4-1 libadwaita-1-0 libwebkitgtk-6.0-4
185
+ gir1.2-webkit-6.0 gir1.2-adw-1`). `xplat workshop doctor` probes them without
186
+ opening a window. On any other OS the row reads `remote-host-required` — run
187
+ the same commands in the Linux host's own checkout rather than bridging
188
+ remotely. Linux resolves `.linux` leaves ahead of `.web` and shared modules,
189
+ matching the consumer app's Linux build; a story listing `linux` under
190
+ `targets` mounts, while foreign leaves stay out of the bundle.
191
+
192
+ ## Platform-leaf filtering
193
+
194
+ A platform-suffixed declaration has no loader on an incompatible target.
195
+ It stays visible with its reason. Required capabilities are currently
196
+ unknown/unqualified in this preview and cannot silently become supported.
197
+
198
+ ```ts
199
+ // A story saved in counter.stories.ios.tsrx:
200
+ id: 'counter/ios-only',
201
+ title: 'Examples / iOS counter',
202
+ targets: ['ios'],
203
+ ```
204
+
205
+ ## Shell
206
+
207
+ Search by title or ID. Wide windows show catalog, stage and controls; narrower
208
+ windows open Stories or Controls separately. Escape closes a workshop panel and
209
+ restores its opener. Composition Enter does not apply a draft. Stage-only mode
210
+ keeps an accessible return/Reset action. Environment background and bounds are
211
+ preview settings, separate from component props; changing them keeps the session.
212
+ The target menu lists all six real host statuses without simulating a host.
213
+
214
+ ```sh
215
+ pnpm exec xplat workshop dev --target web --story counter/basic
216
+ ```
217
+
218
+ ## Save and reload
219
+
220
+ Saving component or story source remounts the selected story with its default
221
+ inputs. Search and shell settings remain. A failed edit leaves the last good
222
+ stage visibly stale; correcting it restores a fresh successful revision.
223
+
224
+ ```sh
225
+ # Keep this command running while saving src/counter.stories.tsrx:
226
+ pnpm exec xplat workshop dev --target web --story counter/basic
227
+ ```
228
+
229
+ The DOM-host adapter uses the pinned Storybook 10.6.1 `composeStory` subset
230
+ already used by the private visual harness. Octane owns rendering; workshop
231
+ owns cancellation, fixture/provider lifetime and reset. Arbitrary CSF addons
232
+ are outside this supported subset. Web and Linux share the same DOM shell and
233
+ save/remount wiring; WebKitGTK input, focus and reload semantics still need
234
+ actual-Linux-host qualification. Other native composition imports need their
235
+ own host qualification.
236
+
237
+ ## macOS target
238
+
239
+ The `macos` target runs the same catalog and session in a small AppKit shell on
240
+ the real JavaScriptCore/NativeScript host — the same runtime `xplat dev` uses
241
+ for packaged macOS apps. It needs a macOS arm64 machine and the app's matching
242
+ macOS dependencies (`@octane-xplat/macos-renderer` and
243
+ `@nativescript/macos-node-api`). Story windows and sheets opened through `openWindow` or
244
+ `mountSheet` are workshop-owned and close when the story generation retires.
245
+
246
+ ```sh
247
+ pnpm exec xplat workshop dev --target macos
248
+ ```
249
+
250
+ Set `OCTANE_WORKSHOP_OFFSCREEN=1` to run the window without desktop focus, and
251
+ `OCTANE_MACOS_AUTOMATION=1` to enable the stdin qualification protocol
252
+ (`catalog`, `select`, `control`, `unset`, `press`, `reset`, `query`, `panel`,
253
+ `stage-only`, `status`, `close`).
254
+
255
+ ## Commands
256
+
257
+ Run these from the consuming package, rather than the monorepo root.
258
+
259
+ ```sh
260
+ pnpm exec xplat workshop init
261
+ pnpm exec xplat workshop doctor
262
+ pnpm exec xplat workshop list
263
+ pnpm exec xplat workshop dev --target web --story counter/basic
264
+ ```
265
+
266
+ Configure includes/excludes, global provider modules and static assets explicitly.
267
+ Relative provider and asset paths resolve from the consuming app.
268
+
269
+ ```ts
270
+ import { defineWorkshopConfig } from '@octane-xplat/workshop'
271
+
272
+ export default defineWorkshopConfig({
273
+ stories: { include: ['src/**/*.stories.tsrx', 'src/**/*.stories.web.tsrx'] },
274
+ assets: { examples: './public' },
275
+ })
276
+ ```
277
+
278
+ ## Production exclusion
279
+
280
+ Keep stories out of the production app's imports. The workshop creates a separate
281
+ development page and does not replace the app's production entry. Build the
282
+ ordinary app to ship it; installing workshop as a development dependency alone
283
+ does not exclude a story explicitly imported by production code.
284
+
285
+ ```sh
286
+ pnpm run build
287
+ ```
288
+
289
+ For repository maintainers, the packed regression compiles source in an external consumer,
290
+ runs the same browser acceptance catalog, measures warm HMR, and checks a normal
291
+ production bundle for story/fixture/workshop markers. Generate dependency declarations first.
292
+
293
+ ```sh
294
+ pnpm --filter @octane-xplat/lib typegen
295
+ pnpm --filter @octane-xplat/ui typegen
296
+ pnpm --filter @octane-xplat/workshop typegen
297
+ pnpm --filter @octane-xplat/workshop test
298
+ pnpm --filter @octane-xplat/workshop test:packed
299
+ pnpm --filter @octane-xplat/workshop test:packed:linux
300
+ ```
301
+
302
+ The packed checks compile the published source with the consumer's toolchain.
303
+ They use headless Chromium and capture no images.
304
+ The Linux packed check verifies packed export resolution, catalog applicability,
305
+ the production linux server wiring and production-build exclusion in a plain
306
+ DOM — the GJS window, GTK input/focus and WebKitGTK reload remain actual-host
307
+ gates that no DOM check substitutes for.
@@ -0,0 +1,50 @@
1
+ //#region src/contract.ts
2
+ var WORKSHOP_TARGETS = [
3
+ "web",
4
+ "ios",
5
+ "android",
6
+ "macos",
7
+ "windows",
8
+ "linux"
9
+ ];
10
+ /** Version of the authoring contract — bumped on breaking changes. */
11
+ var WORKSHOP_CONTRACT_VERSION = 1;
12
+ /** Declare a story. Validates cheap authoring rules eagerly. */
13
+ function defineStory(definition) {
14
+ assertLiteralAuthoring(definition);
15
+ return definition;
16
+ }
17
+ function assertLiteralAuthoring(definition) {
18
+ const fail = (message) => {
19
+ throw new Error(`defineStory: ${message}`);
20
+ };
21
+ if (typeof definition?.id !== "string" || !definition.id) fail("a story requires a non-empty string id");
22
+ if (typeof definition.title !== "string" || !definition.title) fail(`story "${definition.id}" requires a non-empty string title`);
23
+ const targets = definition.targets;
24
+ if (!(targets === "all" || Array.isArray(targets) && targets.length > 0 && targets.every((t) => WORKSHOP_TARGETS.includes(t)))) fail(`story "${definition.id}" — targets must be 'all' or a subset of: ${WORKSHOP_TARGETS.join(", ")}`);
25
+ if (typeof definition.component !== "function") fail(`story "${definition.id}" — component must be an Octane component`);
26
+ if (definition.args == null || typeof definition.args !== "object") fail(`story "${definition.id}" — args must be an object`);
27
+ if ("state" in definition && typeof definition.state !== "function") fail(`story "${definition.id}" — state must be a factory function`);
28
+ if ("fixture" in definition && typeof definition.fixture !== "function") fail(`story "${definition.id}" — fixture must be a factory function`);
29
+ if (definition.args && typeof definition.args === "object") {
30
+ for (const key of ["state", "fixture"]) if (key in definition.args) fail(`story "${definition.id}" — args may not declare reserved injected prop "${key}"`);
31
+ }
32
+ if (definition.controls) for (const [key, control] of Object.entries(definition.controls)) {
33
+ if (!control || typeof control !== "object") fail(`story "${definition.id}" — control "${key}" must be an object`);
34
+ const type = control.type;
35
+ if (![
36
+ "boolean",
37
+ "string",
38
+ "number",
39
+ "enum"
40
+ ].includes(type ?? "")) fail(`story "${definition.id}" — control "${key}" has unknown type "${type}" (expected boolean, string, number or enum)`);
41
+ if (type === "enum" && !Array.isArray(control.options)) fail(`story "${definition.id}" — enum control "${key}" requires an options array`);
42
+ if (type === "number" && typeof control.min === "number" && typeof control.max === "number" && control.min > control.max) fail(`story "${definition.id}" — number control "${key}" has min > max`);
43
+ }
44
+ if (definition.providers !== void 0 && !Array.isArray(definition.providers)) fail(`story "${definition.id}" — providers must be an array`);
45
+ }
46
+ function defineWorkshopConfig(config) {
47
+ return config;
48
+ }
49
+ //#endregion
50
+ export { WORKSHOP_CONTRACT_VERSION, WORKSHOP_TARGETS, defineStory, defineWorkshopConfig };
package/package.json ADDED
@@ -0,0 +1,127 @@
1
+ {
2
+ "name": "@octane-xplat/workshop",
3
+ "version": "0.0.0-nightly.g2e22171",
4
+ "description": "Development-only component workshop for Octane Xplat — typed stories, metadata discovery, adaptive shell, lifecycle and warm reload",
5
+ "keywords": [
6
+ "components",
7
+ "cross-platform",
8
+ "dev-tools",
9
+ "octane",
10
+ "stories",
11
+ "storybook",
12
+ "workshop",
13
+ "xplat"
14
+ ],
15
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/workshop",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/octane-xplat/octane-xplat",
20
+ "directory": "packages/workshop"
21
+ },
22
+ "files": [
23
+ "types",
24
+ "dist/node",
25
+ "src",
26
+ "MOBILE.md",
27
+ "!src/**/*.test.*",
28
+ "!src/**/*.spec.*"
29
+ ],
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./types/generated/index.d.ts",
34
+ "node": "./dist/node/index.js",
35
+ "default": "./src/index.ts"
36
+ },
37
+ "./web": {
38
+ "types": "./types/generated/web/index.web.d.ts",
39
+ "default": "./src/web/index.web.ts"
40
+ },
41
+ "./vite": {
42
+ "types": "./src/vite.d.ts",
43
+ "default": "./src/vite.mjs"
44
+ },
45
+ "./node": {
46
+ "types": "./src/node.d.ts",
47
+ "default": "./src/node.mjs"
48
+ },
49
+ "./package.json": "./package.json",
50
+ "./mobile": {
51
+ "types": "./types/generated/mobile/index.mobile.d.ts",
52
+ "default": "./src/mobile/index.mobile.ts"
53
+ },
54
+ "./vite-mobile": {
55
+ "types": "./src/vite-mobile.d.ts",
56
+ "default": "./src/vite-mobile.mjs"
57
+ },
58
+ "./macos": {
59
+ "types": "./types/generated/macos/index.macos.d.ts",
60
+ "default": "./src/macos/index.macos.ts"
61
+ }
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "dependencies": {
67
+ "@babel/core": "7.29.7",
68
+ "@babel/plugin-transform-unicode-property-regex": "7.29.7",
69
+ "@octane-xplat/cli": "0.0.0-nightly.g2e22171",
70
+ "@octane-xplat/platform": "0.0.0-nightly.g2e22171",
71
+ "@octane-xplat/ui": "0.0.0-nightly.g2e22171",
72
+ "storybook": "10.6.1",
73
+ "tinyglobby": "0.2.17"
74
+ },
75
+ "devDependencies": {
76
+ "@nativescript-community/octane": "0.2.5",
77
+ "@nativescript/core": "9.1.3",
78
+ "@nativescript/macos-node-api": "0.4.4-next.2026-08-09-31292056208",
79
+ "@nativescript/types": "9.1.4",
80
+ "@octane-xplat/macos-renderer": "0.0.0-nightly.g2e22171",
81
+ "@octanejs/vite-plugin": "0.1.61",
82
+ "octane": "0.11.0",
83
+ "tsrx-typegen": "0.3.0",
84
+ "typescript": "5.9.3",
85
+ "vite": "8.3.0"
86
+ },
87
+ "peerDependencies": {
88
+ "@nativescript-community/octane": ">=0.2.4 <1",
89
+ "@nativescript/core": ">=9.1.3 <10",
90
+ "@nativescript/macos-node-api": "0.4.4-next.2026-08-09-31292056208",
91
+ "@octane-xplat/macos-renderer": ">=0.14.0 <1",
92
+ "@octanejs/vite-plugin": ">=0.1.61",
93
+ "octane": ">=0.6.3 <1",
94
+ "vite": ">=8"
95
+ },
96
+ "peerDependenciesMeta": {
97
+ "vite": {
98
+ "optional": true
99
+ },
100
+ "@nativescript/core": {
101
+ "optional": true
102
+ },
103
+ "@nativescript-community/octane": {
104
+ "optional": true
105
+ },
106
+ "@octane-xplat/macos-renderer": {
107
+ "optional": true
108
+ },
109
+ "@nativescript/macos-node-api": {
110
+ "optional": true
111
+ }
112
+ },
113
+ "scripts": {
114
+ "typegen": "tsrx-typegen --target workshop",
115
+ "typecheck": "node ../../scripts/typecheck.mjs",
116
+ "test": "vitest run && tsrx-tsc --noEmit -p tsconfig.test.json",
117
+ "test:e2e": "pnpm --filter @xplat/workshop-demo test:e2e",
118
+ "test:packed": "pnpm build && node tests/packed-consumer.web.mjs",
119
+ "test:packed:linux": "node tests/packed-consumer.linux.mjs",
120
+ "test:packed:macos": "node tests/packed-consumer.macos.mjs",
121
+ "test:packed:windows": "node tests/packed-windows-preflight.mjs",
122
+ "pack:check": "tsrx-typegen --pack-check",
123
+ "test:types": "tsrx-tsc --noEmit -p tsconfig.test.json",
124
+ "test:packed:mobile": "node tests/packed-consumer.mobile.mjs",
125
+ "build": "vite build"
126
+ }
127
+ }