@blastlauncher/web 0.0.0-stage → 3.0.0-alpha.2
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 +7 -0
- package/README.md +85 -2
- package/assets/almostnode.mjs +735 -0
- package/assets/blast-sources.json +1 -0
- package/assets/blast-web.js +35 -0
- package/assets/esbuild.mjs +2695 -0
- package/assets/esbuild.wasm +0 -0
- package/dist/browser-bundle.d.ts +54 -0
- package/dist/browser-bundle.js +441 -0
- package/dist/browser-install.d.ts +66 -0
- package/dist/browser-install.js +326 -0
- package/dist/bundle-config.d.ts +10 -0
- package/dist/bundle-config.js +31 -0
- package/dist/bundles.d.ts +29 -0
- package/dist/bundles.js +126 -0
- package/dist/capabilities.d.ts +66 -0
- package/dist/capabilities.js +288 -0
- package/dist/custom-element.d.ts +6 -0
- package/dist/custom-element.js +36 -0
- package/dist/default-renderer.d.ts +27 -0
- package/dist/default-renderer.js +145 -0
- package/dist/default-styles.d.ts +2 -0
- package/dist/default-styles.js +28 -0
- package/dist/extension-files.d.ts +21 -0
- package/dist/extension-files.js +86 -0
- package/dist/handlers.d.ts +18 -0
- package/dist/handlers.js +22 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/launch.d.ts +11 -0
- package/dist/launch.js +29 -0
- package/dist/prebuilt.d.ts +24 -0
- package/dist/prebuilt.js +112 -0
- package/dist/render.d.ts +62 -0
- package/dist/render.js +288 -0
- package/dist/runtime-port.d.ts +5 -0
- package/dist/runtime-port.js +8 -0
- package/dist/sandbox.d.ts +25 -0
- package/dist/sandbox.js +71 -0
- package/dist/session.d.ts +77 -0
- package/dist/session.js +520 -0
- package/dist/source.d.ts +16 -0
- package/dist/source.js +42 -0
- package/package.json +33 -4
- package/sandbox/almostnode-full.mjs +19713 -0
- package/sandbox/index.html +1 -0
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { loadPrebuiltRuntime } from "./prebuilt.js";
|
|
10
|
+
let warm;
|
|
11
|
+
const packagedAssets = () => new URL(typeof BLAST_PACKAGED_ASSETS === "string" ? BLAST_PACKAGED_ASSETS : "../assets/", import.meta.url);
|
|
12
|
+
/** `path` under `assetsUrl`, which defaults to the package's own `assets/` directory. */
|
|
13
|
+
export const assetUrl = (options, path) => new URL(path, options.assetsUrl
|
|
14
|
+
? new URL(options.assetsUrl, globalThis.location?.href)
|
|
15
|
+
: packagedAssets()).toString();
|
|
16
|
+
const keyOf = (options) => [options.sandboxUrl, assetUrl(options, "./"), options.bundlesUrl, options.buildUrl].join("\n");
|
|
17
|
+
export function disposeSandbox(runtime) {
|
|
18
|
+
try {
|
|
19
|
+
runtime?.terminate?.();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A sandbox that never finished starting has nothing to terminate.
|
|
23
|
+
}
|
|
24
|
+
runtime?.iframe?.remove();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A cross-origin almostnode sandbox (decision 0013). `data-blast` on its frame
|
|
28
|
+
* says whether it is idle (`warm`) or runs a command (`session`). The prebuilt
|
|
29
|
+
* runtime, when the bundle index has one, loads before any command code.
|
|
30
|
+
*/
|
|
31
|
+
async function createSandbox(options, kind) {
|
|
32
|
+
const almost = (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ assetUrl(options, "almostnode.mjs"))));
|
|
33
|
+
const runtime = await almost.createRuntime(new almost.VirtualFS(), {
|
|
34
|
+
sandbox: options.sandboxUrl,
|
|
35
|
+
});
|
|
36
|
+
runtime.iframe?.setAttribute("data-blast", kind);
|
|
37
|
+
try {
|
|
38
|
+
const code = options.bundlesUrl
|
|
39
|
+
? await loadPrebuiltRuntime({ bundlesUrl: options.bundlesUrl, buildUrl: options.buildUrl })
|
|
40
|
+
: undefined;
|
|
41
|
+
if (code)
|
|
42
|
+
await runtime.execute(`${code}\nmodule.exports = null;`, "/runtime.cjs");
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
disposeSandbox(runtime);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return runtime;
|
|
49
|
+
}
|
|
50
|
+
/** Starts a sandbox now so the next `mount()` with the same options skips that wait. */
|
|
51
|
+
export function prewarm(options) {
|
|
52
|
+
const key = keyOf(options);
|
|
53
|
+
if (warm?.key === key)
|
|
54
|
+
return;
|
|
55
|
+
const previous = warm;
|
|
56
|
+
warm = { key, runtime: createSandbox(options, "warm").catch(() => undefined) };
|
|
57
|
+
void previous?.runtime.then(disposeSandbox);
|
|
58
|
+
}
|
|
59
|
+
/** The warm sandbox for these options if there is one, otherwise a new one. */
|
|
60
|
+
export async function acquireSandbox(options) {
|
|
61
|
+
if (warm?.key === keyOf(options)) {
|
|
62
|
+
const pending = warm.runtime;
|
|
63
|
+
warm = undefined;
|
|
64
|
+
const runtime = await pending;
|
|
65
|
+
if (runtime) {
|
|
66
|
+
runtime.iframe?.setAttribute("data-blast", "session");
|
|
67
|
+
return runtime;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return createSandbox(options, "session");
|
|
71
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ControllerInput, type ControllerResult, type ControllerView } from "@blastlauncher/client-core";
|
|
2
|
+
import type { SceneMessage } from "@blastlauncher/protocol";
|
|
3
|
+
import { type BrowserEsbuild } from "./browser-bundle.js";
|
|
4
|
+
import { type Theme, type WebStatus, type WebView } from "./default-renderer.js";
|
|
5
|
+
import { type WebHandlers } from "./handlers.js";
|
|
6
|
+
import { type Preference } from "@blastlauncher/protocol/preference-fields";
|
|
7
|
+
export type { WebHandlers, WebStatus, WebView };
|
|
8
|
+
type PreferenceValues = Record<string, string | number | boolean>;
|
|
9
|
+
export type WebMountOptions = {
|
|
10
|
+
/** `raycast/extensions/<name>` or a `https://github.com/<owner>/<repo>/tree/<ref>/<path>` URL. */
|
|
11
|
+
extension?: string;
|
|
12
|
+
/** Alias of `extension` for a GitHub URL. */
|
|
13
|
+
source?: string;
|
|
14
|
+
/** Command to run; defaults to the first command in the manifest. Ignored with `root`. */
|
|
15
|
+
command?: string;
|
|
16
|
+
/** Navigation title until the command sets one; defaults to the manifest's command title. */
|
|
17
|
+
commandTitle?: string;
|
|
18
|
+
/** Origin that serves the cross-origin sandbox page (decision 0013). */
|
|
19
|
+
sandboxUrl: string;
|
|
20
|
+
/**
|
|
21
|
+
* Where the package's `assets/` (`almostnode.mjs`, `esbuild.mjs`, `esbuild.wasm`,
|
|
22
|
+
* `blast-sources.json`) are served; defaults to `assets/` beside this module.
|
|
23
|
+
*/
|
|
24
|
+
assetsUrl?: string;
|
|
25
|
+
/** Allow `<assetsUrl>/fixtures/<name>` to stand in for GitHub (tests and demos). */
|
|
26
|
+
fixtures?: boolean;
|
|
27
|
+
/** Show the extension's commands as the engine root view instead of running one. */
|
|
28
|
+
root?: boolean;
|
|
29
|
+
/** Preference values, applied over the manifest defaults and saved values. */
|
|
30
|
+
preferences?: PreferenceValues;
|
|
31
|
+
/** Prebuilt bundle index (usually `/bundles.json`) used before fetching GitHub sources. */
|
|
32
|
+
bundlesUrl?: string;
|
|
33
|
+
/** JSON `{ "sha": … }`; a bundle index with another `buildSha` is ignored. */
|
|
34
|
+
buildUrl?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Asks for preferences: missing required ones before a launch, or any when
|
|
37
|
+
* the extension calls `openExtensionPreferences`/`openCommandPreferences`.
|
|
38
|
+
* Submitted values are saved; returning undefined cancels.
|
|
39
|
+
*/
|
|
40
|
+
preferenceForm?: (request: WebPreferenceRequest) => PreferenceValues | undefined | Promise<PreferenceValues | undefined>;
|
|
41
|
+
handlers?: WebHandlers;
|
|
42
|
+
/** Replaces the built-in renderer: receives every view and a callback for input. */
|
|
43
|
+
render?: (view: WebView, input: (value: ControllerInput) => ControllerResult | undefined, frame: WebFrame) => void;
|
|
44
|
+
/** Called when the command ends without an error; default: back to the root, or a status. */
|
|
45
|
+
onExit?: (exit: WebExit) => void;
|
|
46
|
+
theme?: Theme;
|
|
47
|
+
/** esbuild-wasm, initialised (or a loader that initialises it); default: `<assetsUrl>/esbuild.mjs`. */
|
|
48
|
+
esbuild?: BrowserEsbuild | (() => Promise<BrowserEsbuild>);
|
|
49
|
+
onStatus?: (status: WebStatus) => void;
|
|
50
|
+
};
|
|
51
|
+
export type WebPreferenceRequest = {
|
|
52
|
+
extension: string;
|
|
53
|
+
command: string;
|
|
54
|
+
/** `command` lists the extension's and the command's fields; `extension` only the former. */
|
|
55
|
+
scope: "extension" | "command";
|
|
56
|
+
fields: Preference[];
|
|
57
|
+
values: PreferenceValues;
|
|
58
|
+
};
|
|
59
|
+
/** What a custom renderer may need beyond the view: the scene it came from and the status. */
|
|
60
|
+
export type WebFrame = {
|
|
61
|
+
scene?: SceneMessage;
|
|
62
|
+
status: WebStatus;
|
|
63
|
+
};
|
|
64
|
+
/** `quit`: the command closed its window; `exit`: its process ended with code 0. */
|
|
65
|
+
export type WebExit = {
|
|
66
|
+
reason: "quit" | "exit";
|
|
67
|
+
view?: ControllerView;
|
|
68
|
+
};
|
|
69
|
+
export type WebMount = {
|
|
70
|
+
close(): void;
|
|
71
|
+
/** Sends input to the running command (or the root); the result says whether a key was handled. */
|
|
72
|
+
input(value: ControllerInput): ControllerResult | undefined;
|
|
73
|
+
/** The running command's current view. */
|
|
74
|
+
view(): ControllerView | undefined;
|
|
75
|
+
};
|
|
76
|
+
/** Runs one extension in the sandbox and draws it into `element`. */
|
|
77
|
+
export declare function startSession(element: HTMLElement, options: WebMountOptions): WebMount;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { createController, createRootController, } from "@blastlauncher/client-core";
|
|
10
|
+
import { bundleForBrowser } from "./browser-bundle.js";
|
|
11
|
+
import { installForBrowser } from "./browser-install.js";
|
|
12
|
+
import { aliases } from "./bundle-config.js";
|
|
13
|
+
import { createBrowserCapabilities, createBrowserPreferenceStore, } from "./capabilities.js";
|
|
14
|
+
import { createDefaultRenderer, } from "./default-renderer.js";
|
|
15
|
+
import { commandEntry, fetchExtensionFiles, manifestCommands } from "./extension-files.js";
|
|
16
|
+
import { createDefaultHandlers } from "./handlers.js";
|
|
17
|
+
import { createLaunchGeneration, withLaunchTimeout } from "./launch.js";
|
|
18
|
+
import { missingPreferences, parsePreferences, preferenceDefaults, preferenceFields, } from "@blastlauncher/protocol/preference-fields";
|
|
19
|
+
import { loadPrebuiltCommand } from "./prebuilt.js";
|
|
20
|
+
import { acceptsRuntimePortRequest } from "./runtime-port.js";
|
|
21
|
+
import { acquireSandbox, assetUrl, disposeSandbox } from "./sandbox.js";
|
|
22
|
+
const initialized = new WeakSet();
|
|
23
|
+
const LAUNCH_TIMEOUT_MS = 30_000;
|
|
24
|
+
const RUNTIME_ENTRY = "/packages/web/sandbox/runtime-entry.ts";
|
|
25
|
+
const platformName = () => {
|
|
26
|
+
const name = navigator.platform || "";
|
|
27
|
+
if (/mac|iphone|ipad/i.test(name))
|
|
28
|
+
return "macOS";
|
|
29
|
+
return /win/i.test(name) ? "Windows" : "Linux";
|
|
30
|
+
};
|
|
31
|
+
const commandMode = (value) => value === "no-view" || value === "menu-bar" || value === "model-provider" ? value : "view";
|
|
32
|
+
function fromManifest(manifest, name, source) {
|
|
33
|
+
const declarations = Array.isArray(manifest.commands) ? manifest.commands : [];
|
|
34
|
+
const declaration = declarations.find((item) => item?.name === name);
|
|
35
|
+
return {
|
|
36
|
+
extensionName: String(manifest.name ?? source),
|
|
37
|
+
commands: manifestCommands(manifest),
|
|
38
|
+
extensionPreferences: parsePreferences(manifest.preferences, "Extension preferences"),
|
|
39
|
+
commandPreferences: parsePreferences(declaration?.preferences, `Command ${name} preferences`),
|
|
40
|
+
mode: commandMode(declaration?.mode),
|
|
41
|
+
...(typeof declaration?.title === "string" ? { title: declaration.title } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function fromCatalog(manifest, name) {
|
|
45
|
+
return {
|
|
46
|
+
extensionName: manifest.name,
|
|
47
|
+
commands: Object.keys(manifest.commands),
|
|
48
|
+
extensionPreferences: manifest.preferences,
|
|
49
|
+
commandPreferences: manifest.commands[name] ?? [],
|
|
50
|
+
mode: commandMode(manifest.modes?.[name]),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Runs one extension in the sandbox and draws it into `element`. */
|
|
54
|
+
export function startSession(element, options) {
|
|
55
|
+
const given = options.source ?? options.extension;
|
|
56
|
+
if (!given)
|
|
57
|
+
throw new TypeError("mount requires extension or source");
|
|
58
|
+
const source = given;
|
|
59
|
+
if (!options.sandboxUrl)
|
|
60
|
+
throw new TypeError("mount requires sandboxUrl");
|
|
61
|
+
const asset = (path) => assetUrl(options, path);
|
|
62
|
+
const handlers = { ...createDefaultHandlers(), ...options.handlers };
|
|
63
|
+
const launches = createLaunchGeneration();
|
|
64
|
+
const timers = new Set();
|
|
65
|
+
const platform = platformName();
|
|
66
|
+
let closed = false;
|
|
67
|
+
let status = { text: "Loading extension…", progress: true };
|
|
68
|
+
let controller;
|
|
69
|
+
let root;
|
|
70
|
+
let port;
|
|
71
|
+
let runtime;
|
|
72
|
+
let removePortListener;
|
|
73
|
+
let scene;
|
|
74
|
+
let mode = "view";
|
|
75
|
+
let extensionName = source;
|
|
76
|
+
const input = (value) => {
|
|
77
|
+
if (closed)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (controller) {
|
|
80
|
+
const result = controller.input(value);
|
|
81
|
+
handle(result);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
if (!root)
|
|
85
|
+
return undefined;
|
|
86
|
+
const result = root.input(value);
|
|
87
|
+
for (const effect of result.effects)
|
|
88
|
+
if (effect.type === "launch")
|
|
89
|
+
void run(effect.command);
|
|
90
|
+
return { events: [], effects: [] };
|
|
91
|
+
};
|
|
92
|
+
const renderer = options.render
|
|
93
|
+
? undefined
|
|
94
|
+
: createDefaultRenderer(element, ((value) => void input(value)), {
|
|
95
|
+
...(options.theme === undefined ? {} : { theme: options.theme }),
|
|
96
|
+
actionsKey: platform === "macOS" ? "⌘K" : "Ctrl+K",
|
|
97
|
+
});
|
|
98
|
+
const draw = (view) => {
|
|
99
|
+
if (closed)
|
|
100
|
+
return;
|
|
101
|
+
if (options.render)
|
|
102
|
+
options.render(view, input, { ...(scene ? { scene } : {}), status });
|
|
103
|
+
else
|
|
104
|
+
renderer?.draw(view, status);
|
|
105
|
+
};
|
|
106
|
+
const setStatus = (text, error = false, progress = false) => {
|
|
107
|
+
status = { text, ...(error ? { error } : {}), ...(progress ? { progress } : {}) };
|
|
108
|
+
options.onStatus?.(status);
|
|
109
|
+
};
|
|
110
|
+
function handle(result) {
|
|
111
|
+
for (const event of result.events)
|
|
112
|
+
port?.postMessage({
|
|
113
|
+
type: "event",
|
|
114
|
+
node: event.node,
|
|
115
|
+
handler: event.handler,
|
|
116
|
+
args: event.args,
|
|
117
|
+
});
|
|
118
|
+
for (const effect of result.effects) {
|
|
119
|
+
if (effect.type === "quit") {
|
|
120
|
+
// `closeMainWindow()` asks to quit, but a no-view command is done only
|
|
121
|
+
// when the runtime reports its exit status.
|
|
122
|
+
if (mode !== "no-view")
|
|
123
|
+
finish("quit");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const launch = launches.current();
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
timers.delete(timer);
|
|
129
|
+
if (!launches.isCurrent(launch) || !controller)
|
|
130
|
+
return;
|
|
131
|
+
handle(controller.tick(effect.token));
|
|
132
|
+
}, effect.ms);
|
|
133
|
+
timers.add(timer);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Tears down the running command; the root (if any) stays. */
|
|
138
|
+
function stopCommand() {
|
|
139
|
+
launches.next();
|
|
140
|
+
for (const timer of timers)
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
timers.clear();
|
|
143
|
+
removePortListener?.();
|
|
144
|
+
removePortListener = undefined;
|
|
145
|
+
port?.close();
|
|
146
|
+
port = undefined;
|
|
147
|
+
controller = undefined;
|
|
148
|
+
scene = undefined;
|
|
149
|
+
disposeSandbox(runtime);
|
|
150
|
+
runtime = undefined;
|
|
151
|
+
}
|
|
152
|
+
function finish(reason) {
|
|
153
|
+
const view = controller?.view();
|
|
154
|
+
stopCommand();
|
|
155
|
+
if (options.onExit)
|
|
156
|
+
options.onExit({ reason, ...(view ? { view } : {}) });
|
|
157
|
+
else if (root) {
|
|
158
|
+
root.reset();
|
|
159
|
+
draw(root.view());
|
|
160
|
+
setStatus("Ready");
|
|
161
|
+
}
|
|
162
|
+
else if (view)
|
|
163
|
+
setStatus("Command finished");
|
|
164
|
+
}
|
|
165
|
+
function fail(error) {
|
|
166
|
+
if (closed)
|
|
167
|
+
return;
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
stopCommand();
|
|
170
|
+
setStatus(message, true);
|
|
171
|
+
if (root) {
|
|
172
|
+
root.fail("Command failed", message);
|
|
173
|
+
draw(root.view());
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
draw({ type: "error", message });
|
|
177
|
+
}
|
|
178
|
+
let esbuildModule;
|
|
179
|
+
const loadEsbuild = async () => {
|
|
180
|
+
if (typeof options.esbuild === "function")
|
|
181
|
+
return options.esbuild();
|
|
182
|
+
if (options.esbuild)
|
|
183
|
+
return options.esbuild;
|
|
184
|
+
esbuildModule ??= (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ asset("esbuild.mjs"))));
|
|
185
|
+
if (!initialized.has(esbuildModule)) {
|
|
186
|
+
initialized.add(esbuildModule);
|
|
187
|
+
await esbuildModule.initialize({ wasmURL: asset("esbuild.wasm"), worker: false });
|
|
188
|
+
}
|
|
189
|
+
return esbuildModule;
|
|
190
|
+
};
|
|
191
|
+
let fetched;
|
|
192
|
+
const fetchExtension = () => (fetched ??= fetchExtensionFiles(source, {
|
|
193
|
+
fixturesUrl: options.fixtures ? asset("fixtures/") : undefined,
|
|
194
|
+
}));
|
|
195
|
+
/** Asks the embedder for preferences and saves what it submits. */
|
|
196
|
+
async function askPreferences(declared, command, scope, values) {
|
|
197
|
+
if (!options.preferenceForm)
|
|
198
|
+
return undefined;
|
|
199
|
+
const store = createBrowserPreferenceStore(declared.extensionName, handlers.storage);
|
|
200
|
+
const fields = scope === "extension"
|
|
201
|
+
? declared.extensionPreferences
|
|
202
|
+
: preferenceFields(declared.extensionPreferences, declared.commandPreferences);
|
|
203
|
+
const supplied = await options.preferenceForm({
|
|
204
|
+
extension: declared.extensionName,
|
|
205
|
+
command,
|
|
206
|
+
scope,
|
|
207
|
+
fields,
|
|
208
|
+
values,
|
|
209
|
+
});
|
|
210
|
+
if (!supplied)
|
|
211
|
+
return undefined;
|
|
212
|
+
const extensionNames = new Set(declared.extensionPreferences.map((field) => field.name));
|
|
213
|
+
const extensionValues = {};
|
|
214
|
+
const commandValues = {};
|
|
215
|
+
for (const [name, value] of Object.entries(supplied))
|
|
216
|
+
(extensionNames.has(name) ? extensionValues : commandValues)[name] = value;
|
|
217
|
+
store.update(extensionValues, command, commandValues);
|
|
218
|
+
return supplied;
|
|
219
|
+
}
|
|
220
|
+
async function run(commandName) {
|
|
221
|
+
// A command launched from the running one (`launchCommand`) replaces it.
|
|
222
|
+
stopCommand();
|
|
223
|
+
const generation = launches.current();
|
|
224
|
+
const current = () => !closed && launches.isCurrent(generation);
|
|
225
|
+
try {
|
|
226
|
+
setStatus("Fetching extension…", false, true);
|
|
227
|
+
draw({ type: "loading", extension: source, command: commandName });
|
|
228
|
+
const built = options.bundlesUrl
|
|
229
|
+
? await loadPrebuiltCommand({ bundlesUrl: options.bundlesUrl, buildUrl: options.buildUrl }, source, commandName)
|
|
230
|
+
: undefined;
|
|
231
|
+
if (!current())
|
|
232
|
+
return;
|
|
233
|
+
let files;
|
|
234
|
+
let declared;
|
|
235
|
+
let entry;
|
|
236
|
+
let code;
|
|
237
|
+
if (built) {
|
|
238
|
+
files = built.files;
|
|
239
|
+
declared = fromCatalog(built.manifest, commandName);
|
|
240
|
+
entry = `/src/${commandName}.tsx`;
|
|
241
|
+
code = built.code;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const extension = await fetchExtension();
|
|
245
|
+
if (!current())
|
|
246
|
+
return;
|
|
247
|
+
files = extension.files;
|
|
248
|
+
declared = fromManifest(extension.manifest, commandName, source);
|
|
249
|
+
entry = commandEntry(extension.files, commandName);
|
|
250
|
+
setStatus("Installing…", false, true);
|
|
251
|
+
const sources = (await (await fetch(asset("blast-sources.json"))).json());
|
|
252
|
+
const installed = await installForBrowser({
|
|
253
|
+
fetch: (value, init) => fetch(value, init),
|
|
254
|
+
manifest: extension.manifest,
|
|
255
|
+
...(extension.lockfile === undefined ? {} : { lockfile: extension.lockfile }),
|
|
256
|
+
});
|
|
257
|
+
if (!current())
|
|
258
|
+
return;
|
|
259
|
+
setStatus("Bundling…", false, true);
|
|
260
|
+
const esbuild = await loadEsbuild();
|
|
261
|
+
if (!current())
|
|
262
|
+
return;
|
|
263
|
+
const bundleFiles = new Map(Object.entries(sources));
|
|
264
|
+
for (const [path, value] of extension.files)
|
|
265
|
+
bundleFiles.set(path, value);
|
|
266
|
+
code = (await bundleForBrowser({
|
|
267
|
+
esbuild,
|
|
268
|
+
files: bundleFiles,
|
|
269
|
+
packages: installed.packages,
|
|
270
|
+
command: entry,
|
|
271
|
+
runtimeEntry: RUNTIME_ENTRY,
|
|
272
|
+
aliases,
|
|
273
|
+
})).code;
|
|
274
|
+
if (!current())
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
extensionName = declared.extensionName;
|
|
278
|
+
mode = declared.mode;
|
|
279
|
+
const store = createBrowserPreferenceStore(extensionName, handlers.storage);
|
|
280
|
+
let values = {
|
|
281
|
+
...preferenceDefaults(declared.extensionPreferences, declared.commandPreferences),
|
|
282
|
+
...store.values(commandName),
|
|
283
|
+
...options.preferences,
|
|
284
|
+
};
|
|
285
|
+
const fields = preferenceFields(declared.extensionPreferences, declared.commandPreferences);
|
|
286
|
+
const missing = missingPreferences(fields, values);
|
|
287
|
+
if (missing.length > 0) {
|
|
288
|
+
if (!options.preferenceForm)
|
|
289
|
+
throw new Error(`Missing required preferences: ${missing.map((item) => item.title || item.name).join(", ")}`);
|
|
290
|
+
const supplied = await askPreferences(declared, commandName, "command", values);
|
|
291
|
+
if (!current() || !supplied)
|
|
292
|
+
return;
|
|
293
|
+
values = { ...values, ...supplied };
|
|
294
|
+
if (missingPreferences(fields, values).length)
|
|
295
|
+
throw new Error("Required preferences are missing");
|
|
296
|
+
setStatus("Fetching extension…", false, true);
|
|
297
|
+
draw({ type: "loading", extension: source, command: commandName });
|
|
298
|
+
}
|
|
299
|
+
setStatus("Starting…", false, true);
|
|
300
|
+
const active = await acquireSandbox(options);
|
|
301
|
+
if (!current()) {
|
|
302
|
+
disposeSandbox(active);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
runtime = active;
|
|
306
|
+
const pathSegment = extensionName.replace(/[^A-Za-z0-9._-]/gu, "_") || "extension";
|
|
307
|
+
const supportPath = `/blast/support/${pathSegment}`;
|
|
308
|
+
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
|
+
}
|
|
333
|
+
const context = { extensionName, commandName, source };
|
|
334
|
+
const capabilities = createBrowserCapabilities({
|
|
335
|
+
catalog: {
|
|
336
|
+
extensions: [
|
|
337
|
+
{ name: extensionName, commands: declared.commands.map((name) => ({ name })) },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
...(handlers.storage ? { storage: handlers.storage } : {}),
|
|
341
|
+
...(handlers.clipboard ? { clipboard: handlers.clipboard } : {}),
|
|
342
|
+
...(handlers.openUrl ? { openUrl: handlers.openUrl } : {}),
|
|
343
|
+
status: (text, error) => setStatus(text, error === true),
|
|
344
|
+
launchCommand: async (_source, command) => {
|
|
345
|
+
void run(command);
|
|
346
|
+
},
|
|
347
|
+
openPreferences: (scope) => {
|
|
348
|
+
if (!options.preferenceForm) {
|
|
349
|
+
setStatus("Preferences are not available in this embed", true);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Saved preferences reach a command when it launches, so it runs again.
|
|
353
|
+
void askPreferences(declared, commandName, scope, { ...values }).then((supplied) => {
|
|
354
|
+
if (supplied && current())
|
|
355
|
+
void run(commandName);
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
const answer = async (message) => {
|
|
360
|
+
const custom = await handlers.capability?.(message, context);
|
|
361
|
+
const result = custom ?? (await capabilities(message, context));
|
|
362
|
+
if (!current())
|
|
363
|
+
return;
|
|
364
|
+
port?.postMessage({ type: "capability.result", id: message.id, ...result });
|
|
365
|
+
if (!result.ok)
|
|
366
|
+
setStatus(result.error.message, true);
|
|
367
|
+
};
|
|
368
|
+
const channel = new MessageChannel();
|
|
369
|
+
port = channel.port1;
|
|
370
|
+
port.start();
|
|
371
|
+
let firstScene;
|
|
372
|
+
const onMessage = (message) => {
|
|
373
|
+
if (!current())
|
|
374
|
+
return;
|
|
375
|
+
if (message.type === "scene") {
|
|
376
|
+
if (!controller)
|
|
377
|
+
return;
|
|
378
|
+
scene = message;
|
|
379
|
+
const result = controller.scene(message);
|
|
380
|
+
if (firstScene)
|
|
381
|
+
setStatus("Ready");
|
|
382
|
+
firstScene?.resolve();
|
|
383
|
+
firstScene = undefined;
|
|
384
|
+
draw(controller.view());
|
|
385
|
+
handle(result);
|
|
386
|
+
}
|
|
387
|
+
else if (message.type === "capability.request") {
|
|
388
|
+
answer(message).catch((error) => {
|
|
389
|
+
if (!current())
|
|
390
|
+
return;
|
|
391
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
392
|
+
port?.postMessage({
|
|
393
|
+
type: "capability.result",
|
|
394
|
+
id: message.id,
|
|
395
|
+
ok: false,
|
|
396
|
+
error: { code: "DENIED", message: text },
|
|
397
|
+
});
|
|
398
|
+
setStatus(text, true);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else if (message.type === "exit") {
|
|
402
|
+
if (message.code === 0)
|
|
403
|
+
finish("exit");
|
|
404
|
+
else
|
|
405
|
+
fail(new Error(`Command exited with code ${message.code}`));
|
|
406
|
+
}
|
|
407
|
+
else if (message.type === "error")
|
|
408
|
+
fail(new Error(message.message));
|
|
409
|
+
};
|
|
410
|
+
port.addEventListener("message", (event) => onMessage(event.data));
|
|
411
|
+
let transferred = false;
|
|
412
|
+
let resolvePort;
|
|
413
|
+
const portReady = new Promise((resolve) => (resolvePort = resolve));
|
|
414
|
+
const transferPort = (event) => {
|
|
415
|
+
if (!current())
|
|
416
|
+
return;
|
|
417
|
+
const frame = active.iframe?.contentWindow ?? undefined;
|
|
418
|
+
if (!acceptsRuntimePortRequest(event.source, frame, transferred, event.data))
|
|
419
|
+
return;
|
|
420
|
+
transferred = true;
|
|
421
|
+
removePortListener?.();
|
|
422
|
+
frame?.postMessage({ type: "blast-port" }, "*", [channel.port2]);
|
|
423
|
+
resolvePort();
|
|
424
|
+
};
|
|
425
|
+
window.addEventListener("message", transferPort);
|
|
426
|
+
removePortListener = () => window.removeEventListener("message", transferPort);
|
|
427
|
+
await withLaunchTimeout((async () => {
|
|
428
|
+
await active.execute(`${code}\nmodule.exports = null;`, `${assetsPath}/bundle.cjs`);
|
|
429
|
+
if (current())
|
|
430
|
+
await portReady;
|
|
431
|
+
})(), LAUNCH_TIMEOUT_MS, "waiting for runtime port");
|
|
432
|
+
if (!current() || !transferred)
|
|
433
|
+
return;
|
|
434
|
+
const title = options.commandTitle ?? declared.title;
|
|
435
|
+
controller = createController({ platform, ...(title ? { fallbackTitle: title } : {}) });
|
|
436
|
+
const launched = controller;
|
|
437
|
+
launched.subscribe((view) => {
|
|
438
|
+
if (current() && controller === launched)
|
|
439
|
+
draw(view);
|
|
440
|
+
});
|
|
441
|
+
const shown = new Promise((resolve, reject) => (firstScene = { resolve, reject }));
|
|
442
|
+
port.postMessage({
|
|
443
|
+
type: "init",
|
|
444
|
+
protocol: 1,
|
|
445
|
+
command: { extension: extensionName, name: commandName, entry, mode },
|
|
446
|
+
preferences: { ...values },
|
|
447
|
+
environment: {
|
|
448
|
+
extensionName,
|
|
449
|
+
commandName,
|
|
450
|
+
ownerOrAuthorName: "Browser",
|
|
451
|
+
assetsPath,
|
|
452
|
+
supportPath,
|
|
453
|
+
isDevelopment: false,
|
|
454
|
+
appearance: options.theme === "light" ? "light" : "dark",
|
|
455
|
+
textSize: "medium",
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
await withLaunchTimeout(shown, LAUNCH_TIMEOUT_MS, "waiting for first scene");
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (current())
|
|
462
|
+
fail(error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
void (async () => {
|
|
466
|
+
try {
|
|
467
|
+
setStatus("Fetching extension…", false, true);
|
|
468
|
+
draw({
|
|
469
|
+
type: "loading",
|
|
470
|
+
extension: source,
|
|
471
|
+
...(options.command ? { command: options.command } : {}),
|
|
472
|
+
});
|
|
473
|
+
if (!options.root) {
|
|
474
|
+
const command = options.command ?? manifestCommands((await fetchExtension()).manifest)[0];
|
|
475
|
+
if (!closed)
|
|
476
|
+
await run(command ?? "index");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const manifest = (await fetchExtension()).manifest;
|
|
480
|
+
if (closed)
|
|
481
|
+
return;
|
|
482
|
+
extensionName = String(manifest.name ?? source);
|
|
483
|
+
const declared = Array.isArray(manifest.commands) ? manifest.commands : [];
|
|
484
|
+
const commands = declared.flatMap((item) => typeof item?.name === "string"
|
|
485
|
+
? [
|
|
486
|
+
{
|
|
487
|
+
extension: extensionName,
|
|
488
|
+
extensionTitle: String(manifest.title ?? extensionName),
|
|
489
|
+
command: item.name,
|
|
490
|
+
title: typeof item.title === "string" ? item.title : item.name,
|
|
491
|
+
...(typeof item.subtitle === "string" ? { subtitle: item.subtitle } : {}),
|
|
492
|
+
mode: item.mode === "no-view" || item.mode === "menu-bar" ? item.mode : "view",
|
|
493
|
+
},
|
|
494
|
+
]
|
|
495
|
+
: []);
|
|
496
|
+
root = createRootController(commands);
|
|
497
|
+
root.subscribe((view) => {
|
|
498
|
+
if (!controller)
|
|
499
|
+
draw(view);
|
|
500
|
+
});
|
|
501
|
+
setStatus("Ready");
|
|
502
|
+
draw(root.view());
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
fail(error);
|
|
506
|
+
}
|
|
507
|
+
})();
|
|
508
|
+
return {
|
|
509
|
+
input,
|
|
510
|
+
view: () => controller?.view(),
|
|
511
|
+
close() {
|
|
512
|
+
if (closed)
|
|
513
|
+
return;
|
|
514
|
+
stopCommand();
|
|
515
|
+
closed = true;
|
|
516
|
+
// A custom `render` owns what it drew into `element`.
|
|
517
|
+
renderer?.destroy();
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type GitHubExtensionSource = {
|
|
2
|
+
kind: "github";
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
ref: string;
|
|
6
|
+
path: string;
|
|
7
|
+
};
|
|
8
|
+
export type LocalExtensionSource = {
|
|
9
|
+
kind: "local";
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export type ExtensionSource = GitHubExtensionSource | LocalExtensionSource;
|
|
13
|
+
/** Parses only the source shape accepted by the browser demo. */
|
|
14
|
+
export declare function parseExtensionSource(value: string): GitHubExtensionSource;
|
|
15
|
+
export declare function isTextExtension(path: string): boolean;
|
|
16
|
+
export declare function canUseLocalFixture(source: GitHubExtensionSource, enabled: boolean): boolean;
|