@cloudflare/worker-bundler 0.0.0 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +494 -0
- package/dist/esbuild.wasm +0 -0
- package/dist/index.d.ts +383 -0
- package/dist/index.js +1681 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -8
package/dist/index.js
ADDED
|
@@ -0,0 +1,1681 @@
|
|
|
1
|
+
import * as esbuild from "esbuild-wasm/lib/browser.js";
|
|
2
|
+
import esbuildWasm from "./esbuild.wasm";
|
|
3
|
+
import { parse } from "es-module-lexer/js";
|
|
4
|
+
import * as resolveExports from "resolve.exports";
|
|
5
|
+
import { parse as parse$1 } from "smol-toml";
|
|
6
|
+
import * as semver from "semver";
|
|
7
|
+
import { transform } from "sucrase";
|
|
8
|
+
//#region src/resolver.ts
|
|
9
|
+
const DEFAULT_EXTENSIONS = [
|
|
10
|
+
".ts",
|
|
11
|
+
".tsx",
|
|
12
|
+
".js",
|
|
13
|
+
".jsx",
|
|
14
|
+
".mts",
|
|
15
|
+
".mjs",
|
|
16
|
+
".json"
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a module specifier to a file path in the virtual file system.
|
|
20
|
+
*
|
|
21
|
+
* Handles:
|
|
22
|
+
* - Relative imports (./foo, ../bar)
|
|
23
|
+
* - Package imports (lodash, @scope/pkg)
|
|
24
|
+
* - Package.json exports field
|
|
25
|
+
* - Extension resolution (.ts, .tsx, .js, etc.)
|
|
26
|
+
* - Index file resolution (foo/index.ts)
|
|
27
|
+
*
|
|
28
|
+
* @param specifier - The import specifier (e.g., './utils', 'lodash')
|
|
29
|
+
* @param options - Resolution options
|
|
30
|
+
* @returns Resolved path or external marker
|
|
31
|
+
*/
|
|
32
|
+
function resolveModule(specifier, options) {
|
|
33
|
+
const { files, importer = "", conditions = ["import", "browser"], extensions = DEFAULT_EXTENSIONS } = options;
|
|
34
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
35
|
+
const resolved = resolveRelative(specifier, importer, files, extensions);
|
|
36
|
+
if (resolved) return {
|
|
37
|
+
path: resolved,
|
|
38
|
+
external: false
|
|
39
|
+
};
|
|
40
|
+
throw new Error(`Cannot resolve relative import '${specifier}' from '${importer}'`);
|
|
41
|
+
}
|
|
42
|
+
return resolvePackage(specifier, files, conditions, extensions);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a relative import
|
|
46
|
+
*/
|
|
47
|
+
function resolveRelative(specifier, importer, files, extensions) {
|
|
48
|
+
return resolveWithExtensions(joinPaths(getDirectory$1(importer), specifier), files, extensions);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a package specifier
|
|
52
|
+
*/
|
|
53
|
+
function resolvePackage(specifier, files, conditions, extensions) {
|
|
54
|
+
const { packageName, subpath } = parsePackageSpecifier(specifier);
|
|
55
|
+
const packageJson = files[`node_modules/${packageName}/package.json`];
|
|
56
|
+
if (!packageJson) return {
|
|
57
|
+
path: specifier,
|
|
58
|
+
external: true
|
|
59
|
+
};
|
|
60
|
+
let pkg;
|
|
61
|
+
try {
|
|
62
|
+
pkg = JSON.parse(packageJson);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`Invalid package.json for ${packageName}`);
|
|
65
|
+
}
|
|
66
|
+
const entrySubpath = subpath ? `./${subpath}` : ".";
|
|
67
|
+
try {
|
|
68
|
+
const resolved = resolveExports.resolve(pkg, entrySubpath, { conditions });
|
|
69
|
+
if (resolved && resolved.length > 0) {
|
|
70
|
+
const resolvedPath = resolved[0];
|
|
71
|
+
if (resolvedPath) {
|
|
72
|
+
const fullPath = `node_modules/${packageName}/${normalizeRelativePath(resolvedPath)}`;
|
|
73
|
+
if (fullPath in files) return {
|
|
74
|
+
path: fullPath,
|
|
75
|
+
external: false
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
const legacyEntry = resolveExports.legacy(pkg, { fields: ["module", "main"] });
|
|
81
|
+
if (legacyEntry && typeof legacyEntry === "string") {
|
|
82
|
+
const fullPath = `node_modules/${packageName}/${normalizeRelativePath(legacyEntry)}`;
|
|
83
|
+
if (fullPath in files) return {
|
|
84
|
+
path: fullPath,
|
|
85
|
+
external: false
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const indexPath = resolveWithExtensions(`node_modules/${packageName}${subpath ? `/${subpath}` : ""}`, files, extensions);
|
|
89
|
+
if (indexPath) return {
|
|
90
|
+
path: indexPath,
|
|
91
|
+
external: false
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
path: specifier,
|
|
95
|
+
external: true
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Try to resolve a path with various extensions and index files
|
|
100
|
+
*/
|
|
101
|
+
function resolveWithExtensions(path, files, extensions) {
|
|
102
|
+
const normalized = normalizePath(path);
|
|
103
|
+
if (normalized in files) return normalized;
|
|
104
|
+
for (const ext of extensions) {
|
|
105
|
+
const withExt = normalized + ext;
|
|
106
|
+
if (withExt in files) return withExt;
|
|
107
|
+
}
|
|
108
|
+
for (const ext of extensions) {
|
|
109
|
+
const indexPath = `${normalized}/index${ext}`;
|
|
110
|
+
if (indexPath in files) return indexPath;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a package specifier into package name and subpath
|
|
115
|
+
*/
|
|
116
|
+
function parsePackageSpecifier(specifier) {
|
|
117
|
+
if (specifier.startsWith("@")) {
|
|
118
|
+
const parts = specifier.split("/");
|
|
119
|
+
if (parts.length >= 2) return {
|
|
120
|
+
packageName: `${parts[0]}/${parts[1]}`,
|
|
121
|
+
subpath: parts.slice(2).join("/") || void 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const slashIndex = specifier.indexOf("/");
|
|
125
|
+
if (slashIndex === -1) return {
|
|
126
|
+
packageName: specifier,
|
|
127
|
+
subpath: void 0
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
packageName: specifier.slice(0, slashIndex),
|
|
131
|
+
subpath: specifier.slice(slashIndex + 1)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get the directory of a file path
|
|
136
|
+
*/
|
|
137
|
+
function getDirectory$1(filePath) {
|
|
138
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
139
|
+
if (lastSlash === -1) return "";
|
|
140
|
+
return filePath.slice(0, lastSlash);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Join two paths
|
|
144
|
+
*/
|
|
145
|
+
function joinPaths(base, relative) {
|
|
146
|
+
if (relative.startsWith("/")) return relative.slice(1);
|
|
147
|
+
const baseParts = base ? base.split("/") : [];
|
|
148
|
+
const relativeParts = relative.split("/");
|
|
149
|
+
for (const part of relativeParts) if (part === "..") baseParts.pop();
|
|
150
|
+
else if (part !== ".") baseParts.push(part);
|
|
151
|
+
return baseParts.join("/");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a path (remove ./ prefix, handle multiple slashes)
|
|
155
|
+
*/
|
|
156
|
+
function normalizePath(path) {
|
|
157
|
+
return path.replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Normalize a relative path from package.json
|
|
161
|
+
*/
|
|
162
|
+
function normalizeRelativePath(path) {
|
|
163
|
+
if (path.startsWith("./")) return path.slice(2);
|
|
164
|
+
if (path.startsWith("/")) return path.slice(1);
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse imports from a JavaScript/TypeScript source file.
|
|
169
|
+
*
|
|
170
|
+
* Uses es-module-lexer for accurate parsing of ES module syntax.
|
|
171
|
+
* Falls back to regex for JSX files since es-module-lexer doesn't
|
|
172
|
+
* handle JSX syntax (e.g., `<div>` is not valid JavaScript).
|
|
173
|
+
*/
|
|
174
|
+
function parseImports(code) {
|
|
175
|
+
try {
|
|
176
|
+
const [imports] = parse(code);
|
|
177
|
+
const specifiers = [];
|
|
178
|
+
for (const imp of imports) if (imp.n !== void 0) specifiers.push(imp.n);
|
|
179
|
+
return [...new Set(specifiers)];
|
|
180
|
+
} catch {
|
|
181
|
+
return parseImportsRegex(code);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Regex-based fallback for parsing imports.
|
|
186
|
+
* Used when es-module-lexer fails (e.g., on JSX/TSX files).
|
|
187
|
+
*/
|
|
188
|
+
function parseImportsRegex(code) {
|
|
189
|
+
const imports = [];
|
|
190
|
+
for (const match of code.matchAll(/import\s+(?:(?:[\w*{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
191
|
+
const specifier = match[1];
|
|
192
|
+
if (specifier) imports.push(specifier);
|
|
193
|
+
}
|
|
194
|
+
for (const match of code.matchAll(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
195
|
+
const specifier = match[1];
|
|
196
|
+
if (specifier) imports.push(specifier);
|
|
197
|
+
}
|
|
198
|
+
for (const match of code.matchAll(/export\s+(?:[\w*{}\s,]+\s+)?from\s+['"]([^'"]+)['"]/g)) {
|
|
199
|
+
const specifier = match[1];
|
|
200
|
+
if (specifier) imports.push(specifier);
|
|
201
|
+
}
|
|
202
|
+
return [...new Set(imports)];
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/bundler.ts
|
|
206
|
+
/**
|
|
207
|
+
* esbuild-wasm bundling functionality.
|
|
208
|
+
*/
|
|
209
|
+
/**
|
|
210
|
+
* Bundle files using esbuild-wasm
|
|
211
|
+
*/
|
|
212
|
+
async function bundleWithEsbuild(files, entryPoint, externals, target, minify, sourcemap, nodejsCompat) {
|
|
213
|
+
await initializeEsbuild();
|
|
214
|
+
const result = await esbuild.build({
|
|
215
|
+
entryPoints: [entryPoint],
|
|
216
|
+
bundle: true,
|
|
217
|
+
write: false,
|
|
218
|
+
format: "esm",
|
|
219
|
+
platform: nodejsCompat ? "node" : "browser",
|
|
220
|
+
target,
|
|
221
|
+
minify,
|
|
222
|
+
sourcemap: sourcemap ? "inline" : false,
|
|
223
|
+
plugins: [{
|
|
224
|
+
name: "virtual-fs",
|
|
225
|
+
setup(build) {
|
|
226
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
227
|
+
if (args.kind === "entry-point") return {
|
|
228
|
+
path: args.path,
|
|
229
|
+
namespace: "virtual"
|
|
230
|
+
};
|
|
231
|
+
if (args.path.startsWith(".")) {
|
|
232
|
+
const resolved = resolveRelativePath(args.resolveDir, args.path, files);
|
|
233
|
+
if (resolved) return {
|
|
234
|
+
path: resolved,
|
|
235
|
+
namespace: "virtual"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (!args.path.startsWith("/") && !args.path.startsWith(".")) {
|
|
239
|
+
if (externals.includes(args.path) || externals.some((e) => args.path.startsWith(`${e}/`) || args.path.startsWith(e))) return {
|
|
240
|
+
path: args.path,
|
|
241
|
+
external: true
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
const result = resolveModule(args.path, { files });
|
|
245
|
+
if (!result.external) return {
|
|
246
|
+
path: result.path,
|
|
247
|
+
namespace: "virtual"
|
|
248
|
+
};
|
|
249
|
+
} catch {}
|
|
250
|
+
return {
|
|
251
|
+
path: args.path,
|
|
252
|
+
external: true
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const normalizedPath = args.path.startsWith("/") ? args.path.slice(1) : args.path;
|
|
256
|
+
if (normalizedPath in files) return {
|
|
257
|
+
path: normalizedPath,
|
|
258
|
+
namespace: "virtual"
|
|
259
|
+
};
|
|
260
|
+
return {
|
|
261
|
+
path: args.path,
|
|
262
|
+
external: true
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
build.onLoad({
|
|
266
|
+
filter: /.*/,
|
|
267
|
+
namespace: "virtual"
|
|
268
|
+
}, (args) => {
|
|
269
|
+
const content = files[args.path];
|
|
270
|
+
if (content === void 0) return { errors: [{ text: `File not found: ${args.path}` }] };
|
|
271
|
+
const loader = getLoader(args.path);
|
|
272
|
+
const lastSlash = args.path.lastIndexOf("/");
|
|
273
|
+
return {
|
|
274
|
+
contents: content,
|
|
275
|
+
loader,
|
|
276
|
+
resolveDir: lastSlash >= 0 ? args.path.slice(0, lastSlash) : ""
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}],
|
|
281
|
+
outfile: "bundle.js"
|
|
282
|
+
});
|
|
283
|
+
const output = result.outputFiles?.[0];
|
|
284
|
+
if (!output) throw new Error("No output generated from esbuild");
|
|
285
|
+
const modules = { "bundle.js": output.text };
|
|
286
|
+
const warnings = result.warnings.map((w) => w.text);
|
|
287
|
+
if (warnings.length > 0) return {
|
|
288
|
+
mainModule: "bundle.js",
|
|
289
|
+
modules,
|
|
290
|
+
warnings
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
mainModule: "bundle.js",
|
|
294
|
+
modules
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resolve a relative path against a directory within the virtual filesystem.
|
|
299
|
+
*/
|
|
300
|
+
function resolveRelativePath(resolveDir, relativePath, files) {
|
|
301
|
+
const dir = resolveDir.replace(/^\//, "");
|
|
302
|
+
const parts = dir ? dir.split("/") : [];
|
|
303
|
+
const relParts = relativePath.split("/");
|
|
304
|
+
for (const part of relParts) if (part === "..") parts.pop();
|
|
305
|
+
else if (part !== ".") parts.push(part);
|
|
306
|
+
const resolved = parts.join("/");
|
|
307
|
+
if (resolved in files) return resolved;
|
|
308
|
+
const extensions = [
|
|
309
|
+
".ts",
|
|
310
|
+
".tsx",
|
|
311
|
+
".js",
|
|
312
|
+
".jsx",
|
|
313
|
+
".mts",
|
|
314
|
+
".mjs"
|
|
315
|
+
];
|
|
316
|
+
for (const ext of extensions) if (resolved + ext in files) return resolved + ext;
|
|
317
|
+
for (const ext of extensions) {
|
|
318
|
+
const indexPath = `${resolved}/index${ext}`;
|
|
319
|
+
if (indexPath in files) return indexPath;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function getLoader(path) {
|
|
323
|
+
if (path.endsWith(".ts") || path.endsWith(".mts")) return "ts";
|
|
324
|
+
if (path.endsWith(".tsx")) return "tsx";
|
|
325
|
+
if (path.endsWith(".jsx")) return "jsx";
|
|
326
|
+
if (path.endsWith(".json")) return "json";
|
|
327
|
+
if (path.endsWith(".css")) return "css";
|
|
328
|
+
return "js";
|
|
329
|
+
}
|
|
330
|
+
let esbuildInitialized = false;
|
|
331
|
+
let esbuildInitializePromise = null;
|
|
332
|
+
/**
|
|
333
|
+
* Initialize the esbuild bundler.
|
|
334
|
+
* This is called automatically when needed.
|
|
335
|
+
*/
|
|
336
|
+
async function initializeEsbuild() {
|
|
337
|
+
if (esbuildInitialized) return;
|
|
338
|
+
if (esbuildInitializePromise) return esbuildInitializePromise;
|
|
339
|
+
esbuildInitializePromise = (async () => {
|
|
340
|
+
try {
|
|
341
|
+
await esbuild.initialize({
|
|
342
|
+
wasmModule: esbuildWasm,
|
|
343
|
+
worker: false
|
|
344
|
+
});
|
|
345
|
+
esbuildInitialized = true;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (error instanceof Error && error.message.includes("Cannot call \"initialize\" more than once")) {
|
|
348
|
+
esbuildInitialized = true;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
try {
|
|
355
|
+
await esbuildInitializePromise;
|
|
356
|
+
} catch (error) {
|
|
357
|
+
esbuildInitializePromise = null;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/config.ts
|
|
363
|
+
/**
|
|
364
|
+
* Wrangler configuration parsing.
|
|
365
|
+
*
|
|
366
|
+
* Parses wrangler.toml, wrangler.json, and wrangler.jsonc files
|
|
367
|
+
* to extract compatibility settings.
|
|
368
|
+
*/
|
|
369
|
+
/**
|
|
370
|
+
* Parse wrangler configuration from files.
|
|
371
|
+
*
|
|
372
|
+
* Looks for wrangler.toml, wrangler.json, or wrangler.jsonc in the files
|
|
373
|
+
* and extracts compatibility_date and compatibility_flags.
|
|
374
|
+
*
|
|
375
|
+
* @param files - Virtual file system
|
|
376
|
+
* @returns Parsed wrangler config, or undefined if no config file found
|
|
377
|
+
*/
|
|
378
|
+
function parseWranglerConfig(files) {
|
|
379
|
+
const tomlContent = files["wrangler.toml"];
|
|
380
|
+
if (tomlContent) return parseWranglerToml(tomlContent);
|
|
381
|
+
const jsonContent = files["wrangler.json"];
|
|
382
|
+
if (jsonContent) return parseWranglerJson(jsonContent);
|
|
383
|
+
const jsoncContent = files["wrangler.jsonc"];
|
|
384
|
+
if (jsoncContent) return parseWranglerJsonc(jsoncContent);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Parse wrangler.toml content
|
|
388
|
+
*/
|
|
389
|
+
function parseWranglerToml(content) {
|
|
390
|
+
try {
|
|
391
|
+
return extractWranglerConfig(parse$1(content));
|
|
392
|
+
} catch {
|
|
393
|
+
return {};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Parse wrangler.json content
|
|
398
|
+
*/
|
|
399
|
+
function parseWranglerJson(content) {
|
|
400
|
+
try {
|
|
401
|
+
return extractWranglerConfig(JSON.parse(content));
|
|
402
|
+
} catch {
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Parse wrangler.jsonc content (JSON with comments)
|
|
408
|
+
*/
|
|
409
|
+
function parseWranglerJsonc(content) {
|
|
410
|
+
try {
|
|
411
|
+
const jsonContent = stripJsonComments(content);
|
|
412
|
+
return extractWranglerConfig(JSON.parse(jsonContent));
|
|
413
|
+
} catch {
|
|
414
|
+
return {};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Extract wrangler config fields from parsed config object.
|
|
419
|
+
* Handles both snake_case (toml) and camelCase (json) formats.
|
|
420
|
+
*/
|
|
421
|
+
function extractWranglerConfig(config) {
|
|
422
|
+
const result = {};
|
|
423
|
+
const main = config["main"];
|
|
424
|
+
if (typeof main === "string") result.main = main;
|
|
425
|
+
const date = config["compatibility_date"] ?? config["compatibilityDate"];
|
|
426
|
+
if (typeof date === "string") result.compatibilityDate = date;
|
|
427
|
+
const flags = config["compatibility_flags"] ?? config["compatibilityFlags"];
|
|
428
|
+
if (Array.isArray(flags) && flags.every((f) => typeof f === "string")) result.compatibilityFlags = flags;
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Strip comments from JSONC content.
|
|
433
|
+
* Handles both single-line (//) and multi-line comments.
|
|
434
|
+
*/
|
|
435
|
+
function stripJsonComments(content) {
|
|
436
|
+
let result = "";
|
|
437
|
+
let i = 0;
|
|
438
|
+
let inString = false;
|
|
439
|
+
let stringChar = "";
|
|
440
|
+
while (i < content.length) {
|
|
441
|
+
const char = content[i];
|
|
442
|
+
const nextChar = content[i + 1];
|
|
443
|
+
if ((char === "\"" || char === "'") && (i === 0 || content[i - 1] !== "\\")) {
|
|
444
|
+
if (!inString) {
|
|
445
|
+
inString = true;
|
|
446
|
+
stringChar = char;
|
|
447
|
+
} else if (char === stringChar) inString = false;
|
|
448
|
+
result += char;
|
|
449
|
+
i++;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (inString) {
|
|
453
|
+
result += char;
|
|
454
|
+
i++;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (char === "/" && nextChar === "/") {
|
|
458
|
+
while (i < content.length && content[i] !== "\n") i++;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (char === "/" && nextChar === "*") {
|
|
462
|
+
i += 2;
|
|
463
|
+
while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
464
|
+
i += 2;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
result += char;
|
|
468
|
+
i++;
|
|
469
|
+
}
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Check if nodejs_compat flag is enabled in the config.
|
|
474
|
+
*/
|
|
475
|
+
function hasNodejsCompat(config) {
|
|
476
|
+
return config?.compatibilityFlags?.includes("nodejs_compat") ?? false;
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/installer.ts
|
|
480
|
+
/**
|
|
481
|
+
* NPM package installer for virtual file systems.
|
|
482
|
+
*
|
|
483
|
+
* This module fetches packages from the npm registry and populates
|
|
484
|
+
* a virtual node_modules directory structure.
|
|
485
|
+
*/
|
|
486
|
+
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
487
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
488
|
+
/**
|
|
489
|
+
* Fetch with a timeout.
|
|
490
|
+
* Throws an error if the request takes longer than the specified timeout.
|
|
491
|
+
*/
|
|
492
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
493
|
+
const controller = new AbortController();
|
|
494
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
495
|
+
try {
|
|
496
|
+
return await fetch(url, {
|
|
497
|
+
...options,
|
|
498
|
+
signal: controller.signal
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timed out after ${timeoutMs}ms: ${url}`);
|
|
502
|
+
throw error;
|
|
503
|
+
} finally {
|
|
504
|
+
clearTimeout(timeoutId);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Install npm dependencies into a virtual file system.
|
|
509
|
+
*
|
|
510
|
+
* Reads the package.json from the files, resolves all dependencies,
|
|
511
|
+
* and populates node_modules with the package contents.
|
|
512
|
+
*
|
|
513
|
+
* @param files - Virtual file system containing package.json
|
|
514
|
+
* @param options - Installation options
|
|
515
|
+
* @returns Files with node_modules populated
|
|
516
|
+
*/
|
|
517
|
+
async function installDependencies(files, options = {}) {
|
|
518
|
+
const { dev = false, registry = NPM_REGISTRY } = options;
|
|
519
|
+
const result = {
|
|
520
|
+
files: { ...files },
|
|
521
|
+
installed: [],
|
|
522
|
+
warnings: []
|
|
523
|
+
};
|
|
524
|
+
const packageJsonContent = files["package.json"];
|
|
525
|
+
if (!packageJsonContent) return result;
|
|
526
|
+
let packageJson;
|
|
527
|
+
try {
|
|
528
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
529
|
+
} catch {
|
|
530
|
+
result.warnings.push("Failed to parse package.json");
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
const depsToInstall = {
|
|
534
|
+
...packageJson.dependencies,
|
|
535
|
+
...dev ? packageJson.devDependencies : {}
|
|
536
|
+
};
|
|
537
|
+
if (Object.keys(depsToInstall).length === 0) return result;
|
|
538
|
+
const installedPackages = /* @__PURE__ */ new Map();
|
|
539
|
+
const inProgress = /* @__PURE__ */ new Map();
|
|
540
|
+
await Promise.all(Object.entries(depsToInstall).map(([name, versionRange]) => installPackage(name, versionRange, result, installedPackages, inProgress, registry)));
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Install a single package and its dependencies recursively.
|
|
545
|
+
*/
|
|
546
|
+
async function installPackage(name, versionRange, result, installedPackages, inProgress, registry) {
|
|
547
|
+
if (installedPackages.has(name)) return;
|
|
548
|
+
const existing = inProgress.get(name);
|
|
549
|
+
if (existing) return existing;
|
|
550
|
+
const installPromise = (async () => {
|
|
551
|
+
try {
|
|
552
|
+
const metadata = await fetchPackageMetadata(name, registry);
|
|
553
|
+
const version = resolveVersion(versionRange, metadata);
|
|
554
|
+
if (!version) {
|
|
555
|
+
result.warnings.push(`Could not resolve version for ${name}@${versionRange}`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const versionMetadata = metadata.versions[version];
|
|
559
|
+
if (!versionMetadata) {
|
|
560
|
+
result.warnings.push(`Version ${version} not found for ${name}`);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
installedPackages.set(name, version);
|
|
564
|
+
result.installed.push(`${name}@${version}`);
|
|
565
|
+
const packageFiles = await fetchPackageFiles(name, versionMetadata);
|
|
566
|
+
for (const [filePath, content] of Object.entries(packageFiles)) result.files[`node_modules/${name}/${filePath}`] = content;
|
|
567
|
+
const deps = versionMetadata.dependencies ?? {};
|
|
568
|
+
await Promise.all(Object.entries(deps).map(([depName, depVersion]) => installPackage(depName, depVersion, result, installedPackages, inProgress, registry)));
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
571
|
+
result.warnings.push(`Failed to install ${name}: ${message}`);
|
|
572
|
+
}
|
|
573
|
+
})();
|
|
574
|
+
inProgress.set(name, installPromise);
|
|
575
|
+
try {
|
|
576
|
+
await installPromise;
|
|
577
|
+
} finally {
|
|
578
|
+
inProgress.delete(name);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Fetch package metadata from npm registry.
|
|
583
|
+
*/
|
|
584
|
+
async function fetchPackageMetadata(name, registry) {
|
|
585
|
+
const response = await fetchWithTimeout(`${registry}/${name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : name}`, { headers: { Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" } });
|
|
586
|
+
if (!response.ok) throw new Error(`Failed to fetch package metadata: ${response.status}`);
|
|
587
|
+
return await response.json();
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Resolve a semver range to a specific version.
|
|
591
|
+
*/
|
|
592
|
+
function resolveVersion(range, metadata) {
|
|
593
|
+
if (range === "latest" || range === "*") return metadata["dist-tags"]["latest"];
|
|
594
|
+
if (metadata.versions[range]) return range;
|
|
595
|
+
if (metadata["dist-tags"][range]) return metadata["dist-tags"][range];
|
|
596
|
+
const versions = Object.keys(metadata.versions);
|
|
597
|
+
return semver.maxSatisfying(versions, range) ?? void 0;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Fetch and extract package files from npm tarball.
|
|
601
|
+
*/
|
|
602
|
+
async function fetchPackageFiles(name, metadata) {
|
|
603
|
+
const tarballUrl = metadata.dist?.tarball;
|
|
604
|
+
if (!tarballUrl) throw new Error(`No tarball URL for ${name}`);
|
|
605
|
+
const response = await fetchWithTimeout(tarballUrl, {}, DEFAULT_TIMEOUT_MS * 2);
|
|
606
|
+
if (!response.ok) throw new Error(`Failed to fetch tarball: ${response.status}`);
|
|
607
|
+
const buffer = await response.arrayBuffer();
|
|
608
|
+
return extractTarball(new Uint8Array(buffer));
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Extract files from a gzipped tarball.
|
|
612
|
+
*
|
|
613
|
+
* npm packages are distributed as .tgz files (gzipped tar).
|
|
614
|
+
* The contents are in a "package/" directory.
|
|
615
|
+
*/
|
|
616
|
+
async function extractTarball(data) {
|
|
617
|
+
return parseTar(await decompress(data));
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Decompress gzip data using DecompressionStream.
|
|
621
|
+
*/
|
|
622
|
+
async function decompress(data) {
|
|
623
|
+
const ds = new DecompressionStream("gzip");
|
|
624
|
+
const writer = ds.writable.getWriter();
|
|
625
|
+
const reader = ds.readable.getReader();
|
|
626
|
+
writer.write(data).catch(() => {});
|
|
627
|
+
writer.close().catch(() => {});
|
|
628
|
+
const chunks = [];
|
|
629
|
+
let totalLength = 0;
|
|
630
|
+
while (true) {
|
|
631
|
+
const { done, value } = await reader.read();
|
|
632
|
+
if (done) break;
|
|
633
|
+
chunks.push(value);
|
|
634
|
+
totalLength += value.length;
|
|
635
|
+
}
|
|
636
|
+
const result = new Uint8Array(totalLength);
|
|
637
|
+
let offset = 0;
|
|
638
|
+
for (const chunk of chunks) {
|
|
639
|
+
result.set(chunk, offset);
|
|
640
|
+
offset += chunk.length;
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Parse a tar archive and extract text files.
|
|
646
|
+
*
|
|
647
|
+
* TAR format:
|
|
648
|
+
* - 512-byte header blocks
|
|
649
|
+
* - File content (padded to 512 bytes)
|
|
650
|
+
* - Two empty blocks at the end
|
|
651
|
+
*/
|
|
652
|
+
function parseTar(data) {
|
|
653
|
+
const files = {};
|
|
654
|
+
const textDecoder = new TextDecoder();
|
|
655
|
+
let offset = 0;
|
|
656
|
+
while (offset < data.length - 512) {
|
|
657
|
+
const header = data.slice(offset, offset + 512);
|
|
658
|
+
if (header.every((b) => b === 0)) break;
|
|
659
|
+
const name = readString(header, 0, 100);
|
|
660
|
+
const sizeStr = readString(header, 124, 12);
|
|
661
|
+
const typeFlag = header[156];
|
|
662
|
+
const size = parseInt(sizeStr.trim(), 8) || 0;
|
|
663
|
+
offset += 512;
|
|
664
|
+
if ((typeFlag === 48 || typeFlag === 0) && size > 0) {
|
|
665
|
+
const content = data.slice(offset, offset + size);
|
|
666
|
+
let filePath = name;
|
|
667
|
+
if (filePath.startsWith("package/")) filePath = filePath.slice(8);
|
|
668
|
+
if (isTextFile(filePath)) try {
|
|
669
|
+
files[filePath] = textDecoder.decode(content);
|
|
670
|
+
} catch {}
|
|
671
|
+
}
|
|
672
|
+
offset += Math.ceil(size / 512) * 512;
|
|
673
|
+
}
|
|
674
|
+
return files;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Read a null-terminated string from a buffer.
|
|
678
|
+
*/
|
|
679
|
+
function readString(buffer, offset, length) {
|
|
680
|
+
const bytes = buffer.slice(offset, offset + length);
|
|
681
|
+
const nullIndex = bytes.indexOf(0);
|
|
682
|
+
const relevantBytes = nullIndex >= 0 ? bytes.slice(0, nullIndex) : bytes;
|
|
683
|
+
return new TextDecoder().decode(relevantBytes);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Check if a file path is likely a text file.
|
|
687
|
+
*/
|
|
688
|
+
function isTextFile(path) {
|
|
689
|
+
const textExtensions = [
|
|
690
|
+
".js",
|
|
691
|
+
".mjs",
|
|
692
|
+
".cjs",
|
|
693
|
+
".ts",
|
|
694
|
+
".mts",
|
|
695
|
+
".cts",
|
|
696
|
+
".tsx",
|
|
697
|
+
".jsx",
|
|
698
|
+
".json",
|
|
699
|
+
".md",
|
|
700
|
+
".txt",
|
|
701
|
+
".css",
|
|
702
|
+
".html",
|
|
703
|
+
".yml",
|
|
704
|
+
".yaml",
|
|
705
|
+
".toml",
|
|
706
|
+
".xml",
|
|
707
|
+
".svg",
|
|
708
|
+
".map",
|
|
709
|
+
".d.ts",
|
|
710
|
+
".d.mts",
|
|
711
|
+
".d.cts"
|
|
712
|
+
];
|
|
713
|
+
const configFiles = [
|
|
714
|
+
"LICENSE",
|
|
715
|
+
"README",
|
|
716
|
+
"CHANGELOG",
|
|
717
|
+
"package.json",
|
|
718
|
+
"tsconfig.json",
|
|
719
|
+
".npmignore",
|
|
720
|
+
".gitignore"
|
|
721
|
+
];
|
|
722
|
+
const fileName = path.split("/").pop() ?? "";
|
|
723
|
+
if (configFiles.some((f) => fileName.toUpperCase().startsWith(f.toUpperCase()))) return true;
|
|
724
|
+
return textExtensions.some((ext) => path.toLowerCase().endsWith(ext));
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Check if files contain a package.json with dependencies that need installing.
|
|
728
|
+
*/
|
|
729
|
+
function hasDependencies(files) {
|
|
730
|
+
const packageJson = files["package.json"];
|
|
731
|
+
if (!packageJson) return false;
|
|
732
|
+
try {
|
|
733
|
+
const deps = JSON.parse(packageJson).dependencies ?? {};
|
|
734
|
+
return Object.keys(deps).length > 0;
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/transformer.ts
|
|
741
|
+
/**
|
|
742
|
+
* Transform TypeScript/JSX code to JavaScript using Sucrase.
|
|
743
|
+
*
|
|
744
|
+
* Sucrase is a super-fast TypeScript transformer that:
|
|
745
|
+
* - Strips type annotations
|
|
746
|
+
* - Transforms JSX
|
|
747
|
+
* - Is ~20x faster than Babel
|
|
748
|
+
* - Works in any JS environment (no WASM needed)
|
|
749
|
+
*
|
|
750
|
+
* @param code - Source code to transform
|
|
751
|
+
* @param options - Transform options
|
|
752
|
+
* @returns Transformed code
|
|
753
|
+
*/
|
|
754
|
+
function transformCode(code, options) {
|
|
755
|
+
const { filePath, sourceMap = false, jsxRuntime = "automatic", jsxImportSource = "react", production = false } = options;
|
|
756
|
+
const transforms = [];
|
|
757
|
+
if (isTypeScriptFile(filePath)) transforms.push("typescript");
|
|
758
|
+
if (isJsxFile(filePath)) {
|
|
759
|
+
if (jsxRuntime !== "preserve") transforms.push("jsx");
|
|
760
|
+
}
|
|
761
|
+
if (transforms.length === 0) return { code };
|
|
762
|
+
const transformOptions = {
|
|
763
|
+
transforms,
|
|
764
|
+
filePath,
|
|
765
|
+
jsxRuntime,
|
|
766
|
+
jsxImportSource,
|
|
767
|
+
production,
|
|
768
|
+
preserveDynamicImport: true,
|
|
769
|
+
disableESTransforms: true
|
|
770
|
+
};
|
|
771
|
+
if (sourceMap) transformOptions.sourceMapOptions = { compiledFilename: filePath.replace(/\.(tsx?|mts)$/, ".js") };
|
|
772
|
+
const result = transform(code, transformOptions);
|
|
773
|
+
if (result.sourceMap) return {
|
|
774
|
+
code: result.code,
|
|
775
|
+
sourceMap: JSON.stringify(result.sourceMap)
|
|
776
|
+
};
|
|
777
|
+
return { code: result.code };
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Check if a file path is a TypeScript file
|
|
781
|
+
*/
|
|
782
|
+
function isTypeScriptFile(filePath) {
|
|
783
|
+
return /\.(ts|tsx|mts)$/.test(filePath);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Check if a file path is a JSX file
|
|
787
|
+
*/
|
|
788
|
+
function isJsxFile(filePath) {
|
|
789
|
+
return /\.(jsx|tsx)$/.test(filePath);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Check if a file path is any JavaScript/TypeScript file
|
|
793
|
+
*/
|
|
794
|
+
function isJavaScriptFile(filePath) {
|
|
795
|
+
return /\.(js|jsx|ts|tsx|mjs|mts)$/.test(filePath);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Get the output path for a transformed file
|
|
799
|
+
*/
|
|
800
|
+
function getOutputPath(filePath) {
|
|
801
|
+
return filePath.replace(/\.tsx?$/, ".js").replace(/\.mts$/, ".mjs");
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Transform all files and resolve their dependencies.
|
|
805
|
+
* This produces multiple modules instead of a single bundle.
|
|
806
|
+
*/
|
|
807
|
+
async function transformAndResolve(files, entryPoint, externals) {
|
|
808
|
+
const modules = {};
|
|
809
|
+
const warnings = [];
|
|
810
|
+
const processed = /* @__PURE__ */ new Set();
|
|
811
|
+
const toProcess = [entryPoint];
|
|
812
|
+
const pathMap = /* @__PURE__ */ new Map();
|
|
813
|
+
while (toProcess.length > 0) {
|
|
814
|
+
const filePath = toProcess.pop();
|
|
815
|
+
if (!filePath || processed.has(filePath)) continue;
|
|
816
|
+
processed.add(filePath);
|
|
817
|
+
const content = files[filePath];
|
|
818
|
+
if (content === void 0) {
|
|
819
|
+
warnings.push(`File not found: ${filePath}`);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const outputPath = isTypeScriptFile(filePath) ? getOutputPath(filePath) : filePath;
|
|
823
|
+
pathMap.set(filePath, outputPath);
|
|
824
|
+
if (!isJavaScriptFile(filePath)) {
|
|
825
|
+
if (filePath.endsWith(".json")) try {
|
|
826
|
+
modules[filePath] = { json: JSON.parse(content) };
|
|
827
|
+
} catch {
|
|
828
|
+
warnings.push(`Failed to parse JSON file: ${filePath}`);
|
|
829
|
+
}
|
|
830
|
+
else modules[filePath] = { text: content };
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const imports = parseImports(content);
|
|
834
|
+
for (const specifier of imports) {
|
|
835
|
+
if (externals.includes(specifier) || externals.some((e) => specifier.startsWith(`${e}/`) || specifier.startsWith(e))) continue;
|
|
836
|
+
try {
|
|
837
|
+
const resolved = resolveModule(specifier, {
|
|
838
|
+
files,
|
|
839
|
+
importer: filePath
|
|
840
|
+
});
|
|
841
|
+
if (!resolved.external && !processed.has(resolved.path)) toProcess.push(resolved.path);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
warnings.push(`Failed to resolve '${specifier}' from ${filePath}: ${error instanceof Error ? error.message : error}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
for (const [sourcePath, outputPath] of pathMap) {
|
|
848
|
+
const content = files[sourcePath];
|
|
849
|
+
if (content === void 0 || !isJavaScriptFile(sourcePath)) continue;
|
|
850
|
+
let transformedCode;
|
|
851
|
+
if (isTypeScriptFile(sourcePath)) try {
|
|
852
|
+
transformedCode = transformCode(content, { filePath: sourcePath }).code;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
warnings.push(`Failed to transform ${sourcePath}: ${error instanceof Error ? error.message : error}`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
else transformedCode = content;
|
|
858
|
+
transformedCode = rewriteImports(transformedCode, sourcePath, files, pathMap, externals);
|
|
859
|
+
modules[outputPath] = transformedCode;
|
|
860
|
+
}
|
|
861
|
+
const mainModule = isTypeScriptFile(entryPoint) ? getOutputPath(entryPoint) : entryPoint;
|
|
862
|
+
if (warnings.length > 0) return {
|
|
863
|
+
mainModule,
|
|
864
|
+
modules,
|
|
865
|
+
warnings
|
|
866
|
+
};
|
|
867
|
+
return {
|
|
868
|
+
mainModule,
|
|
869
|
+
modules
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Rewrite import specifiers to use full output paths.
|
|
874
|
+
* This is necessary because the Worker Loader expects imports to match registered module names.
|
|
875
|
+
*/
|
|
876
|
+
function rewriteImports(code, importer, files, pathMap, externals) {
|
|
877
|
+
const importExportRegex = /(import\s+(?:[\w*{}\s,]+\s+from\s+)?|export\s+(?:[\w*{}\s,]+\s+)?from\s+)(['"])([^'"]+)\2/g;
|
|
878
|
+
const importerOutputPath = pathMap.get(importer) ?? importer;
|
|
879
|
+
return code.replace(importExportRegex, (match, prefix, quote, specifier) => {
|
|
880
|
+
if (externals.includes(specifier) || externals.some((e) => specifier.startsWith(`${e}/`) || specifier.startsWith(e))) return match;
|
|
881
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) try {
|
|
882
|
+
const resolved = resolveModule(specifier, {
|
|
883
|
+
files,
|
|
884
|
+
importer
|
|
885
|
+
});
|
|
886
|
+
if (resolved.external) return match;
|
|
887
|
+
const resolvedOutputPath = pathMap.get(resolved.path) ?? resolved.path;
|
|
888
|
+
if (resolved.path.startsWith("node_modules/")) return `${prefix}${quote}/${resolvedOutputPath}${quote}`;
|
|
889
|
+
return `${prefix}${quote}${calculateRelativePath(importerOutputPath, resolvedOutputPath)}${quote}`;
|
|
890
|
+
} catch {
|
|
891
|
+
return match;
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
const resolved = resolveModule(specifier, {
|
|
895
|
+
files,
|
|
896
|
+
importer
|
|
897
|
+
});
|
|
898
|
+
if (resolved.external) return match;
|
|
899
|
+
return `${prefix}${quote}${calculateRelativePath(importerOutputPath, pathMap.get(resolved.path) ?? resolved.path)}${quote}`;
|
|
900
|
+
} catch {
|
|
901
|
+
return match;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Calculate relative path from one file to another.
|
|
907
|
+
*/
|
|
908
|
+
function calculateRelativePath(from, to) {
|
|
909
|
+
const fromDir = getDirectory(from);
|
|
910
|
+
const toDir = getDirectory(to);
|
|
911
|
+
const toFile = to.split("/").pop() ?? to;
|
|
912
|
+
if (fromDir === toDir) return `./${toFile}`;
|
|
913
|
+
const fromParts = fromDir ? fromDir.split("/") : [];
|
|
914
|
+
const toParts = toDir ? toDir.split("/") : [];
|
|
915
|
+
let commonLength = 0;
|
|
916
|
+
while (commonLength < fromParts.length && commonLength < toParts.length && fromParts[commonLength] === toParts[commonLength]) commonLength++;
|
|
917
|
+
const upCount = fromParts.length - commonLength;
|
|
918
|
+
const downParts = toParts.slice(commonLength);
|
|
919
|
+
let relativePath = "";
|
|
920
|
+
if (upCount === 0) relativePath = "./";
|
|
921
|
+
else relativePath = "../".repeat(upCount);
|
|
922
|
+
if (downParts.length > 0) relativePath += `${downParts.join("/")}/`;
|
|
923
|
+
return relativePath + toFile;
|
|
924
|
+
}
|
|
925
|
+
function getDirectory(filePath) {
|
|
926
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
927
|
+
if (lastSlash === -1) return "";
|
|
928
|
+
return filePath.slice(0, lastSlash);
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/utils.ts
|
|
932
|
+
/**
|
|
933
|
+
* Detect entry point from wrangler config, package.json, or use defaults.
|
|
934
|
+
* Priority: wrangler main > package.json exports/module/main > default paths
|
|
935
|
+
*/
|
|
936
|
+
function detectEntryPoint(files, wranglerConfig) {
|
|
937
|
+
if (wranglerConfig?.main) return normalizeEntryPath(wranglerConfig.main);
|
|
938
|
+
const packageJsonContent = files["package.json"];
|
|
939
|
+
if (packageJsonContent) try {
|
|
940
|
+
const pkg = JSON.parse(packageJsonContent);
|
|
941
|
+
if (pkg.exports) {
|
|
942
|
+
if (typeof pkg.exports === "string") return normalizeEntryPath(pkg.exports);
|
|
943
|
+
const dotExport = pkg.exports["."];
|
|
944
|
+
if (dotExport) {
|
|
945
|
+
if (typeof dotExport === "string") return normalizeEntryPath(dotExport);
|
|
946
|
+
if (typeof dotExport === "object" && dotExport !== null) {
|
|
947
|
+
const exp = dotExport;
|
|
948
|
+
const entry = exp["import"] ?? exp["default"] ?? exp["module"];
|
|
949
|
+
if (typeof entry === "string") return normalizeEntryPath(entry);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (pkg.module) return normalizeEntryPath(pkg.module);
|
|
954
|
+
if (pkg.main) return normalizeEntryPath(pkg.main);
|
|
955
|
+
} catch {}
|
|
956
|
+
for (const entry of [
|
|
957
|
+
"src/index.ts",
|
|
958
|
+
"src/index.js",
|
|
959
|
+
"src/index.mts",
|
|
960
|
+
"src/index.mjs",
|
|
961
|
+
"index.ts",
|
|
962
|
+
"index.js",
|
|
963
|
+
"src/worker.ts",
|
|
964
|
+
"src/worker.js"
|
|
965
|
+
]) if (entry in files) return entry;
|
|
966
|
+
}
|
|
967
|
+
function normalizeEntryPath(path) {
|
|
968
|
+
if (path.startsWith("./")) return path.slice(2);
|
|
969
|
+
return path;
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/experimental.ts
|
|
973
|
+
let warningShown = false;
|
|
974
|
+
function showExperimentalWarning(fn) {
|
|
975
|
+
if (!warningShown) {
|
|
976
|
+
warningShown = true;
|
|
977
|
+
console.warn(`[worker-bundler] ${fn}(): This package is experimental and its API may change without notice.`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region src/mime.ts
|
|
982
|
+
/**
|
|
983
|
+
* MIME type inference from file extensions.
|
|
984
|
+
*/
|
|
985
|
+
const MIME_TYPES = {
|
|
986
|
+
".html": "text/html; charset=utf-8",
|
|
987
|
+
".htm": "text/html; charset=utf-8",
|
|
988
|
+
".js": "application/javascript; charset=utf-8",
|
|
989
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
990
|
+
".css": "text/css; charset=utf-8",
|
|
991
|
+
".json": "application/json; charset=utf-8",
|
|
992
|
+
".png": "image/png",
|
|
993
|
+
".jpg": "image/jpeg",
|
|
994
|
+
".jpeg": "image/jpeg",
|
|
995
|
+
".gif": "image/gif",
|
|
996
|
+
".svg": "image/svg+xml",
|
|
997
|
+
".ico": "image/x-icon",
|
|
998
|
+
".webp": "image/webp",
|
|
999
|
+
".avif": "image/avif",
|
|
1000
|
+
".woff": "font/woff",
|
|
1001
|
+
".woff2": "font/woff2",
|
|
1002
|
+
".ttf": "font/ttf",
|
|
1003
|
+
".otf": "font/otf",
|
|
1004
|
+
".eot": "application/vnd.ms-fontobject",
|
|
1005
|
+
".mp3": "audio/mpeg",
|
|
1006
|
+
".mp4": "video/mp4",
|
|
1007
|
+
".webm": "video/webm",
|
|
1008
|
+
".ogg": "audio/ogg",
|
|
1009
|
+
".wav": "audio/wav",
|
|
1010
|
+
".pdf": "application/pdf",
|
|
1011
|
+
".xml": "application/xml",
|
|
1012
|
+
".txt": "text/plain; charset=utf-8",
|
|
1013
|
+
".csv": "text/csv; charset=utf-8",
|
|
1014
|
+
".zip": "application/zip",
|
|
1015
|
+
".gz": "application/gzip",
|
|
1016
|
+
".tar": "application/x-tar",
|
|
1017
|
+
".wasm": "application/wasm",
|
|
1018
|
+
".webmanifest": "application/manifest+json",
|
|
1019
|
+
".map": "application/json"
|
|
1020
|
+
};
|
|
1021
|
+
/**
|
|
1022
|
+
* Get the file extension from a path (including the dot).
|
|
1023
|
+
*/
|
|
1024
|
+
function getExtension(path) {
|
|
1025
|
+
const lastDot = path.lastIndexOf(".");
|
|
1026
|
+
if (lastDot === -1) return "";
|
|
1027
|
+
if (lastDot < path.lastIndexOf("/")) return "";
|
|
1028
|
+
return path.slice(lastDot).toLowerCase();
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Infer MIME type from a file path.
|
|
1032
|
+
* Returns undefined if the type is unknown.
|
|
1033
|
+
*/
|
|
1034
|
+
function inferContentType(path) {
|
|
1035
|
+
return MIME_TYPES[getExtension(path)];
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Whether a content type represents a text-based format
|
|
1039
|
+
* (used to decide text vs binary module storage).
|
|
1040
|
+
*/
|
|
1041
|
+
function isTextContentType(contentType) {
|
|
1042
|
+
return contentType.startsWith("text/") || contentType.includes("json") || contentType.includes("xml") || contentType.includes("javascript") || contentType.includes("svg");
|
|
1043
|
+
}
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/asset-handler.ts
|
|
1046
|
+
/**
|
|
1047
|
+
* Asset request handler for serving static assets.
|
|
1048
|
+
*
|
|
1049
|
+
* Key design: the manifest (routing metadata) is separated from the
|
|
1050
|
+
* storage (content retrieval). This lets you plug in any backend —
|
|
1051
|
+
* in-memory, KV, R2, Workspace, etc.
|
|
1052
|
+
*
|
|
1053
|
+
* Inspired by Cloudflare's Workers Static Assets behavior and
|
|
1054
|
+
* cloudflare-asset-worker by Timo Wilhelm.
|
|
1055
|
+
*/
|
|
1056
|
+
/**
|
|
1057
|
+
* Create an in-memory storage backend from a pathname->content map.
|
|
1058
|
+
* This is the zero-config default for small asset sets.
|
|
1059
|
+
*/
|
|
1060
|
+
function createMemoryStorage(assets) {
|
|
1061
|
+
const map = new Map(Object.entries(assets));
|
|
1062
|
+
return { get(pathname) {
|
|
1063
|
+
return Promise.resolve(map.get(pathname) ?? null);
|
|
1064
|
+
} };
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Normalize user config with defaults.
|
|
1068
|
+
*/
|
|
1069
|
+
function normalizeConfig(config) {
|
|
1070
|
+
const staticRedirects = {};
|
|
1071
|
+
if (config?.redirects?.static) {
|
|
1072
|
+
let lineNumber = 1;
|
|
1073
|
+
for (const [path, rule] of Object.entries(config.redirects.static)) staticRedirects[path] = {
|
|
1074
|
+
...rule,
|
|
1075
|
+
lineNumber: lineNumber++
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
html_handling: config?.html_handling ?? "auto-trailing-slash",
|
|
1080
|
+
not_found_handling: config?.not_found_handling ?? "none",
|
|
1081
|
+
redirects: {
|
|
1082
|
+
static: staticRedirects,
|
|
1083
|
+
dynamic: config?.redirects?.dynamic ?? {}
|
|
1084
|
+
},
|
|
1085
|
+
headers: config?.headers ?? {}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Compute a simple hash for ETag generation.
|
|
1090
|
+
* Uses a fast string hash (FNV-1a) for text, or SHA-256 for binary.
|
|
1091
|
+
*/
|
|
1092
|
+
async function computeETag(content) {
|
|
1093
|
+
if (typeof content === "string") {
|
|
1094
|
+
let hash = 2166136261;
|
|
1095
|
+
for (let i = 0; i < content.length; i++) {
|
|
1096
|
+
hash ^= content.charCodeAt(i);
|
|
1097
|
+
hash = hash * 16777619 >>> 0;
|
|
1098
|
+
}
|
|
1099
|
+
return hash.toString(16).padStart(8, "0");
|
|
1100
|
+
}
|
|
1101
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
|
|
1102
|
+
return [...new Uint8Array(hashBuffer).slice(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Build an AssetManifest from a pathname->content mapping.
|
|
1106
|
+
* Only computes metadata (content types, ETags) — doesn't store content.
|
|
1107
|
+
*/
|
|
1108
|
+
async function buildAssetManifest(assets) {
|
|
1109
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
1110
|
+
const entries = Object.entries(assets);
|
|
1111
|
+
await Promise.all(entries.map(async ([pathname, content]) => {
|
|
1112
|
+
const contentType = inferContentType(pathname);
|
|
1113
|
+
const etag = await computeETag(content);
|
|
1114
|
+
manifest.set(pathname, {
|
|
1115
|
+
contentType,
|
|
1116
|
+
etag
|
|
1117
|
+
});
|
|
1118
|
+
}));
|
|
1119
|
+
return manifest;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Convenience: build both a manifest and an in-memory storage from assets.
|
|
1123
|
+
*/
|
|
1124
|
+
async function buildAssets(assets) {
|
|
1125
|
+
return {
|
|
1126
|
+
manifest: await buildAssetManifest(assets),
|
|
1127
|
+
storage: createMemoryStorage(assets)
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Check if a pathname exists in the manifest.
|
|
1132
|
+
*/
|
|
1133
|
+
function exists(manifest, pathname) {
|
|
1134
|
+
return manifest.get(pathname);
|
|
1135
|
+
}
|
|
1136
|
+
const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
|
|
1137
|
+
const escapeRegex = (s) => s.replaceAll(ESCAPE_REGEX_CHARACTERS, String.raw`\$&`);
|
|
1138
|
+
const PLACEHOLDER_REGEX = /:([A-Za-z]\w*)/g;
|
|
1139
|
+
function replacer(str, replacements) {
|
|
1140
|
+
for (const [key, value] of Object.entries(replacements)) str = str.replaceAll(`:${key}`, value);
|
|
1141
|
+
return str;
|
|
1142
|
+
}
|
|
1143
|
+
function generateRuleRegExp(rule) {
|
|
1144
|
+
rule = rule.split("*").map((s) => escapeRegex(s)).join("(?<splat>.*)");
|
|
1145
|
+
const matches = rule.matchAll(PLACEHOLDER_REGEX);
|
|
1146
|
+
for (const match of matches) rule = rule.split(match[0]).join(`(?<${match[1]}>[^/]+)`);
|
|
1147
|
+
return new RegExp("^" + rule + "$");
|
|
1148
|
+
}
|
|
1149
|
+
function matchStaticRedirects(config, host, pathname) {
|
|
1150
|
+
const withHost = config.redirects.static[`https://${host}${pathname}`];
|
|
1151
|
+
const withoutHost = config.redirects.static[pathname];
|
|
1152
|
+
if (withHost && withoutHost) return withHost.lineNumber < withoutHost.lineNumber ? withHost : withoutHost;
|
|
1153
|
+
return withHost || withoutHost;
|
|
1154
|
+
}
|
|
1155
|
+
function matchDynamicRedirects(config, request) {
|
|
1156
|
+
const { pathname } = new URL(request.url);
|
|
1157
|
+
for (const [pattern, rule] of Object.entries(config.redirects.dynamic)) try {
|
|
1158
|
+
const result = generateRuleRegExp(pattern).exec(pathname);
|
|
1159
|
+
if (result) {
|
|
1160
|
+
const target = replacer(rule.to, result.groups || {}).trim();
|
|
1161
|
+
return {
|
|
1162
|
+
status: rule.status,
|
|
1163
|
+
to: target
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
} catch {}
|
|
1167
|
+
}
|
|
1168
|
+
function handleRedirects(request, config) {
|
|
1169
|
+
const url = new URL(request.url);
|
|
1170
|
+
const { search, host } = url;
|
|
1171
|
+
let { pathname } = url;
|
|
1172
|
+
const staticMatch = matchStaticRedirects(config, host, pathname);
|
|
1173
|
+
const dynamicMatch = staticMatch ? void 0 : matchDynamicRedirects(config, request);
|
|
1174
|
+
const match = staticMatch ?? dynamicMatch;
|
|
1175
|
+
let proxied = false;
|
|
1176
|
+
if (match) if (match.status === 200) {
|
|
1177
|
+
pathname = new URL(match.to, request.url).pathname;
|
|
1178
|
+
proxied = true;
|
|
1179
|
+
} else {
|
|
1180
|
+
const destination = new URL(match.to, request.url);
|
|
1181
|
+
const location = destination.origin === url.origin ? `${destination.pathname}${destination.search || search}${destination.hash}` : `${destination.href}`;
|
|
1182
|
+
return new Response(null, {
|
|
1183
|
+
status: match.status,
|
|
1184
|
+
headers: { Location: location }
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
proxied,
|
|
1189
|
+
pathname
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function generateGlobRegExp(pattern) {
|
|
1193
|
+
const escaped = pattern.split("*").map((s) => escapeRegex(s)).join(".*");
|
|
1194
|
+
return new RegExp("^" + escaped + "$");
|
|
1195
|
+
}
|
|
1196
|
+
function attachCustomHeaders(request, response, config) {
|
|
1197
|
+
if (Object.keys(config.headers).length === 0) return response;
|
|
1198
|
+
const { pathname } = new URL(request.url);
|
|
1199
|
+
const setMap = /* @__PURE__ */ new Set();
|
|
1200
|
+
for (const [pattern, rules] of Object.entries(config.headers)) {
|
|
1201
|
+
try {
|
|
1202
|
+
if (!generateGlobRegExp(pattern).test(pathname)) continue;
|
|
1203
|
+
} catch {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (rules.unset) for (const key of rules.unset) response.headers.delete(key);
|
|
1207
|
+
if (rules.set) for (const [key, value] of Object.entries(rules.set)) if (setMap.has(key.toLowerCase())) response.headers.append(key, value);
|
|
1208
|
+
else {
|
|
1209
|
+
response.headers.set(key, value);
|
|
1210
|
+
setMap.add(key.toLowerCase());
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return response;
|
|
1214
|
+
}
|
|
1215
|
+
function decodePath(pathname) {
|
|
1216
|
+
return pathname.split("/").map((segment) => {
|
|
1217
|
+
try {
|
|
1218
|
+
return decodeURIComponent(segment);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return segment;
|
|
1221
|
+
}
|
|
1222
|
+
}).join("/").replaceAll(/\/+/g, "/");
|
|
1223
|
+
}
|
|
1224
|
+
function encodePath(pathname) {
|
|
1225
|
+
return pathname.split("/").map((segment) => {
|
|
1226
|
+
try {
|
|
1227
|
+
return encodeURIComponent(segment);
|
|
1228
|
+
} catch {
|
|
1229
|
+
return segment;
|
|
1230
|
+
}
|
|
1231
|
+
}).join("/");
|
|
1232
|
+
}
|
|
1233
|
+
function getIntent(pathname, manifest, config, skipRedirects = false, acceptsHtml = true) {
|
|
1234
|
+
switch (config.html_handling) {
|
|
1235
|
+
case "auto-trailing-slash": return htmlAutoTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml);
|
|
1236
|
+
case "force-trailing-slash": return htmlForceTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml);
|
|
1237
|
+
case "drop-trailing-slash": return htmlDropTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml);
|
|
1238
|
+
case "none": return htmlNone(pathname, manifest, config, acceptsHtml);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function assetIntent(pathname, meta, status = 200) {
|
|
1242
|
+
return {
|
|
1243
|
+
type: "asset",
|
|
1244
|
+
pathname,
|
|
1245
|
+
meta,
|
|
1246
|
+
status
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function redirectIntent(to) {
|
|
1250
|
+
return {
|
|
1251
|
+
type: "redirect",
|
|
1252
|
+
to
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Safe redirect: only redirect if the file exists and the destination
|
|
1257
|
+
* itself resolves to the same asset (avoids redirect loops).
|
|
1258
|
+
*/
|
|
1259
|
+
function safeRedirect(file, destination, manifest, config, skip) {
|
|
1260
|
+
if (skip) return void 0;
|
|
1261
|
+
if (!exists(manifest, destination)) {
|
|
1262
|
+
const intent = getIntent(destination, manifest, config, true);
|
|
1263
|
+
if (intent?.type === "asset" && intent.meta.etag === exists(manifest, file)?.etag) return redirectIntent(destination);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function htmlAutoTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml) {
|
|
1267
|
+
let meta;
|
|
1268
|
+
let redirect;
|
|
1269
|
+
const exactMeta = exists(manifest, pathname);
|
|
1270
|
+
if (pathname.endsWith("/index")) {
|
|
1271
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1272
|
+
if (redirect = safeRedirect(`${pathname}.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
|
|
1273
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -6)}.html`, pathname.slice(0, -6), manifest, config, skipRedirects)) return redirect;
|
|
1274
|
+
} else if (pathname.endsWith("/index.html")) {
|
|
1275
|
+
if (redirect = safeRedirect(pathname, pathname.slice(0, -10), manifest, config, skipRedirects)) return redirect;
|
|
1276
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -11)}.html`, pathname.slice(0, -11), manifest, config, skipRedirects)) return redirect;
|
|
1277
|
+
} else if (pathname.endsWith("/")) {
|
|
1278
|
+
const indexPath = `${pathname}index.html`;
|
|
1279
|
+
if (meta = exists(manifest, indexPath)) return assetIntent(indexPath, meta);
|
|
1280
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -1)}.html`, pathname.slice(0, -1), manifest, config, skipRedirects)) return redirect;
|
|
1281
|
+
} else if (pathname.endsWith(".html")) {
|
|
1282
|
+
if (redirect = safeRedirect(pathname, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
|
|
1283
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -5)}/index.html`, `${pathname.slice(0, -5)}/`, manifest, config, skipRedirects)) return redirect;
|
|
1284
|
+
}
|
|
1285
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1286
|
+
const htmlPath = `${pathname}.html`;
|
|
1287
|
+
if (meta = exists(manifest, htmlPath)) return assetIntent(htmlPath, meta);
|
|
1288
|
+
if (redirect = safeRedirect(`${pathname}/index.html`, `${pathname}/`, manifest, config, skipRedirects)) return redirect;
|
|
1289
|
+
return notFound(pathname, manifest, config, acceptsHtml);
|
|
1290
|
+
}
|
|
1291
|
+
function htmlForceTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml) {
|
|
1292
|
+
let meta;
|
|
1293
|
+
let redirect;
|
|
1294
|
+
const exactMeta = exists(manifest, pathname);
|
|
1295
|
+
if (pathname.endsWith("/index")) {
|
|
1296
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1297
|
+
if (redirect = safeRedirect(`${pathname}.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
|
|
1298
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -6)}.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
|
|
1299
|
+
} else if (pathname.endsWith("/index.html")) {
|
|
1300
|
+
if (redirect = safeRedirect(pathname, pathname.slice(0, -10), manifest, config, skipRedirects)) return redirect;
|
|
1301
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -11)}.html`, pathname.slice(0, -10), manifest, config, skipRedirects)) return redirect;
|
|
1302
|
+
} else if (pathname.endsWith("/")) {
|
|
1303
|
+
let p = `${pathname}index.html`;
|
|
1304
|
+
if (meta = exists(manifest, p)) return assetIntent(p, meta);
|
|
1305
|
+
p = `${pathname.slice(0, -1)}.html`;
|
|
1306
|
+
if (meta = exists(manifest, p)) return assetIntent(p, meta);
|
|
1307
|
+
} else if (pathname.endsWith(".html")) {
|
|
1308
|
+
if (redirect = safeRedirect(pathname, `${pathname.slice(0, -5)}/`, manifest, config, skipRedirects)) return redirect;
|
|
1309
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1310
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -5)}/index.html`, `${pathname.slice(0, -5)}/`, manifest, config, skipRedirects)) return redirect;
|
|
1311
|
+
}
|
|
1312
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1313
|
+
if (redirect = safeRedirect(`${pathname}.html`, `${pathname}/`, manifest, config, skipRedirects)) return redirect;
|
|
1314
|
+
if (redirect = safeRedirect(`${pathname}/index.html`, `${pathname}/`, manifest, config, skipRedirects)) return redirect;
|
|
1315
|
+
return notFound(pathname, manifest, config, acceptsHtml);
|
|
1316
|
+
}
|
|
1317
|
+
function htmlDropTrailingSlash(pathname, manifest, config, skipRedirects, acceptsHtml) {
|
|
1318
|
+
let meta;
|
|
1319
|
+
let redirect;
|
|
1320
|
+
const exactMeta = exists(manifest, pathname);
|
|
1321
|
+
if (pathname.endsWith("/index")) {
|
|
1322
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1323
|
+
if (pathname === "/index") {
|
|
1324
|
+
if (redirect = safeRedirect("/index.html", "/", manifest, config, skipRedirects)) return redirect;
|
|
1325
|
+
} else {
|
|
1326
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -6)}.html`, pathname.slice(0, -6), manifest, config, skipRedirects)) return redirect;
|
|
1327
|
+
if (redirect = safeRedirect(`${pathname}.html`, pathname.slice(0, -6), manifest, config, skipRedirects)) return redirect;
|
|
1328
|
+
}
|
|
1329
|
+
} else if (pathname.endsWith("/index.html")) if (pathname === "/index.html") {
|
|
1330
|
+
if (redirect = safeRedirect("/index.html", "/", manifest, config, skipRedirects)) return redirect;
|
|
1331
|
+
} else {
|
|
1332
|
+
if (redirect = safeRedirect(pathname, pathname.slice(0, -11), manifest, config, skipRedirects)) return redirect;
|
|
1333
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1334
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -11)}.html`, pathname.slice(0, -11), manifest, config, skipRedirects)) return redirect;
|
|
1335
|
+
}
|
|
1336
|
+
else if (pathname.endsWith("/")) if (pathname === "/") {
|
|
1337
|
+
if (meta = exists(manifest, "/index.html")) return assetIntent("/index.html", meta);
|
|
1338
|
+
} else {
|
|
1339
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -1)}.html`, pathname.slice(0, -1), manifest, config, skipRedirects)) return redirect;
|
|
1340
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -1)}/index.html`, pathname.slice(0, -1), manifest, config, skipRedirects)) return redirect;
|
|
1341
|
+
}
|
|
1342
|
+
else if (pathname.endsWith(".html")) {
|
|
1343
|
+
if (redirect = safeRedirect(pathname, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
|
|
1344
|
+
if (redirect = safeRedirect(`${pathname.slice(0, -5)}/index.html`, pathname.slice(0, -5), manifest, config, skipRedirects)) return redirect;
|
|
1345
|
+
}
|
|
1346
|
+
if (exactMeta) return assetIntent(pathname, exactMeta);
|
|
1347
|
+
let p = `${pathname}.html`;
|
|
1348
|
+
if (meta = exists(manifest, p)) return assetIntent(p, meta);
|
|
1349
|
+
p = `${pathname}/index.html`;
|
|
1350
|
+
if (meta = exists(manifest, p)) return assetIntent(p, meta);
|
|
1351
|
+
return notFound(pathname, manifest, config, acceptsHtml);
|
|
1352
|
+
}
|
|
1353
|
+
function htmlNone(pathname, manifest, config, acceptsHtml) {
|
|
1354
|
+
const meta = exists(manifest, pathname);
|
|
1355
|
+
return meta ? assetIntent(pathname, meta) : notFound(pathname, manifest, config, acceptsHtml);
|
|
1356
|
+
}
|
|
1357
|
+
function notFound(pathname, manifest, config, acceptsHtml = true) {
|
|
1358
|
+
switch (config.not_found_handling) {
|
|
1359
|
+
case "single-page-application": {
|
|
1360
|
+
if (!acceptsHtml) return void 0;
|
|
1361
|
+
const meta = exists(manifest, "/index.html");
|
|
1362
|
+
if (meta) return assetIntent("/index.html", meta, 200);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
case "404-page": {
|
|
1366
|
+
let cwd = pathname;
|
|
1367
|
+
while (cwd) {
|
|
1368
|
+
cwd = cwd.slice(0, cwd.lastIndexOf("/"));
|
|
1369
|
+
const p = `${cwd}/404.html`;
|
|
1370
|
+
const meta = exists(manifest, p);
|
|
1371
|
+
if (meta) return assetIntent(p, meta, 404);
|
|
1372
|
+
}
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
default: return;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const CACHE_CONTROL_REVALIDATE = "public, max-age=0, must-revalidate";
|
|
1379
|
+
const CACHE_CONTROL_IMMUTABLE = "public, max-age=31536000, immutable";
|
|
1380
|
+
function getCacheControl(pathname) {
|
|
1381
|
+
if (/\.[a-f0-9]{8,}\.\w+$/.test(pathname)) return CACHE_CONTROL_IMMUTABLE;
|
|
1382
|
+
return CACHE_CONTROL_REVALIDATE;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Handle an asset request. Returns a Response if an asset matches,
|
|
1386
|
+
* or null if the request should fall through to the user's Worker.
|
|
1387
|
+
*
|
|
1388
|
+
* @param request - The incoming HTTP request
|
|
1389
|
+
* @param manifest - Asset manifest (pathname -> metadata)
|
|
1390
|
+
* @param storage - Storage backend for fetching content
|
|
1391
|
+
* @param config - Asset serving configuration
|
|
1392
|
+
*/
|
|
1393
|
+
async function handleAssetRequest(request, manifest, storage, config) {
|
|
1394
|
+
const normalized = normalizeConfig(config);
|
|
1395
|
+
const method = request.method.toUpperCase();
|
|
1396
|
+
if (!["GET", "HEAD"].includes(method)) return null;
|
|
1397
|
+
const redirectResult = handleRedirects(request, normalized);
|
|
1398
|
+
if (redirectResult instanceof Response) return attachCustomHeaders(request, redirectResult, normalized);
|
|
1399
|
+
const { pathname } = redirectResult;
|
|
1400
|
+
const decodedPathname = decodePath(pathname);
|
|
1401
|
+
const intent = getIntent(decodedPathname, manifest, normalized, false, (request.headers.get("Accept") || "").includes("text/html"));
|
|
1402
|
+
if (!intent) return null;
|
|
1403
|
+
if (intent.type === "redirect") {
|
|
1404
|
+
const url = new URL(request.url);
|
|
1405
|
+
const encodedDest = encodePath(intent.to);
|
|
1406
|
+
return attachCustomHeaders(request, new Response(null, {
|
|
1407
|
+
status: 307,
|
|
1408
|
+
headers: { Location: encodedDest + url.search }
|
|
1409
|
+
}), normalized);
|
|
1410
|
+
}
|
|
1411
|
+
const encodedPathname = encodePath(decodedPathname);
|
|
1412
|
+
if (encodedPathname !== pathname) {
|
|
1413
|
+
const url = new URL(request.url);
|
|
1414
|
+
return attachCustomHeaders(request, new Response(null, {
|
|
1415
|
+
status: 307,
|
|
1416
|
+
headers: { Location: encodedPathname + url.search }
|
|
1417
|
+
}), normalized);
|
|
1418
|
+
}
|
|
1419
|
+
const { pathname: assetPath, meta, status } = intent;
|
|
1420
|
+
const strongETag = `"${meta.etag}"`;
|
|
1421
|
+
const weakETag = `W/${strongETag}`;
|
|
1422
|
+
const ifNoneMatch = request.headers.get("If-None-Match") || "";
|
|
1423
|
+
const eTags = new Set(ifNoneMatch.split(",").map((t) => t.trim()));
|
|
1424
|
+
const headers = new Headers();
|
|
1425
|
+
headers.set("ETag", strongETag);
|
|
1426
|
+
if (meta.contentType) headers.set("Content-Type", meta.contentType);
|
|
1427
|
+
headers.set("Cache-Control", getCacheControl(decodedPathname));
|
|
1428
|
+
if (eTags.has(weakETag) || eTags.has(strongETag)) return attachCustomHeaders(request, new Response(null, {
|
|
1429
|
+
status: 304,
|
|
1430
|
+
headers
|
|
1431
|
+
}), normalized);
|
|
1432
|
+
let body = null;
|
|
1433
|
+
if (method !== "HEAD") body = await storage.get(assetPath);
|
|
1434
|
+
return attachCustomHeaders(request, new Response(body, {
|
|
1435
|
+
status,
|
|
1436
|
+
headers
|
|
1437
|
+
}), normalized);
|
|
1438
|
+
}
|
|
1439
|
+
//#endregion
|
|
1440
|
+
//#region src/_asset-runtime-code.ts
|
|
1441
|
+
const ASSET_RUNTIME_CODE = "var L={\".html\":\"text/html; charset=utf-8\",\".htm\":\"text/html; charset=utf-8\",\".js\":\"application/javascript; charset=utf-8\",\".mjs\":\"application/javascript; charset=utf-8\",\".css\":\"text/css; charset=utf-8\",\".json\":\"application/json; charset=utf-8\",\".png\":\"image/png\",\".jpg\":\"image/jpeg\",\".jpeg\":\"image/jpeg\",\".gif\":\"image/gif\",\".svg\":\"image/svg+xml\",\".ico\":\"image/x-icon\",\".webp\":\"image/webp\",\".avif\":\"image/avif\",\".woff\":\"font/woff\",\".woff2\":\"font/woff2\",\".ttf\":\"font/ttf\",\".otf\":\"font/otf\",\".eot\":\"application/vnd.ms-fontobject\",\".mp3\":\"audio/mpeg\",\".mp4\":\"video/mp4\",\".webm\":\"video/webm\",\".ogg\":\"audio/ogg\",\".wav\":\"audio/wav\",\".pdf\":\"application/pdf\",\".xml\":\"application/xml\",\".txt\":\"text/plain; charset=utf-8\",\".csv\":\"text/csv; charset=utf-8\",\".zip\":\"application/zip\",\".gz\":\"application/gzip\",\".tar\":\"application/x-tar\",\".wasm\":\"application/wasm\",\".webmanifest\":\"application/manifest+json\",\".map\":\"application/json\"};function P(t){let e=t.lastIndexOf(\".\");if(e===-1)return\"\";let n=t.lastIndexOf(\"/\");return e<n?\"\":t.slice(e).toLowerCase()}function M(t){let e=P(t);return L[e]}function O(t){let e=new Map(Object.entries(t));return{get(n){return Promise.resolve(e.get(n)??null)}}}function W(t){let e={};if(t?.redirects?.static){let n=1;for(let[s,i]of Object.entries(t.redirects.static))e[s]={...i,lineNumber:n++}}return{html_handling:t?.html_handling??\"auto-trailing-slash\",not_found_handling:t?.not_found_handling??\"none\",redirects:{static:e,dynamic:t?.redirects?.dynamic??{}},headers:t?.headers??{}}}async function U(t){if(typeof t==\"string\"){let s=2166136261;for(let i=0;i<t.length;i++)s^=t.charCodeAt(i),s=s*16777619>>>0;return s.toString(16).padStart(8,\"0\")}let e=await crypto.subtle.digest(\"SHA-256\",t);return[...new Uint8Array(e).slice(0,8)].map(s=>s.toString(16).padStart(2,\"0\")).join(\"\")}async function H(t){let e=new Map,n=Object.entries(t);return await Promise.all(n.map(async([s,i])=>{let l=M(s),r=await U(i);e.set(s,{contentType:l,etag:r})})),e}async function lt(t){let e=await H(t),n=O(t);return{manifest:e,storage:n}}function d(t,e){return t.get(e)}var B=/[-/\\\\^$*+?.()|[\\]{}]/g,j=t=>t.replaceAll(B,String.raw`\\$&`),D=/:([A-Za-z]\\w*)/g;function G(t,e){for(let[n,s]of Object.entries(e))t=t.replaceAll(`:${n}`,s);return t}function F(t){t=t.split(\"*\").map(n=>j(n)).join(\"(?<splat>.*)\");let e=t.matchAll(D);for(let n of e)t=t.split(n[0]).join(`(?<${n[1]}>[^/]+)`);return new RegExp(\"^\"+t+\"$\")}function X(t,e,n){let s=t.redirects.static[`https://${e}${n}`],i=t.redirects.static[n];return s&&i?s.lineNumber<i.lineNumber?s:i:s||i}function V(t,e){let{pathname:n}=new URL(e.url);for(let[s,i]of Object.entries(t.redirects.dynamic))try{let r=F(s).exec(n);if(r){let o=G(i.to,r.groups||{}).trim();return{status:i.status,to:o}}}catch{}}function Y(t,e){let n=new URL(t.url),{search:s,host:i}=n,{pathname:l}=n,r=X(e,i,l),o=r?void 0:V(e,t),c=r??o,h=!1;if(c)if(c.status===200)l=new URL(c.to,t.url).pathname,h=!0;else{let g=new URL(c.to,t.url),x=g.origin===n.origin?`${g.pathname}${g.search||s}${g.hash}`:`${g.href}`;return new Response(null,{status:c.status,headers:{Location:x}})}return{proxied:h,pathname:l}}function Z(t){let e=t.split(\"*\").map(n=>j(n)).join(\".*\");return new RegExp(\"^\"+e+\"$\")}function A(t,e,n){if(Object.keys(n.headers).length===0)return e;let{pathname:s}=new URL(t.url),i=new Set;for(let[l,r]of Object.entries(n.headers)){try{if(!Z(l).test(s))continue}catch{continue}if(r.unset)for(let o of r.unset)e.headers.delete(o);if(r.set)for(let[o,c]of Object.entries(r.set))i.has(o.toLowerCase())?e.headers.append(o,c):(e.headers.set(o,c),i.add(o.toLowerCase()))}return e}function J(t){return t.split(\"/\").map(e=>{try{return decodeURIComponent(e)}catch{return e}}).join(\"/\").replaceAll(/\\/+/g,\"/\")}function E(t){return t.split(\"/\").map(e=>{try{return encodeURIComponent(e)}catch{return e}}).join(\"/\")}function I(t,e,n,s=!1,i=!0){switch(n.html_handling){case\"auto-trailing-slash\":return Q(t,e,n,s,i);case\"force-trailing-slash\":return q(t,e,n,s,i);case\"drop-trailing-slash\":return k(t,e,n,s,i);case\"none\":return tt(t,e,n,i)}}function a(t,e,n=200){return{type:\"asset\",pathname:t,meta:e,status:n}}function K(t){return{type:\"redirect\",to:t}}function u(t,e,n,s,i){if(!i&&!d(n,e)){let l=I(e,n,s,!0);if(l?.type===\"asset\"&&l.meta.etag===d(n,t)?.etag)return K(e)}}function Q(t,e,n,s,i){let l,r,o=d(e,t);if(t.endsWith(\"/index\")){if(o)return a(t,o);if((r=u(`${t}.html`,t.slice(0,-5),e,n,s))||(r=u(`${t.slice(0,-6)}.html`,t.slice(0,-6),e,n,s)))return r}else if(t.endsWith(\"/index.html\")){if((r=u(t,t.slice(0,-10),e,n,s))||(r=u(`${t.slice(0,-11)}.html`,t.slice(0,-11),e,n,s)))return r}else if(t.endsWith(\"/\")){let h=`${t}index.html`;if(l=d(e,h))return a(h,l);if(r=u(`${t.slice(0,-1)}.html`,t.slice(0,-1),e,n,s))return r}else if(t.endsWith(\".html\")&&((r=u(t,t.slice(0,-5),e,n,s))||(r=u(`${t.slice(0,-5)}/index.html`,`${t.slice(0,-5)}/`,e,n,s))))return r;if(o)return a(t,o);let c=`${t}.html`;return(l=d(e,c))?a(c,l):(r=u(`${t}/index.html`,`${t}/`,e,n,s))?r:b(t,e,n,i)}function q(t,e,n,s,i){let l,r,o=d(e,t);if(t.endsWith(\"/index\")){if(o)return a(t,o);if((r=u(`${t}.html`,t.slice(0,-5),e,n,s))||(r=u(`${t.slice(0,-6)}.html`,t.slice(0,-5),e,n,s)))return r}else if(t.endsWith(\"/index.html\")){if((r=u(t,t.slice(0,-10),e,n,s))||(r=u(`${t.slice(0,-11)}.html`,t.slice(0,-10),e,n,s)))return r}else if(t.endsWith(\"/\")){let c=`${t}index.html`;if((l=d(e,c))||(c=`${t.slice(0,-1)}.html`,l=d(e,c)))return a(c,l)}else if(t.endsWith(\".html\")){if(r=u(t,`${t.slice(0,-5)}/`,e,n,s))return r;if(o)return a(t,o);if(r=u(`${t.slice(0,-5)}/index.html`,`${t.slice(0,-5)}/`,e,n,s))return r}return o?a(t,o):(r=u(`${t}.html`,`${t}/`,e,n,s))||(r=u(`${t}/index.html`,`${t}/`,e,n,s))?r:b(t,e,n,i)}function k(t,e,n,s,i){let l,r,o=d(e,t);if(t.endsWith(\"/index\")){if(o)return a(t,o);if(t===\"/index\"){if(r=u(\"/index.html\",\"/\",e,n,s))return r}else if((r=u(`${t.slice(0,-6)}.html`,t.slice(0,-6),e,n,s))||(r=u(`${t}.html`,t.slice(0,-6),e,n,s)))return r}else if(t.endsWith(\"/index.html\"))if(t===\"/index.html\"){if(r=u(\"/index.html\",\"/\",e,n,s))return r}else{if(r=u(t,t.slice(0,-11),e,n,s))return r;if(o)return a(t,o);if(r=u(`${t.slice(0,-11)}.html`,t.slice(0,-11),e,n,s))return r}else if(t.endsWith(\"/\")){if(t===\"/\"){if(l=d(e,\"/index.html\"))return a(\"/index.html\",l)}else if((r=u(`${t.slice(0,-1)}.html`,t.slice(0,-1),e,n,s))||(r=u(`${t.slice(0,-1)}/index.html`,t.slice(0,-1),e,n,s)))return r}else if(t.endsWith(\".html\")&&((r=u(t,t.slice(0,-5),e,n,s))||(r=u(`${t.slice(0,-5)}/index.html`,t.slice(0,-5),e,n,s))))return r;if(o)return a(t,o);let c=`${t}.html`;return(l=d(e,c))||(c=`${t}/index.html`,l=d(e,c))?a(c,l):b(t,e,n,i)}function tt(t,e,n,s){let i=d(e,t);return i?a(t,i):b(t,e,n,s)}function b(t,e,n,s=!0){switch(n.not_found_handling){case\"single-page-application\":{if(!s)return;let i=d(e,\"/index.html\");return i?a(\"/index.html\",i,200):void 0}case\"404-page\":{let i=t;for(;i;){i=i.slice(0,i.lastIndexOf(\"/\"));let l=`${i}/404.html`,r=d(e,l);if(r)return a(l,r,404)}return}default:return}}var et=\"public, max-age=0, must-revalidate\",nt=\"public, max-age=31536000, immutable\";function rt(t){return/\\.[a-f0-9]{8,}\\.\\w+$/.test(t)?nt:et}async function ot(t,e,n,s){let i=W(s),l=t.method.toUpperCase();if(![\"GET\",\"HEAD\"].includes(l))return null;let r=Y(t,i);if(r instanceof Response)return A(t,r,i);let{pathname:o}=r,c=J(o),g=(t.headers.get(\"Accept\")||\"\").includes(\"text/html\"),x=I(c,e,i,!1,g);if(!x)return null;if(x.type===\"redirect\"){let f=new URL(t.url),y=E(x.to),v=new Response(null,{status:307,headers:{Location:y+f.search}});return A(t,v,i)}let $=E(c);if($!==o){let f=new URL(t.url),y=new Response(null,{status:307,headers:{Location:$+f.search}});return A(t,y,i)}let{pathname:N,meta:p,status:S}=x,w=`\"${p.etag}\"`,_=`W/${w}`,z=t.headers.get(\"If-None-Match\")||\"\",C=new Set(z.split(\",\").map(f=>f.trim())),m=new Headers;if(m.set(\"ETag\",w),p.contentType&&m.set(\"Content-Type\",p.contentType),m.set(\"Cache-Control\",rt(c)),C.has(_)||C.has(w)){let f=new Response(null,{status:304,headers:m});return A(t,f,i)}let R=null;l!==\"HEAD\"&&(R=await n.get(N));let T=new Response(R,{status:S,headers:m});return A(t,T,i)}export{H as buildAssetManifest,lt as buildAssets,U as computeETag,O as createMemoryStorage,ot as handleAssetRequest,W as normalizeConfig};\n";
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/app.ts
|
|
1444
|
+
/**
|
|
1445
|
+
* App bundler: builds a full-stack app (server Worker + client bundle + static assets)
|
|
1446
|
+
* for the Worker Loader binding.
|
|
1447
|
+
*/
|
|
1448
|
+
/**
|
|
1449
|
+
* Creates a full-stack app bundle from source files.
|
|
1450
|
+
*
|
|
1451
|
+
* This function:
|
|
1452
|
+
* 1. Bundles client entry point(s) for the browser (if provided)
|
|
1453
|
+
* 2. Collects static assets
|
|
1454
|
+
* 3. Bundles the server Worker
|
|
1455
|
+
* 4. Generates a server wrapper that serves assets and falls through to user code
|
|
1456
|
+
* 5. Returns everything ready for the Worker Loader
|
|
1457
|
+
*/
|
|
1458
|
+
async function createApp(options) {
|
|
1459
|
+
showExperimentalWarning("createApp");
|
|
1460
|
+
let { files, bundle = true, externals = [], target = "es2022", minify = false, sourcemap = false, registry } = options;
|
|
1461
|
+
externals = ["cloudflare:", ...externals];
|
|
1462
|
+
const wranglerConfig = parseWranglerConfig(files);
|
|
1463
|
+
const nodejsCompat = hasNodejsCompat(wranglerConfig);
|
|
1464
|
+
const installWarnings = [];
|
|
1465
|
+
if (hasDependencies(files)) {
|
|
1466
|
+
const installResult = await installDependencies(files, registry ? { registry } : {});
|
|
1467
|
+
files = installResult.files;
|
|
1468
|
+
installWarnings.push(...installResult.warnings);
|
|
1469
|
+
}
|
|
1470
|
+
const clientEntries = options.client ? Array.isArray(options.client) ? options.client : [options.client] : [];
|
|
1471
|
+
const clientOutputs = {};
|
|
1472
|
+
const clientBundles = [];
|
|
1473
|
+
for (const clientEntry of clientEntries) {
|
|
1474
|
+
if (!(clientEntry in files)) throw new Error(`Client entry point "${clientEntry}" not found in files.`);
|
|
1475
|
+
const bundleModule = (await bundleWithEsbuild(files, clientEntry, externals, "es2022", minify, sourcemap, false)).modules["bundle.js"];
|
|
1476
|
+
if (typeof bundleModule === "string") {
|
|
1477
|
+
const outputPath = `/${clientEntry.replace(/^src\//, "").replace(/\.(tsx?|jsx?)$/, ".js")}`;
|
|
1478
|
+
clientOutputs[outputPath] = bundleModule;
|
|
1479
|
+
clientBundles.push(outputPath);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const allAssets = {};
|
|
1483
|
+
if (options.assets) for (const [pathname, content] of Object.entries(options.assets)) {
|
|
1484
|
+
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
1485
|
+
allAssets[normalizedPath] = content;
|
|
1486
|
+
}
|
|
1487
|
+
for (const [pathname, content] of Object.entries(clientOutputs)) allAssets[pathname] = content;
|
|
1488
|
+
const assetManifest = await buildAssetManifest(allAssets);
|
|
1489
|
+
const serverEntry = options.server ?? detectEntryPoint(files, wranglerConfig);
|
|
1490
|
+
if (!serverEntry) throw new Error("Could not determine server entry point. Specify the 'server' option.");
|
|
1491
|
+
if (!(serverEntry in files)) throw new Error(`Server entry point "${serverEntry}" not found in files.`);
|
|
1492
|
+
let serverResult;
|
|
1493
|
+
if (bundle) serverResult = await bundleWithEsbuild(files, serverEntry, externals, target, minify, sourcemap, nodejsCompat);
|
|
1494
|
+
else serverResult = await transformAndResolve(files, serverEntry, externals);
|
|
1495
|
+
const modules = { ...serverResult.modules };
|
|
1496
|
+
for (const [pathname, content] of Object.entries(allAssets)) {
|
|
1497
|
+
const moduleName = `__assets${pathname}`;
|
|
1498
|
+
if (typeof content === "string") modules[moduleName] = { text: content };
|
|
1499
|
+
else modules[moduleName] = { data: content };
|
|
1500
|
+
}
|
|
1501
|
+
const manifestJson = {};
|
|
1502
|
+
for (const [pathname, meta] of assetManifest) manifestJson[pathname] = {
|
|
1503
|
+
contentType: meta.contentType,
|
|
1504
|
+
etag: meta.etag
|
|
1505
|
+
};
|
|
1506
|
+
modules["__asset-manifest.json"] = { json: manifestJson };
|
|
1507
|
+
const assetPathnames = [...assetManifest.keys()];
|
|
1508
|
+
const doOption = options.durableObject;
|
|
1509
|
+
const doClassName = doOption ? typeof doOption === "object" && doOption.className ? doOption.className : "App" : void 0;
|
|
1510
|
+
modules["__app-wrapper.js"] = doClassName ? generateDOAppWrapper(serverResult.mainModule, assetPathnames, doClassName, options.assetConfig) : generateAppWrapper(serverResult.mainModule, assetPathnames, options.assetConfig);
|
|
1511
|
+
modules["__asset-runtime.js"] = ASSET_RUNTIME_CODE;
|
|
1512
|
+
const result = {
|
|
1513
|
+
mainModule: "__app-wrapper.js",
|
|
1514
|
+
modules,
|
|
1515
|
+
assetManifest,
|
|
1516
|
+
assetConfig: options.assetConfig,
|
|
1517
|
+
clientBundles: clientBundles.length > 0 ? clientBundles : void 0,
|
|
1518
|
+
durableObjectClassName: doClassName
|
|
1519
|
+
};
|
|
1520
|
+
if (wranglerConfig !== void 0) result.wranglerConfig = wranglerConfig;
|
|
1521
|
+
if (installWarnings.length > 0) result.warnings = [...serverResult.warnings ?? [], ...installWarnings];
|
|
1522
|
+
else if (serverResult.warnings) result.warnings = serverResult.warnings;
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Generate the asset imports + initialization preamble shared by both wrappers.
|
|
1527
|
+
* Returns the import statements and the initialization code that creates
|
|
1528
|
+
* the manifest Map, memory storage, and ASSET_CONFIG for handleAssetRequest.
|
|
1529
|
+
*/
|
|
1530
|
+
function generateAssetPreamble(assetPathnames, assetConfig) {
|
|
1531
|
+
const configJson = JSON.stringify(assetConfig ?? {});
|
|
1532
|
+
const imports = [];
|
|
1533
|
+
const mapEntries = [];
|
|
1534
|
+
for (let i = 0; i < assetPathnames.length; i++) {
|
|
1535
|
+
const pathname = assetPathnames[i];
|
|
1536
|
+
const moduleName = `__assets${pathname}`;
|
|
1537
|
+
const varName = `__asset_${i}`;
|
|
1538
|
+
imports.push(`import ${varName} from "./${moduleName}";`);
|
|
1539
|
+
mapEntries.push(` ${JSON.stringify(pathname)}: ${varName}`);
|
|
1540
|
+
}
|
|
1541
|
+
return {
|
|
1542
|
+
importsBlock: [
|
|
1543
|
+
"import { handleAssetRequest, createMemoryStorage } from \"./__asset-runtime.js\";",
|
|
1544
|
+
"import manifestJson from \"./__asset-manifest.json\";",
|
|
1545
|
+
...imports
|
|
1546
|
+
].join("\n"),
|
|
1547
|
+
initBlock: `
|
|
1548
|
+
const ASSET_CONFIG = ${configJson};
|
|
1549
|
+
${`const ASSET_CONTENT = {\n${mapEntries.join(",\n")}\n};`}
|
|
1550
|
+
|
|
1551
|
+
// Build manifest Map and storage at module init time
|
|
1552
|
+
const manifest = new Map(Object.entries(manifestJson));
|
|
1553
|
+
const storage = createMemoryStorage(ASSET_CONTENT);
|
|
1554
|
+
`.trimStart()
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Generate the app wrapper module source.
|
|
1559
|
+
* This Worker serves assets first, then falls through to the user's server.
|
|
1560
|
+
*
|
|
1561
|
+
* Uses the pre-built __asset-runtime.js module for full asset handling
|
|
1562
|
+
* (all HTML modes, redirects, custom headers, ETag caching, etc.)
|
|
1563
|
+
*/
|
|
1564
|
+
function generateAppWrapper(userServerModule, assetPathnames, assetConfig) {
|
|
1565
|
+
const { importsBlock, initBlock } = generateAssetPreamble(assetPathnames, assetConfig);
|
|
1566
|
+
return `
|
|
1567
|
+
import userWorker from "./${userServerModule}";
|
|
1568
|
+
${importsBlock}
|
|
1569
|
+
|
|
1570
|
+
${initBlock}
|
|
1571
|
+
export default {
|
|
1572
|
+
async fetch(request, env, ctx) {
|
|
1573
|
+
const assetResponse = await handleAssetRequest(request, manifest, storage, ASSET_CONFIG);
|
|
1574
|
+
if (assetResponse) return assetResponse;
|
|
1575
|
+
|
|
1576
|
+
// Fall through to user's Worker
|
|
1577
|
+
if (typeof userWorker === "object" && userWorker !== null && typeof userWorker.fetch === "function") {
|
|
1578
|
+
return userWorker.fetch(request, env, ctx);
|
|
1579
|
+
}
|
|
1580
|
+
if (typeof userWorker === "function") {
|
|
1581
|
+
return userWorker(request, env, ctx);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
return new Response("Not Found", { status: 404 });
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
`.trim();
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Generate a Durable Object class wrapper module source.
|
|
1591
|
+
* Exports a named class that serves assets first, then delegates to the
|
|
1592
|
+
* user's server code. If the user's default export is a class (DurableObject
|
|
1593
|
+
* subclass), the wrapper extends it so `this.ctx.storage` works naturally.
|
|
1594
|
+
* Otherwise, it wraps the fetch handler in a DurableObject.
|
|
1595
|
+
*
|
|
1596
|
+
* Uses the pre-built __asset-runtime.js module for full asset handling.
|
|
1597
|
+
*/
|
|
1598
|
+
function generateDOAppWrapper(userServerModule, assetPathnames, className, assetConfig) {
|
|
1599
|
+
const { importsBlock, initBlock } = generateAssetPreamble(assetPathnames, assetConfig);
|
|
1600
|
+
return `
|
|
1601
|
+
import { DurableObject } from "cloudflare:workers";
|
|
1602
|
+
import userExport from "./${userServerModule}";
|
|
1603
|
+
${importsBlock}
|
|
1604
|
+
|
|
1605
|
+
${initBlock}
|
|
1606
|
+
// Determine base class: if user exported a DurableObject subclass, extend it
|
|
1607
|
+
// so this.ctx.storage works naturally. Regular functions and plain objects are
|
|
1608
|
+
// wrapped in a minimal DurableObject that delegates fetch().
|
|
1609
|
+
// NOTE: This check uses prototype presence — regular (non-arrow) functions also
|
|
1610
|
+
// have .prototype, but the system prompt instructs class exports for DO mode.
|
|
1611
|
+
const BaseClass = (typeof userExport === "function" && userExport.prototype)
|
|
1612
|
+
? userExport
|
|
1613
|
+
: class extends DurableObject {
|
|
1614
|
+
async fetch(request) {
|
|
1615
|
+
if (typeof userExport === "object" && userExport !== null && typeof userExport.fetch === "function") {
|
|
1616
|
+
return userExport.fetch(request, this.env, this.ctx);
|
|
1617
|
+
}
|
|
1618
|
+
return new Response("Not Found", { status: 404 });
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
export class ${className} extends BaseClass {
|
|
1623
|
+
async fetch(request) {
|
|
1624
|
+
const assetResponse = await handleAssetRequest(request, manifest, storage, ASSET_CONFIG);
|
|
1625
|
+
if (assetResponse) return assetResponse;
|
|
1626
|
+
return super.fetch(request);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
`.trim();
|
|
1630
|
+
}
|
|
1631
|
+
//#endregion
|
|
1632
|
+
//#region src/index.ts
|
|
1633
|
+
/**
|
|
1634
|
+
* Dynamic Worker Bundler
|
|
1635
|
+
*
|
|
1636
|
+
* Creates worker bundles from source files for Cloudflare's Worker Loader binding.
|
|
1637
|
+
*/
|
|
1638
|
+
/**
|
|
1639
|
+
* Creates a worker bundle from source files.
|
|
1640
|
+
*
|
|
1641
|
+
* This function performs:
|
|
1642
|
+
* 1. Entry point detection (from package.json or defaults)
|
|
1643
|
+
* 2. Auto-installation of npm dependencies (if package.json has dependencies)
|
|
1644
|
+
* 3. TypeScript/JSX transformation (via Sucrase)
|
|
1645
|
+
* 4. Module resolution (handling imports/exports)
|
|
1646
|
+
* 5. Optional bundling (combining all modules into one)
|
|
1647
|
+
*
|
|
1648
|
+
* @param options - Configuration options
|
|
1649
|
+
* @returns The main module path and all modules
|
|
1650
|
+
*/
|
|
1651
|
+
async function createWorker(options) {
|
|
1652
|
+
showExperimentalWarning("createWorker");
|
|
1653
|
+
let { files, bundle = true, externals = [], target = "es2022", minify = false, sourcemap = false, registry } = options;
|
|
1654
|
+
externals = ["cloudflare:", ...externals];
|
|
1655
|
+
const wranglerConfig = parseWranglerConfig(files);
|
|
1656
|
+
const nodejsCompat = hasNodejsCompat(wranglerConfig);
|
|
1657
|
+
const installWarnings = [];
|
|
1658
|
+
if (hasDependencies(files)) {
|
|
1659
|
+
const installResult = await installDependencies(files, registry ? { registry } : {});
|
|
1660
|
+
files = installResult.files;
|
|
1661
|
+
installWarnings.push(...installResult.warnings);
|
|
1662
|
+
}
|
|
1663
|
+
const entryPoint = options.entryPoint ?? detectEntryPoint(files, wranglerConfig);
|
|
1664
|
+
if (!entryPoint) throw new Error("Could not determine entry point. Please specify entryPoint option.");
|
|
1665
|
+
if (!(entryPoint in files)) throw new Error(`Entry point "${entryPoint}" not found in files.`);
|
|
1666
|
+
if (bundle) {
|
|
1667
|
+
const result = await bundleWithEsbuild(files, entryPoint, externals, target, minify, sourcemap, nodejsCompat);
|
|
1668
|
+
if (wranglerConfig !== void 0) result.wranglerConfig = wranglerConfig;
|
|
1669
|
+
if (installWarnings.length > 0) result.warnings = [...result.warnings ?? [], ...installWarnings];
|
|
1670
|
+
return result;
|
|
1671
|
+
} else {
|
|
1672
|
+
const result = await transformAndResolve(files, entryPoint, externals);
|
|
1673
|
+
if (wranglerConfig !== void 0) result.wranglerConfig = wranglerConfig;
|
|
1674
|
+
if (installWarnings.length > 0) result.warnings = [...result.warnings ?? [], ...installWarnings];
|
|
1675
|
+
return result;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
//#endregion
|
|
1679
|
+
export { buildAssetManifest, buildAssets, createApp, createMemoryStorage, createWorker, handleAssetRequest, inferContentType, isTextContentType };
|
|
1680
|
+
|
|
1681
|
+
//# sourceMappingURL=index.js.map
|