import { build, version as esbuildVersion } from "esbuild"; import { Effect } from "effect"; import { ToolSandboxError } from "../seams/tool-sandbox";import type { ToolchainRef } from "./descriptor"; const BUNDLE_TARGET = "es2022";const unknownMessage = (cause: unknown): string => { // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: esbuild rejects with unknown host errors return cause instanceof Error ? cause.message : String(cause);}; /** The toolchain (esbuild version + target) recorded into the descriptor so a * re-collect on a different esbuild is not falsely claimed byte-identical. */export const toolchainRef = (): ToolchainRef => ({ bundler: "esbuild", bundlerVersion: esbuildVersion, target: BUNDLE_TARGET,}); // ---------------------------------------------------------------------------// bundle — the FDI pipeline's bundle stage.//// One esbuild pass per artifact entry. Platform modules (`executor:app`,// `executor:ui`, react, recharts, ...) stay EXTERNAL — the sandbox shim// provides the real behavior. A small allowlist of schema libraries (`zod`,// `@standard-schema/spec`) is INLINED from node_modules so the author's schema// runs in the sandbox AND its `toJSONSchema` is reachable to extract the// descriptor's input/output JSON Schema. Every other bare import is rejected// with a diagnostic: npm deps in user bundles are out of scope.//// Output is a single CJS string with the externals left as `require(...)`// calls the sandbox resolves.// --------------------------------------------------------------------------- /** Modules the platform provides inside the sandbox (kept external, resolved by * the sandbox `require` shim). */export const PLATFORM_MODULES = new Set<string>([ "executor:app", "executor:ui", "executor:ui/components", "react", "react/jsx-runtime", "react-dom", "recharts", "lucide-react",]); /** npm modules we deliberately inline (schema runtime that must run in the * sandbox). Everything else bare is rejected. */export const INLINABLE_MODULES = new Set<string>(["zod", "@standard-schema/spec"]); const isInlinable = (path: string): boolean => { if (INLINABLE_MODULES.has(path)) return true; // zod subpaths (`zod/v4`, `zod/mini`) are fine too. return [...INLINABLE_MODULES].some((m) => path === m || path.startsWith(`${m}/`));}; export interface BundleInput { readonly files: ReadonlyMap<string, string>; readonly entry: string;} export interface BundleOutput { readonly code: string;} const resolveRelative = (base: string, rel: string): string => { const parts = (base ? base.split("/") : []).concat(rel.split("/")); const out: string[] = []; for (const part of parts) { if (part === "" || part === ".") continue; if (part === "..") out.pop(); else out.push(part); } return out.join("/");}; const candidates = (path: string): string[] => { if (/\.(tsx?|jsx?)$/.test(path)) return [path]; return [ path, `${path}.ts`, `${path}.tsx`, `${path}.js`, `${path}.jsx`, `${path}/index.ts`, `${path}/index.tsx`, ];}; const FILESET_NS = "fileset";const VIRTUAL_ENTRY = "executor-apps://entry"; /** The virtual entry esbuild starts from: re-export the author entry AND expose * the shared zod instance as a global so the sandbox collect driver can call * `toJSONSchema` against the SAME zod the author bundled. */const virtualEntrySource = (authorEntry: string) => `import __artifactModule from ${JSON.stringify(`/${authorEntry}`)};import * as __zod from "zod";globalThis.__artifact = __artifactModule;globalThis.__zodToJson = function (schema) { if (!schema) return undefined; try { return __zod.toJSONSchema(schema, { io: "input", unrepresentable: "any" }); } catch (e) { try { return __zod.toJSONSchema(schema); } catch (e2) { return {}; } }};globalThis.__zodToJsonOutput = function (schema) { if (!schema) return undefined; try { return __zod.toJSONSchema(schema, { io: "output", unrepresentable: "any" }); } catch (e) { try { return __zod.toJSONSchema(schema); } catch (e2) { return {}; } }};`; const fileSetPlugin = (files: ReadonlyMap<string, string>, authorEntry: string) => ({ name: "executor-apps-fileset", // eslint-disable-next-line @typescript-eslint/no-explicit-any setup(build2: any) { const norm = (p: string) => p.replace(/^\.\//, "").replace(/\/\.\//g, "/"); build2.onResolve( { filter: /.*/ }, (args: { path: string; importer: string; namespace?: string; resolveDir?: string }) => { if (args.path === VIRTUAL_ENTRY) { return { path: VIRTUAL_ENTRY, namespace: "virtual" }; } // Imports originating OUTSIDE the file set (from inlined npm modules // like zod, or the virtual entry's `import "zod"`) are left to // esbuild's default node resolution. We only govern file-set imports. const fromFileSet = args.namespace === FILESET_NS || args.namespace === "virtual"; if (!fromFileSet) return undefined; if (PLATFORM_MODULES.has(args.path)) { return { path: args.path, external: true }; } if (isInlinable(args.path)) { // Inline from node_modules via default resolution. return undefined; } // Absolute (virtual entry uses `/tools/x.ts`) or relative -> file set. if (args.path.startsWith("/")) { const joined = norm(args.path.slice(1)); for (const candidate of candidates(joined)) { if (files.has(candidate)) return { path: candidate, namespace: FILESET_NS }; } return { errors: [{ text: `cannot resolve "${args.path}" in the app file set` }] }; } if (args.path.startsWith(".")) { const base = args.importer.split("/").slice(0, -1).join("/"); const joined = norm(resolveRelative(base, args.path)); for (const candidate of candidates(joined)) { if (files.has(candidate)) return { path: candidate, namespace: FILESET_NS }; } return { errors: [ { text: `cannot resolve "${args.path}" from "${args.importer}" in the app file set` }, ], }; } return { errors: [ { text: `bare import "${args.path}" is not allowed: apps may only import platform modules (${[...PLATFORM_MODULES].join(", ")}), the schema runtime (zod), or files within the scope`, }, ], }; }, ); build2.onLoad({ filter: /.*/, namespace: "virtual" }, () => ({ contents: virtualEntrySource(authorEntry), loader: "ts", resolveDir: process.cwd(), })); build2.onLoad({ filter: /.*/, namespace: FILESET_NS }, (args: { path: string }) => { const contents = files.get(args.path); if (contents === undefined) { return { errors: [{ text: `missing file in set: ${args.path}` }] }; } const loader = args.path.endsWith(".tsx") ? "tsx" : args.path.endsWith(".ts") ? "ts" : args.path.endsWith(".jsx") ? "jsx" : "js"; return { contents, loader, resolveDir: process.cwd() }; }); },}); /** Bundle one entry from the file set to a single CJS string. */export const bundleEntry = (input: BundleInput): Effect.Effect<BundleOutput, ToolSandboxError> => Effect.tryPromise({ try: async () => { const result = await build({ entryPoints: [VIRTUAL_ENTRY], bundle: true, write: false, format: "cjs", platform: "neutral", target: BUNDLE_TARGET, minify: false, treeShaking: true, jsx: "automatic", logLevel: "silent", external: [...PLATFORM_MODULES], plugins: [fileSetPlugin(input.files, input.entry) as never], }); const out = result.outputFiles?.[0]?.text; // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: esbuild adapter reports an impossible missing output through the existing tryPromise path if (out === undefined) throw new Error("esbuild produced no output"); return { code: out }; }, catch: (cause) => new ToolSandboxError({ kind: "bundle", message: `bundle failed for ${input.entry}: ${unknownMessage(cause)}`, cause, }), });
import { build, version as esbuildVersion } from "esbuild";
import { Effect } from "effect";
import { ToolSandboxError } from "../seams/tool-sandbox";
import type { ToolchainRef } from "./descriptor";
const BUNDLE_TARGET = "es2022";
const unknownMessage = (cause: unknown): string => {
// oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: esbuild rejects with unknown host errors
return cause instanceof Error ? cause.message : String(cause);
};
/** The toolchain (esbuild version + target) recorded into the descriptor so a
* re-collect on a different esbuild is not falsely claimed byte-identical. */
export const toolchainRef = (): ToolchainRef => ({
bundler: "esbuild",
bundlerVersion: esbuildVersion,
target: BUNDLE_TARGET,
});
// ---------------------------------------------------------------------------
// bundle — the FDI pipeline's bundle stage.
//
// One esbuild pass per artifact entry. Platform modules (`executor:app`,
// `executor:ui`, react, recharts, ...) stay EXTERNAL — the sandbox shim
// provides the real behavior. A small allowlist of schema libraries (`zod`,
// `@standard-schema/spec`) is INLINED from node_modules so the author's schema
// runs in the sandbox AND its `toJSONSchema` is reachable to extract the
// descriptor's input/output JSON Schema. Every other bare import is rejected
// with a diagnostic: npm deps in user bundles are out of scope.
//
// Output is a single CJS string with the externals left as `require(...)`
// calls the sandbox resolves.
// ---------------------------------------------------------------------------
/** Modules the platform provides inside the sandbox (kept external, resolved by
* the sandbox `require` shim). */
export const PLATFORM_MODULES = new Set<string>([
"executor:app",
"executor:ui",
"executor:ui/components",
"react",
"react/jsx-runtime",
"react-dom",
"recharts",
"lucide-react",
]);
/** npm modules we deliberately inline (schema runtime that must run in the
* sandbox). Everything else bare is rejected. */
export const INLINABLE_MODULES = new Set<string>(["zod", "@standard-schema/spec"]);
const isInlinable = (path: string): boolean => {
if (INLINABLE_MODULES.has(path)) return true;
// zod subpaths (`zod/v4`, `zod/mini`) are fine too.
return [...INLINABLE_MODULES].some((m) => path === m || path.startsWith(`${m}/`));
};
export interface BundleInput {
readonly files: ReadonlyMap<string, string>;
readonly entry: string;
}
export interface BundleOutput {
readonly code: string;
}
const resolveRelative = (base: string, rel: string): string => {
const parts = (base ? base.split("/") : []).concat(rel.split("/"));
const out: string[] = [];
for (const part of parts) {
if (part === "" || part === ".") continue;
if (part === "..") out.pop();
else out.push(part);
}
return out.join("/");
};
const candidates = (path: string): string[] => {
if (/\.(tsx?|jsx?)$/.test(path)) return [path];
return [
path,
`${path}.ts`,
`${path}.tsx`,
`${path}.js`,
`${path}.jsx`,
`${path}/index.ts`,
`${path}/index.tsx`,
];
};
const FILESET_NS = "fileset";
const VIRTUAL_ENTRY = "executor-apps://entry";
/** The virtual entry esbuild starts from: re-export the author entry AND expose
* the shared zod instance as a global so the sandbox collect driver can call
* `toJSONSchema` against the SAME zod the author bundled. */
const virtualEntrySource = (authorEntry: string) => `
import __artifactModule from ${JSON.stringify(`/${authorEntry}`)};
import * as __zod from "zod";
globalThis.__artifact = __artifactModule;
globalThis.__zodToJson = function (schema) {
if (!schema) return undefined;
try {
return __zod.toJSONSchema(schema, { io: "input", unrepresentable: "any" });
} catch (e) {
try { return __zod.toJSONSchema(schema); } catch (e2) { return {}; }
}
};
globalThis.__zodToJsonOutput = function (schema) {
if (!schema) return undefined;
try {
return __zod.toJSONSchema(schema, { io: "output", unrepresentable: "any" });
} catch (e) {
try { return __zod.toJSONSchema(schema); } catch (e2) { return {}; }
}
};
`;
const fileSetPlugin = (files: ReadonlyMap<string, string>, authorEntry: string) => ({
name: "executor-apps-fileset",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setup(build2: any) {
const norm = (p: string) => p.replace(/^\.\//, "").replace(/\/\.\//g, "/");
build2.onResolve(
{ filter: /.*/ },
(args: { path: string; importer: string; namespace?: string; resolveDir?: string }) => {
if (args.path === VIRTUAL_ENTRY) {
return { path: VIRTUAL_ENTRY, namespace: "virtual" };
}
// Imports originating OUTSIDE the file set (from inlined npm modules
// like zod, or the virtual entry's `import "zod"`) are left to
// esbuild's default node resolution. We only govern file-set imports.
const fromFileSet = args.namespace === FILESET_NS || args.namespace === "virtual";
if (!fromFileSet) return undefined;
if (PLATFORM_MODULES.has(args.path)) {
return { path: args.path, external: true };
}
if (isInlinable(args.path)) {
// Inline from node_modules via default resolution.
return undefined;
}
// Absolute (virtual entry uses `/tools/x.ts`) or relative -> file set.
if (args.path.startsWith("/")) {
const joined = norm(args.path.slice(1));
for (const candidate of candidates(joined)) {
if (files.has(candidate)) return { path: candidate, namespace: FILESET_NS };
}
return { errors: [{ text: `cannot resolve "${args.path}" in the app file set` }] };
}
if (args.path.startsWith(".")) {
const base = args.importer.split("/").slice(0, -1).join("/");
const joined = norm(resolveRelative(base, args.path));
for (const candidate of candidates(joined)) {
if (files.has(candidate)) return { path: candidate, namespace: FILESET_NS };
}
return {
errors: [
{ text: `cannot resolve "${args.path}" from "${args.importer}" in the app file set` },
],
};
}
return {
errors: [
{
text: `bare import "${args.path}" is not allowed: apps may only import platform modules (${[...PLATFORM_MODULES].join(", ")}), the schema runtime (zod), or files within the scope`,
},
],
};
},
);
build2.onLoad({ filter: /.*/, namespace: "virtual" }, () => ({
contents: virtualEntrySource(authorEntry),
loader: "ts",
resolveDir: process.cwd(),
}));
build2.onLoad({ filter: /.*/, namespace: FILESET_NS }, (args: { path: string }) => {
const contents = files.get(args.path);
if (contents === undefined) {
return { errors: [{ text: `missing file in set: ${args.path}` }] };
}
const loader = args.path.endsWith(".tsx")
? "tsx"
: args.path.endsWith(".ts")
? "ts"
: args.path.endsWith(".jsx")
? "jsx"
: "js";
return { contents, loader, resolveDir: process.cwd() };
});
},
});
/** Bundle one entry from the file set to a single CJS string. */
export const bundleEntry = (input: BundleInput): Effect.Effect<BundleOutput, ToolSandboxError> =>
Effect.tryPromise({
try: async () => {
const result = await build({
entryPoints: [VIRTUAL_ENTRY],
bundle: true,
write: false,
format: "cjs",
platform: "neutral",
target: BUNDLE_TARGET,
minify: false,
treeShaking: true,
jsx: "automatic",
logLevel: "silent",
external: [...PLATFORM_MODULES],
plugins: [fileSetPlugin(input.files, input.entry) as never],
});
const out = result.outputFiles?.[0]?.text;
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: esbuild adapter reports an impossible missing output through the existing tryPromise path
if (out === undefined) throw new Error("esbuild produced no output");
return { code: out };
},
catch: (cause) =>
new ToolSandboxError({
kind: "bundle",
message: `bundle failed for ${input.entry}: ${unknownMessage(cause)}`,
cause,
}),
});