File
Blob: types/scripts/build-worker.ts
| 1 | // Copyright (c) 2026 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | import assert from "node:assert"; |
| 6 | import fs from "node:fs/promises"; |
| 7 | import path from "node:path"; |
| 8 | import { build, type PluginBuild, type OnResolveArgs, type OnLoadArgs } from "esbuild"; |
| 9 | import { CommentsData } from "src/transforms"; |
| 10 | import cloudflareComments from "../src/cloudflare"; |
| 11 | import { collateStandardComments } from "../src/standards"; |
| 12 | import { getFilePath } from "../src/utils"; |
| 13 | |
| 14 | async function readPath(rootPath: string): Promise<string> { |
| 15 | try { |
| 16 | return await fs.readFile(rootPath, "utf8"); |
| 17 | } catch (e) { |
| 18 | if (!(e && typeof e === "object" && "code" in e && e.code === "EISDIR")) |
| 19 | throw e; |
| 20 | const fileNames = await fs.readdir(rootPath); |
| 21 | const contentsPromises = fileNames.map((fileName) => { |
| 22 | const filePath = path.join(rootPath, fileName); |
| 23 | return readPath(filePath); |
| 24 | }); |
| 25 | const contents = await Promise.all(contentsPromises); |
| 26 | return contents.join("\n"); |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | async function readParamNames(): Promise<Record<string, Record<string, string[]>>> { |
| 31 | // Support methods defined in parent classes |
| 32 | const additionalClassNames: [string, string][] = [ |
| 33 | ["DurableObjectStorageOperations", "DurableObjectStorage"], |
| 34 | ["DurableObjectStorageOperations", "DurableObjectTransaction"], |
| 35 | ]; |
| 36 | |
| 37 | const data = await fs.readFile(getFilePath("src/workerd/tools/param-names.json"), "utf8"); |
| 38 | const recordArray = JSON.parse(data) as { |
| 39 | fully_qualified_parent_name: string[]; |
| 40 | function_like_name: string; |
| 41 | index: number; |
| 42 | name: string; |
| 43 | }[]; |
| 44 | |
| 45 | function registerApi( |
| 46 | structureName: string, |
| 47 | record: (typeof recordArray)[number] |
| 48 | ): void { |
| 49 | let functionName: string = record.function_like_name; |
| 50 | if (functionName.endsWith("_")) functionName = functionName.slice(0, -1); |
| 51 | // `constructor` is a reserved property name |
| 52 | if (functionName === "constructor") functionName = `$${functionName}`; |
| 53 | |
| 54 | result[structureName] ??= {}; |
| 55 | const structureRecord = result[structureName]; |
| 56 | |
| 57 | structureRecord[functionName] ??= []; |
| 58 | const functionArray = structureRecord[functionName]; |
| 59 | functionArray[record.index] = record.name; |
| 60 | } |
| 61 | |
| 62 | const result: Record<string, Record<string, string[]>> = {}; |
| 63 | for (const record of recordArray) { |
| 64 | const structureName: string = record.fully_qualified_parent_name |
| 65 | .filter(Boolean) |
| 66 | .join("::"); |
| 67 | |
| 68 | registerApi(structureName, record); |
| 69 | |
| 70 | for (const [className, renamedClass] of additionalClassNames) { |
| 71 | if (structureName.includes(className)) { |
| 72 | registerApi(structureName.replace(className, renamedClass), record); |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | return result; |
| 77 | } |
| 78 | |
| 79 | export function readComments(): CommentsData { |
| 80 | const comments = collateStandardComments( |
| 81 | path.join( |
| 82 | path.dirname(require.resolve("typescript")), |
| 83 | "lib.webworker.d.ts" |
| 84 | ), |
| 85 | path.join( |
| 86 | path.dirname(require.resolve("typescript")), |
| 87 | "lib.webworker.iterable.d.ts" |
| 88 | ) |
| 89 | ); |
| 90 | |
| 91 | // We want to deep merge here so that our comment overrides can be very targeted |
| 92 | for (const [name, members] of Object.entries(cloudflareComments)) { |
| 93 | comments[name] ??= {}; |
| 94 | for (const [member, comment] of Object.entries(members)) { |
| 95 | const apiEntry = comments[name]; |
| 96 | assert(apiEntry !== undefined); |
| 97 | apiEntry[member] = comment as string; |
| 98 | } |
| 99 | } |
| 100 | return comments; |
| 101 | } |
| 102 | |
| 103 | if (require.main === module) |
| 104 | void build({ |
| 105 | logLevel: "info", |
| 106 | format: "esm", |
| 107 | target: "esnext", |
| 108 | external: ["node:*", "workerd:*"], |
| 109 | bundle: true, |
| 110 | minify: true, |
| 111 | outdir: getFilePath("types/dist"), |
| 112 | outExtension: { ".js": ".mjs" }, |
| 113 | entryPoints: [getFilePath("types/src/worker/index.ts")], |
| 114 | plugins: [ |
| 115 | { |
| 116 | name: "raw", |
| 117 | setup(build: PluginBuild): void { |
| 118 | build.onResolve({ filter: /^raw:/ }, (args: OnResolveArgs): { |
| 119 | namespace: string, |
| 120 | path: string |
| 121 | } => { |
| 122 | const resolved = path.resolve(args.resolveDir, args.path.slice(4)); |
| 123 | return { namespace: "raw", path: resolved }; |
| 124 | }); |
| 125 | build.onLoad({ namespace: "raw", filter: /.*/ }, async (args: OnLoadArgs) => { |
| 126 | const contents = await readPath(args.path); |
| 127 | return { contents, loader: "text" }; |
| 128 | }); |
| 129 | }, |
| 130 | }, |
| 131 | { |
| 132 | name: "virtual", |
| 133 | setup(build: PluginBuild): void { |
| 134 | build.onResolve({ filter: /^virtual:/ }, (args: OnResolveArgs) => { |
| 135 | return { |
| 136 | namespace: "virtual", |
| 137 | path: args.path.substring("virtual:".length), |
| 138 | }; |
| 139 | }); |
| 140 | build.onLoad({ namespace: "virtual", filter: /.*/ }, async (args: OnLoadArgs) => { |
| 141 | if (args.path === "param-names.json") { |
| 142 | const contents = await readParamNames(); |
| 143 | return { contents: JSON.stringify(contents), loader: "json" }; |
| 144 | } |
| 145 | if (args.path === "comments.json") { |
| 146 | const comments = readComments(); |
| 147 | return { contents: JSON.stringify(comments), loader: "json" }; |
| 148 | } |
| 149 | }); |
| 150 | }, |
| 151 | }, |
| 152 | ], |
| 153 | }); |