Skip to content

Commit e351afc

Browse files
authored
_worker.js/ directory support in Pages (#2966)
* `_worker.js/` directory support in Pages * Refactor some of the shared no bundle logic and address misc PR comments
1 parent 98e6630 commit e351afc

File tree

22 files changed

+438
-142
lines changed

22 files changed

+438
-142
lines changed

.changeset/cuddly-rules-rest.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
feat: Add support for the undocumented `_worker.js/` directory in Pages

fixtures/local-mode-tests/package.json

+2-19
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,8 @@
99
"main": "index.js",
1010
"scripts": {
1111
"check:type": "tsc && tsc -p tests/tsconfig.json",
12-
"test": "cross-env NODE_ENV=local-testing NODE_OPTIONS=--experimental-vm-modules npx jest --forceExit",
13-
"test:ci": "cross-env NODE_ENV=local-testing NODE_OPTIONS=--experimental-vm-modules npx jest --forceExit"
14-
},
15-
"jest": {
16-
"restoreMocks": true,
17-
"testRegex": ".*.(test|spec)\\.[jt]sx?$",
18-
"testTimeout": 30000,
19-
"transform": {
20-
"^.+\\.c?(t|j)sx?$": [
21-
"esbuild-jest",
22-
{
23-
"sourcemap": true
24-
}
25-
]
26-
},
27-
"transformIgnorePatterns": [
28-
"node_modules/(?!find-up|locate-path|p-locate|p-limit|p-timeout|p-queue|yocto-queue|path-exists|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream|get-port|supports-color|pretty-bytes)",
29-
"wrangler-dist/cli.js"
30-
]
12+
"test": "npx vitest",
13+
"test:ci": "npx vitest"
3114
},
3215
"devDependencies": {
3316
"@cloudflare/workers-types": "^4.20221111.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "pages-workerjs-directory",
3+
"version": "0.0.0",
4+
"private": true,
5+
"sideEffects": false,
6+
"scripts": {
7+
"check:type": "tsc",
8+
"dev": "npx wrangler pages dev public --port 8794",
9+
"test": "npx vitest",
10+
"test:ci": "npx vitest"
11+
},
12+
"devDependencies": {
13+
"undici": "^5.9.1"
14+
},
15+
"engines": {
16+
"node": ">=16.13"
17+
}
18+
}
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import staticMod from "./static.js";
2+
import add from "./add.wasm";
3+
4+
export default {
5+
async fetch(request, env) {
6+
const { pathname } = new URL(request.url);
7+
8+
if (pathname === "/wasm") {
9+
const addModule = await WebAssembly.instantiate(add);
10+
return new Response(addModule.exports.add(1, 2).toString());
11+
}
12+
13+
if (pathname === "/static") {
14+
return new Response(staticMod);
15+
}
16+
17+
if (pathname !== "/") {
18+
return new Response((await import(`./${pathname.slice(1)}`)).default);
19+
}
20+
21+
return env.ASSETS.fetch(request);
22+
},
23+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "test";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default "static";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Hello, world!</h1>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { execSync } from "node:child_process";
2+
import { readFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import path, { join, resolve } from "node:path";
5+
import { fetch } from "undici";
6+
import { describe, it } from "vitest";
7+
import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived";
8+
9+
describe.concurrent("Pages _worker.js/ directory", () => {
10+
it("should support non-bundling with 'dev'", async ({ expect }) => {
11+
const { ip, port, stop } = await runWranglerPagesDev(
12+
resolve(__dirname, ".."),
13+
"public",
14+
["--port=0"]
15+
);
16+
await expect(
17+
fetch(`http://${ip}:${port}/`).then((resp) => resp.text())
18+
).resolves.toContain("Hello, world!");
19+
await expect(
20+
fetch(`http://${ip}:${port}/wasm`).then((resp) => resp.text())
21+
).resolves.toContain("3");
22+
await expect(
23+
fetch(`http://${ip}:${port}/static`).then((resp) => resp.text())
24+
).resolves.toContain("static");
25+
await expect(
26+
fetch(`http://${ip}:${port}/other-script`).then((resp) => resp.text())
27+
).resolves.toContain("test");
28+
await stop();
29+
});
30+
31+
it("should bundle", async ({ expect }) => {
32+
const dir = tmpdir();
33+
const file = join(dir, "./_worker.bundle");
34+
35+
execSync(
36+
`npx wrangler pages functions build --build-output-directory public --outfile ${file} --bindings="{\\"d1_databases\\":{\\"FOO\\":{}}}"`,
37+
{
38+
cwd: path.resolve(__dirname, ".."),
39+
}
40+
);
41+
42+
expect(readFileSync(file, "utf-8")).toContain("D1_ERROR");
43+
expect(readFileSync(file, "utf-8")).toContain('"static"');
44+
});
45+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"esModuleInterop": true,
5+
"module": "CommonJS",
6+
"lib": ["ES2020"],
7+
"types": ["node"],
8+
"moduleResolution": "node",
9+
"noEmit": true
10+
},
11+
"include": ["tests", "../../node-types.d.ts"]
12+
}

packages/wrangler/src/__tests__/pages/functions-build.test.ts

+70
Original file line numberDiff line numberDiff line change
@@ -449,4 +449,74 @@ export default {
449449
hello.js:2:36: ERROR: Could not resolve \\"node:async_hooks\\""
450450
`);
451451
});
452+
453+
it("should compile a _worker.js/ directory", async () => {
454+
mkdirSync("public");
455+
mkdirSync("public/_worker.js");
456+
writeFileSync(
457+
"public/_worker.js/index.js",
458+
`
459+
import { cat } from "./cat.js";
460+
461+
export default {
462+
async fetch(request, env) {
463+
return new Response("Hello from _worker.js/index.js" + cat);
464+
},
465+
};`
466+
);
467+
writeFileSync(
468+
"public/_worker.js/cat.js",
469+
`
470+
export const cat = "cat";`
471+
);
472+
473+
await runWrangler(`pages functions build --outfile=public/_worker.bundle`);
474+
475+
expect(existsSync("public/_worker.bundle")).toBe(true);
476+
expect(std.out).toMatchInlineSnapshot(`
477+
"🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/workers-sdk/issues/new/choose
478+
✨ Compiled Worker successfully"
479+
`);
480+
481+
const workerBundleContents = readFileSync("public/_worker.bundle", "utf-8");
482+
const workerBundleWithConstantData = replaceRandomWithConstantData(
483+
workerBundleContents,
484+
[
485+
[/------formdata-undici-0.[0-9]*/g, "------formdata-undici-0.test"],
486+
[/bundledWorker-0.[0-9]*.mjs/g, "bundledWorker-0.test.mjs"],
487+
[/bundledWorker-0.[0-9]*.map/g, "bundledWorker-0.test.map"],
488+
]
489+
);
490+
491+
expect(workerBundleWithConstantData).toMatchInlineSnapshot(`
492+
"------formdata-undici-0.test
493+
Content-Disposition: form-data; name=\\"metadata\\"
494+
495+
{\\"main_module\\":\\"bundledWorker-0.test.mjs\\"}
496+
------formdata-undici-0.test
497+
Content-Disposition: form-data; name=\\"bundledWorker-0.test.mjs\\"; filename=\\"bundledWorker-0.test.mjs\\"
498+
Content-Type: application/javascript+module
499+
500+
import { cat } from \\"./cat.js\\";
501+
var worker_default = {
502+
async fetch(request, env) {
503+
return new Response(\\"Hello from _worker.js/index.js\\" + cat);
504+
}
505+
};
506+
export {
507+
worker_default as default
508+
};
509+
//# sourceMappingURL=bundledWorker-0.test.mjs.map
510+
511+
------formdata-undici-0.test
512+
Content-Disposition: form-data; name=\\"cat.js\\"; filename=\\"cat.js\\"
513+
Content-Type: application/javascript+module
514+
515+
516+
export const cat = \\"cat\\";
517+
------formdata-undici-0.test--"
518+
`);
519+
520+
expect(std.err).toMatchInlineSnapshot(`""`);
521+
});
452522
});

packages/wrangler/src/api/dev.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { startApiDev, startDev } from "../dev";
33
import { logger } from "../logger";
44

55
import type { Environment } from "../config";
6+
import type { Rule } from "../config/environment";
67
import type { EnablePagesAssetsServiceBindingOptions } from "../miniflare-cli/types";
78
import type { RequestInit, Response, RequestInfo } from "undici";
89

@@ -42,6 +43,9 @@ export interface UnstableDevOptions {
4243
bucket_name: string;
4344
preview_bucket_name?: string;
4445
}[];
46+
processEntrypoint?: boolean;
47+
moduleRoot?: string;
48+
rules?: Rule[];
4549
logLevel?: "none" | "info" | "error" | "log" | "warn" | "debug"; // Specify logging level [choices: "debug", "info", "log", "warn", "error", "none"] [default: "log"]
4650
inspect?: boolean;
4751
local?: boolean;
@@ -150,6 +154,7 @@ export async function unstable_dev(
150154
},
151155
config: options?.config,
152156
env: options?.env,
157+
processEntrypoint: !!options?.processEntrypoint,
153158
bundle: options?.bundle,
154159
compatibilityDate: options?.compatibilityDate,
155160
compatibilityFlags: options?.compatibilityFlags,

packages/wrangler/src/api/pages/publish.tsx

+28-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readFileSync } from "node:fs";
1+
import { existsSync, lstatSync, readFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join, resolve as resolvePath } from "node:path";
44
import { cwd } from "node:process";
@@ -14,6 +14,7 @@ import {
1414
import {
1515
buildRawWorker,
1616
checkRawWorker,
17+
traverseAndBuildWorkerJSDirectory,
1718
} from "../../pages/functions/buildWorker";
1819
import { validateRoutes } from "../../pages/functions/routes-validation";
1920
import { upload } from "../../pages/upload";
@@ -65,7 +66,7 @@ interface PagesPublishOptions {
6566

6667
/**
6768
* Whether to run bundling on `_worker.js` before deploying.
68-
* Default: false
69+
* Default: true
6970
*/
7071
bundle?: boolean;
7172

@@ -95,9 +96,12 @@ export async function publish({
9596
_redirects: string | undefined,
9697
_routesGenerated: string | undefined,
9798
_routesCustom: string | undefined,
99+
_workerJSIsDirectory = false,
98100
_workerJS: string | undefined;
99101

100-
const workerScriptPath = resolvePath(directory, "_worker.js");
102+
bundle = bundle ?? true;
103+
104+
const _workerPath = resolvePath(directory, "_worker.js");
101105

102106
try {
103107
_headers = readFileSync(join(directory, "_headers"), "utf-8");
@@ -116,7 +120,10 @@ export async function publish({
116120
} catch {}
117121

118122
try {
119-
_workerJS = readFileSync(workerScriptPath, "utf-8");
123+
_workerJSIsDirectory = lstatSync(_workerPath).isDirectory();
124+
if (!_workerJSIsDirectory) {
125+
_workerJS = readFileSync(_workerPath, "utf-8");
126+
}
120127
} catch {}
121128

122129
// Grab the bindings from the API, we need these for shims and other such hacky inserts
@@ -240,16 +247,23 @@ export async function publish({
240247
* Advanced Mode
241248
* https://developers.cloudflare.com/pages/platform/functions/#advanced-mode
242249
*
243-
* When using a _worker.js file, the entire /functions directory is ignored
250+
* When using a _worker.js file or _worker.js/ directory, the entire /functions directory is ignored
244251
* – this includes its routing and middleware characteristics.
245252
*/
246-
if (_workerJS) {
253+
if (_workerJSIsDirectory) {
254+
workerBundle = await traverseAndBuildWorkerJSDirectory({
255+
workerJSDirectory: _workerPath,
256+
buildOutputDirectory: directory,
257+
d1Databases,
258+
nodejsCompat,
259+
});
260+
} else if (_workerJS) {
247261
if (bundle) {
248262
const outfile = join(tmpdir(), `./bundledWorker-${Math.random()}.mjs`);
249263
workerBundle = await buildRawWorker({
250-
workerScriptPath,
264+
workerScriptPath: _workerPath,
251265
outfile,
252-
directory: directory ?? ".",
266+
directory,
253267
local: false,
254268
sourcemap: true,
255269
watch: false,
@@ -258,17 +272,19 @@ export async function publish({
258272
nodejsCompat,
259273
});
260274
} else {
261-
await checkRawWorker(workerScriptPath, () => {});
262-
// TODO: Replace this with the cool new no-bundle stuff when that lands: https://github.com/cloudflare/workers-sdk/pull/2769
275+
await checkRawWorker(_workerPath, () => {});
276+
// TODO: Let users configure this in the future.
263277
workerBundle = {
264278
modules: [],
265279
dependencies: {},
266280
stop: undefined,
267-
resolvedEntryPointPath: workerScriptPath,
281+
resolvedEntryPointPath: _workerPath,
268282
bundleType: "esm",
269283
};
270284
}
285+
}
271286

287+
if (_workerJS || _workerJSIsDirectory) {
272288
const workerBundleContents = await createUploadWorkerBundleContents(
273289
workerBundle as BundleResult
274290
);
@@ -302,7 +318,7 @@ export async function publish({
302318
* Pages Functions
303319
* https://developers.cloudflare.com/pages/platform/functions/
304320
*/
305-
if (builtFunctions && !_workerJS) {
321+
if (builtFunctions && !_workerJS && !_workerJSIsDirectory) {
306322
const workerBundleContents = await createUploadWorkerBundleContents(
307323
workerBundle as BundleResult
308324
);

packages/wrangler/src/bundle.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ export async function bundleWorker(
117117
entry: Entry,
118118
destination: string,
119119
options: {
120+
// When `bundle` is set to false, we apply shims to the Worker, but won't pull in any imports
121+
bundle?: boolean;
120122
serveAssetsFromWorker: boolean;
121123
assets?: StaticAssetsConfig;
122124
betaD1Shims?: string[];
@@ -149,6 +151,7 @@ export async function bundleWorker(
149151
}
150152
): Promise<BundleResult> {
151153
const {
154+
bundle = true,
152155
serveAssetsFromWorker,
153156
betaD1Shims,
154157
doBindings,
@@ -350,7 +353,7 @@ export async function bundleWorker(
350353

351354
const buildOptions: esbuild.BuildOptions & { metafile: true } = {
352355
entryPoints: [inputEntry.file],
353-
bundle: true,
356+
bundle,
354357
absWorkingDir: entry.directory,
355358
outdir: destination,
356359
entryNames: entryName || path.parse(entry.file).name,
@@ -362,7 +365,7 @@ export async function bundleWorker(
362365
}
363366
: {}),
364367
inject,
365-
external: ["__STATIC_CONTENT_MANIFEST"],
368+
external: bundle ? ["__STATIC_CONTENT_MANIFEST"] : undefined,
366369
format: entry.format === "modules" ? "esm" : "iife",
367370
target: COMMON_ESBUILD_OPTIONS.target,
368371
sourcemap: sourcemap ?? true, // this needs to use ?? to accept false

0 commit comments

Comments
 (0)