@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.
@@ -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 };