1import type * as rolldown from "rolldown";
2
3/**
4 * Matches `?raw` or `&raw` query suffixes, mirroring Vite's `rawRE`
5 * (see `vite/src/node/utils.ts`). Used to gate both `resolveId` and
6 * `load` so the plugin only inspects ids that opt in.
7 */
8export const RAW_RE: RegExp = /(\?|&)raw(?:&|$)/;
9
10/**
11 * Rolldown plugin that adds Vite-style `?raw` import support to the
12 * Alchemy bundler.
13 *
14 * Importing a file with the `?raw` suffix inlines its contents as the
15 * default export of a JS module:
16 *
17 * ```ts
18 * import sql from "./schema.sql?raw";
19 * // ^ string — the file contents read as UTF-8
20 * ```
21 *
22 * This plugin only implements `?raw`. Vite's `?url` and `?inline`
23 * variants both rely on a browser asset pipeline (URL serving / `data:`
24 * fallback) that has no analogue in the server-side bundles produced
25 * here (Cloudflare Workers, AWS Lambda), so they are intentionally
26 * omitted.
27 */
28export const rawPlugin = (): rolldown.Plugin => ({
29 name: "alchemy:raw",
30 resolveId: {
31 filter: { id: RAW_RE },
32 async handler(source, importer) {
33 const [base, query] = splitFileAndPostfix(source);
34 const resolved = await this.resolve(base, importer, { skipSelf: true });
35 if (!resolved || resolved.external) return null;
36 return { id: resolved.id + query, moduleSideEffects: false };
37 },
38 },
39 load: {
40 filter: { id: RAW_RE },
41 async handler(id) {
42 const file = id.replace(/[?#].*$/, "");
43 const contents = await this.fs.readFile(file, { encoding: "utf8" });
44 return {
45 code: `export default ${JSON.stringify(contents)};`,
46 // Empty mappings = "this module has no meaningful source map".
47 // The generated code is a synthetic `export default "..."` that
48 // doesn't correspond line-for-line to any source file (the
49 // underlying file isn't JS), so there's nothing to map. Supplying
50 // `{ mappings: "" }` (rather than omitting `map`) opts the module
51 // out of source-map generation without making rolldown synthesize
52 // a default one from the loaded code.
53 map: { mappings: "" },
54 // NOTE: matches Vite — avoids a double `export default` if the
55 // file extension would otherwise be picked up by another loader
56 // (e.g. `?raw&.json`).
57 moduleType: "js",
58 };
59 },
60 },
61});
62
63/**
64 * Splits an id into its file portion and the query/hash postfix (kept
65 * with the leading `?` / `#`). Matches Vite's `splitFileAndPostfix`.
66 *
67 * @example
68 * splitFileAndPostfix("./foo.txt?raw") // => ["./foo.txt", "?raw"]
69 * splitFileAndPostfix("./foo.txt") // => ["./foo.txt", ""]
70 */
71export function splitFileAndPostfix(id: string): [string, string] {
72 const queryIdx = id.indexOf("?");
73 const hashIdx = id.indexOf("#");
74 const idx =
75 queryIdx === -1
76 ? hashIdx
77 : hashIdx === -1
78 ? queryIdx
79 : Math.min(queryIdx, hashIdx);
80 if (idx === -1) return [id, ""];
81 return [id.slice(0, idx), id.slice(idx)];
82}