diff --git a/.changeset/cuddly-rules-rest.md b/.changeset/cuddly-rules-rest.md
new file mode 100644
index 0000000000000..6d102be5778e3
--- /dev/null
+++ b/.changeset/cuddly-rules-rest.md
@@ -0,0 +1,5 @@
+---
+"wrangler": patch
+---
+
+feat: Add support for the undocumented `_worker.js/` directory in Pages
diff --git a/fixtures/pages-workerjs-directory/package.json b/fixtures/pages-workerjs-directory/package.json
new file mode 100644
index 0000000000000..a2fd7e5e418b2
--- /dev/null
+++ b/fixtures/pages-workerjs-directory/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "pages-workerjs-directory",
+ "version": "0.0.0",
+ "private": true,
+ "sideEffects": false,
+ "scripts": {
+ "check:type": "tsc",
+ "dev": "npx wrangler pages dev public --port 8794",
+ "test": "npx vitest",
+ "test:ci": "npx vitest"
+ },
+ "devDependencies": {
+ "undici": "^5.9.1"
+ },
+ "engines": {
+ "node": ">=16.13"
+ }
+}
diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/index.js b/fixtures/pages-workerjs-directory/public/_worker.js/index.js
new file mode 100644
index 0000000000000..3380296683bc9
--- /dev/null
+++ b/fixtures/pages-workerjs-directory/public/_worker.js/index.js
@@ -0,0 +1,10 @@
+export default {
+ async fetch(request, env) {
+ const { pathname } = new URL(request.url);
+ if (pathname !== "/") {
+ return new Response((await import(`./${pathname.slice(1)}`)).default);
+ }
+
+ return env.ASSETS.fetch(request);
+ },
+};
diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/other-script.js b/fixtures/pages-workerjs-directory/public/_worker.js/other-script.js
new file mode 100644
index 0000000000000..58c57157d36c5
--- /dev/null
+++ b/fixtures/pages-workerjs-directory/public/_worker.js/other-script.js
@@ -0,0 +1 @@
+export default "test";
diff --git a/fixtures/pages-workerjs-directory/public/index.html b/fixtures/pages-workerjs-directory/public/index.html
new file mode 100644
index 0000000000000..9f735a6307ae0
--- /dev/null
+++ b/fixtures/pages-workerjs-directory/public/index.html
@@ -0,0 +1 @@
+
Hello, world!
diff --git a/fixtures/pages-workerjs-directory/tests/index.test.ts b/fixtures/pages-workerjs-directory/tests/index.test.ts
new file mode 100644
index 0000000000000..2db76f11382a2
--- /dev/null
+++ b/fixtures/pages-workerjs-directory/tests/index.test.ts
@@ -0,0 +1,21 @@
+import { resolve } from "node:path";
+import { fetch } from "undici";
+import { describe, it } from "vitest";
+import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived";
+
+describe.concurrent("Pages _worker.js/ directory", () => {
+ it("should support non-bundling with 'dev'", async ({ expect }) => {
+ const { ip, port, stop } = await runWranglerPagesDev(
+ resolve(__dirname, ".."),
+ "public",
+ ["--port=0"]
+ );
+ await expect(
+ fetch(`http://${ip}:${port}/`).then((resp) => resp.text())
+ ).resolves.toContain("Hello, world!");
+ await expect(
+ fetch(`http://${ip}:${port}/other-script`).then((resp) => resp.text())
+ ).resolves.toContain("test");
+ await stop();
+ });
+});
diff --git a/fixtures/pages-workerjs-directory/tsconfig.json b/fixtures/pages-workerjs-directory/tsconfig.json
new file mode 100644
index 0000000000000..6eb14e3584b77
--- /dev/null
+++ b/fixtures/pages-workerjs-directory/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "esModuleInterop": true,
+ "module": "CommonJS",
+ "lib": ["ES2020"],
+ "types": ["node"],
+ "moduleResolution": "node",
+ "noEmit": true
+ },
+ "include": ["tests", "../../node-types.d.ts"]
+}
diff --git a/packages/wrangler/src/__tests__/pages/functions-build.test.ts b/packages/wrangler/src/__tests__/pages/functions-build.test.ts
index bf28996512c0a..6076d44c2cfa9 100644
--- a/packages/wrangler/src/__tests__/pages/functions-build.test.ts
+++ b/packages/wrangler/src/__tests__/pages/functions-build.test.ts
@@ -449,4 +449,74 @@ export default {
hello.js:2:36: ERROR: Could not resolve \\"node:async_hooks\\""
`);
});
+
+ it("should compile a _worker.js/ directory", async () => {
+ mkdirSync("public");
+ mkdirSync("public/_worker.js");
+ writeFileSync(
+ "public/_worker.js/index.js",
+ `
+import { cat } from "./cat.js";
+
+export default {
+ async fetch(request, env) {
+ return new Response("Hello from _worker.js/index.js" + cat);
+ },
+};`
+ );
+ writeFileSync(
+ "public/_worker.js/cat.js",
+ `
+export const cat = "cat";`
+ );
+
+ await runWrangler(
+ `pages functions build --outfile=public/_worker.bundle --compatibility-flag=nodejs_compat`
+ );
+
+ expect(existsSync("public/_worker.bundle")).toBe(true);
+ expect(std.out).toMatchInlineSnapshot(`
+ "🚧 'wrangler pages ' is a beta command. Please report any issues to https://github.com/cloudflare/workers-sdk/issues/new/choose
+ ✨ Compiled Worker successfully"
+ `);
+
+ const workerBundleContents = readFileSync("public/_worker.bundle", "utf-8");
+ const workerBundleWithConstantData = replaceRandomWithConstantData(
+ workerBundleContents,
+ [
+ [/------formdata-undici-0.[0-9]*/g, "------formdata-undici-0.test"],
+ [/functionsWorker-0.[0-9]*.js/g, "functionsWorker-0.test.js"],
+ ]
+ );
+
+ expect(workerBundleWithConstantData).toMatchInlineSnapshot(`
+ "------formdata-undici-0.test
+ Content-Disposition: form-data; name=\\"metadata\\"
+
+ {\\"main_module\\":\\"functionsWorker-0.test.js\\"}
+ ------formdata-undici-0.test
+ Content-Disposition: form-data; name=\\"functionsWorker-0.test.js\\"; filename=\\"functionsWorker-0.test.js\\"
+ Content-Type: application/javascript+module
+
+ import { cat } from \\"./cat.js\\";
+ var worker_default = {
+ async fetch(request, env) {
+ return new Response(\\"Hello from _worker.js/index.js\\" + cat);
+ }
+ };
+ export {
+ worker_default as default
+ };
+
+ ------formdata-undici-0.test
+ Content-Disposition: form-data; name=\\"cat.js\\"; filename=\\"cat.js\\"
+ Content-Type: application/javascript+module
+
+
+ export const cat = \\"cat\\";
+ ------formdata-undici-0.test--"
+ `);
+
+ expect(std.err).toMatchInlineSnapshot(`""`);
+ });
});
diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts
index ecd86996ac3d1..91edae6720746 100644
--- a/packages/wrangler/src/api/dev.ts
+++ b/packages/wrangler/src/api/dev.ts
@@ -3,6 +3,7 @@ import { startApiDev, startDev } from "../dev";
import { logger } from "../logger";
import type { Environment } from "../config";
+import type { Rule } from "../config/environment";
import type { EnablePagesAssetsServiceBindingOptions } from "../miniflare-cli/types";
import type { RequestInit, Response, RequestInfo } from "undici";
@@ -42,6 +43,9 @@ export interface UnstableDevOptions {
bucket_name: string;
preview_bucket_name?: string;
}[];
+ bundleEntrypoint?: boolean;
+ moduleRoot?: string;
+ rules?: Rule[];
logLevel?: "none" | "info" | "error" | "log" | "warn" | "debug"; // Specify logging level [choices: "debug", "info", "log", "warn", "error", "none"] [default: "log"]
inspect?: boolean;
local?: boolean;
@@ -150,6 +154,7 @@ export async function unstable_dev(
},
config: options?.config,
env: options?.env,
+ bundleEntrypoint: !!options?.bundleEntrypoint,
bundle: options?.bundle,
compatibilityDate: options?.compatibilityDate,
compatibilityFlags: options?.compatibilityFlags,
diff --git a/packages/wrangler/src/api/pages/publish.tsx b/packages/wrangler/src/api/pages/publish.tsx
index 5d7eb431b3b41..5a9ff0fd9be05 100644
--- a/packages/wrangler/src/api/pages/publish.tsx
+++ b/packages/wrangler/src/api/pages/publish.tsx
@@ -1,4 +1,4 @@
-import { existsSync, readFileSync } from "node:fs";
+import { existsSync, lstatSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve as resolvePath } from "node:path";
import { cwd } from "node:process";
@@ -17,6 +17,7 @@ import {
} from "../../pages/functions/buildWorker";
import { validateRoutes } from "../../pages/functions/routes-validation";
import { upload } from "../../pages/upload";
+import traverseModuleGraph from "../../traverse-module-graph";
import { createUploadWorkerBundleContents } from "./create-worker-bundle-contents";
import type { BundleResult } from "../../bundle";
import type { Project, Deployment } from "@cloudflare/types";
@@ -95,9 +96,10 @@ export async function publish({
_redirects: string | undefined,
_routesGenerated: string | undefined,
_routesCustom: string | undefined,
+ _workerJSDirectory = false,
_workerJS: string | undefined;
- const workerScriptPath = resolvePath(directory, "_worker.js");
+ const _workerPath = resolvePath(directory, "_worker.js");
try {
_headers = readFileSync(join(directory, "_headers"), "utf-8");
@@ -116,7 +118,10 @@ export async function publish({
} catch {}
try {
- _workerJS = readFileSync(workerScriptPath, "utf-8");
+ _workerJSDirectory = lstatSync(_workerPath).isDirectory();
+ if (!_workerJSDirectory) {
+ _workerJS = readFileSync(_workerPath, "utf-8");
+ }
} catch {}
// Grab the bindings from the API, we need these for shims and other such hacky inserts
@@ -243,13 +248,28 @@ export async function publish({
* When using a _worker.js file, the entire /functions directory is ignored
* – this includes its routing and middleware characteristics.
*/
- if (_workerJS) {
+ if (_workerJSDirectory) {
+ workerBundle = await traverseModuleGraph(
+ {
+ file: resolvePath(join(_workerPath, "index.js")),
+ directory: resolvePath(_workerPath),
+ format: "modules",
+ moduleRoot: resolvePath(_workerPath),
+ },
+ [
+ {
+ type: "ESModule",
+ globs: ["**/*.js"],
+ },
+ ]
+ );
+ } else if (_workerJS) {
if (bundle) {
const outfile = join(tmpdir(), `./bundledWorker-${Math.random()}.mjs`);
workerBundle = await buildRawWorker({
- workerScriptPath,
+ workerScriptPath: _workerPath,
outfile,
- directory: directory ?? ".",
+ directory,
local: false,
sourcemap: true,
watch: false,
@@ -258,13 +278,13 @@ export async function publish({
nodejsCompat,
});
} else {
- await checkRawWorker(workerScriptPath, () => {});
- // TODO: Replace this with the cool new no-bundle stuff when that lands: https://github.com/cloudflare/workers-sdk/pull/2769
+ await checkRawWorker(_workerPath, () => {});
+ // TODO: Let users configure this in the future.
workerBundle = {
modules: [],
dependencies: {},
stop: undefined,
- resolvedEntryPointPath: workerScriptPath,
+ resolvedEntryPointPath: _workerPath,
bundleType: "esm",
};
}
diff --git a/packages/wrangler/src/bundle.ts b/packages/wrangler/src/bundle.ts
index 77021fdf2bb21..7719cd41101a1 100644
--- a/packages/wrangler/src/bundle.ts
+++ b/packages/wrangler/src/bundle.ts
@@ -117,6 +117,7 @@ export async function bundleWorker(
entry: Entry,
destination: string,
options: {
+ bundle?: boolean;
serveAssetsFromWorker: boolean;
assets?: StaticAssetsConfig;
betaD1Shims?: string[];
@@ -149,6 +150,7 @@ export async function bundleWorker(
}
): Promise {
const {
+ bundle = true,
serveAssetsFromWorker,
betaD1Shims,
doBindings,
@@ -350,7 +352,7 @@ export async function bundleWorker(
const buildOptions: esbuild.BuildOptions & { metafile: true } = {
entryPoints: [inputEntry.file],
- bundle: true,
+ bundle,
absWorkingDir: entry.directory,
outdir: destination,
entryNames: entryName || path.parse(entry.file).name,
@@ -362,7 +364,7 @@ export async function bundleWorker(
}
: {}),
inject,
- external: ["__STATIC_CONTENT_MANIFEST"],
+ external: bundle ? ["__STATIC_CONTENT_MANIFEST"] : undefined,
format: entry.format === "modules" ? "esm" : "iife",
target: COMMON_ESBUILD_OPTIONS.target,
sourcemap: sourcemap ?? true, // this needs to use ?? to accept false
diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx
index 238809ca3e7b0..5e1e52148100b 100644
--- a/packages/wrangler/src/dev.tsx
+++ b/packages/wrangler/src/dev.tsx
@@ -28,7 +28,7 @@ import {
printWranglerBanner,
} from "./index";
import type { Config, Environment } from "./config";
-import type { Route } from "./config/environment";
+import type { Route, Rule } from "./config/environment";
import type { LoggerLevel } from "./logger";
import type { EnablePagesAssetsServiceBindingOptions } from "./miniflare-cli/types";
import type { CfWorkerInit } from "./worker";
@@ -334,6 +334,9 @@ export type AdditionalDevProps = {
preview_bucket_name?: string;
}[];
d1Databases?: Environment["d1_databases"];
+ bundleEntrypoint?: boolean;
+ moduleRoot?: string;
+ rules?: Rule[];
};
type StartDevOptions = DevArguments &
@@ -424,7 +427,8 @@ export async function startDev(args: StartDevOptions) {
zone={zoneId}
host={host}
routes={routes}
- rules={getRules(configParam)}
+ bundleEntrypoint={!!args.bundleEntrypoint}
+ rules={args.rules ?? getRules(configParam)}
legacyEnv={isLegacyEnv(configParam)}
minify={args.minify ?? configParam.minify}
legacyNodeCompat={legacyNodeCompat}
@@ -560,7 +564,8 @@ export async function startApiDev(args: StartDevOptions) {
zone: zoneId,
host: host,
routes: routes,
- rules: getRules(configParam),
+ bundleEntrypoint: !!args.bundleEntrypoint,
+ rules: args.rules ?? getRules(configParam),
legacyEnv: isLegacyEnv(configParam),
minify: args.minify ?? configParam.minify,
legacyNodeCompat,
@@ -687,7 +692,7 @@ async function validateDevServerSettings(
config: Config
) {
const entry = await getEntry(
- { assets: args.assets, script: args.script },
+ { assets: args.assets, script: args.script, moduleRoot: args.moduleRoot },
config,
"dev"
);
diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx
index 998332e7c15ea..fdb3a2e4c8246 100644
--- a/packages/wrangler/src/dev/dev.tsx
+++ b/packages/wrangler/src/dev/dev.tsx
@@ -116,6 +116,7 @@ export type DevProps = {
initialPort: number;
initialIp: string;
inspectorPort: number;
+ bundleEntrypoint: boolean;
rules: Config["rules"];
accountId: string | undefined;
initialMode: "local" | "remote";
@@ -271,6 +272,7 @@ function DevSession(props: DevSessionProps) {
entry: props.entry,
destination: directory,
jsxFactory: props.jsxFactory,
+ bundleEntrypoint: props.bundleEntrypoint,
rules: props.rules,
jsxFragment: props.jsxFragment,
serveAssetsFromWorker: Boolean(
diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts
index 3a56f1ef0ee07..c0b111763b6e2 100644
--- a/packages/wrangler/src/dev/start-server.ts
+++ b/packages/wrangler/src/dev/start-server.ts
@@ -92,6 +92,7 @@ export async function startDevServer(
entry: props.entry,
destination: directory.name,
jsxFactory: props.jsxFactory,
+ bundleEntrypoint: props.bundleEntrypoint,
rules: props.rules,
jsxFragment: props.jsxFragment,
serveAssetsFromWorker: Boolean(
@@ -205,6 +206,7 @@ async function runEsbuild({
destination,
jsxFactory,
jsxFragment,
+ bundleEntrypoint,
rules,
assets,
betaD1Shims,
@@ -227,6 +229,7 @@ async function runEsbuild({
destination: string | undefined;
jsxFactory: string | undefined;
jsxFragment: string | undefined;
+ bundleEntrypoint: boolean;
rules: Config["rules"];
assets: Config["assets"];
betaD1Shims?: string[];
@@ -247,40 +250,71 @@ async function runEsbuild({
}): Promise {
if (!destination) return;
+ let traverseModuleGraphResult:
+ | Awaited>
+ | undefined;
+ let bundleResult: Awaited> | undefined;
+ if (noBundle) {
+ traverseModuleGraphResult = await traverseModuleGraph(entry, rules);
+ }
+
+ if (bundleEntrypoint || !noBundle) {
+ bundleResult = await bundleWorker(entry, destination, {
+ bundle: !noBundle,
+ disableModuleCollection: noBundle,
+ serveAssetsFromWorker,
+ jsxFactory,
+ jsxFragment,
+ rules,
+ tsconfig,
+ minify,
+ legacyNodeCompat,
+ nodejsCompat,
+ define,
+ checkFetch: true,
+ assets: assets && {
+ ...assets,
+ // disable the cache in dev
+ bypassCache: true,
+ },
+ betaD1Shims,
+ workerDefinitions,
+ services,
+ firstPartyWorkerDevFacade,
+ targetConsumer: "dev", // We are starting a dev server
+ testScheduled,
+ local,
+ experimentalLocal,
+ doBindings,
+ });
+ }
+
const {
- resolvedEntryPointPath,
- bundleType,
modules,
dependencies,
+ resolvedEntryPointPath,
+ bundleType,
sourceMapPath,
- }: Awaited> = noBundle
- ? await traverseModuleGraph(entry, rules)
- : await bundleWorker(entry, destination, {
- serveAssetsFromWorker,
- jsxFactory,
- jsxFragment,
- rules,
- tsconfig,
- minify,
- legacyNodeCompat,
- nodejsCompat,
- define,
- checkFetch: true,
- assets: assets && {
- ...assets,
- // disable the cache in dev
- bypassCache: true,
- },
- betaD1Shims,
- workerDefinitions,
- services,
- firstPartyWorkerDevFacade,
- targetConsumer: "dev", // We are starting a dev server
- testScheduled,
- local,
- experimentalLocal,
- doBindings,
- });
+ }: Awaited> = {
+ modules: (traverseModuleGraphResult?.modules ??
+ bundleResult?.modules) as Awaited<
+ ReturnType
+ >["modules"],
+ dependencies: (bundleResult?.dependencies ??
+ traverseModuleGraphResult?.dependencies) as Awaited<
+ ReturnType
+ >["dependencies"],
+ resolvedEntryPointPath: (bundleResult?.resolvedEntryPointPath ??
+ traverseModuleGraphResult?.resolvedEntryPointPath) as Awaited<
+ ReturnType
+ >["resolvedEntryPointPath"],
+ bundleType: (bundleResult?.bundleType ??
+ traverseModuleGraphResult?.bundleType) as Awaited<
+ ReturnType
+ >["bundleType"],
+ stop: bundleResult?.stop,
+ sourceMapPath: bundleResult?.sourceMapPath,
+ };
return {
id: 0,
diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts
index 9e7121eeb01c7..af2ec05a99a84 100644
--- a/packages/wrangler/src/dev/use-esbuild.ts
+++ b/packages/wrangler/src/dev/use-esbuild.ts
@@ -26,6 +26,7 @@ export function useEsbuild({
destination,
jsxFactory,
jsxFragment,
+ bundleEntrypoint,
rules,
assets,
serveAssetsFromWorker,
@@ -49,6 +50,7 @@ export function useEsbuild({
destination: string | undefined;
jsxFactory: string | undefined;
jsxFragment: string | undefined;
+ bundleEntrypoint: boolean;
rules: Config["rules"];
assets: Config["assets"];
define: Config["define"];
@@ -100,42 +102,73 @@ export function useEsbuild({
async function build() {
if (!destination) return;
+ let traverseModuleGraphResult:
+ | Awaited>
+ | undefined;
+ let bundleResult: Awaited> | undefined;
+ if (noBundle) {
+ traverseModuleGraphResult = await traverseModuleGraph(entry, rules);
+ }
+
+ if (bundleEntrypoint || !noBundle) {
+ bundleResult = await bundleWorker(entry, destination, {
+ bundle: !noBundle,
+ disableModuleCollection: noBundle,
+ serveAssetsFromWorker,
+ jsxFactory,
+ jsxFragment,
+ rules,
+ watch: watchMode,
+ tsconfig,
+ minify,
+ legacyNodeCompat,
+ nodejsCompat,
+ betaD1Shims,
+ doBindings: durableObjects.bindings,
+ define,
+ checkFetch: true,
+ assets: assets && {
+ ...assets,
+ // disable the cache in dev
+ bypassCache: true,
+ },
+ workerDefinitions,
+ services,
+ firstPartyWorkerDevFacade,
+ local,
+ targetConsumer,
+ testScheduled,
+ experimentalLocal,
+ });
+ }
+
const {
- resolvedEntryPointPath,
- bundleType,
modules,
dependencies,
+ resolvedEntryPointPath,
+ bundleType,
stop,
sourceMapPath,
- }: Awaited> = noBundle
- ? await traverseModuleGraph(entry, rules)
- : await bundleWorker(entry, destination, {
- serveAssetsFromWorker,
- jsxFactory,
- jsxFragment,
- rules,
- watch: watchMode,
- tsconfig,
- minify,
- legacyNodeCompat,
- nodejsCompat,
- betaD1Shims,
- doBindings: durableObjects.bindings,
- define,
- checkFetch: true,
- assets: assets && {
- ...assets,
- // disable the cache in dev
- bypassCache: true,
- },
- workerDefinitions,
- services,
- firstPartyWorkerDevFacade,
- local,
- targetConsumer,
- testScheduled,
- experimentalLocal,
- });
+ }: Awaited> = {
+ modules: (traverseModuleGraphResult?.modules ??
+ bundleResult?.modules) as Awaited<
+ ReturnType
+ >["modules"],
+ dependencies: (bundleResult?.dependencies ??
+ traverseModuleGraphResult?.dependencies) as Awaited<
+ ReturnType
+ >["dependencies"],
+ resolvedEntryPointPath: (bundleResult?.resolvedEntryPointPath ??
+ traverseModuleGraphResult?.resolvedEntryPointPath) as Awaited<
+ ReturnType
+ >["resolvedEntryPointPath"],
+ bundleType: (bundleResult?.bundleType ??
+ traverseModuleGraphResult?.bundleType) as Awaited<
+ ReturnType
+ >["bundleType"],
+ stop: bundleResult?.stop,
+ sourceMapPath: bundleResult?.sourceMapPath,
+ };
// Capture the `stop()` method to use as the `useEffect()` destructor.
stopWatching = stop;
@@ -180,6 +213,7 @@ export function useEsbuild({
jsxFactory,
jsxFragment,
serveAssetsFromWorker,
+ bundleEntrypoint,
rules,
tsconfig,
exit,
diff --git a/packages/wrangler/src/entry.ts b/packages/wrangler/src/entry.ts
index 5f7da5e4355ba..8984704d6bff9 100644
--- a/packages/wrangler/src/entry.ts
+++ b/packages/wrangler/src/entry.ts
@@ -35,6 +35,7 @@ export async function getEntry(
script?: string;
format?: CfScriptFormat | undefined;
assets?: string | undefined;
+ moduleRoot?: string;
},
config: Config,
command: "dev" | "publish" | "types"
@@ -113,7 +114,7 @@ export async function getEntry(
file,
directory,
format,
- moduleRoot: config.base_dir ?? path.dirname(file),
+ moduleRoot: args.moduleRoot ?? config.base_dir ?? path.dirname(file),
};
}
diff --git a/packages/wrangler/src/pages/build.ts b/packages/wrangler/src/pages/build.ts
index 268d85d435945..7052c0c91ae39 100644
--- a/packages/wrangler/src/pages/build.ts
+++ b/packages/wrangler/src/pages/build.ts
@@ -1,9 +1,17 @@
-import { existsSync, mkdirSync, writeFileSync } from "node:fs";
-import { basename, dirname, relative, resolve as resolvePath } from "node:path";
+import { existsSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
+import {
+ basename,
+ dirname,
+ join,
+ relative,
+ resolve,
+ resolve as resolvePath,
+} from "node:path";
import { createUploadWorkerBundleContents } from "../api/pages/create-worker-bundle-contents";
import { FatalError } from "../errors";
import { logger } from "../logger";
import * as metrics from "../metrics";
+import traverseModuleGraph from "../traverse-module-graph";
import { buildFunctions } from "./buildFunctions";
import { isInPagesCI } from "./constants";
import {
@@ -208,21 +216,64 @@ export const Handler = async (args: PagesBuildArgs) => {
* and if we were able to resolve _worker.js
*/
if (workerScriptPath) {
- /**
- * `buildRawWorker` builds `_worker.js`, but doesn't give us the bundle
- * we want to return, which includes the external dependencies (like wasm,
- * binary, text). Let's output that build result to memory and only write
- * to disk once we have the final bundle
- */
- bundle = await buildRawWorker({
- workerScriptPath,
- outdir,
- directory: buildOutputDirectory,
- local: false,
- sourcemap,
- watch,
- betaD1Shims: d1Databases,
- });
+ if (lstatSync(workerScriptPath).isDirectory()) {
+ const entrypoint = resolve(join(workerScriptPath, "index.js"));
+
+ const traverseModuleGraphResult = await traverseModuleGraph(
+ {
+ file: entrypoint,
+ directory: resolve(workerScriptPath),
+ format: "modules",
+ moduleRoot: resolve(workerScriptPath),
+ },
+ [
+ {
+ type: "ESModule",
+ globs: ["**/*.js"],
+ },
+ ]
+ );
+
+ const bundleResult = await buildRawWorker({
+ workerScriptPath: entrypoint,
+ bundle: false,
+ outdir,
+ directory: buildOutputDirectory,
+ local: false,
+ sourcemap,
+ watch,
+ betaD1Shims: d1Databases,
+ });
+
+ bundle = {
+ modules: (traverseModuleGraphResult?.modules ??
+ bundleResult?.modules) as BundleResult["modules"],
+ dependencies: (bundleResult?.dependencies ??
+ traverseModuleGraphResult?.dependencies) as BundleResult["dependencies"],
+ resolvedEntryPointPath: (bundleResult?.resolvedEntryPointPath ??
+ traverseModuleGraphResult?.resolvedEntryPointPath) as BundleResult["resolvedEntryPointPath"],
+ bundleType: (bundleResult?.bundleType ??
+ traverseModuleGraphResult?.bundleType) as BundleResult["bundleType"],
+ stop: bundleResult?.stop,
+ sourceMapPath: bundleResult?.sourceMapPath,
+ };
+ } else {
+ /**
+ * `buildRawWorker` builds `_worker.js`, but doesn't give us the bundle
+ * we want to return, which includes the external dependencies (like wasm,
+ * binary, text). Let's output that build result to memory and only write
+ * to disk once we have the final bundle
+ */
+ bundle = await buildRawWorker({
+ workerScriptPath,
+ outdir,
+ directory: buildOutputDirectory,
+ local: false,
+ sourcemap,
+ watch,
+ betaD1Shims: d1Databases,
+ });
+ }
} else {
try {
/**
diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts
index edf1ed96ee3c6..9e3dd658ca851 100644
--- a/packages/wrangler/src/pages/dev.ts
+++ b/packages/wrangler/src/pages/dev.ts
@@ -1,5 +1,5 @@
import { execSync, spawn } from "node:child_process";
-import { existsSync, readFileSync } from "node:fs";
+import { existsSync, lstatSync, readFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { watch } from "chokidar";
@@ -255,7 +255,12 @@ export const Handler = async ({
directory !== undefined
? join(directory, singleWorkerScriptPath)
: singleWorkerScriptPath;
+ const usingWorkerDirectory =
+ existsSync(workerScriptPath) && lstatSync(workerScriptPath).isDirectory();
const usingWorkerScript = existsSync(workerScriptPath);
+ // TODO: Here lies a known bug. If you specify both `--bundle` and `--no-bundle`, this behavior is undefined and you will get unexpected results.
+ // There is no sane way to get the true value out of yargs, so here we are.
+ const enableBundling = bundle ?? !noBundle;
const functionsDirectory = "./functions";
let usingFunctions = !usingWorkerScript && existsSync(functionsDirectory);
@@ -270,9 +275,6 @@ export const Handler = async ({
await checkRawWorker(workerScriptPath, () => scriptReadyResolve());
};
- // TODO: Here lies a known bug. If you specify both `--bundle` and `--no-bundle`, this behavior is undefined and you will get unexpected results.
- // There is no sane way to get the true value out of yargs, so here we are.
- const enableBundling = bundle ?? !noBundle;
if (enableBundling) {
// We want to actually run the `_worker.js` script through the bundler
// So update the final path to the script that will be uploaded and
@@ -281,7 +283,9 @@ export const Handler = async ({
runBuild = async () => {
try {
await buildRawWorker({
- workerScriptPath,
+ workerScriptPath: usingWorkerDirectory
+ ? join(workerScriptPath, "index.js")
+ : workerScriptPath,
outfile: scriptPath,
directory: directory ?? ".",
nodejsCompat,
@@ -396,7 +400,10 @@ export const Handler = async ({
let entrypoint = scriptPath;
// custom _routes.json apply only to Functions or Advanced Mode Pages projects
- if (directory && (usingFunctions || usingWorkerScript)) {
+ if (
+ directory &&
+ (usingFunctions || usingWorkerScript || usingWorkerDirectory)
+ ) {
const routesJSONPath = join(directory, "_routes.json");
if (existsSync(routesJSONPath)) {
@@ -538,6 +545,17 @@ export const Handler = async ({
r2: r2s.map((binding) => {
return { binding: binding.toString(), bucket_name: "" };
}),
+ bundleEntrypoint: true,
+ moduleRoot: workerScriptPath,
+ rules: usingWorkerDirectory
+ ? [
+ {
+ type: "ESModule",
+ globs: ["**/*.js"],
+ },
+ ]
+ : undefined,
+ bundle: enableBundling,
persist,
persistTo,
inspect: undefined,
diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts
index b6f3e59a68933..7bb3de201c748 100644
--- a/packages/wrangler/src/pages/functions/buildWorker.ts
+++ b/packages/wrangler/src/pages/functions/buildWorker.ts
@@ -164,6 +164,7 @@ export type RawOptions = {
outfile?: string;
outdir?: string;
directory: string;
+ bundle?: boolean;
minify?: boolean;
sourcemap?: boolean;
watch?: boolean;
@@ -188,6 +189,7 @@ export function buildRawWorker({
outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`),
outdir,
directory,
+ bundle = true,
minify = false,
sourcemap = false,
watch = false,
@@ -207,6 +209,7 @@ export function buildRawWorker({
},
outdir ? resolve(outdir) : resolve(outfile),
{
+ bundle,
minify,
sourcemap,
watch,