@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,404 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { globSync } from "tinyglobby";
3
+ //#region src/build/discover.ts
4
+ /**
5
+ * Node-side story discovery — static, metadata-only.
6
+ *
7
+ * Story modules are never evaluated: this scanner reads `defineStory` option
8
+ * literals out of the source text. `id`, `title`, and `targets` must be
9
+ * literal values; anything computed fails with an actionable diagnostic.
10
+ */
11
+ var ALL_TARGETS = [
12
+ "web",
13
+ "ios",
14
+ "android",
15
+ "macos",
16
+ "windows",
17
+ "linux"
18
+ ];
19
+ var DEFAULT_INCLUDE = ["**/*.stories.{ts,tsx,tsrx,mts,mjs,js,jsx}", "**/*.stories.{web,mobile,ios,android,macos,windows,linux}.{ts,tsx,tsrx,mts,mjs,js,jsx}"];
20
+ var DEFAULT_EXCLUDE = [
21
+ "**/node_modules/**",
22
+ "**/dist/**",
23
+ "**/types/**"
24
+ ];
25
+ var SUFFIX_TARGETS = {
26
+ web: ["web"],
27
+ mobile: ["ios", "android"],
28
+ ios: ["ios"],
29
+ android: ["android"],
30
+ macos: ["macos"],
31
+ windows: ["windows"],
32
+ linux: ["linux"]
33
+ };
34
+ var SUFFIX_RE = /\.stories\.(web|mobile|ios|android|macos|windows|linux)\.[cm]?[jt]srx?$/;
35
+ function storyFileTag(file) {
36
+ return SUFFIX_RE.exec(file)?.[1] ?? null;
37
+ }
38
+ function applicableTargets(declared, file) {
39
+ const tag = storyFileTag(file);
40
+ if (!tag) return declared;
41
+ const implied = SUFFIX_TARGETS[tag] ?? [];
42
+ return declared.filter((t) => implied.includes(t));
43
+ }
44
+ function normalizeInclude(include) {
45
+ if (include === void 0) return DEFAULT_INCLUDE;
46
+ return Array.isArray(include) ? [...include] : [include];
47
+ }
48
+ function normalizeExclude(exclude) {
49
+ const extra = exclude === void 0 ? [] : Array.isArray(exclude) ? [...exclude] : [exclude];
50
+ return [...DEFAULT_EXCLUDE, ...extra];
51
+ }
52
+ /** Strip strings, template literals, and comments while preserving length. */
53
+ function maskSource(source) {
54
+ const out = source.split("");
55
+ let i = 0;
56
+ const n = source.length;
57
+ const blank = (from, to) => {
58
+ for (let k = from; k < to; k++) out[k] = out[k] === "\n" ? "\n" : " ";
59
+ };
60
+ while (i < n) {
61
+ const ch = source[i];
62
+ if (ch === "/" && source[i + 1] === "/") {
63
+ let j = i;
64
+ while (j < n && source[j] !== "\n") j++;
65
+ blank(i, j);
66
+ i = j;
67
+ } else if (ch === "/" && source[i + 1] === "*") {
68
+ let j = source.indexOf("*/", i + 2);
69
+ j = j === -1 ? n : j + 2;
70
+ blank(i, j);
71
+ i = j;
72
+ } else if (ch === "'" || ch === "\"") {
73
+ let j = i + 1;
74
+ while (j < n) if (source[j] === "\\") j += 2;
75
+ else if (source[j] === ch) {
76
+ j++;
77
+ break;
78
+ } else j++;
79
+ j = Math.min(j, n);
80
+ blank(i + 1, Math.max(i + 1, j - 1));
81
+ i = j;
82
+ } else if (ch === "`") {
83
+ let j = i + 1;
84
+ while (j < n) if (source[j] === "\\") {
85
+ blank(j, j + 2);
86
+ j += 2;
87
+ } else if (source[j] === "`") {
88
+ j++;
89
+ break;
90
+ } else if (source[j] === "$" && source[j + 1] === "{") {
91
+ let depth = 1;
92
+ let k = j + 2;
93
+ while (k < n && depth > 0) {
94
+ if (source[k] === "{") depth++;
95
+ else if (source[k] === "}") depth--;
96
+ else if (source[k] === "'" || source[k] === "\"") {
97
+ let q = k + 1;
98
+ while (q < n && source[q] !== source[k]) q += source[q] === "\\" ? 2 : 1;
99
+ k = Math.min(q + 1, n);
100
+ continue;
101
+ }
102
+ k++;
103
+ }
104
+ j = k;
105
+ } else {
106
+ blank(j, j + 1);
107
+ j++;
108
+ }
109
+ i = j;
110
+ } else i++;
111
+ }
112
+ return out.join("");
113
+ }
114
+ /** Find matching close for an opening bracket at index `open` in masked text. */
115
+ function matchBracket(masked, open, openCh, closeCh) {
116
+ let depth = 0;
117
+ for (let i = open; i < masked.length; i++) {
118
+ const ch = masked[i];
119
+ if (ch === openCh) depth++;
120
+ else if (ch === closeCh) {
121
+ depth--;
122
+ if (depth === 0) return i;
123
+ }
124
+ }
125
+ return -1;
126
+ }
127
+ /** Split a masked object-literal body into top-level key → raw value text. */
128
+ function objectFields(masked, source, openBrace, closeBrace) {
129
+ const fields = /* @__PURE__ */ new Map();
130
+ let i = openBrace + 1;
131
+ while (i < closeBrace) {
132
+ while (i < closeBrace && /[\s,]/.test(masked[i])) i++;
133
+ if (i >= closeBrace) break;
134
+ const m = /^([A-Za-z_$][\w$]*|'[\s\S]*'|"[\s\S]*"|\d+)\s*:/.exec(masked.slice(i, closeBrace));
135
+ if (!m) return {
136
+ fields,
137
+ error: `unsupported object syntax near "${masked.slice(i, i + 24).trim()}"`
138
+ };
139
+ const rawKey = m[1];
140
+ const key = rawKey.startsWith("'") || rawKey.startsWith("\"") ? source.slice(i, i + m[0].indexOf(":")).trim().slice(1, -1) : rawKey;
141
+ let v = i + m[0].length;
142
+ const start = v;
143
+ let depth = 0;
144
+ while (v < closeBrace) {
145
+ const ch = masked[v];
146
+ if (ch === "{" || ch === "[" || ch === "(") depth++;
147
+ else if (ch === "}" || ch === "]" || ch === ")") {
148
+ if (depth === 0) break;
149
+ depth--;
150
+ } else if (ch === "," && depth === 0) break;
151
+ v++;
152
+ }
153
+ fields.set(key, {
154
+ start,
155
+ end: v
156
+ });
157
+ i = v + 1;
158
+ }
159
+ return { fields };
160
+ }
161
+ /** Evaluate a literal-only expression slice; throws on non-literal content. */
162
+ function literalValue(masked, source, start, end, label, file) {
163
+ const text = masked.slice(start, end).trim();
164
+ const fail = () => {
165
+ throw new Error(`${file}: story metadata "${label}" must be a literal value — got "${source.slice(start, end).trim().slice(0, 60)}"`);
166
+ };
167
+ if (text === "") fail();
168
+ if (text === "true") return true;
169
+ if (text === "false") return false;
170
+ if (text === "null") return null;
171
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
172
+ if (/^'[^']*'$/.test(text) || /^"[^"]*"$/.test(text)) {
173
+ const value = source.slice(start, end).trim().slice(1, -1);
174
+ if (value.includes("\\")) fail();
175
+ return value;
176
+ }
177
+ if (text.startsWith("`") || text === "") fail();
178
+ if (text.startsWith("[")) {
179
+ const openIdx = masked.indexOf("[", start);
180
+ const close = matchBracket(masked, openIdx, "[", "]");
181
+ if (close < 0 || masked.slice(close + 1, end).trim()) fail();
182
+ const items = [];
183
+ let seg = openIdx + 1;
184
+ let depth = 0;
185
+ const push = (end2) => {
186
+ if (masked.slice(seg, end2).trim()) items.push(literalValue(masked, source, seg, end2, label, file));
187
+ };
188
+ for (let i = openIdx + 1; i < close; i++) {
189
+ const ch = masked[i];
190
+ if (ch === "{" || ch === "[" || ch === "(") depth++;
191
+ else if (ch === "}" || ch === "]" || ch === ")") depth--;
192
+ else if (ch === "," && depth === 0) {
193
+ push(i);
194
+ seg = i + 1;
195
+ }
196
+ }
197
+ push(close);
198
+ return items;
199
+ }
200
+ fail();
201
+ }
202
+ var DECL_RE = /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*$/;
203
+ /**
204
+ * Extract story metadata declarations from source text. Returns
205
+ * { stories: [...], diagnostics: [...] } — never throws on malformed input.
206
+ */
207
+ function scanStoryFile(source, file) {
208
+ const masked = maskSource(source);
209
+ const stories = [];
210
+ const diagnostics = [];
211
+ const exported = /* @__PURE__ */ new Set();
212
+ for (const m of masked.matchAll(/export\s*\{([^}]*)\}/g)) for (const part of m[1].split(",")) {
213
+ const name = part.trim().split(/\s+as\s+/).pop()?.trim();
214
+ if (name) exported.add(name);
215
+ }
216
+ let search = 0;
217
+ for (;;) {
218
+ const at = masked.indexOf("defineStory", search);
219
+ if (at === -1) break;
220
+ search = at + 11;
221
+ const before = masked.slice(0, at);
222
+ const after = masked.slice(at + 11);
223
+ if (/[\w$.]$/.test(before) || /^[\w$]/.test(after)) continue;
224
+ let exportName = "default";
225
+ const stmtStart = Math.max(before.lastIndexOf(";"), before.lastIndexOf("}"), before.lastIndexOf("{"));
226
+ const stmt = before.slice(stmtStart + 1);
227
+ const decl = DECL_RE.exec(stmt);
228
+ let exportedStory = false;
229
+ if (decl && decl.index + decl[0].length === stmt.length) {
230
+ exportName = decl[1];
231
+ exportedStory = /export\s+(const|let|var)\s/.test(stmt) || exported.has(exportName);
232
+ } else if (/export\s+default\s*$/.test(stmt)) exportedStory = true;
233
+ let i = at + 11;
234
+ while (i < masked.length && /\s/.test(masked[i])) i++;
235
+ if (masked[i] === "<") {
236
+ const closeAngle = matchBracket(masked, i, "<", ">");
237
+ if (closeAngle !== -1) {
238
+ i = closeAngle + 1;
239
+ while (i < masked.length && /\s/.test(masked[i])) i++;
240
+ }
241
+ }
242
+ if (masked[i] !== "(") continue;
243
+ const closeParen = matchBracket(masked, i, "(", ")");
244
+ if (closeParen === -1) {
245
+ diagnostics.push({
246
+ file,
247
+ exportName,
248
+ message: "defineStory call has unbalanced parentheses"
249
+ });
250
+ continue;
251
+ }
252
+ let j = i + 1;
253
+ while (j < closeParen && /\s/.test(masked[j])) j++;
254
+ if (masked[j] !== "{") {
255
+ diagnostics.push({
256
+ file,
257
+ exportName,
258
+ message: "defineStory options must be an object literal"
259
+ });
260
+ continue;
261
+ }
262
+ const closeBrace = matchBracket(masked, j, "{", "}");
263
+ const { fields, error } = objectFields(masked, source, j, closeBrace);
264
+ if (error) {
265
+ diagnostics.push({
266
+ file,
267
+ exportName,
268
+ message: error
269
+ });
270
+ continue;
271
+ }
272
+ const story = {
273
+ file,
274
+ exportName,
275
+ exported: exportedStory
276
+ };
277
+ let failed = false;
278
+ for (const key of [
279
+ "id",
280
+ "title",
281
+ "targets",
282
+ "capabilities"
283
+ ]) {
284
+ const field = fields.get(key);
285
+ if (!field) {
286
+ if (key === "capabilities") continue;
287
+ diagnostics.push({
288
+ file,
289
+ exportName,
290
+ message: `defineStory requires a literal "${key}"`
291
+ });
292
+ failed = true;
293
+ continue;
294
+ }
295
+ try {
296
+ story[key] = literalValue(masked, source, field.start, field.end, key, file);
297
+ } catch (e) {
298
+ diagnostics.push({
299
+ file,
300
+ exportName,
301
+ message: e.message
302
+ });
303
+ failed = true;
304
+ }
305
+ }
306
+ if (!failed && (typeof story.id !== "string" || !story.id || typeof story.title !== "string" || !story.title || !(story.targets === "all" || Array.isArray(story.targets) && story.targets.length > 0 && story.targets.every((t) => ALL_TARGETS.includes(t))) || !(story.capabilities === void 0 || Array.isArray(story.capabilities) && story.capabilities.every((c) => typeof c === "string" && c.length > 0)))) {
307
+ diagnostics.push({
308
+ file,
309
+ exportName,
310
+ message: "story requires non-empty literal id/title, targets: all or a non-empty target array, and a string capability array"
311
+ });
312
+ failed = true;
313
+ }
314
+ if (!failed) stories.push(story);
315
+ }
316
+ return {
317
+ stories,
318
+ diagnostics
319
+ };
320
+ }
321
+ /**
322
+ * Discover story metadata under `root`.
323
+ * → { entries, diagnostics }
324
+ * entries: StoryMeta[] (applicable/reason computed for `target`)
325
+ */
326
+ function discoverStories(root, { include, exclude, target = "web" } = {}) {
327
+ const files = globSync(normalizeInclude(include), {
328
+ cwd: root,
329
+ ignore: normalizeExclude(exclude),
330
+ absolute: true
331
+ });
332
+ const entries = [];
333
+ const diagnostics = [];
334
+ const byId = /* @__PURE__ */ new Map();
335
+ for (const file of files) {
336
+ let source;
337
+ try {
338
+ source = readFileSync(file, "utf8");
339
+ } catch (e) {
340
+ diagnostics.push({
341
+ file,
342
+ message: `could not read story file: ${e.message}`
343
+ });
344
+ continue;
345
+ }
346
+ const { stories, diagnostics: fileDiagnostics } = scanStoryFile(source, file);
347
+ diagnostics.push(...fileDiagnostics);
348
+ for (const story of stories) {
349
+ if (!story.exported) {
350
+ diagnostics.push({
351
+ file,
352
+ exportName: story.exportName,
353
+ message: `defineStory("${story.id}") must be exported to be discovered`
354
+ });
355
+ continue;
356
+ }
357
+ const declared = story.targets === "all" ? ALL_TARGETS : Array.isArray(story.targets) ? story.targets : [story.targets];
358
+ for (const t of declared) if (!ALL_TARGETS.includes(t)) diagnostics.push({
359
+ file,
360
+ exportName: story.exportName,
361
+ message: `story "${story.id}" declares unknown target "${t}"`
362
+ });
363
+ const applicable = applicableTargets(declared.filter((t) => ALL_TARGETS.includes(t)), file);
364
+ const required = story.capabilities ?? [];
365
+ const canRun = applicable.includes(target) && required.length === 0;
366
+ const reason = canRun ? void 0 : applicable.includes(target) && required.length > 0 ? `required capabilities are unknown/unqualified on ${target}: ${required.join(", ")}` : applicable.length === 0 ? `declared targets and the ${storyFileTag(file) ?? "story"} leaf do not overlap` : `story targets ${applicable.join(", ")}, not ${target}`;
367
+ const meta = {
368
+ id: story.id,
369
+ title: story.title,
370
+ targets: applicable,
371
+ capabilities: story.capabilities ?? [],
372
+ file,
373
+ exportName: story.exportName,
374
+ applicable: canRun,
375
+ reason
376
+ };
377
+ const prior = byId.get(story.id) ?? [];
378
+ const conflict = prior.find((prev) => prev.targets.some((t) => meta.targets.includes(t)));
379
+ if (conflict) {
380
+ diagnostics.push({
381
+ file,
382
+ exportName: story.exportName,
383
+ message: `duplicate story id "${story.id}" — also declared in ${conflict.file} (export "${conflict.exportName}")`
384
+ });
385
+ continue;
386
+ }
387
+ byId.set(story.id, [...prior, meta]);
388
+ entries.push(meta);
389
+ }
390
+ }
391
+ entries.sort((a, b) => a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
392
+ const byRow = /* @__PURE__ */ new Map();
393
+ for (const entry of entries) {
394
+ const list = byRow.get(entry.id) ?? [];
395
+ list.push(entry);
396
+ byRow.set(entry.id, list);
397
+ }
398
+ return {
399
+ entries: [...byRow.values()].map((group) => group.find((e) => e.applicable) ?? group[0]),
400
+ diagnostics
401
+ };
402
+ }
403
+ //#endregion
404
+ export { ALL_TARGETS, applicableTargets, discoverStories, maskSource, normalizeExclude, normalizeInclude, scanStoryFile, storyFileTag };
@@ -0,0 +1,77 @@
1
+ import { createRequire } from "node:module";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/build/macos-node.ts
6
+ /** Generate a separate consumer-owned cache profile; leave app entries untouched. */
7
+ async function workshopMacOSDev({ root, config, story }) {
8
+ if (process.platform !== "darwin" || process.arch !== "arm64") throw new Error("The AppKit workshop requires a macOS arm64 execution host");
9
+ const require = createRequire(join(root, "package.json"));
10
+ for (const name of [
11
+ "@octane-xplat/macos-renderer",
12
+ "@nativescript/macos-node-api",
13
+ "octane",
14
+ "@octanejs/vite-plugin",
15
+ "vite"
16
+ ]) try {
17
+ require.resolve(name);
18
+ } catch {
19
+ throw new Error(`AppKit workshop prerequisite missing: ${name}; use the matching versions from your app's macOS setup`);
20
+ }
21
+ const cache = join(root, "node_modules/.cache/xplat/workshop-macos");
22
+ await mkdir(cache, { recursive: true });
23
+ const directory = await mkdtemp(join(cache, "session-"));
24
+ const source = dirname(fileURLToPath(import.meta.url)).replace("/dist/", "/src/");
25
+ const catalogPlugin = join(source, "macos-vite.ts");
26
+ const shell = join(source, "macos/shell.macos.ts");
27
+ const bundle = join(directory, "stories.cjs");
28
+ const shellBundle = join(directory, "shell.cjs");
29
+ const catalogFile = join(directory, "catalog.gen.ts");
30
+ const common = `import { xplatMacOS } from '@octane-xplat/cli/macos/vite'
31
+ import { macOSCatalogPlugin } from ${JSON.stringify(catalogPlugin)}
32
+ const root = ${JSON.stringify(root)}
33
+ `;
34
+ await writeFile(join(directory, "stories.config.ts"), common + `
35
+ export default async ({ mode }) => {
36
+ const settings = await xplatMacOS(mode, { root, packaged: false, hmr: false })
37
+ return { ...settings, plugins: [macOSCatalogPlugin({ root, config: ${JSON.stringify(config)}, catalogFile: ${JSON.stringify(catalogFile)} }), ...settings.plugins],
38
+ build: { ...settings.build, outDir: ${JSON.stringify(directory)}, emptyOutDir: false,
39
+ lib: { entry: ${JSON.stringify(catalogFile)}, formats: ['cjs'], fileName: () => 'stories.cjs' } } }
40
+ }
41
+ `);
42
+ await writeFile(join(directory, "entry.ts"), `import { startMacOSWorkshop } from ${JSON.stringify(shell)}
43
+ startMacOSWorkshop({ bundleFile: ${JSON.stringify(bundle)}, initialStoryId: ${JSON.stringify(story ?? null)},
44
+ offscreen: process.env.OCTANE_WORKSHOP_OFFSCREEN === '1' })
45
+ `);
46
+ await writeFile(join(directory, "shell.config.ts"), common + `
47
+ export default async ({ mode }) => {
48
+ const settings = await xplatMacOS(mode, { root, packaged: true, entry: ${JSON.stringify(join(directory, "entry.ts"))} })
49
+ return { ...settings, build: { ...settings.build, outDir: ${JSON.stringify(directory)}, emptyOutDir: false,
50
+ lib: { entry: ${JSON.stringify(join(directory, "entry.ts"))}, formats: ['cjs'], fileName: () => 'shell.cjs' } } }
51
+ }
52
+ `);
53
+ try {
54
+ const { runMacOSDev } = await import("@octane-xplat/cli/macos");
55
+ return await runMacOSDev(root, {
56
+ workshop: true,
57
+ target: {
58
+ runtime: "appkit-node-api",
59
+ renderer: "appkit",
60
+ dev: {
61
+ productName: "Octane component workshop",
62
+ viteConfig: relative(root, join(directory, "stories.config.ts")),
63
+ bundleFile: relative(root, bundle),
64
+ shellViteConfig: relative(root, join(directory, "shell.config.ts")),
65
+ shellBundleFile: relative(root, shellBundle)
66
+ }
67
+ }
68
+ });
69
+ } finally {
70
+ await rm(directory, {
71
+ recursive: true,
72
+ force: true
73
+ });
74
+ }
75
+ }
76
+ //#endregion
77
+ export { workshopMacOSDev };
@@ -0,0 +1,96 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ //#region src/build/mobile-node.ts
6
+ var CONFIGS = [
7
+ "vite.config.native.mts",
8
+ "vite.config.native.ts",
9
+ "vite.config.mts",
10
+ "vite.config.ts",
11
+ "vite.config.ts"
12
+ ];
13
+ function mobilePrerequisites(root, target, host = process.platform, env = process.env) {
14
+ const req = createRequire(join(root, "package.json"));
15
+ const installed = (name) => {
16
+ try {
17
+ req.resolve(name);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+ const configFile = CONFIGS.map((name) => join(root, name)).find(existsSync);
24
+ const requirements = {
25
+ "nativescript.config.ts": existsSync(join(root, "nativescript.config.ts")),
26
+ "native workshop Vite setup": Boolean(configFile && /withWorkshopMobile/.test(readFileSync(configFile, "utf8"))),
27
+ "@nativescript/core": installed("@nativescript/core"),
28
+ "@nativescript-community/octane": installed("@nativescript-community/octane"),
29
+ "@nativescript-community/vite-octane": installed("@nativescript-community/vite-octane"),
30
+ "@valor/nativescript-websockets": installed("@valor/nativescript-websockets"),
31
+ "nativescript CLI": installed("nativescript")
32
+ };
33
+ if (target === "ios") requirements["macOS / Xcode host"] = host === "darwin" && existsSync("/Applications/Xcode.app");
34
+ else {
35
+ requirements["Android SDK directory"] = Boolean((env.ANDROID_HOME || env.ANDROID_SDK_ROOT) && existsSync(env.ANDROID_HOME || env.ANDROID_SDK_ROOT));
36
+ requirements["JAVA_HOME directory"] = Boolean(env.JAVA_HOME && existsSync(env.JAVA_HOME));
37
+ }
38
+ return {
39
+ backend: "nativescript (Octane universal renderer)",
40
+ requirements,
41
+ status: target === "ios" && host !== "darwin" ? "remote-host-required" : Object.values(requirements).every(Boolean) ? "unqualified-backend" : "missing-prerequisites",
42
+ note: "Mobile adapter installed; actual-host lifecycle, touch, keyboard, screen-reader and IME qualification remain open. Doctor does not launch or select a device."
43
+ };
44
+ }
45
+ function mobileLaunchSpec({ root, target, device, config, story }) {
46
+ if (!["ios", "android"].includes(target)) throw new Error("Expected ios or android");
47
+ if (!device || typeof device !== "string" || device.startsWith("-")) throw new Error("Mobile workshop requires --device <device-id>; it never chooses or boots a device implicitly");
48
+ const packageFile = createRequire(join(root, "package.json")).resolve("nativescript/package.json");
49
+ const manifest = JSON.parse(readFileSync(packageFile, "utf8"));
50
+ const binary = typeof manifest.bin === "string" ? manifest.bin : manifest.bin.ns ?? manifest.bin.nativescript;
51
+ if (!binary) throw new Error("Installed NativeScript CLI has no ns binary");
52
+ return {
53
+ command: process.execPath,
54
+ args: [
55
+ resolve(dirname(packageFile), binary),
56
+ "run",
57
+ target,
58
+ "--device",
59
+ device
60
+ ],
61
+ options: {
62
+ cwd: root,
63
+ stdio: "inherit",
64
+ shell: false,
65
+ env: {
66
+ ...process.env,
67
+ XPLAT_WORKSHOP_TARGET: target,
68
+ XPLAT_WORKSHOP_CONFIG: JSON.stringify(config),
69
+ XPLAT_WORKSHOP_STORY: story ?? ""
70
+ }
71
+ }
72
+ };
73
+ }
74
+ async function launchMobile(options) {
75
+ const spec = mobileLaunchSpec(options);
76
+ const child = spawn(spec.command, spec.args, spec.options);
77
+ const interrupt = () => child.kill("SIGINT");
78
+ const terminate = () => child.kill("SIGTERM");
79
+ process.on("SIGINT", interrupt);
80
+ process.on("SIGTERM", terminate);
81
+ try {
82
+ await new Promise((resolve, reject) => {
83
+ child.once("error", reject);
84
+ child.once("exit", (code, signal) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`NativeScript workshop ${options.target} exited ${signal ?? code}; check launch logs and restart the target`)));
85
+ });
86
+ } finally {
87
+ process.off("SIGINT", interrupt);
88
+ process.off("SIGTERM", terminate);
89
+ }
90
+ return {
91
+ target: options.target,
92
+ exited: true
93
+ };
94
+ }
95
+ //#endregion
96
+ export { launchMobile, mobileLaunchSpec, mobilePrerequisites };
@@ -0,0 +1,40 @@
1
+ import { transformAsync, types } from "@babel/core";
2
+ import unicodeProperties from "@babel/plugin-transform-unicode-property-regex";
3
+ //#region src/build/mobile-regex.ts
4
+ /** Compile Unicode properties for the pinned iOS runtime without changing globals. */
5
+ function transformMobileRegex(code, filename = "workshop-host.ts") {
6
+ return transformAsync(code, {
7
+ filename,
8
+ configFile: false,
9
+ babelrc: false,
10
+ sourceMaps: true,
11
+ plugins: [() => ({ visitor: { NewExpression(path) {
12
+ const node = path.node;
13
+ if (!types.isIdentifier(node.callee, { name: "RegExp" }) || path.scope.getBinding("RegExp") || node.arguments.length !== 2) return;
14
+ const [pattern, flags] = node.arguments;
15
+ if (types.isStringLiteral(pattern) && types.isStringLiteral(flags) && flags.value.includes("u") && /\\[pP]\{/.test(pattern.value)) path.replaceWith(types.regExpLiteral(pattern.value, flags.value));
16
+ } } }), unicodeProperties.default ?? unicodeProperties]
17
+ });
18
+ }
19
+ function mobileRegexPlugin() {
20
+ return {
21
+ name: "xplat-workshop-mobile-unicode-regex",
22
+ async transform(code, id) {
23
+ if (!/\\[pP]\{/.test(code) || !/\.[cm]?js(?:\?|$)/.test(id)) return null;
24
+ const result = await transformMobileRegex(code, id.split("?")[0]);
25
+ return result ? {
26
+ code: result.code,
27
+ map: result.map
28
+ } : null;
29
+ },
30
+ async renderChunk(code, chunk) {
31
+ const result = await transformMobileRegex(code, chunk.fileName);
32
+ return result ? {
33
+ code: result.code,
34
+ map: result.map
35
+ } : null;
36
+ }
37
+ };
38
+ }
39
+ //#endregion
40
+ export { mobileRegexPlugin, transformMobileRegex };