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,