Skip to content

Commit e4af47e

Browse files
committed
runtime: extract sandbox script templates into source modules
1 parent d70a43b commit e4af47e

4 files changed

Lines changed: 173 additions & 143 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export function buildIframeSandboxSrcdoc(channelLiteral: string): string {
2+
return /* html */ `<!doctype html><html><body><script>
3+
const CHANNEL = ${channelLiteral};
4+
window.addEventListener("message", async (event) => {
5+
const data = event.data;
6+
if (!data || data.channel !== CHANNEL) {
7+
return;
8+
}
9+
const request = data.request || {};
10+
const safeSend = (payload) => {
11+
try {
12+
parent.postMessage({ channel: CHANNEL, ...payload }, "*");
13+
return true;
14+
} catch (postError) {
15+
try {
16+
const postMessageError =
17+
postError && typeof postError === "object" && "message" in postError
18+
? String(postError.message)
19+
: String(postError);
20+
parent.postMessage(
21+
{
22+
channel: CHANNEL,
23+
ok: false,
24+
error: "Sandbox response is not serializable: " + postMessageError,
25+
},
26+
"*",
27+
);
28+
} catch {
29+
// Ignore terminal postMessage failures.
30+
}
31+
return false;
32+
}
33+
};
34+
try {
35+
const moduleUrl = URL.createObjectURL(
36+
new Blob([String(request.code ?? "")], { type: "text/javascript" }),
37+
);
38+
try {
39+
const namespace = await import(moduleUrl);
40+
const exportName =
41+
typeof request.exportName === "string" &&
42+
request.exportName.trim().length > 0
43+
? request.exportName.trim()
44+
: "default";
45+
const selected = namespace[exportName];
46+
if (selected === undefined) {
47+
throw new Error(
48+
'Runtime source export "' + exportName + '" is missing',
49+
);
50+
}
51+
const output =
52+
typeof selected === "function"
53+
? await selected(request.runtimeInput ?? {})
54+
: selected;
55+
safeSend({ ok: true, output });
56+
} finally {
57+
URL.revokeObjectURL(moduleUrl);
58+
}
59+
} catch (error) {
60+
const message =
61+
error && typeof error === "object" && "message" in error
62+
? String(error.message)
63+
: String(error);
64+
safeSend({ ok: false, error: message });
65+
}
66+
});
67+
</script></body></html>`;
68+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export function buildShadowRealmBridgeSource(moduleUrl: string): string {
2+
const moduleUrlLiteral = JSON.stringify(moduleUrl);
3+
4+
return /* js */ `
5+
import * as __renderify_ns from ${moduleUrlLiteral};
6+
function __renderify_message(error) {
7+
return error && typeof error === "object" && "message" in error
8+
? String(error.message)
9+
: String(error);
10+
}
11+
export async function __renderify_run(serializedRuntimeInput, exportName) {
12+
try {
13+
const selectedExportName =
14+
typeof exportName === "string" && exportName.trim().length > 0
15+
? exportName.trim()
16+
: "default";
17+
const selected = __renderify_ns[selectedExportName];
18+
if (selected === undefined) {
19+
throw new Error(
20+
'Runtime source export "' + selectedExportName + '" is missing',
21+
);
22+
}
23+
const runtimeInput =
24+
typeof serializedRuntimeInput === "string" &&
25+
serializedRuntimeInput.length > 0
26+
? JSON.parse(serializedRuntimeInput)
27+
: {};
28+
const output =
29+
typeof selected === "function" ? await selected(runtimeInput) : selected;
30+
return JSON.stringify({ ok: true, output });
31+
} catch (error) {
32+
return JSON.stringify({ ok: false, error: __renderify_message(error) });
33+
}
34+
}
35+
`.trim();
36+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
export const WORKER_SANDBOX_SOURCE = /* js */ `
2+
const CHANNEL = "runtime-source";
3+
self.onmessage = async (event) => {
4+
const request = event.data;
5+
if (!request || request.renderifySandbox !== CHANNEL) {
6+
return;
7+
}
8+
const safeSend = (payload) => {
9+
try {
10+
self.postMessage({ renderifySandbox: CHANNEL, id: request.id, ...payload });
11+
return true;
12+
} catch (postError) {
13+
try {
14+
const postMessageError =
15+
postError && typeof postError === "object" && "message" in postError
16+
? String(postError.message)
17+
: String(postError);
18+
self.postMessage({
19+
renderifySandbox: CHANNEL,
20+
id: request.id,
21+
ok: false,
22+
error: "Sandbox response is not serializable: " + postMessageError,
23+
});
24+
} catch {
25+
// Ignore terminal postMessage failures.
26+
}
27+
return false;
28+
}
29+
};
30+
try {
31+
const moduleUrl = URL.createObjectURL(
32+
new Blob([String(request.code ?? "")], { type: "text/javascript" }),
33+
);
34+
try {
35+
const namespace = await import(moduleUrl);
36+
const exportName =
37+
typeof request.exportName === "string" &&
38+
request.exportName.trim().length > 0
39+
? request.exportName.trim()
40+
: "default";
41+
const selected = namespace[exportName];
42+
if (selected === undefined) {
43+
throw new Error(
44+
'Runtime source export "' + exportName + '" is missing',
45+
);
46+
}
47+
const output =
48+
typeof selected === "function"
49+
? await selected(request.runtimeInput ?? {})
50+
: selected;
51+
safeSend({ ok: true, output });
52+
} finally {
53+
URL.revokeObjectURL(moduleUrl);
54+
}
55+
} catch (error) {
56+
const message =
57+
error && typeof error === "object" && "message" in error
58+
? String(error.message)
59+
: String(error);
60+
safeSend({ ok: false, error: message });
61+
}
62+
};
63+
`.trim();

packages/runtime/src/sandbox.ts

Lines changed: 6 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { JsonValue } from "@renderify/ir";
22
import type { RuntimeSourceSandboxMode } from "./runtime-manager.types";
3+
import { buildIframeSandboxSrcdoc } from "./sandbox-iframe-source";
4+
import { buildShadowRealmBridgeSource } from "./sandbox-shadowrealm-bridge-source";
5+
import { WORKER_SANDBOX_SOURCE } from "./sandbox-worker-source";
36

47
export interface RuntimeSandboxResult {
58
mode: RuntimeSourceSandboxMode;
@@ -96,63 +99,8 @@ async function executeSourceInWorkerSandbox(
9699
throw new Error("Worker sandbox is unavailable in this runtime");
97100
}
98101

99-
const workerSource = [
100-
"const CHANNEL = 'runtime-source';",
101-
"self.onmessage = async (event) => {",
102-
" const request = event.data;",
103-
" if (!request || request.renderifySandbox !== CHANNEL) {",
104-
" return;",
105-
" }",
106-
" const safeSend = (payload) => {",
107-
" try {",
108-
" self.postMessage({ renderifySandbox: CHANNEL, id: request.id, ...payload });",
109-
" return true;",
110-
" } catch (postError) {",
111-
" try {",
112-
" const postMessageError = postError && typeof postError === 'object' && 'message' in postError",
113-
" ? String(postError.message)",
114-
" : String(postError);",
115-
" self.postMessage({",
116-
" renderifySandbox: CHANNEL,",
117-
" id: request.id,",
118-
" ok: false,",
119-
" error: `Sandbox response is not serializable: ${postMessageError}`,",
120-
" });",
121-
" } catch {",
122-
" // Ignore terminal postMessage failures.",
123-
" }",
124-
" return false;",
125-
" }",
126-
" };",
127-
" try {",
128-
" const moduleUrl = URL.createObjectURL(new Blob([String(request.code ?? '')], { type: 'text/javascript' }));",
129-
" try {",
130-
" const namespace = await import(moduleUrl);",
131-
" const exportName = typeof request.exportName === 'string' && request.exportName.trim().length > 0",
132-
" ? request.exportName.trim()",
133-
" : 'default';",
134-
" const selected = namespace[exportName];",
135-
" if (selected === undefined) {",
136-
' throw new Error(`Runtime source export "${exportName}" is missing`);',
137-
" }",
138-
" const output = typeof selected === 'function'",
139-
" ? await selected(request.runtimeInput ?? {})",
140-
" : selected;",
141-
" safeSend({ ok: true, output });",
142-
" } finally {",
143-
" URL.revokeObjectURL(moduleUrl);",
144-
" }",
145-
" } catch (error) {",
146-
" const message = error && typeof error === 'object' && 'message' in error",
147-
" ? String(error.message)",
148-
" : String(error);",
149-
" safeSend({ ok: false, error: message });",
150-
" }",
151-
"};",
152-
].join("\n");
153-
154102
const workerUrl = URL.createObjectURL(
155-
new Blob([workerSource], {
103+
new Blob([WORKER_SANDBOX_SOURCE], {
156104
type: "text/javascript",
157105
}),
158106
);
@@ -262,63 +210,7 @@ async function executeSourceInIframeSandbox(
262210

263211
const channel = `renderify-runtime-source-${options.request.id}`;
264212
const channelLiteral = JSON.stringify(channel);
265-
266-
iframe.srcdoc = [
267-
"<!doctype html><html><body><script>",
268-
`const CHANNEL = ${channelLiteral};`,
269-
"window.addEventListener('message', async (event) => {",
270-
" const data = event.data;",
271-
" if (!data || data.channel !== CHANNEL) {",
272-
" return;",
273-
" }",
274-
" const request = data.request || {};",
275-
" const safeSend = (payload) => {",
276-
" try {",
277-
" parent.postMessage({ channel: CHANNEL, ...payload }, '*');",
278-
" return true;",
279-
" } catch (postError) {",
280-
" try {",
281-
" const postMessageError = postError && typeof postError === 'object' && 'message' in postError",
282-
" ? String(postError.message)",
283-
" : String(postError);",
284-
" parent.postMessage({",
285-
" channel: CHANNEL,",
286-
" ok: false,",
287-
" error: `Sandbox response is not serializable: ${postMessageError}`,",
288-
" }, '*');",
289-
" } catch {",
290-
" // Ignore terminal postMessage failures.",
291-
" }",
292-
" return false;",
293-
" }",
294-
" };",
295-
" try {",
296-
" const moduleUrl = URL.createObjectURL(new Blob([String(request.code ?? '')], { type: 'text/javascript' }));",
297-
" try {",
298-
" const namespace = await import(moduleUrl);",
299-
" const exportName = typeof request.exportName === 'string' && request.exportName.trim().length > 0",
300-
" ? request.exportName.trim()",
301-
" : 'default';",
302-
" const selected = namespace[exportName];",
303-
" if (selected === undefined) {",
304-
' throw new Error(`Runtime source export "${exportName}" is missing`);',
305-
" }",
306-
" const output = typeof selected === 'function'",
307-
" ? await selected(request.runtimeInput ?? {})",
308-
" : selected;",
309-
" safeSend({ ok: true, output });",
310-
" } finally {",
311-
" URL.revokeObjectURL(moduleUrl);",
312-
" }",
313-
" } catch (error) {",
314-
" const message = error && typeof error === 'object' && 'message' in error",
315-
" ? String(error.message)",
316-
" : String(error);",
317-
" safeSend({ ok: false, error: message });",
318-
" }",
319-
"});",
320-
"</script></body></html>",
321-
].join("");
213+
iframe.srcdoc = buildIframeSandboxSrcdoc(channelLiteral);
322214

323215
document.body.appendChild(iframe);
324216

@@ -421,36 +313,7 @@ async function executeSourceInShadowRealmSandbox(
421313
}),
422314
);
423315

424-
const bridgeCode = [
425-
`import * as __renderify_ns from ${JSON.stringify(moduleUrl)};`,
426-
"function __renderify_message(error) {",
427-
" return error && typeof error === 'object' && 'message' in error",
428-
" ? String(error.message)",
429-
" : String(error);",
430-
"}",
431-
"export async function __renderify_run(serializedRuntimeInput, exportName) {",
432-
" try {",
433-
" const selectedExportName =",
434-
" typeof exportName === 'string' && exportName.trim().length > 0",
435-
" ? exportName.trim()",
436-
" : 'default';",
437-
" const selected = __renderify_ns[selectedExportName];",
438-
" if (selected === undefined) {",
439-
' throw new Error(`Runtime source export \\"${selectedExportName}\\" is missing`);',
440-
" }",
441-
" const runtimeInput =",
442-
" typeof serializedRuntimeInput === 'string' && serializedRuntimeInput.length > 0",
443-
" ? JSON.parse(serializedRuntimeInput)",
444-
" : {};",
445-
" const output = typeof selected === 'function'",
446-
" ? await selected(runtimeInput)",
447-
" : selected;",
448-
" return JSON.stringify({ ok: true, output });",
449-
" } catch (error) {",
450-
" return JSON.stringify({ ok: false, error: __renderify_message(error) });",
451-
" }",
452-
"}",
453-
].join("\n");
316+
const bridgeCode = buildShadowRealmBridgeSource(moduleUrl);
454317

455318
const bridgeUrl = URL.createObjectURL(
456319
new Blob([bridgeCode], {

0 commit comments

Comments
 (0)