@octane-xplat/workshop 0.0.0-nightly.g14544fe
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/build/discover.js +404 -0
- package/dist/build/macos-node.js +77 -0
- package/dist/build/mobile-node.js +96 -0
- package/dist/build/mobile-regex.js +40 -0
- package/dist/build/node.js +426 -0
- package/dist/build/vite-mobile.js +75 -0
- package/dist/build/vite.js +230 -0
- package/dist/build/windows/preflight.js +83 -0
- package/dist/contract.js +50 -0
- package/dist/index.js +2 -0
- package/package.json +127 -0
- package/src/build/discover.ts +585 -0
- package/src/build/macos/shell.macos.ts +674 -0
- package/src/build/macos-node.ts +98 -0
- package/src/build/macos-vite.ts +48 -0
- package/src/build/mobile-node.ts +127 -0
- package/src/build/mobile-regex.ts +60 -0
- package/src/build/node.d.ts +65 -0
- package/src/build/node.ts +537 -0
- package/src/build/vite-mobile.d.ts +21 -0
- package/src/build/vite-mobile.ts +102 -0
- package/src/build/vite.d.ts +11 -0
- package/src/build/vite.ts +316 -0
- package/src/build/windows/preflight.ts +113 -0
- package/src/catalog.ts +58 -0
- package/src/contract.ts +257 -0
- package/src/controls.ts +159 -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/stage.macos.ts +182 -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/session.ts +520 -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/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
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
// @ts-nocheck — .mjs→.ts port: annotations pending (transitional)
|
|
2
|
+
/**
|
|
3
|
+
* Node backend for `xplat workshop …` — the CLI resolves this module from
|
|
4
|
+
* the consumer's installed @octane-xplat/workshop and delegates.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
import { realpathSync } from 'node:fs'
|
|
13
|
+
import { discoverStories } from './discover.ts'
|
|
14
|
+
import { windowsPreflight, windowsLaunchBlocker } from './windows/preflight.ts'
|
|
15
|
+
import { xplatWorkshop } from './vite.ts'
|
|
16
|
+
import { launchMobile, mobilePrerequisites } from './mobile-node.ts'
|
|
17
|
+
import { xplatBoundary, xplatDevBridge, xplatNodeEnvDefine } from '@octane-xplat/cli/vite'
|
|
18
|
+
|
|
19
|
+
const CONFIG_NAMES = [
|
|
20
|
+
'workshop.config.ts',
|
|
21
|
+
'workshop.config.mts',
|
|
22
|
+
'workshop.config.ts',
|
|
23
|
+
'workshop.config.js',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const importFrom = (cwd, specifier) => {
|
|
27
|
+
const req = createRequire(join(cwd, 'package.json'))
|
|
28
|
+
return import(pathToFileURL(realpathSync(req.resolve(specifier))).href)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class WorkshopUsageError extends Error {
|
|
32
|
+
name = 'WorkshopUsageError'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function consumerRoot(cwd) {
|
|
36
|
+
const dir = resolve(cwd)
|
|
37
|
+
if (!existsSync(join(dir, 'package.json'))) {
|
|
38
|
+
throw new WorkshopUsageError(`no package.json at ${dir} — run inside a consumer app`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return dir
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function findConfigFile(root) {
|
|
45
|
+
for (const name of CONFIG_NAMES) {
|
|
46
|
+
const file = join(root, name)
|
|
47
|
+
if (existsSync(file)) {
|
|
48
|
+
return file
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function loadWorkshopConfig(root) {
|
|
56
|
+
const file = findConfigFile(root)
|
|
57
|
+
if (!file) {
|
|
58
|
+
return { config: {}, file: null }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { loadConfigFromFile } = await importFrom(root, 'vite')
|
|
62
|
+
const result = await loadConfigFromFile({ command: 'serve', mode: 'development' }, file)
|
|
63
|
+
|
|
64
|
+
return { config: result.config?.default ?? result.config ?? {}, file }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- list ----------
|
|
68
|
+
|
|
69
|
+
export async function workshopList({ cwd, target = 'web', json = false } = {}) {
|
|
70
|
+
const root = consumerRoot(cwd)
|
|
71
|
+
const { config } = await loadWorkshopConfig(root)
|
|
72
|
+
const { entries, diagnostics } = discoverStories(root, {
|
|
73
|
+
include: config.stories?.include,
|
|
74
|
+
exclude: config.stories?.exclude,
|
|
75
|
+
target,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (json) {
|
|
79
|
+
console.log(JSON.stringify({ target, entries, diagnostics }, null, 2))
|
|
80
|
+
return { entries, diagnostics }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`workshop stories (target: ${target})`)
|
|
84
|
+
if (!entries.length) {
|
|
85
|
+
console.log(' (none)')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const mark = entry.applicable ? ' ' : '!'
|
|
90
|
+
const reason = entry.applicable ? '' : ` — ${entry.reason}`
|
|
91
|
+
const caps = entry.capabilities.length ? ` [${entry.capabilities.join(', ')}]` : ''
|
|
92
|
+
console.log(`${mark} ${entry.id.padEnd(32)} ${entry.title}${caps}${reason}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const diagnostic of diagnostics) {
|
|
96
|
+
console.error(`! ${relative(root, diagnostic.file)}: ${diagnostic.message}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (diagnostics.length) {
|
|
100
|
+
throw new WorkshopUsageError(`${diagnostics.length} discovery diagnostic(s)`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { entries, diagnostics }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------- doctor ----------
|
|
107
|
+
|
|
108
|
+
const resolveFrom = (cwd, specifier) => {
|
|
109
|
+
try {
|
|
110
|
+
const req = createRequire(join(cwd, 'package.json'))
|
|
111
|
+
return req.resolve(specifier)
|
|
112
|
+
} catch {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function workshopDoctor({ cwd, json = false } = {}) {
|
|
118
|
+
const root = consumerRoot(cwd)
|
|
119
|
+
const report = { root, targets: {} }
|
|
120
|
+
|
|
121
|
+
const webRequirements = {
|
|
122
|
+
vite: Boolean(resolveFrom(root, 'vite')),
|
|
123
|
+
'@octanejs/vite-plugin': Boolean(resolveFrom(root, '@octanejs/vite-plugin')),
|
|
124
|
+
octane: Boolean(resolveFrom(root, 'octane')),
|
|
125
|
+
'@octane-xplat/ui': Boolean(resolveFrom(root, '@octane-xplat/ui')),
|
|
126
|
+
'@octane-xplat/workshop': Boolean(resolveFrom(root, '@octane-xplat/workshop')),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const configFile = findConfigFile(root)
|
|
130
|
+
report.targets.web = {
|
|
131
|
+
backend: 'octane-dom (vite)',
|
|
132
|
+
requirements: { ...webRequirements, 'workshop.config': Boolean(configFile) },
|
|
133
|
+
status: Object.values(webRequirements).every(Boolean) ? 'available' : 'missing-prerequisites',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const target of ['ios', 'android']) {
|
|
137
|
+
report.targets[target] = mobilePrerequisites(root, target)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const macosRequirements = {
|
|
141
|
+
'darwin-arm64 host': process.platform === 'darwin' && process.arch === 'arm64',
|
|
142
|
+
'@octane-xplat/macos-renderer': Boolean(resolveFrom(root, '@octane-xplat/macos-renderer')),
|
|
143
|
+
'@nativescript/macos-node-api': Boolean(resolveFrom(root, '@nativescript/macos-node-api')),
|
|
144
|
+
'@octane-xplat/cli': Boolean(resolveFrom(root, '@octane-xplat/cli/macos')),
|
|
145
|
+
octane: Boolean(resolveFrom(root, 'octane')),
|
|
146
|
+
'@octanejs/vite-plugin': Boolean(resolveFrom(root, '@octanejs/vite-plugin')),
|
|
147
|
+
vite: Boolean(resolveFrom(root, 'vite')),
|
|
148
|
+
'workshop.config': Boolean(configFile),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
report.targets.macos = {
|
|
152
|
+
backend: 'appkit/jsc renderer',
|
|
153
|
+
requirements: macosRequirements,
|
|
154
|
+
status: Object.values(macosRequirements).every(Boolean)
|
|
155
|
+
? 'available'
|
|
156
|
+
: macosRequirements['darwin-arm64 host']
|
|
157
|
+
? 'missing-prerequisites'
|
|
158
|
+
: 'unsupported-host',
|
|
159
|
+
note: "the AppKit workshop needs the app's own macOS renderer and node-api versions",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
report.targets.windows = windowsPreflight(root)
|
|
163
|
+
|
|
164
|
+
report.targets.linux = linuxDoctor(root, webRequirements, configFile)
|
|
165
|
+
|
|
166
|
+
if (json) {
|
|
167
|
+
console.log(JSON.stringify(report, null, 2))
|
|
168
|
+
return report
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`workshop doctor — ${root}`)
|
|
172
|
+
for (const [target, info] of Object.entries(report.targets)) {
|
|
173
|
+
console.log(` ${target.padEnd(8)} ${info.status.padEnd(22)} ${info.backend}`)
|
|
174
|
+
for (const [req, ok] of Object.entries(info.requirements)) {
|
|
175
|
+
if (!ok) {
|
|
176
|
+
console.log(` missing: ${req}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (info.note) {
|
|
181
|
+
console.log(` ${info.note}`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return report
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------- init ----------
|
|
189
|
+
|
|
190
|
+
const CONFIG_TEMPLATE = `import { defineWorkshopConfig } from '@octane-xplat/workshop'
|
|
191
|
+
|
|
192
|
+
export default defineWorkshopConfig({
|
|
193
|
+
// Story modules are discovered statically — id/title/targets must be
|
|
194
|
+
// literal values. Platform leaves like counter.stories.ios.tsrx stay out
|
|
195
|
+
// of incompatible bundles.
|
|
196
|
+
stories: {
|
|
197
|
+
include: [
|
|
198
|
+
'src/**/*.stories.{ts,tsx,tsrx}',
|
|
199
|
+
'src/**/*.stories.{web,mobile,ios,android,macos,windows,linux}.{ts,tsx,tsrx}',
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
// Optional: modules exporting a providers array applied around every story.
|
|
203
|
+
// providerModules: ['./src/demo-providers.tsrx'],
|
|
204
|
+
})
|
|
205
|
+
`
|
|
206
|
+
|
|
207
|
+
const STORY_TEMPLATE = `import { Pressable, Text } from '@octane-xplat/ui'
|
|
208
|
+
import { useStore, type Store } from '@octane-xplat/ui/data'
|
|
209
|
+
import { defineStory } from '@octane-xplat/workshop'
|
|
210
|
+
|
|
211
|
+
type Props = { step: number }
|
|
212
|
+
type State = { count: number }
|
|
213
|
+
type Fixture = { account: string }
|
|
214
|
+
|
|
215
|
+
function WorkshopCounter(p: Props & { state: Store<State>; fixture: Fixture }) {
|
|
216
|
+
const { count } = useStore(p.state)
|
|
217
|
+
return (
|
|
218
|
+
<Pressable
|
|
219
|
+
accessibilityLabel="Increment"
|
|
220
|
+
onPress={() => p.state.update((s) => ({ count: s.count + p.step }))}
|
|
221
|
+
>
|
|
222
|
+
<Text>{p.fixture.account}: {count}</Text>
|
|
223
|
+
</Pressable>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const Basic = defineStory<Props, State, Fixture>({
|
|
228
|
+
id: 'workshop/counter-basic',
|
|
229
|
+
title: 'Workshop / Counter',
|
|
230
|
+
targets: 'all',
|
|
231
|
+
component: WorkshopCounter,
|
|
232
|
+
args: { step: 1 },
|
|
233
|
+
controls: { step: { type: 'number', min: 1, max: 10 } },
|
|
234
|
+
state: () => ({ count: 0 }),
|
|
235
|
+
fixture: () => ({ account: 'Demo' }),
|
|
236
|
+
})
|
|
237
|
+
`
|
|
238
|
+
|
|
239
|
+
export async function workshopInit({ cwd, scripts = true } = {}) {
|
|
240
|
+
const root = consumerRoot(cwd)
|
|
241
|
+
const created = []
|
|
242
|
+
const skipped = []
|
|
243
|
+
|
|
244
|
+
const writeIfAbsent = (file, contents) => {
|
|
245
|
+
if (existsSync(file)) {
|
|
246
|
+
const existing = readFileSync(file, 'utf8')
|
|
247
|
+
skipped.push(
|
|
248
|
+
existing === contents
|
|
249
|
+
? `${relative(root, file)} already up to date`
|
|
250
|
+
: `${relative(root, file)} exists with different content — left untouched; reconcile it manually`,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
257
|
+
writeFileSync(file, contents)
|
|
258
|
+
created.push(relative(root, file))
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
writeIfAbsent(join(root, 'workshop.config.ts'), CONFIG_TEMPLATE)
|
|
263
|
+
writeIfAbsent(join(root, 'src', 'workshop', 'counter.stories.tsrx'), STORY_TEMPLATE)
|
|
264
|
+
|
|
265
|
+
if (scripts) {
|
|
266
|
+
const manifestPath = join(root, 'package.json')
|
|
267
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
268
|
+
manifest.scripts = manifest.scripts ?? {}
|
|
269
|
+
if (!manifest.scripts.workshop) {
|
|
270
|
+
manifest.scripts.workshop = 'xplat workshop dev --target web'
|
|
271
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
|
272
|
+
created.push('package.json script "workshop"')
|
|
273
|
+
} else {
|
|
274
|
+
skipped.push('package.json script "workshop" already exists')
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const hasWorkshop = Boolean(resolveFrom(root, '@octane-xplat/workshop'))
|
|
279
|
+
for (const item of created) {
|
|
280
|
+
console.log(` created ${item}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const item of skipped) {
|
|
284
|
+
console.log(` skipped ${item}`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!hasWorkshop) {
|
|
288
|
+
console.log(' next: pnpm add -D @octane-xplat/workshop')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(' run: pnpm exec xplat workshop dev --target web')
|
|
292
|
+
return { created, skipped }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------- linux host checks ----------
|
|
296
|
+
|
|
297
|
+
const gjsProbe = (args) => {
|
|
298
|
+
try {
|
|
299
|
+
return spawnSync('gjs', args, { encoding: 'utf8', timeout: 10_000 }).status === 0
|
|
300
|
+
} catch {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const GJS_TYPELIB_PROBE =
|
|
306
|
+
'imports.gi.versions.Gtk="4.0";imports.gi.versions.WebKit="6.0";imports.gi.versions.Adw="1";' +
|
|
307
|
+
'void imports.gi.Gtk;void imports.gi.WebKit;void imports.gi.Adw'
|
|
308
|
+
|
|
309
|
+
/** Linux host prerequisites as a missing-item list (empty when satisfied). */
|
|
310
|
+
export function linuxMissingPrerequisites() {
|
|
311
|
+
const missing = []
|
|
312
|
+
if (!gjsProbe(['--version'])) {
|
|
313
|
+
missing.push('gjs')
|
|
314
|
+
} else if (!gjsProbe(['-c', GJS_TYPELIB_PROBE])) {
|
|
315
|
+
missing.push('GTK4/WebKitGTK/Adwaita typelibs (gir1.2-webkit-6.0, gir1.2-adw-1)')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return missing
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The published `@octane-xplat/ui` map needs a `linux` condition or packed
|
|
322
|
+
* consumers silently fall back to the web build, losing openWindow and
|
|
323
|
+
* colorScheme host overrides. */
|
|
324
|
+
function uiHasLinuxExport(root) {
|
|
325
|
+
const manifest = resolveFrom(root, '@octane-xplat/ui/package.json')
|
|
326
|
+
if (!manifest) {
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
return Boolean(JSON.parse(readFileSync(manifest, 'utf8')).exports?.['.']?.linux)
|
|
332
|
+
} catch {
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function linuxDoctor(root, sharedRequirements, configFile) {
|
|
338
|
+
const backend = 'gtk4/webkitgtk dom host (gjs)'
|
|
339
|
+
if (process.platform !== 'linux') {
|
|
340
|
+
return {
|
|
341
|
+
backend,
|
|
342
|
+
requirements: {},
|
|
343
|
+
status: 'remote-host-required',
|
|
344
|
+
note: "Linux runs on the qualified GTK/WebKitGTK runtime — run `xplat workshop dev --target linux` in that host's checkout",
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const requirements = {
|
|
349
|
+
...sharedRequirements,
|
|
350
|
+
'workshop.config': Boolean(configFile),
|
|
351
|
+
gjs: gjsProbe(['--version']),
|
|
352
|
+
'GTK4/WebKitGTK/Adwaita typelibs': gjsProbe(['-c', GJS_TYPELIB_PROBE]),
|
|
353
|
+
'@octane-xplat/ui linux export': uiHasLinuxExport(root),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
backend,
|
|
358
|
+
requirements,
|
|
359
|
+
status: Object.values(requirements).every(Boolean) ? 'available' : 'missing-prerequisites',
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------- dev ----------
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Shared dev-server seam for every DOM target — the caller owns process
|
|
367
|
+
* lifecycle (signal handlers, host subprocess). Exported so qualification
|
|
368
|
+
* harnesses can boot the exact production wiring.
|
|
369
|
+
*/
|
|
370
|
+
export async function startWorkshopServer({ root, config, target, story, port } = {}) {
|
|
371
|
+
const [{ createServer, searchForWorkspaceRoot }, { octane }] = await Promise.all([
|
|
372
|
+
importFrom(root, 'vite'),
|
|
373
|
+
importFrom(root, '@octanejs/vite-plugin'),
|
|
374
|
+
])
|
|
375
|
+
|
|
376
|
+
const assetRoots = Object.values(config.assets ?? {}).map((dir) => resolve(root, dir))
|
|
377
|
+
const server = await createServer({
|
|
378
|
+
root,
|
|
379
|
+
configFile: false,
|
|
380
|
+
mode: 'development',
|
|
381
|
+
logLevel: 'info',
|
|
382
|
+
plugins: [
|
|
383
|
+
// The workshop's 'pre' hotUpdate must run before the octane plugin's
|
|
384
|
+
// own 'pre' hook, so the story catalog answers edits before octane
|
|
385
|
+
// can force a full page reload.
|
|
386
|
+
...xplatWorkshop({ cwd: root, config, target, story }),
|
|
387
|
+
...octane(),
|
|
388
|
+
xplatBoundary(target),
|
|
389
|
+
xplatDevBridge(),
|
|
390
|
+
],
|
|
391
|
+
define: xplatNodeEnvDefine('development'),
|
|
392
|
+
server: {
|
|
393
|
+
...(port ? { port, strictPort: true } : {}),
|
|
394
|
+
fs: {
|
|
395
|
+
allow: [...new Set([searchForWorkspaceRoot(root), root, ...assetRoots].filter(Boolean))],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
await server.listen()
|
|
401
|
+
const address = server.httpServer?.address()
|
|
402
|
+
const resolvedPort = typeof address === 'object' && address ? address.port : port
|
|
403
|
+
const url = `http://localhost:${resolvedPort}/`
|
|
404
|
+
console.log(`WORKSHOP_READY ${url}`)
|
|
405
|
+
server.printUrls()
|
|
406
|
+
return { server, url }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function workshopDev({ cwd, target = 'web', story, port, device } = {}) {
|
|
410
|
+
const root = consumerRoot(cwd)
|
|
411
|
+
if (target === 'macos') {
|
|
412
|
+
const { config } = await loadWorkshopConfig(root)
|
|
413
|
+
const { workshopMacOSDev } = await import('./macos-node.ts')
|
|
414
|
+
return workshopMacOSDev({ root, config, story })
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (target === 'ios' || target === 'android') {
|
|
418
|
+
if (port !== undefined) {
|
|
419
|
+
throw new WorkshopUsageError(
|
|
420
|
+
'Mobile workshop uses the consumer native Vite server port; configure it in the native Vite config',
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const prerequisites = mobilePrerequisites(root, target)
|
|
425
|
+
if (prerequisites.status !== 'unqualified-backend') {
|
|
426
|
+
throw new WorkshopUsageError(
|
|
427
|
+
`Mobile workshop ${target}: ${prerequisites.status}. Run xplat workshop doctor and follow @octane-xplat/workshop/MOBILE.md setup.`,
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const { config } = await loadWorkshopConfig(root)
|
|
432
|
+
try {
|
|
433
|
+
return await launchMobile({ root, target, device, story, config })
|
|
434
|
+
} catch (error) {
|
|
435
|
+
throw new WorkshopUsageError(error.message)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (target === 'windows') {
|
|
440
|
+
throw new WorkshopUsageError(windowsLaunchBlocker(windowsPreflight(root)))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (target !== 'web' && target !== 'linux') {
|
|
444
|
+
throw new WorkshopUsageError(
|
|
445
|
+
`xplat workshop dev --target ${target}: this workshop preview provides web, linux, macos, ios and android launch paths — ` +
|
|
446
|
+
`run \`xplat workshop doctor\` for per-target status`,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const gjsHost = target === 'linux' ? checkLinuxHost(root) : null
|
|
451
|
+
const { config } = await loadWorkshopConfig(root)
|
|
452
|
+
const { server, url } = await startWorkshopServer({ root, config, target, story, port })
|
|
453
|
+
|
|
454
|
+
if (target === 'linux') {
|
|
455
|
+
return runLinuxHost({ server, url, gjsHost })
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const shutdown = async () => {
|
|
459
|
+
await server.close()
|
|
460
|
+
process.exit(0)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
process.on('SIGINT', shutdown)
|
|
464
|
+
process.on('SIGTERM', shutdown)
|
|
465
|
+
return server
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Gate the Linux launch before the dev server binds: the GTK host runs on
|
|
469
|
+
* Linux only, needs gjs + the WebKitGTK/Adwaita typelibs, and launches the
|
|
470
|
+
* canonical host shipped in @octane-xplat/cli. Returns the host script path. */
|
|
471
|
+
function checkLinuxHost(root) {
|
|
472
|
+
if (process.platform !== 'linux') {
|
|
473
|
+
throw new WorkshopUsageError(
|
|
474
|
+
'xplat workshop dev --target linux: the GTK4/WebKitGTK host runs on Linux only — ' +
|
|
475
|
+
"run this command in the Linux host's own checkout. `xplat workshop doctor` reports per-target status",
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const missing = linuxMissingPrerequisites()
|
|
480
|
+
if (missing.length) {
|
|
481
|
+
throw new WorkshopUsageError(
|
|
482
|
+
`xplat workshop dev --target linux: missing host prerequisites: ${missing.join(', ')} — ` +
|
|
483
|
+
'on Debian/Ubuntu install: gjs libgtk-4-1 libadwaita-1-0 libwebkitgtk-6.0-4 gir1.2-webkit-6.0 gir1.2-adw-1',
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const cliManifest = resolveFrom(root, '@octane-xplat/cli/package.json')
|
|
488
|
+
const gjsHost = cliManifest
|
|
489
|
+
? join(dirname(cliManifest), 'src', 'linux', 'host', 'gjs-host.js')
|
|
490
|
+
: null
|
|
491
|
+
|
|
492
|
+
if (!gjsHost || !existsSync(gjsHost)) {
|
|
493
|
+
throw new WorkshopUsageError(
|
|
494
|
+
'xplat workshop dev --target linux: cannot find the GJS host in @octane-xplat/cli ' +
|
|
495
|
+
'(expected cli/src/linux/host/gjs-host.js) — update @octane-xplat/cli',
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return gjsHost
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Launch the canonical GJS/WebKitGTK host at the dev server and own both
|
|
503
|
+
* halves' lifecycle: closing the host window stops the server, a dying
|
|
504
|
+
* server kills the host, and signals shut both down. */
|
|
505
|
+
async function runLinuxHost({ server, url, gjsHost }) {
|
|
506
|
+
// A distinct application id keeps workshop media-permission/config state
|
|
507
|
+
// out of the app under development's directories.
|
|
508
|
+
const host = spawn('gjs', [gjsHost, url], {
|
|
509
|
+
stdio: 'inherit',
|
|
510
|
+
env: { ...process.env, XPLAT_PROBE_APP_ID: 'org.octane.xplat.workshop' },
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
let exiting = false
|
|
514
|
+
const shutdown = async (code = 0) => {
|
|
515
|
+
if (exiting) {
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
exiting = true
|
|
520
|
+
if (host.exitCode === null && !host.killed) {
|
|
521
|
+
host.kill()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
await server.close().catch(() => {})
|
|
525
|
+
process.exit(code)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
host.on('exit', (code) => void shutdown(code ?? 0))
|
|
529
|
+
host.on('error', async (error) => {
|
|
530
|
+
console.error(`xplat workshop: failed to launch the Linux host — ${error.message}`)
|
|
531
|
+
await shutdown(1)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
process.on('SIGINT', () => void shutdown(0))
|
|
535
|
+
process.on('SIGTERM', () => void shutdown(0))
|
|
536
|
+
return server
|
|
537
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { UserConfig } from 'vite'
|
|
2
|
+
import type { WorkshopConfig } from '../../types/generated/contract.js'
|
|
3
|
+
/** Preserves the consumer config and prepends the native workshop's pre HMR hook. */
|
|
4
|
+
export declare function withWorkshopMobile(
|
|
5
|
+
config: UserConfig,
|
|
6
|
+
options?: {
|
|
7
|
+
cwd?: string
|
|
8
|
+
target?: 'ios' | 'android'
|
|
9
|
+
story?: string
|
|
10
|
+
config?: WorkshopConfig
|
|
11
|
+
},
|
|
12
|
+
): UserConfig
|
|
13
|
+
|
|
14
|
+
/** @internal Virtual module identity; exposed for adapter contract checks. */
|
|
15
|
+
export declare const MOBILE_CATALOG_ID: string
|
|
16
|
+
/** @internal Generate a statically filtered native catalog without module evaluation. */
|
|
17
|
+
export declare function mobileCatalogCode(
|
|
18
|
+
root: string,
|
|
19
|
+
config: WorkshopConfig,
|
|
20
|
+
target: 'ios' | 'android',
|
|
21
|
+
): string
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @ts-nocheck — .mjs→.ts port: annotations pending (transitional)
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { mobileRegexPlugin } from './mobile-regex.ts'
|
|
5
|
+
import { discoverStories } from './discover.ts'
|
|
6
|
+
|
|
7
|
+
export const MOBILE_CATALOG_ID = '\0xplat-workshop-mobile-catalog'
|
|
8
|
+
const fsUrl = (file) => `/@fs/${file.replaceAll('\\', '/')}`
|
|
9
|
+
|
|
10
|
+
export function mobileCatalogCode(root, config, target) {
|
|
11
|
+
const { entries, diagnostics } = discoverStories(root, { ...config.stories, target })
|
|
12
|
+
const loaders = entries
|
|
13
|
+
.filter((e) => e.applicable && !diagnostics.length)
|
|
14
|
+
.map(
|
|
15
|
+
(e) =>
|
|
16
|
+
`${JSON.stringify(e.id)}: (stamp) => stamp === undefined ? import(${JSON.stringify(fsUrl(e.file))}) : import(/* @vite-ignore */ ${JSON.stringify(fsUrl(e.file))} + '?t=' + encodeURIComponent(stamp))`,
|
|
17
|
+
)
|
|
18
|
+
.join(',\n')
|
|
19
|
+
|
|
20
|
+
return `import { catalogDidUpdate } from '@octane-xplat/workshop/mobile'
|
|
21
|
+
export const diagnostics = ${JSON.stringify(diagnostics)}
|
|
22
|
+
const loaders = {${loaders}}
|
|
23
|
+
export const catalog = ${JSON.stringify(entries)}.map(entry => loaders[entry.id] ? {...entry, load: loaders[entry.id]} : entry)
|
|
24
|
+
if (import.meta.hot) import.meta.hot.accept(next => catalogDidUpdate(next?.catalog ?? catalog, next?.diagnostics ?? diagnostics))
|
|
25
|
+
`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Apply AFTER resolving the consumer preset, so this pre hook precedes octane(). */
|
|
29
|
+
export function withWorkshopMobile(config, options = {}) {
|
|
30
|
+
const target = options.target ?? process.env.XPLAT_WORKSHOP_TARGET
|
|
31
|
+
if (!target) {
|
|
32
|
+
return config
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!['ios', 'android'].includes(target)) {
|
|
36
|
+
throw new Error('Workshop mobile target must be ios or android')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const root = options.cwd ?? process.cwd()
|
|
40
|
+
const workshop = options.config ?? JSON.parse(process.env.XPLAT_WORKSHOP_CONFIG ?? '{}')
|
|
41
|
+
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
|
|
42
|
+
const main = resolve(root, manifest.main ?? 'src/index.ts')
|
|
43
|
+
const plugin = {
|
|
44
|
+
name: 'xplat-workshop-mobile',
|
|
45
|
+
enforce: 'pre',
|
|
46
|
+
resolveId(id) {
|
|
47
|
+
if (id === MOBILE_CATALOG_ID) {
|
|
48
|
+
return id
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
load(id) {
|
|
52
|
+
if (id === MOBILE_CATALOG_ID) {
|
|
53
|
+
return mobileCatalogCode(root, workshop, target)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Retain the NativeScript polyfills/dev-session bootstrap; replace
|
|
57
|
+
// only its consumer entry, without touching the production file.
|
|
58
|
+
if (id.split('?')[0] !== main) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const presets = workshop.providerModules ?? []
|
|
63
|
+
return `import { startWorkshop } from '@octane-xplat/workshop/mobile'
|
|
64
|
+
import { catalog, diagnostics } from ${JSON.stringify(MOBILE_CATALOG_ID)}
|
|
65
|
+
import '@octane-xplat/ui/theme/tokens.css'
|
|
66
|
+
${presets.map((p, i) => `import * as preset${i} from ${JSON.stringify(fsUrl(resolve(root, p)))}`).join('\n')}
|
|
67
|
+
const workshopRuntime = startWorkshop({catalog, diagnostics, target: ${JSON.stringify(target)}, initialStoryId: ${JSON.stringify(options.story ?? process.env.XPLAT_WORKSHOP_STORY ?? null)}, globalProviders: [${presets.map((_, i) => `...(preset${i}.providers ?? preset${i}.default ?? [])`).join(',')}]})
|
|
68
|
+
if (import.meta.hot) {
|
|
69
|
+
import.meta.hot.accept()
|
|
70
|
+
import.meta.hot.on('vite:error', error => workshopRuntime.controller.session.markCompileError(error.err?.message ?? error))
|
|
71
|
+
import.meta.hot.on('vite:ws:disconnect', () => workshopRuntime.controller.session.markDisconnected())
|
|
72
|
+
import.meta.hot.on('vite:ws:connect', () => workshopRuntime.controller.session.markConnected())
|
|
73
|
+
import.meta.hot.dispose(() => { void workshopRuntime.dispose().catch(error => console.error('Workshop cleanup requires restart:', error)) })
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
},
|
|
77
|
+
hotUpdate: {
|
|
78
|
+
order: 'pre',
|
|
79
|
+
handler(ctx) {
|
|
80
|
+
// NativeScript's active Vite environment is used directly; no
|
|
81
|
+
// browser-only client assumption. All consumer source edits
|
|
82
|
+
// refresh the selected revision, including dependency edits.
|
|
83
|
+
if (
|
|
84
|
+
!/\.(?:tsrx|tsx|ts|jsx|js|mjs)$/.test(ctx.file) ||
|
|
85
|
+
ctx.file.includes('/node_modules/')
|
|
86
|
+
) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const catalog = this.environment.moduleGraph.getModuleById(MOBILE_CATALOG_ID)
|
|
91
|
+
if (!catalog) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.environment.moduleGraph.invalidateModule(catalog)
|
|
96
|
+
return [catalog]
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { ...config, plugins: [plugin, mobileRegexPlugin(), ...(config.plugins ?? [])] }
|
|
102
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
import type { WorkshopConfig } from '../../types/generated/contract.js'
|
|
3
|
+
|
|
4
|
+
export interface XplatWorkshopOptions {
|
|
5
|
+
cwd?: string
|
|
6
|
+
config?: WorkshopConfig
|
|
7
|
+
target?: 'web' | 'ios' | 'android' | 'macos' | 'windows' | 'linux'
|
|
8
|
+
story?: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function xplatWorkshop(options?: XplatWorkshopOptions): Plugin[]
|