Skip to content
File

Blob: types/scripts/build-worker.ts

typescript154 lines
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 
5import assert from "node:assert";
6import fs from "node:fs/promises";
7import path from "node:path";
8import { build, type PluginBuild, type OnResolveArgs, type OnLoadArgs } from "esbuild";
9import { CommentsData } from "src/transforms";
10import cloudflareComments from "../src/cloudflare";
11import { collateStandardComments } from "../src/standards";
12import { getFilePath } from "../src/utils";
13 
14async 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 
30async 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 
79export 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 
103if (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 });