diff --git a/.changeset/unlucky-kangaroos-brake.md b/.changeset/unlucky-kangaroos-brake.md new file mode 100644 index 000000000000..3338a13b98c7 --- /dev/null +++ b/.changeset/unlucky-kangaroos-brake.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +feat: Allow marking external modules (with `--external`) to avoid bundling them when building Pages Functions + +It's useful for Pages Plugins which want to declare a peer dependency. diff --git a/fixtures/pages-functions-app/package.json b/fixtures/pages-functions-app/package.json index cd8893501fcf..928de33f0344 100644 --- a/fixtures/pages-functions-app/package.json +++ b/fixtures/pages-functions-app/package.json @@ -19,5 +19,8 @@ }, "engines": { "node": ">=16.13" + }, + "dependencies": { + "is-odd": "^3.0.1" } } diff --git a/fixtures/pages-functions-app/tests/index.test.ts b/fixtures/pages-functions-app/tests/index.test.ts index 2676f60b4f82..51976b4ca395 100644 --- a/fixtures/pages-functions-app/tests/index.test.ts +++ b/fixtures/pages-functions-app/tests/index.test.ts @@ -124,6 +124,13 @@ describe("Pages Functions", () => { expect(response.status).toBe(502); }); + it("should work with peer externals", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/mounted-plugin/ext`); + const text = await response.text(); + expect(text).toMatchInlineSnapshot(`"42 is even"`); + expect(response.status).toBe(200); + }); + it("should mount a Plugin even if in a parameterized route", async ({ expect, }) => { diff --git a/fixtures/pages-plugin-example/functions/ext.ts b/fixtures/pages-plugin-example/functions/ext.ts new file mode 100644 index 000000000000..550f2adf5f28 --- /dev/null +++ b/fixtures/pages-plugin-example/functions/ext.ts @@ -0,0 +1,5 @@ +import isOdd from "is-odd"; + +export const onRequest: PagesFunction = () => { + return new Response(`42 is ${isOdd(42) ? "odd" : "even"}`); +}; diff --git a/fixtures/pages-plugin-example/package.json b/fixtures/pages-plugin-example/package.json index 22d3692ade46..6c85f0e7b75f 100644 --- a/fixtures/pages-plugin-example/package.json +++ b/fixtures/pages-plugin-example/package.json @@ -10,10 +10,11 @@ "public/" ], "scripts": { - "build": "wrangler pages functions build --plugin --outdir=dist", + "build": "wrangler pages functions build --plugin --outdir=dist --external=is-odd", "check:type": "tsc" }, "devDependencies": { + "is-odd": "^3.0.1", "wrangler": "workspace:*" } } diff --git a/fixtures/pages-plugin-mounted-on-root-app/package.json b/fixtures/pages-plugin-mounted-on-root-app/package.json index 21a64bb7293b..dc5d163280c3 100644 --- a/fixtures/pages-plugin-mounted-on-root-app/package.json +++ b/fixtures/pages-plugin-mounted-on-root-app/package.json @@ -19,5 +19,8 @@ }, "engines": { "node": ">=16.13" + }, + "dependencies": { + "is-odd": "^3.0.1" } } diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 5e7950f2a0aa..3b898e01ce6a 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -85,6 +85,7 @@ export type BundleOptions = { local: boolean; projectRoot: string | undefined; defineNavigatorUserAgent: boolean; + external?: string[]; }; /** @@ -122,6 +123,7 @@ export async function bundleWorker( local, projectRoot, defineNavigatorUserAgent, + external, }: BundleOptions ): Promise { // We create a temporary directory for any one-off files we @@ -284,7 +286,9 @@ export async function bundleWorker( } : {}), inject, - external: bundle ? ["__STATIC_CONTENT_MANIFEST"] : undefined, + external: bundle + ? ["__STATIC_CONTENT_MANIFEST", ...(external ? external : [])] + : undefined, format: entry.format === "modules" ? "esm" : "iife", target: COMMON_ESBUILD_OPTIONS.target, sourcemap: sourcemap ?? true, diff --git a/packages/wrangler/src/pages/build.ts b/packages/wrangler/src/pages/build.ts index 5e32ef757c2e..2879fd655b60 100644 --- a/packages/wrangler/src/pages/build.ts +++ b/packages/wrangler/src/pages/build.ts @@ -123,6 +123,11 @@ export function Options(yargs: CommonYargsArgv) { deprecated: true, hidden: true, }, + external: { + describe: "A list of module imports to exclude from bundling", + type: "string", + array: true, + }, }); } @@ -146,6 +151,7 @@ export const Handler = async (args: PagesBuildArgs) => { nodejsCompat, legacyNodeCompat, defineNavigatorUserAgent, + external, } = validatedArgs; try { @@ -172,6 +178,7 @@ export const Handler = async (args: PagesBuildArgs) => { routesOutputPath, local: false, defineNavigatorUserAgent, + external, }); } catch (e) { if (e instanceof FunctionsNoRoutesError) { @@ -213,6 +220,7 @@ export const Handler = async (args: PagesBuildArgs) => { legacyNodeCompat, workerScriptPath, defineNavigatorUserAgent, + external, } = validatedArgs; /** @@ -243,6 +251,7 @@ export const Handler = async (args: PagesBuildArgs) => { watch, nodejsCompat, defineNavigatorUserAgent, + externalModules: external, }); } } else { @@ -268,6 +277,7 @@ export const Handler = async (args: PagesBuildArgs) => { routesOutputPath, local: false, defineNavigatorUserAgent, + external, }); } catch (e) { if (e instanceof FunctionsNoRoutesError) { diff --git a/packages/wrangler/src/pages/buildFunctions.ts b/packages/wrangler/src/pages/buildFunctions.ts index 531cbf2f8c36..e23d86302a81 100644 --- a/packages/wrangler/src/pages/buildFunctions.ts +++ b/packages/wrangler/src/pages/buildFunctions.ts @@ -39,6 +39,7 @@ export async function buildFunctions({ `./functionsRoutes-${Math.random()}.mjs` ), defineNavigatorUserAgent, + external, }: Partial< Pick< PagesBuildArgs, @@ -51,6 +52,7 @@ export async function buildFunctions({ | "watch" | "plugin" | "buildOutputDirectory" + | "external" > > & { functionsDirectory: string; @@ -120,6 +122,7 @@ export async function buildFunctions({ functionsDirectory: absoluteFunctionsDirectory, local, defineNavigatorUserAgent, + external, }); } else { bundle = await buildWorkerFromFunctions({ @@ -137,6 +140,7 @@ export async function buildFunctions({ legacyNodeCompat, nodejsCompat, defineNavigatorUserAgent, + external, }); } diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index 4651ef7b725b..d3d4af0aa84c 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -24,6 +24,7 @@ export function buildPluginFromFunctions({ functionsDirectory, local, defineNavigatorUserAgent, + external, }: Options) { const entry: Entry = { file: resolve(getBasePath(), "templates/pages-template-plugin.ts"), @@ -51,6 +52,7 @@ export function buildPluginFromFunctions({ nodejsCompat: true, define: {}, doBindings: [], // Pages functions don't support internal Durable Objects + external, plugins: [ buildNotifierPlugin(onEnd), { diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index ddf28b7fa640..9dc99b836687 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -32,6 +32,7 @@ export type Options = { functionsDirectory: string; local: boolean; defineNavigatorUserAgent: boolean; + external?: string[]; }; export function buildWorkerFromFunctions({ @@ -49,6 +50,7 @@ export function buildWorkerFromFunctions({ functionsDirectory, local, defineNavigatorUserAgent, + external, }: Options) { const entry: Entry = { file: resolve(getBasePath(), "templates/pages-template-worker.ts"), @@ -76,6 +78,7 @@ export function buildWorkerFromFunctions({ __FALLBACK_SERVICE__: JSON.stringify(fallbackService), }, doBindings: [], // Pages functions don't support internal Durable Objects + external, plugins: [ buildNotifierPlugin(onEnd), { @@ -170,7 +173,7 @@ export type RawOptions = { outdir?: string; directory: string; bundle?: boolean; - external?: string[]; + externalModules?: string[]; minify?: boolean; sourcemap?: boolean; watch?: boolean; @@ -182,6 +185,7 @@ export type RawOptions = { local: boolean; additionalModules?: CfModule[]; defineNavigatorUserAgent: boolean; + external?: string[]; }; /** @@ -197,7 +201,7 @@ export function buildRawWorker({ outdir, directory, bundle = true, - external, + externalModules, minify = false, sourcemap = false, watch = false, @@ -208,6 +212,7 @@ export function buildRawWorker({ local, additionalModules = [], defineNavigatorUserAgent, + external, }: RawOptions) { const entry: Entry = { file: workerScriptPath, @@ -215,7 +220,7 @@ export function buildRawWorker({ format: "modules", moduleRoot: resolve(directory), }; - const moduleCollector = external + const moduleCollector = externalModules ? noopModuleCollector : createModuleCollector({ entry, findAdditionalModules: false }); @@ -230,10 +235,11 @@ export function buildRawWorker({ nodejsCompat, define: {}, doBindings: [], // Pages functions don't support internal Durable Objects + external, plugins: [ ...plugins, buildNotifierPlugin(onEnd), - ...(external + ...(externalModules ? [ // In some cases, we want to enable bundling in esbuild so that we can flatten a shim around the entrypoint, but we still don't want to actually bundle in all the chunks that a Worker references. // This plugin allows us to mark those chunks as external so they are not inlined. @@ -241,7 +247,11 @@ export function buildRawWorker({ name: "external-fixer", setup(pluginBuild) { pluginBuild.onResolve({ filter: /.*/ }, async (args) => { - if (external.includes(resolve(args.resolveDir, args.path))) { + if ( + externalModules.includes( + resolve(args.resolveDir, args.path) + ) + ) { return { path: args.path, external: true }; } }); @@ -296,7 +306,9 @@ export async function traverseAndBuildWorkerJSDirectory({ const bundleResult = await buildRawWorker({ workerScriptPath: entrypoint, bundle: true, - external: additionalModules.map((m) => join(workerJSDirectory, m.name)), + externalModules: additionalModules.map((m) => + join(workerJSDirectory, m.name) + ), outfile, directory: buildOutputDirectory, local: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbf8fedf9b4a..e9b5aadcd94f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,6 +355,10 @@ importers: version: link:../../packages/wrangler fixtures/pages-functions-app: + dependencies: + is-odd: + specifier: ^3.0.1 + version: 3.0.1 devDependencies: '@cloudflare/workers-tsconfig': specifier: workspace:* @@ -413,11 +417,18 @@ importers: fixtures/pages-plugin-example: devDependencies: + is-odd: + specifier: ^3.0.1 + version: 3.0.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler fixtures/pages-plugin-mounted-on-root-app: + dependencies: + is-odd: + specifier: ^3.0.1 + version: 3.0.1 devDependencies: '@cloudflare/workers-tsconfig': specifier: workspace:* @@ -14127,10 +14138,20 @@ packages: kind-of: 3.2.2 dev: true + /is-number@6.0.0: + resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6/538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==} + engines: {node: '>=0.10.0'} + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + /is-odd@3.0.1: + resolution: {integrity: sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==} + engines: {node: '>=4'} + dependencies: + is-number: 6.0.0 + /is-path-cwd@2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'}