@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.
- package/LICENSE +21 -0
- package/MOBILE.md +143 -0
- package/README.md +307 -0
- package/dist/node/index.js +50 -0
- package/package.json +127 -0
- package/src/catalog.ts +58 -0
- package/src/contract.ts +257 -0
- package/src/controls.ts +159 -0
- package/src/discover.mjs +584 -0
- package/src/errors.ts +47 -0
- package/src/fixture.ts +158 -0
- package/src/index.ts +6 -0
- package/src/macos/controller.ts +170 -0
- package/src/macos/index.macos.ts +2 -0
- package/src/macos/shell.macos.mjs +673 -0
- package/src/macos/stage.macos.ts +182 -0
- package/src/macos-node.mjs +95 -0
- package/src/macos-vite.mjs +47 -0
- package/src/mobile/controller.ts +134 -0
- package/src/mobile/entry.mobile.ts +607 -0
- package/src/mobile/index.mobile.ts +3 -0
- package/src/mobile/mount.mobile.ts +60 -0
- package/src/mobile/tree.mobile.tsrx +17 -0
- package/src/mobile-node.mjs +126 -0
- package/src/mobile-regex.mjs +60 -0
- package/src/node.d.ts +65 -0
- package/src/node.mjs +536 -0
- package/src/session.ts +520 -0
- package/src/vite-mobile.d.ts +21 -0
- package/src/vite-mobile.mjs +101 -0
- package/src/vite.d.ts +11 -0
- package/src/vite.mjs +315 -0
- package/src/web/entry.web.ts +270 -0
- package/src/web/index.web.ts +7 -0
- package/src/web/mount.web.ts +86 -0
- package/src/web/shell.css +449 -0
- package/src/web/shell.web.tsrx +759 -0
- package/src/windows/preflight.mjs +113 -0
- package/types/generated/.tsrx-typegen-manifest.json +24 -0
- package/types/generated/catalog.d.ts +30 -0
- package/types/generated/contract.d.ts +145 -0
- package/types/generated/controls.d.ts +28 -0
- package/types/generated/errors.d.ts +13 -0
- package/types/generated/fixture.d.ts +53 -0
- package/types/generated/index.d.ts +6 -0
- package/types/generated/macos/controller.d.ts +52 -0
- package/types/generated/macos/index.macos.d.ts +2 -0
- package/types/generated/macos/stage.macos.d.ts +10 -0
- package/types/generated/mobile/controller.d.ts +31 -0
- package/types/generated/mobile/entry.mobile.d.ts +22 -0
- package/types/generated/mobile/index.mobile.d.ts +3 -0
- package/types/generated/mobile/mount.mobile.d.ts +4 -0
- package/types/generated/mobile/tree.mobile.d.ts +4 -0
- package/types/generated/session.d.ts +84 -0
- package/types/generated/web/entry.web.d.ts +53 -0
- package/types/generated/web/index.web.d.ts +7 -0
- package/types/generated/web/mount.web.d.ts +2 -0
- 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
|
+
}
|