char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somdo web builds in a subprocessd1201e2

main
5.9 KiB165 linesraw
1// vite and its plugins live only as long as one frontend build subprocess.
2
3import { encodeBase64 } from "@std/encoding/base64";
4import { fromFileUrl, isAbsolute } from "@std/path";
5import aftercare from "@char/aftercare/vite";
6import deno from "@deno/vite-plugin";
7import { build, type Plugin, type Rolldown } from "vite";
8import { HOST_GLOBAL, HOST_MODULES } from "@berry/api/host.ts";
9
10export type FrontendBuild = {
11  chunks: { fileName: string; code: string; isEntry: boolean }[];
12  assets: { fileName: string; source: string; binary: boolean }[];
13  dependencies: string[];
14};
15
16const isHost = (specifier: unknown): specifier is string =>
17  (HOST_MODULES as readonly unknown[]).includes(specifier);
18
19/** Imports of the host's modules become reads of the instances the app published, as `const { … } = host[…]`. */
20function hostModules(): Plugin {
21  return {
22    name: "berry-host-modules",
23    renderChunk(code) {
24      let rewritten = "";
25      let at = 0;
26      for (const node of this.parse(code).body) {
27        if (node.type !== "ImportDeclaration" || !isHost(node.source.value)) continue;
28        const host = `globalThis[${JSON.stringify(HOST_GLOBAL)}][${
29          JSON.stringify(node.source.value)
30        }]`;
31        const bindings: string[] = [];
32        const statements: string[] = [];
33        for (const specifier of node.specifiers) {
34          if (specifier.type === "ImportNamespaceSpecifier") {
35            statements.push(`const ${specifier.local.name} = ${host};`);
36            continue;
37          }
38          const imported = specifier.type === "ImportDefaultSpecifier"
39            ? "default"
40            : specifier.imported.type === "Identifier"
41            ? specifier.imported.name
42            : JSON.stringify(specifier.imported.value);
43          bindings.push(
44            imported === specifier.local.name ? imported : `${imported}: ${specifier.local.name}`,
45          );
46        }
47        if (bindings.length > 0) statements.push(`const { ${bindings.join(", ")} } = ${host};`);
48        // one line for one line, so the source map still lines up
49        rewritten += code.slice(at, node.start) + statements.join(" ");
50        at = node.end;
51      }
52      return at === 0 ? null : { code: rewritten + code.slice(at), map: null };
53    },
54  };
55}
56
57async function buildFrontend(entry: string): Promise<FrontendBuild> {
58  const root = fromFileUrl(new URL("../", import.meta.url));
59  const dependencies = new Set([entry]);
60  function track<C extends Rolldown.PluginContext, A extends unknown[], R>(
61    hook: (this: C, ...args: A) => R,
62  ): (this: C, ...args: A) => R {
63    return function (this: C, ...args: A): R {
64      const context = new Proxy(this, {
65        get(target, key) {
66          if (key === "addWatchFile") {
67            return (path: string) => {
68              if (isAbsolute(path)) dependencies.add(path);
69              target.addWatchFile(path);
70            };
71          }
72          const value = Reflect.get(target, key, target);
73          return typeof value === "function" ? value.bind(target) : value;
74        },
75      });
76      return Reflect.apply(hook, context, args);
77    };
78  }
79  const result = await build({
80    root,
81    configFile: false,
82    mode: "production",
83    // a failure rejects with its message; the caller reports it
84    logLevel: "silent",
85    publicDir: false,
86    // assets resolve against the module that names them, beside it under /ext/<name>/
87    base: "./",
88    plugins: [
89      deno({
90        onLoad({ id }) {
91          if (id.startsWith("file:")) dependencies.add(fromFileUrl(id));
92        },
93      }),
94      aftercare(),
95      hostModules(),
96      {
97        name: "berry-build-inputs",
98        configResolved(config) {
99          // vite hides rolldown's watched files, including css imports and worker dependencies
100          for (const plugin of config.plugins) {
101            const load = plugin.load;
102            if (load) {
103              plugin.load = typeof load === "function"
104                ? track(load)
105                : { ...load, handler: track(load.handler) };
106            }
107            const transform = plugin.transform;
108            if (transform) {
109              plugin.transform = typeof transform === "function"
110                ? track(transform)
111                : { ...transform, handler: track(transform.handler) };
112            }
113          }
114        },
115        generateBundle() {
116          for (const id of this.getModuleIds()) {
117            const path = id.split("?")[0];
118            if (isAbsolute(path)) dependencies.add(path);
119          }
120        },
121      },
122    ],
123    build: {
124      write: false,
125      minify: false,
126      sourcemap: "inline",
127      modulePreload: false,
128      // not library mode, which inlines every asset however large, such as a wasm module
129      rolldownOptions: {
130        input: entry,
131        external: [...HOST_MODULES],
132        preserveEntrySignatures: "strict",
133        output: {
134          format: "es",
135          // a dynamic import is a chunk of its own, so an extension can leave heavy code until it needs it
136          entryFileNames: "[name]-[hash].js",
137          chunkFileNames: "[name]-[hash].js",
138          assetFileNames: "[name]-[hash][extname]",
139        },
140      },
141    },
142  });
143  const outputs = (Array.isArray(result) ? result : [result as Rolldown.RolldownOutput])
144    .flatMap((r) => r.output);
145  return {
146    chunks: outputs.filter((o): o is Rolldown.OutputChunk => o.type === "chunk")
147      .map(({ fileName, code, isEntry }) => ({ fileName, code, isEntry })),
148    assets: outputs.filter((o): o is Rolldown.OutputAsset => o.type === "asset")
149      .map(({ fileName, source }) => ({
150        fileName,
151        source: typeof source === "string" ? source : encodeBase64(source),
152        binary: typeof source !== "string",
153      })),
154    dependencies: [...dependencies],
155  };
156}
157
158if (import.meta.main) {
159  try {
160    console.log(JSON.stringify(await buildFrontend(Deno.args[0])));
161  } catch (error) {
162    console.error(error instanceof Error ? error.message : String(error));
163    Deno.exit(1);
164  }
165}