@blastlauncher/web 3.0.0-alpha.0 → 3.0.0-alpha.3

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/bundles.d.ts CHANGED
@@ -3,6 +3,10 @@ import type { CommandMode } from "@blastlauncher/protocol";
3
3
  export type CatalogArgument = {
4
4
  name: string;
5
5
  required: boolean;
6
+ /** The manifest argument type (`text`, `password`, `dropdown`). */
7
+ type?: string;
8
+ /** A dropdown argument's option values, in order. */
9
+ values?: string[];
6
10
  };
7
11
  export type CatalogManifest = {
8
12
  name: string;
package/dist/bundles.js CHANGED
@@ -92,7 +92,15 @@ export function parseBundleIndex(value) {
92
92
  const entry = argument;
93
93
  if (!entry || typeof entry.name !== "string" || typeof entry.required !== "boolean")
94
94
  throw new Error(`Invalid bundle manifest argument: ${key}/${name}`);
95
- return { name: entry.name, required: entry.required };
95
+ const values = Array.isArray(entry.values)
96
+ ? entry.values.filter((option) => typeof option === "string")
97
+ : [];
98
+ return {
99
+ name: entry.name,
100
+ required: entry.required,
101
+ ...(typeof entry.type === "string" ? { type: entry.type } : {}),
102
+ ...(values.length ? { values } : {}),
103
+ };
96
104
  });
97
105
  }
98
106
  manifest = {
@@ -42,7 +42,8 @@ export type BrowserCapabilityOptions = {
42
42
  storage?: BrowserStorage;
43
43
  clipboard?: ClipboardBridge;
44
44
  openUrl?: (url: string) => void;
45
- launchCommand?: (source: string, command: string) => Promise<void>;
45
+ /** `args` are the launched command's manifest argument values. */
46
+ launchCommand?: (source: string, command: string, args: Record<string, string>) => Promise<void>;
46
47
  status?: (message: string, error?: boolean) => void;
47
48
  openPreferences?: (scope: "extension" | "command", context: CapabilityContext) => void;
48
49
  };
@@ -259,7 +259,12 @@ export function createBrowserCapabilities(options) {
259
259
  }
260
260
  if (!options.launchCommand)
261
261
  return denial("Command launching is unavailable in this browser session");
262
- await options.launchCommand(context.source, command);
262
+ const args = {};
263
+ if (params.arguments && typeof params.arguments === "object")
264
+ for (const [name, value] of Object.entries(params.arguments))
265
+ if (typeof value === "string")
266
+ args[name] = value;
267
+ await options.launchCommand(context.source, command, args);
263
268
  return { ok: true, value: null };
264
269
  }
265
270
  if (request.name === "oauth.authorize")
@@ -15,6 +15,23 @@ export type FetchExtensionOptions = {
15
15
  export declare function resolveExtensionSource(value: string): string;
16
16
  /** Fetches an extension's files from a local fixture server or GitHub. */
17
17
  export declare function fetchExtensionFiles(source: string, options?: FetchExtensionOptions): Promise<FetchedExtension>;
18
+ /**
19
+ * Where an extension file lives under the command's assets directory, or
20
+ * undefined when the bundle already holds it: `assets/*` sit at its top,
21
+ * `src/` data files (not code) keep their place below it, and other files
22
+ * (`package.json`) keep their path.
23
+ */
24
+ export declare function assetRelativePath(path: string): string | undefined;
25
+ /** An extension's files keyed by `assetRelativePath`, as prebuilt bundles list them. */
26
+ export declare function extensionAssets(files: ReadonlyMap<string, BrowserFile>): Array<[string, BrowserFile]>;
27
+ /**
28
+ * The files a command's sandbox needs before it runs: the extension's assets
29
+ * (keyed by `assetRelativePath`) under `assetsPath` (decision 0013), its
30
+ * `package.json` also one level up (bundled code reads
31
+ * `__dirname/../package.json`), and markers that create the assets and support
32
+ * directories.
33
+ */
34
+ export declare function sandboxFiles(assets: Iterable<readonly [string, BrowserFile]>, assetsPath: string, supportPath: string): Array<[string, BrowserFile]>;
18
35
  /** The source file a command's bundle starts from. */
19
36
  export declare function commandEntry(files: Map<string, BrowserFile>, name: string): string;
20
37
  /** Command names a manifest declares, in order. */
@@ -66,6 +66,49 @@ export async function fetchExtensionFiles(source, options = {}) {
66
66
  const lockfile = files.get("/package-lock.json");
67
67
  return { files, manifest, root, ...(typeof lockfile === "string" ? { lockfile } : {}) };
68
68
  }
69
+ /**
70
+ * Where an extension file lives under the command's assets directory, or
71
+ * undefined when the bundle already holds it: `assets/*` sit at its top,
72
+ * `src/` data files (not code) keep their place below it, and other files
73
+ * (`package.json`) keep their path.
74
+ */
75
+ export function assetRelativePath(path) {
76
+ if (path.includes("/node_modules/"))
77
+ return undefined;
78
+ if (path.startsWith("/assets/"))
79
+ return path.slice("/assets/".length) || undefined;
80
+ if (path.startsWith("/src/"))
81
+ return /\.(?:cjs|css|html?|js|jsx|mjs|ts|tsx)$/iu.test(path)
82
+ ? undefined
83
+ : path.slice("/src/".length);
84
+ return path.replace(/^\//u, "") || undefined;
85
+ }
86
+ /** An extension's files keyed by `assetRelativePath`, as prebuilt bundles list them. */
87
+ export function extensionAssets(files) {
88
+ return [...files].flatMap(([path, value]) => {
89
+ const relative = assetRelativePath(path);
90
+ return relative ? [[relative, value]] : [];
91
+ });
92
+ }
93
+ /**
94
+ * The files a command's sandbox needs before it runs: the extension's assets
95
+ * (keyed by `assetRelativePath`) under `assetsPath` (decision 0013), its
96
+ * `package.json` also one level up (bundled code reads
97
+ * `__dirname/../package.json`), and markers that create the assets and support
98
+ * directories.
99
+ */
100
+ export function sandboxFiles(assets, assetsPath, supportPath) {
101
+ const result = [
102
+ [`${supportPath}/.keep`, ""],
103
+ [`${assetsPath}/.keep`, ""],
104
+ ];
105
+ for (const [relative, value] of assets) {
106
+ result.push([`${assetsPath}/${relative}`, value]);
107
+ if (relative === "package.json")
108
+ result.push([`${assetsPath.slice(0, assetsPath.lastIndexOf("/"))}/package.json`, value]);
109
+ }
110
+ return result;
111
+ }
69
112
  /** The source file a command's bundle starts from. */
70
113
  export function commandEntry(files, name) {
71
114
  for (const extension of ["tsx", "ts", "jsx", "js"]) {
package/dist/sandbox.d.ts CHANGED
@@ -18,6 +18,15 @@ export type SandboxOptions = {
18
18
  };
19
19
  /** `path` under `assetsUrl`, which defaults to the package's own `assets/` directory. */
20
20
  export declare const assetUrl: (options: Pick<SandboxOptions, "assetsUrl">, path: string) => string;
21
+ /**
22
+ * Writes files into the sandbox's own file system before a command runs.
23
+ * Writes to `getVFS()` never reach it (the sandbox page keeps its own
24
+ * VirtualFS, and almostnode's sync would decode binary files as text), and a
25
+ * message posted to the frame right after `createRuntime` can still land on
26
+ * its initial `about:blank`. `execute` is the channel every command already
27
+ * runs through, so the files go as one script, base64 for exact bytes.
28
+ */
29
+ export declare function writeSandboxFiles(runtime: Pick<SandboxRuntime, "execute">, files: ReadonlyArray<readonly [string, string | Uint8Array]>): Promise<void>;
21
30
  export declare function disposeSandbox(runtime: SandboxRuntime | undefined): void;
22
31
  /** Starts a sandbox now so the next `mount()` with the same options skips that wait. */
23
32
  export declare function prewarm(options: SandboxOptions): void;
package/dist/sandbox.js CHANGED
@@ -14,6 +14,30 @@ export const assetUrl = (options, path) => new URL(path, options.assetsUrl
14
14
  ? new URL(options.assetsUrl, globalThis.location?.href)
15
15
  : packagedAssets()).toString();
16
16
  const keyOf = (options) => [options.sandboxUrl, assetUrl(options, "./"), options.bundlesUrl, options.buildUrl].join("\n");
17
+ const base64 = (value) => {
18
+ const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
19
+ let binary = "";
20
+ for (let start = 0; start < bytes.length; start += 0x8000)
21
+ binary += String.fromCharCode(...bytes.subarray(start, start + 0x8000));
22
+ return btoa(binary);
23
+ };
24
+ /**
25
+ * Writes files into the sandbox's own file system before a command runs.
26
+ * Writes to `getVFS()` never reach it (the sandbox page keeps its own
27
+ * VirtualFS, and almostnode's sync would decode binary files as text), and a
28
+ * message posted to the frame right after `createRuntime` can still land on
29
+ * its initial `about:blank`. `execute` is the channel every command already
30
+ * runs through, so the files go as one script, base64 for exact bytes.
31
+ */
32
+ export async function writeSandboxFiles(runtime, files) {
33
+ const encoded = JSON.stringify(files.map(([path, value]) => [path, base64(value)]));
34
+ await runtime.execute(`const fs = require("fs");
35
+ for (const [path, data] of ${encoded}) {
36
+ fs.mkdirSync(path.slice(0, path.lastIndexOf("/")) || "/", { recursive: true });
37
+ fs.writeFileSync(path, Buffer.from(data, "base64"));
38
+ }
39
+ module.exports = null;`, "/blast/write-files.cjs");
40
+ }
17
41
  export function disposeSandbox(runtime) {
18
42
  try {
19
43
  runtime?.terminate?.();
package/dist/session.d.ts CHANGED
@@ -26,6 +26,8 @@ export type WebMountOptions = {
26
26
  fixtures?: boolean;
27
27
  /** Show the extension's commands as the engine root view instead of running one. */
28
28
  root?: boolean;
29
+ /** Manifest argument values for `command` (`LaunchProps.arguments`). */
30
+ arguments?: Record<string, string>;
29
31
  /** Preference values, applied over the manifest defaults and saved values. */
30
32
  preferences?: PreferenceValues;
31
33
  /** Prebuilt bundle index (usually `/bundles.json`) used before fetching GitHub sources. */
package/dist/session.js CHANGED
@@ -12,13 +12,13 @@ import { installForBrowser } from "./browser-install.js";
12
12
  import { aliases } from "./bundle-config.js";
13
13
  import { createBrowserCapabilities, createBrowserPreferenceStore, } from "./capabilities.js";
14
14
  import { createDefaultRenderer, } from "./default-renderer.js";
15
- import { commandEntry, fetchExtensionFiles, manifestCommands } from "./extension-files.js";
15
+ import { commandEntry, extensionAssets, fetchExtensionFiles, manifestCommands, sandboxFiles, } from "./extension-files.js";
16
16
  import { createDefaultHandlers } from "./handlers.js";
17
17
  import { createLaunchGeneration, withLaunchTimeout } from "./launch.js";
18
18
  import { missingPreferences, parsePreferences, preferenceDefaults, preferenceFields, } from "@blastlauncher/protocol/preference-fields";
19
19
  import { loadPrebuiltCommand } from "./prebuilt.js";
20
20
  import { acceptsRuntimePortRequest } from "./runtime-port.js";
21
- import { acquireSandbox, assetUrl, disposeSandbox } from "./sandbox.js";
21
+ import { acquireSandbox, assetUrl, disposeSandbox, writeSandboxFiles, } from "./sandbox.js";
22
22
  const initialized = new WeakSet();
23
23
  const LAUNCH_TIMEOUT_MS = 30_000;
24
24
  const RUNTIME_ENTRY = "/packages/web/sandbox/runtime-entry.ts";
@@ -86,7 +86,7 @@ export function startSession(element, options) {
86
86
  const result = root.input(value);
87
87
  for (const effect of result.effects)
88
88
  if (effect.type === "launch")
89
- void run(effect.command);
89
+ void run(effect.command, {});
90
90
  return { events: [], effects: [] };
91
91
  };
92
92
  const renderer = options.render
@@ -217,7 +217,7 @@ export function startSession(element, options) {
217
217
  store.update(extensionValues, command, commandValues);
218
218
  return supplied;
219
219
  }
220
- async function run(commandName) {
220
+ async function run(commandName, args) {
221
221
  // A command launched from the running one (`launchCommand`) replaces it.
222
222
  stopCommand();
223
223
  const generation = launches.current();
@@ -230,12 +230,13 @@ export function startSession(element, options) {
230
230
  : undefined;
231
231
  if (!current())
232
232
  return;
233
- let files;
233
+ /** The extension's files by `assetRelativePath`, for the sandbox. */
234
+ let assets;
234
235
  let declared;
235
236
  let entry;
236
237
  let code;
237
238
  if (built) {
238
- files = built.files;
239
+ assets = [...built.files].map(([path, value]) => [path.replace(/^\//u, ""), value]);
239
240
  declared = fromCatalog(built.manifest, commandName);
240
241
  entry = `/src/${commandName}.tsx`;
241
242
  code = built.code;
@@ -244,7 +245,7 @@ export function startSession(element, options) {
244
245
  const extension = await fetchExtension();
245
246
  if (!current())
246
247
  return;
247
- files = extension.files;
248
+ assets = extensionAssets(extension.files);
248
249
  declared = fromManifest(extension.manifest, commandName, source);
249
250
  entry = commandEntry(extension.files, commandName);
250
251
  setStatus("Installing…", false, true);
@@ -306,30 +307,9 @@ export function startSession(element, options) {
306
307
  const pathSegment = extensionName.replace(/[^A-Za-z0-9._-]/gu, "_") || "extension";
307
308
  const supportPath = `/blast/support/${pathSegment}`;
308
309
  const assetsPath = `/blast/assets/${pathSegment}`;
309
- const vfs = active.getVFS();
310
- vfs.mkdirSync(supportPath, { recursive: true });
311
- vfs.mkdirSync(assetsPath, { recursive: true });
312
- // A marker syncs directories created after the sandbox was initialised.
313
- vfs.writeFileSync(`${supportPath}/.keep`, "");
314
- vfs.writeFileSync(`${assetsPath}/.keep`, "");
315
- for (const [path, value] of files) {
316
- if (path.includes("/node_modules/"))
317
- continue;
318
- const relative = path.startsWith("/assets/")
319
- ? path.slice("/assets/".length)
320
- : path.startsWith("/src/")
321
- ? /\.(?:cjs|css|html?|js|jsx|mjs|ts|tsx)$/iu.test(path)
322
- ? undefined
323
- : path.slice("/src/".length)
324
- : path.replace(/^\//u, "");
325
- if (!relative)
326
- continue;
327
- const target = `${assetsPath}/${relative}`;
328
- vfs.mkdirSync(target.slice(0, target.lastIndexOf("/")), { recursive: true });
329
- vfs.writeFileSync(target, value);
330
- if (path === "/package.json")
331
- vfs.writeFileSync("/blast/assets/package.json", value);
332
- }
310
+ await writeSandboxFiles(active, sandboxFiles(assets, assetsPath, supportPath));
311
+ if (!current())
312
+ return;
333
313
  const context = { extensionName, commandName, source };
334
314
  const capabilities = createBrowserCapabilities({
335
315
  catalog: {
@@ -341,8 +321,8 @@ export function startSession(element, options) {
341
321
  ...(handlers.clipboard ? { clipboard: handlers.clipboard } : {}),
342
322
  ...(handlers.openUrl ? { openUrl: handlers.openUrl } : {}),
343
323
  status: (text, error) => setStatus(text, error === true),
344
- launchCommand: async (_source, command) => {
345
- void run(command);
324
+ launchCommand: async (_source, command, launchArgs) => {
325
+ void run(command, launchArgs);
346
326
  },
347
327
  openPreferences: (scope) => {
348
328
  if (!options.preferenceForm) {
@@ -352,7 +332,7 @@ export function startSession(element, options) {
352
332
  // Saved preferences reach a command when it launches, so it runs again.
353
333
  void askPreferences(declared, commandName, scope, { ...values }).then((supplied) => {
354
334
  if (supplied && current())
355
- void run(commandName);
335
+ void run(commandName, args);
356
336
  });
357
337
  },
358
338
  });
@@ -442,7 +422,13 @@ export function startSession(element, options) {
442
422
  port.postMessage({
443
423
  type: "init",
444
424
  protocol: 1,
445
- command: { extension: extensionName, name: commandName, entry, mode },
425
+ command: {
426
+ extension: extensionName,
427
+ name: commandName,
428
+ entry,
429
+ mode,
430
+ ...(Object.keys(args).length ? { arguments: { ...args } } : {}),
431
+ },
446
432
  preferences: { ...values },
447
433
  environment: {
448
434
  extensionName,
@@ -473,7 +459,7 @@ export function startSession(element, options) {
473
459
  if (!options.root) {
474
460
  const command = options.command ?? manifestCommands((await fetchExtension()).manifest)[0];
475
461
  if (!closed)
476
- await run(command ?? "index");
462
+ await run(command ?? "index", { ...options.arguments });
477
463
  return;
478
464
  }
479
465
  const manifest = (await fetchExtension()).manifest;
package/package.json CHANGED
@@ -9,11 +9,11 @@
9
9
  }
10
10
  },
11
11
  "dependencies": {
12
- "@blastlauncher/client-core": "3.0.0-alpha.0",
13
- "@blastlauncher/protocol": "3.0.0-alpha.0",
12
+ "@blastlauncher/client-core": "3.0.0-alpha.3",
13
+ "@blastlauncher/protocol": "3.0.0-alpha.3",
14
14
  "semver": "7.8.5"
15
15
  },
16
- "version": "3.0.0-alpha.0",
16
+ "version": "3.0.0-alpha.3",
17
17
  "license": "MIT",
18
18
  "repository": {
19
19
  "type": "git",