|
| 1 | +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --no-check |
| 2 | +import { load } from "../../loader.ts"; |
| 3 | +import { build, LoadResponse } from "../../mod.ts"; |
| 4 | +// import { load } from "https://deno.land/x/eszip@v0.18.0/loader.ts"; |
| 5 | +// import { build, LoadResponse } from "https://deno.land/x/eszip@v0.18.0/mod.ts"; |
| 6 | + |
| 7 | +import * as path from "https://deno.land/std@0.127.0/path/mod.ts"; |
| 8 | + |
| 9 | +const STAGE1_SPECIFIER = "acme:stage1"; |
| 10 | +const STAGE2_SPECIFIER = "acme:stage2"; |
| 11 | + |
| 12 | +function stage1() { |
| 13 | + return ` |
| 14 | + import * as stage2 from "${STAGE2_SPECIFIER}"; |
| 15 | + import { serve } from "https://deno.land/std@0.127.0/http/server.ts"; |
| 16 | +
|
| 17 | + await serve(async (req) => { |
| 18 | + const url = new URL(req.url); |
| 19 | + const handler = stage2.config.urls[url.pathname]; |
| 20 | + const handlerFn = stage2.handlers[handler]; |
| 21 | + return handlerFn ? await handlerFn(req) : new Response("DEFAULT"); |
| 22 | + }); |
| 23 | + `; |
| 24 | +} |
| 25 | + |
| 26 | +function inlineTsMod(specifier: string, content: string): LoadResponse { |
| 27 | + return { |
| 28 | + content, |
| 29 | + headers: { |
| 30 | + "content-type": "application/typescript", |
| 31 | + }, |
| 32 | + kind: "module", |
| 33 | + specifier, |
| 34 | + }; |
| 35 | +} |
| 36 | + |
| 37 | +function stage1Loader() { |
| 38 | + return async (specifier: string): Promise<LoadResponse | undefined> => { |
| 39 | + if (specifier === STAGE1_SPECIFIER) { |
| 40 | + // Load stage 1 wrapper from net/disk or codegenerated in-memory |
| 41 | + return inlineTsMod(specifier, stage1()); |
| 42 | + } else if (specifier === STAGE2_SPECIFIER) { |
| 43 | + // Dangling reference to stage2 |
| 44 | + return { kind: "external", specifier }; |
| 45 | + } |
| 46 | + // Falling back to the default loading logic. |
| 47 | + return load(specifier); |
| 48 | + }; |
| 49 | +} |
| 50 | + |
| 51 | +interface Stage2Config { |
| 52 | + paths: Record<string, string>; // handler_name => entrypoint_path (absolute) |
| 53 | + urls: Record<string, string>; // url => handler_name mapping |
| 54 | + localSrcRoot: string; |
| 55 | +} |
| 56 | + |
| 57 | +function stage2(conf: Stage2Config) { |
| 58 | + const handlerNames = Object.keys(conf.paths); |
| 59 | + const handlerImports = handlerNames.map((name) => { |
| 60 | + return `import ${name} from "${nsSpec(conf, name)}";`; |
| 61 | + }).join("\n"); |
| 62 | + return ` |
| 63 | + ${handlerImports} |
| 64 | + export const handlers = { ${handlerNames.join(", ")} }; |
| 65 | + export const config = ${JSON.stringify(conf)}; |
| 66 | + `; |
| 67 | +} |
| 68 | + |
| 69 | +/// Namespaced specifier, file:///src/... (relative to root) |
| 70 | +function nsSpec(conf: Stage2Config, name: string) { |
| 71 | + const filepath = conf.paths[name]; |
| 72 | + const rel = path.relative(conf.localSrcRoot, filepath); |
| 73 | + return path.toFileUrl(path.join("/src", rel)); |
| 74 | +} |
| 75 | + |
| 76 | +function stage2Loader(conf: Stage2Config) { |
| 77 | + return async (specifier: string): Promise<LoadResponse | undefined> => { |
| 78 | + if (specifier === STAGE2_SPECIFIER) { |
| 79 | + // Codegen stage2 from config to include handler specifics |
| 80 | + return inlineTsMod(specifier, stage2(conf)); |
| 81 | + } else if (specifier.startsWith("file:///src/")) { |
| 82 | + // Local specifier (handler entrypoint or relative import) |
| 83 | + const localRoot = path.toFileUrl(conf.localSrcRoot).toString(); |
| 84 | + const trueSpec = specifier.replace("file:///src", localRoot); |
| 85 | + const resp = await load(trueSpec); |
| 86 | + return resp && { |
| 87 | + ...resp, |
| 88 | + specifier, // preserve original spec |
| 89 | + }; |
| 90 | + } |
| 91 | + // Falling back to the default loading logic. |
| 92 | + return await load(specifier); |
| 93 | + }; |
| 94 | +} |
| 95 | + |
| 96 | +function unifiedLoader(conf: Stage2Config) { |
| 97 | + const s2Loader = stage2Loader(conf); |
| 98 | + return async (specifier: string): Promise<LoadResponse | undefined> => { |
| 99 | + if (specifier === STAGE1_SPECIFIER) { |
| 100 | + return inlineTsMod(specifier, stage1()); |
| 101 | + } |
| 102 | + return s2Loader(specifier); |
| 103 | + }; |
| 104 | +} |
| 105 | + |
| 106 | +async function main(allArgs: string[]) { |
| 107 | + // Specifics of how you obtain the list of handlers is up to you |
| 108 | + // e.g: conf/project files, FS walking, etc... |
| 109 | + const cwd = Deno.cwd(); |
| 110 | + const stage2Config: Stage2Config = { |
| 111 | + paths: { |
| 112 | + foo: path.join(cwd, "handlers", "foo.ts"), |
| 113 | + bar: path.join(cwd, "handlers", "bar", "index.ts"), |
| 114 | + }, |
| 115 | + urls: { |
| 116 | + "/foo": "foo", |
| 117 | + "/kungfu": "foo", |
| 118 | + "/bar": "bar", |
| 119 | + "/drink": "bar", |
| 120 | + }, |
| 121 | + localSrcRoot: cwd, |
| 122 | + }; |
| 123 | + |
| 124 | + const [cmd, ...args] = allArgs; |
| 125 | + switch (cmd) { |
| 126 | + // Produces an ESZIP with only stage1 |
| 127 | + case "stage1": { |
| 128 | + const [destPath] = args; |
| 129 | + const bytes = await build([STAGE1_SPECIFIER], stage1Loader()); |
| 130 | + return await Deno.writeFile(destPath, bytes); |
| 131 | + } |
| 132 | + // Produces an ESZIP with only stage2 |
| 133 | + case "stage2": { |
| 134 | + const [destPath] = args; |
| 135 | + const bytes = await build([STAGE2_SPECIFIER], stage2Loader(stage2Config)); |
| 136 | + return await Deno.writeFile(destPath, bytes); |
| 137 | + } |
| 138 | + // Produces an ESZIP with stage1 & stage2 |
| 139 | + case "unified": { |
| 140 | + const [destPath] = args; |
| 141 | + const bytes = await build( |
| 142 | + [STAGE1_SPECIFIER], |
| 143 | + unifiedLoader(stage2Config), |
| 144 | + ); |
| 145 | + return await Deno.writeFile(destPath, bytes); |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | +await main(Deno.args); |
0 commit comments