| 1 | import 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 | */ |
| 8 | export 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 | */ |
| 28 | export 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 | */ |
| 71 | export 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 | } |