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