diff --git a/.changeset/chatty-balloons-impress.md b/.changeset/chatty-balloons-impress.md new file mode 100644 index 000000000000..270e16af2540 --- /dev/null +++ b/.changeset/chatty-balloons-impress.md @@ -0,0 +1,44 @@ +--- +"wrangler": minor +--- + +feat: Support runtime-agnostic polyfills + +Previously, Wrangler treated any imports of `node:*` modules as build-time errors (unless one of the two Node.js compatibility modes was enabled). This is sometimes overly aggressive, since those imports are often not hit at runtime (for instance, it was impossible to write a library that worked across Node.JS and Workers, using Node packages only when running in Node). Here's an example of a function that would cause Wrangler to fail to build: + +```ts +export function randomBytes(length: number) { + if (navigator.userAgent !== "Cloudflare-Workers") { + return new Uint8Array(require("node:crypto").randomBytes(length)); + } else { + return crypto.getRandomValues(new Uint8Array(length)); + } +} +``` + +This function _should_ work in both Workers and Node, since it gates Node-specific functionality behind a user agent check, and falls back to the built-in Workers crypto API. Instead, Wrangler detected the `node:crypto` import and failed with the following error: + +``` +✘ [ERROR] Could not resolve "node:crypto" + + src/randomBytes.ts:5:36: + 5 │ ... return new Uint8Array(require('node:crypto').randomBytes(length)); + ╵ ~~~~~~~~~~~~~ + + The package "node:crypto" wasn't found on the file system but is built into node. + Add "node_compat = true" to your wrangler.toml file to enable Node.js compatibility. +``` + +This change turns that Wrangler build failure into a warning, which users can choose to ignore if they know the import of `node:*` APIs is safe (because it will never trigger at runtime, for instance): + +``` +▲ [WARNING] The package "node:crypto" wasn't found on the file system but is built into node. + + Your Worker may throw errors at runtime unless you enable the "nodejs_compat" + compatibility flag. Refer to + https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. + Imported from: + - src/randomBytes.ts +``` + +However, in a lot of cases, it's possible to know at _build_ time whether the import is safe. This change also injects `navigator.userAgent` into `esbuild`'s bundle settings as a predefined constant, which means that `esbuild` can tree-shake away imports of `node:*` APIs that are guaranteed not to be hit at runtime, supressing the warning entirely. diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index f7a073c3755b..190b962a850f 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -7938,7 +7938,7 @@ export default{ }); describe("`nodejs_compat` compatibility flag", () => { - it('when absent, should error on any "external" `node:*` imports', async () => { + it('when absent, should warn on any "external" `node:*` imports', async () => { writeWranglerToml(); fs.writeFileSync( "index.js", @@ -7948,15 +7948,18 @@ export default{ export default {} ` ); - let err: esbuild.BuildFailure | undefined; - try { - await runWrangler("deploy index.js --dry-run"); // expecting this to throw, as node compatibility isn't enabled - } catch (e) { - err = e as esbuild.BuildFailure; - } - expect( - esbuild.formatMessagesSync(err?.errors ?? [], { kind: "error" }).join() - ).toMatch(/Could not resolve "node:async_hooks"/); + await runWrangler("deploy index.js --dry-run"); + + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] The package \\"node:async_hooks\\" wasn't found on the file system but is built into node. + + Your Worker may throw errors at runtime unless you enable the \\"nodejs_compat\\" compatibility flag. + Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. Imported + from: + - index.js + + " + `); }); it('when present, should support any "external" `node:*` imports', async () => { diff --git a/packages/wrangler/src/__tests__/navigator-user-agent.test.ts b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts new file mode 100644 index 000000000000..0f37527f4a27 --- /dev/null +++ b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts @@ -0,0 +1,193 @@ +import assert from "node:assert"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import dedent from "ts-dedent"; +import { bundleWorker } from "../deployment-bundle/bundle"; +import { noopModuleCollector } from "../deployment-bundle/module-collection"; +import { isNavigatorDefined } from "../navigator-user-agent"; +import { mockConsoleMethods } from "./helpers/mock-console"; +import { runInTempDir } from "./helpers/run-in-tmp"; + +/* + * This file contains inline comments with the word "javascript" + * This signals to a compatible editor extension that the template string + * contents should be syntax-highlighted as JavaScript. One such extension + * is zjcompt.es6-string-javascript, but there are others. + */ + +async function seedFs(files: Record): Promise { + for (const [location, contents] of Object.entries(files)) { + await mkdir(path.dirname(location), { recursive: true }); + await writeFile(location, contents); + } +} + +describe("isNavigatorDefined", () => { + test("default", () => { + expect(isNavigatorDefined(undefined)).toBe(false); + }); + + test("modern date", () => { + expect(isNavigatorDefined("2024-01-01")).toBe(true); + }); + + test("old date", () => { + expect(isNavigatorDefined("2000-01-01")).toBe(false); + }); + + test("switch date", () => { + expect(isNavigatorDefined("2022-03-21")).toBe(true); + }); + + test("before date", () => { + expect(isNavigatorDefined("2022-03-20")).toBe(false); + }); + + test("old date, but with flag", () => { + expect(isNavigatorDefined("2000-01-01", ["global_navigator"])).toBe(true); + }); + + test("old date, with disable flag", () => { + expect(isNavigatorDefined("2000-01-01", ["no_global_navigator"])).toBe( + false + ); + }); + + test("new date, but with disable flag", () => { + expect(isNavigatorDefined("2024-01-01", ["no_global_navigator"])).toBe( + false + ); + }); + + test("new date, with enable flag", () => { + expect(isNavigatorDefined("2024-01-01", ["global_navigator"])).toBe(true); + }); + + test("errors with disable and enable flags specified", () => { + try { + isNavigatorDefined("2024-01-01", [ + "no_global_navigator", + "global_navigator", + ]); + assert(false, "Unreachable"); + } catch (e) { + expect(e).toMatchInlineSnapshot( + `[AssertionError: Can't both enable and disable a flag]` + ); + } + }); +}); + +// Does bundleWorker respect the value of `defineNavigatorUserAgent`? +describe("defineNavigatorUserAgent is respected", () => { + runInTempDir(); + const std = mockConsoleMethods(); + + it("defineNavigatorUserAgent = false, navigator preserved", async () => { + await seedFs({ + "src/index.js": dedent/* javascript */ ` + function randomBytes(length) { + if (navigator.userAgent !== "Cloudflare-Workers") { + return new Uint8Array(require("node:crypto").randomBytes(length)); + } else { + return crypto.getRandomValues(new Uint8Array(length)); + } + } + export default { + async fetch(request, env) { + return new Response(randomBytes(10)) + }, + }; + `, + }); + + await bundleWorker( + { + file: path.resolve("src/index.js"), + directory: process.cwd(), + format: "modules", + moduleRoot: path.dirname(path.resolve("src/index.js")), + }, + path.resolve("dist"), + { + bundle: true, + additionalModules: [], + moduleCollector: noopModuleCollector, + serveAssetsFromWorker: false, + doBindings: [], + define: {}, + checkFetch: false, + targetConsumer: "deploy", + local: true, + projectRoot: process.cwd(), + defineNavigatorUserAgent: false, + } + ); + + // Build time warning that the dynamic import of `require("node:crypto")` may not be safe + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] The package \\"node:crypto\\" wasn't found on the file system but is built into node. + + Your Worker may throw errors at runtime unless you enable the \\"nodejs_compat\\" compatibility flag. + Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. Imported + from: + - src/index.js + + " + `); + const fileContents = await readFile("dist/index.js", "utf8"); + + // navigator.userAgent should have been preserved as-is + expect(fileContents).toContain("navigator.userAgent"); + }); + + it("defineNavigatorUserAgent = true, navigator treeshaken", async () => { + await seedFs({ + "src/index.js": dedent/* javascript */ ` + function randomBytes(length) { + if (navigator.userAgent !== "Cloudflare-Workers") { + return new Uint8Array(require("node:crypto").randomBytes(length)); + } else { + return crypto.getRandomValues(new Uint8Array(length)); + } + } + export default { + async fetch(request, env) { + return new Response(randomBytes(10)) + }, + }; + `, + }); + + await bundleWorker( + { + file: path.resolve("src/index.js"), + directory: process.cwd(), + format: "modules", + moduleRoot: path.dirname(path.resolve("src/index.js")), + }, + path.resolve("dist"), + { + bundle: true, + additionalModules: [], + moduleCollector: noopModuleCollector, + serveAssetsFromWorker: false, + doBindings: [], + define: {}, + checkFetch: false, + targetConsumer: "deploy", + local: true, + projectRoot: process.cwd(), + defineNavigatorUserAgent: true, + } + ); + + // Build time warning is suppressed, because esbuild treeshakes the relevant code path + expect(std.warn).toMatchInlineSnapshot(`""`); + + const fileContents = await readFile("dist/index.js", "utf8"); + + // navigator.userAgent should have been defined, and so should not be present in the bundle + expect(fileContents).not.toContain("navigator.userAgent"); + }); +}); diff --git a/packages/wrangler/src/__tests__/pages/functions-build.test.ts b/packages/wrangler/src/__tests__/pages/functions-build.test.ts index 5327ac0a58fe..3e3693189e1c 100644 --- a/packages/wrangler/src/__tests__/pages/functions-build.test.ts +++ b/packages/wrangler/src/__tests__/pages/functions-build.test.ts @@ -413,7 +413,7 @@ export default { ); }); - it("should error at Node.js imports when the `nodejs_compat` compatibility flag is not set", async () => { + it("should warn at Node.js imports when the `nodejs_compat` compatibility flag is not set", async () => { mkdirSync("functions"); writeFileSync( "functions/hello.js", @@ -428,17 +428,18 @@ export default { ); await expect( - runWrangler(`pages functions build --outfile=public/_worker.bundle`) - ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Build failed with 1 error: - hello.js:2:36: ERROR: Could not resolve \\"node:async_hooks\\"" - `); - expect(std.err).toContain( - 'The package "node:async_hooks" wasn\'t found on the file system but is built into node.' - ); - expect(std.err).toContain( - 'Add the "nodejs_compat" compatibility flag to your Pages project and make sure to prefix the module name with "node:" to enable Node.js compatibility.' + await runWrangler(`pages functions build --outfile=public/_worker.bundle`) ); + expect(std.warn).toMatchInlineSnapshot(` + "▲ [WARNING] The package \\"node:async_hooks\\" wasn't found on the file system but is built into node. + + Your Worker may throw errors at runtime unless you enable the \\"nodejs_compat\\" compatibility flag. + Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. Imported + from: + - hello.js + + " + `); }); it("should compile a _worker.js/ directory", async () => { diff --git a/packages/wrangler/src/api/pages/deploy.tsx b/packages/wrangler/src/api/pages/deploy.tsx index 254adc695135..2068de864eb7 100644 --- a/packages/wrangler/src/api/pages/deploy.tsx +++ b/packages/wrangler/src/api/pages/deploy.tsx @@ -5,6 +5,7 @@ import { File, FormData } from "undici"; import { fetchResult } from "../../cfetch"; import { FatalError } from "../../errors"; import { logger } from "../../logger"; +import { isNavigatorDefined } from "../../navigator-user-agent"; import { buildFunctions } from "../../pages/buildFunctions"; import { MAX_DEPLOYMENT_ATTEMPTS } from "../../pages/constants"; import { @@ -143,6 +144,10 @@ export async function deploy({ const nodejsCompat = deploymentConfig.compatibility_flags?.includes("nodejs_compat"); + const defineNavigatorUserAgent = isNavigatorDefined( + deploymentConfig.compatibility_date, + deploymentConfig.compatibility_flags + ); /** * Evaluate if this is an Advanced Mode or Pages Functions project. If Advanced Mode, we'll * go ahead and upload `_worker.js` as is, but if Pages Functions, we need to attempt to build @@ -175,6 +180,7 @@ export async function deploy({ routesOutputPath, local: false, nodejsCompat, + defineNavigatorUserAgent, }); builtFunctions = readFileSync( @@ -254,6 +260,7 @@ export async function deploy({ workerJSDirectory: _workerPath, buildOutputDirectory: directory, nodejsCompat, + defineNavigatorUserAgent, }); } else if (_workerJS) { if (bundle) { @@ -270,6 +277,7 @@ export async function deploy({ watch: false, onEnd: () => {}, nodejsCompat, + defineNavigatorUserAgent, }); } else { await checkRawWorker(_workerPath, () => {}); diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 602a37083233..35995e76681d 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -26,6 +26,7 @@ import { getMigrationsToUpload } from "../durable"; import { UserError } from "../errors"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; +import { isNavigatorDefined } from "../navigator-user-agent"; import { APIError, ParseError } from "../parse"; import { getWranglerTmpDir } from "../paths"; import { getQueue, putConsumer } from "../queues/client"; @@ -520,6 +521,10 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m targetConsumer: "deploy", local: false, projectRoot: props.projectRoot, + defineNavigatorUserAgent: isNavigatorDefined( + props.compatibilityDate ?? config.compatibility_date, + props.compatibilityFlags ?? config.compatibility_flags + ), } ); @@ -536,6 +541,19 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m dependencies[modulePath] = { bytesInOutput }; } + // Add modules to dependencies for size warning + for (const module of modules) { + const modulePath = + module.filePath === undefined + ? module.name + : path.relative("", module.filePath); + const bytesInOutput = + typeof module.content === "string" + ? Buffer.byteLength(module.content) + : module.content.byteLength; + dependencies[modulePath] = { bytesInOutput }; + } + const content = readFileSync(resolvedEntryPointPath, { encoding: "utf-8", }); diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 66c5b87065da..c3797f66ebb7 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -86,6 +86,7 @@ export type BundleOptions = { forPages?: boolean; local: boolean; projectRoot: string | undefined; + defineNavigatorUserAgent: boolean; }; /** @@ -124,6 +125,7 @@ export async function bundleWorker( forPages, local, projectRoot, + defineNavigatorUserAgent, }: BundleOptions ): Promise { // We create a temporary directory for any one-off files we @@ -312,6 +314,9 @@ export async function bundleWorker( conditions: BUILD_CONDITIONS, ...(process.env.NODE_ENV && { define: { + ...(defineNavigatorUserAgent + ? { "navigator.userAgent": `"Cloudflare-Workers"` } + : {}), // use process.env["NODE_ENV" + ""] so that esbuild doesn't replace it // when we do a build of wrangler. (re: https://github.com/cloudflare/workers-sdk/issues/1477) "process.env.NODE_ENV": `"${process.env["NODE_ENV" + ""]}"`, @@ -328,7 +333,7 @@ export async function bundleWorker( ...(legacyNodeCompat ? [NodeGlobalsPolyfills({ buffer: true }), NodeModulesPolyfills()] : []), - ...(nodejsCompat ? [nodejsCompatPlugin] : []), + nodejsCompatPlugin(!!nodejsCompat), cloudflareInternalPlugin, buildResultPlugin, ...(plugins || []), diff --git a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts index a7587f453713..3b6b2094f572 100644 --- a/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts +++ b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts @@ -1,13 +1,76 @@ +import { relative } from "path"; +import chalk from "chalk"; +import { logger } from "../../logger"; import type { Plugin } from "esbuild"; +// Infinite loop detection +const seen = new Set(); + +// Prevent multiple warnings per package +const warnedPackaged = new Map(); + /** * An esbuild plugin that will mark any `node:...` imports as external. */ -export const nodejsCompatPlugin: Plugin = { +export const nodejsCompatPlugin: (silenceWarnings: boolean) => Plugin = ( + silenceWarnings +) => ({ name: "nodejs_compat imports plugin", setup(pluginBuild) { - pluginBuild.onResolve({ filter: /node:.*/ }, () => { - return { external: true }; + seen.clear(); + warnedPackaged.clear(); + pluginBuild.onResolve( + { filter: /node:.*/ }, + async ({ path, kind, resolveDir, ...opts }) => { + const specifier = `${path}:${kind}:${resolveDir}:${opts.importer}`; + if (seen.has(specifier)) { + return; + } + + seen.add(specifier); + // Try to resolve this import as a normal package + const result = await pluginBuild.resolve(path, { + kind, + resolveDir, + importer: opts.importer, + }); + + if (result.errors.length > 0) { + // esbuild couldn't resolve the package + // We should warn the user, but not fail the build + + if (!warnedPackaged.has(path)) { + warnedPackaged.set(path, [opts.importer]); + } else { + warnedPackaged.set(path, [ + ...warnedPackaged.get(path), + opts.importer, + ]); + } + return { external: true }; + } + // This is a normal package—don't treat it specially + return result; + } + ); + // Wait until the build finishes to log warnings, so that all files which import a package + // can be collated + pluginBuild.onEnd(() => { + if (!silenceWarnings) + warnedPackaged.forEach((importers: string[], path: string) => { + logger.warn( + `The package "${path}" wasn't found on the file system but is built into node. +Your Worker may throw errors at runtime unless you enable the "nodejs_compat" compatibility flag. Refer to https://developers.cloudflare.com/workers/runtime-apis/nodejs/ for more details. Imported from: +${importers + .map( + (i) => + ` - ${chalk.blue( + relative(pluginBuild.initialOptions.absWorkingDir ?? "/", i) + )}` + ) + .join("\n")}` + ); + }); }); }, -}; +}); diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index ff6a091f8714..8291ef0bc744 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -24,6 +24,7 @@ import { unregisterWorker, } from "../dev-registry"; import { logger } from "../logger"; +import { isNavigatorDefined } from "../navigator-user-agent"; import openInBrowser from "../open-in-browser"; import { getWranglerTmpDir } from "../paths"; import { openInspector } from "./inspect"; @@ -354,6 +355,10 @@ function DevSession(props: DevSessionProps) { experimentalLocal: props.experimentalLocal, projectRoot: props.projectRoot, onBundleStart, + defineNavigatorUserAgent: isNavigatorDefined( + props.compatibilityDate, + props.compatibilityFlags + ), }); useEffect(() => { if (bundle) onReloadStart(bundle); diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts index e7c9ef98f1c4..dd1646115fff 100644 --- a/packages/wrangler/src/dev/start-server.ts +++ b/packages/wrangler/src/dev/start-server.ts @@ -19,6 +19,7 @@ import { stopWorkerRegistry, } from "../dev-registry"; import { logger } from "../logger"; +import { isNavigatorDefined } from "../navigator-user-agent"; import { getWranglerTmpDir } from "../paths"; import { localPropsToConfigBundle, maybeRegisterLocalWorker } from "./local"; import { DEFAULT_WORKER_NAME, MiniflareServer } from "./miniflare"; @@ -139,6 +140,10 @@ export async function startDevServer( local: props.local, doBindings: props.bindings.durable_objects?.bindings ?? [], projectRoot: props.projectRoot, + defineNavigatorUserAgent: isNavigatorDefined( + props.compatibilityDate, + props.compatibilityFlags + ), }); if (props.local) { @@ -290,6 +295,7 @@ async function runEsbuild({ local, doBindings, projectRoot, + defineNavigatorUserAgent, }: { entry: Entry; destination: string; @@ -313,6 +319,7 @@ async function runEsbuild({ local: boolean; doBindings: DurableObjectBindings; projectRoot: string | undefined; + defineNavigatorUserAgent: boolean; }): Promise { if (noBundle) { additionalModules = dedupeModulesByName([ @@ -359,6 +366,7 @@ async function runEsbuild({ testScheduled, doBindings, projectRoot, + defineNavigatorUserAgent, }) : undefined; diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 0c3af4824a15..8c3f3f5708dd 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -58,6 +58,7 @@ export function useEsbuild({ experimentalLocal, projectRoot, onBundleStart, + defineNavigatorUserAgent, }: { entry: Entry; destination: string | undefined; @@ -84,6 +85,7 @@ export function useEsbuild({ experimentalLocal: boolean | undefined; projectRoot: string | undefined; onBundleStart: () => void; + defineNavigatorUserAgent: boolean; }): EsbuildBundle | undefined { const [bundle, setBundle] = useState(); const { exit } = useApp(); @@ -190,6 +192,7 @@ export function useEsbuild({ plugins: [onEnd], local, projectRoot, + defineNavigatorUserAgent, }) : undefined; diff --git a/packages/wrangler/src/navigator-user-agent.ts b/packages/wrangler/src/navigator-user-agent.ts new file mode 100644 index 000000000000..b4f15a0d451a --- /dev/null +++ b/packages/wrangler/src/navigator-user-agent.ts @@ -0,0 +1,21 @@ +import assert from "node:assert"; + +export function isNavigatorDefined( + compatibility_date: string | undefined, + compatibility_flags: string[] = [] +) { + assert( + !( + compatibility_flags.includes("global_navigator") && + compatibility_flags.includes("no_global_navigator") + ), + "Can't both enable and disable a flag" + ); + if (compatibility_flags.includes("global_navigator")) { + return true; + } + if (compatibility_flags.includes("no_global_navigator")) { + return false; + } + return !!compatibility_date && compatibility_date >= "2022-03-21"; +} diff --git a/packages/wrangler/src/pages/build.ts b/packages/wrangler/src/pages/build.ts index d009890ec139..ef7eb2c103bd 100644 --- a/packages/wrangler/src/pages/build.ts +++ b/packages/wrangler/src/pages/build.ts @@ -5,6 +5,7 @@ import { writeAdditionalModules } from "../deployment-bundle/find-additional-mod import { FatalError, UserError } from "../errors"; import { logger } from "../logger"; import * as metrics from "../metrics"; +import { isNavigatorDefined } from "../navigator-user-agent"; import { buildFunctions } from "./buildFunctions"; import { EXIT_CODE_FUNCTIONS_NO_ROUTES_ERROR, @@ -126,6 +127,7 @@ export const Handler = async (args: PagesBuildArgs) => { plugin, nodejsCompat, legacyNodeCompat, + defineNavigatorUserAgent, } = validatedArgs; try { @@ -151,6 +153,7 @@ export const Handler = async (args: PagesBuildArgs) => { nodejsCompat, routesOutputPath, local: false, + defineNavigatorUserAgent, }); } catch (e) { if (e instanceof FunctionsNoRoutesError) { @@ -188,6 +191,7 @@ export const Handler = async (args: PagesBuildArgs) => { nodejsCompat, legacyNodeCompat, workerScriptPath, + defineNavigatorUserAgent, } = validatedArgs; /** @@ -200,6 +204,7 @@ export const Handler = async (args: PagesBuildArgs) => { workerJSDirectory: workerScriptPath, buildOutputDirectory, nodejsCompat, + defineNavigatorUserAgent, }); } else { /** @@ -216,6 +221,7 @@ export const Handler = async (args: PagesBuildArgs) => { sourcemap, watch, nodejsCompat, + defineNavigatorUserAgent, }); } } else { @@ -240,6 +246,7 @@ export const Handler = async (args: PagesBuildArgs) => { nodejsCompat, routesOutputPath, local: false, + defineNavigatorUserAgent, }); } catch (e) { if (e instanceof FunctionsNoRoutesError) { @@ -278,7 +285,7 @@ type WorkerBundleArgs = Omit & { buildOutputDirectory: string; legacyNodeCompat: boolean; nodejsCompat: boolean; - + defineNavigatorUserAgent: boolean; workerScriptPath: string; }; type PluginArgs = Omit< @@ -289,6 +296,7 @@ type PluginArgs = Omit< outdir: string; legacyNodeCompat: boolean; nodejsCompat: boolean; + defineNavigatorUserAgent: boolean; }; type ValidatedArgs = WorkerBundleArgs | PluginArgs; @@ -357,6 +365,10 @@ const validateArgs = (args: PagesBuildArgs): ValidatedArgs => { ); } const nodejsCompat = !!args.compatibilityFlags?.includes("nodejs_compat"); + const defineNavigatorUserAgent = isNavigatorDefined( + args.compatibilityDate, + args.compatibilityFlags + ); if (legacyNodeCompat && nodejsCompat) { throw new UserError( "The `nodejs_compat` compatibility flag cannot be used in conjunction with the legacy `--node-compat` flag. If you want to use the Workers runtime Node.js compatibility features, please remove the `--node-compat` argument from your CLI command." @@ -404,5 +416,6 @@ We looked for the Functions directory (${basename( workerScriptPath, nodejsCompat, legacyNodeCompat, + defineNavigatorUserAgent, } as ValidatedArgs; }; diff --git a/packages/wrangler/src/pages/buildFunctions.ts b/packages/wrangler/src/pages/buildFunctions.ts index b30273537bf3..531cbf2f8c36 100644 --- a/packages/wrangler/src/pages/buildFunctions.ts +++ b/packages/wrangler/src/pages/buildFunctions.ts @@ -38,6 +38,7 @@ export async function buildFunctions({ getPagesTmpDir(), `./functionsRoutes-${Math.random()}.mjs` ), + defineNavigatorUserAgent, }: Partial< Pick< PagesBuildArgs, @@ -61,6 +62,7 @@ export async function buildFunctions({ // Allow `routesModule` to be fixed, so we don't create a new file in the // temporary directory each time routesModule?: string; + defineNavigatorUserAgent: boolean; }) { RUNNING_BUILDERS.forEach( (runningBuilder) => runningBuilder.stop && runningBuilder.stop() @@ -117,6 +119,7 @@ export async function buildFunctions({ legacyNodeCompat, functionsDirectory: absoluteFunctionsDirectory, local, + defineNavigatorUserAgent, }); } else { bundle = await buildWorkerFromFunctions({ @@ -133,6 +136,7 @@ export async function buildFunctions({ buildOutputDirectory, legacyNodeCompat, nodejsCompat, + defineNavigatorUserAgent, }); } diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index ad937556ceb9..d66ae30777a9 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -9,6 +9,7 @@ import { esbuildAliasExternalPlugin } from "../deployment-bundle/esbuild-plugins import { FatalError } from "../errors"; import { logger } from "../logger"; import * as metrics from "../metrics"; +import { isNavigatorDefined } from "../navigator-user-agent"; import { getBasePath } from "../paths"; import * as shellquote from "../utils/shell-quote"; import { buildFunctions } from "./buildFunctions"; @@ -304,6 +305,10 @@ export const Handler = async ({ let scriptPath = ""; const nodejsCompat = compatibilityFlags?.includes("nodejs_compat"); + const defineNavigatorUserAgent = isNavigatorDefined( + compatibilityDate, + compatibilityFlags + ); let modules: CfModule[] = []; if (usingWorkerDirectory) { @@ -312,6 +317,7 @@ export const Handler = async ({ workerJSDirectory: workerScriptPath, buildOutputDirectory: directory ?? ".", nodejsCompat, + defineNavigatorUserAgent, }); modules = bundleResult.modules; scriptPath = bundleResult.resolvedEntryPointPath; @@ -360,6 +366,7 @@ export const Handler = async ({ sourcemap: true, watch: false, onEnd: () => scriptReadyResolve(), + defineNavigatorUserAgent, }); } catch (e: unknown) { logger.warn("Failed to bundle _worker.js.", e); @@ -416,6 +423,7 @@ export const Handler = async ({ nodejsCompat, local: true, routesModule, + defineNavigatorUserAgent, }); await metrics.sendMetricsEvent("build pages functions"); }; diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index 3fafa8e94c53..4651ef7b725b 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -23,6 +23,7 @@ export function buildPluginFromFunctions({ legacyNodeCompat, functionsDirectory, local, + defineNavigatorUserAgent, }: Options) { const entry: Entry = { file: resolve(getBasePath(), "templates/pages-template-plugin.ts"), @@ -107,5 +108,6 @@ export function buildPluginFromFunctions({ forPages: true, local, projectRoot: getPagesProjectRoot(), + defineNavigatorUserAgent, }); } diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index 9472a4e6ecca..4beed8c91db4 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -31,6 +31,7 @@ export type Options = { nodejsCompat?: boolean; functionsDirectory: string; local: boolean; + defineNavigatorUserAgent: boolean; }; export function buildWorkerFromFunctions({ @@ -47,6 +48,7 @@ export function buildWorkerFromFunctions({ nodejsCompat, functionsDirectory, local, + defineNavigatorUserAgent, }: Options) { const entry: Entry = { file: resolve(getBasePath(), "templates/pages-template-worker.ts"), @@ -158,6 +160,7 @@ export function buildWorkerFromFunctions({ forPages: true, local, projectRoot: getPagesProjectRoot(), + defineNavigatorUserAgent, }); } @@ -178,6 +181,7 @@ export type RawOptions = { nodejsCompat?: boolean; local: boolean; additionalModules?: CfModule[]; + defineNavigatorUserAgent: boolean; }; /** @@ -203,6 +207,7 @@ export function buildRawWorker({ nodejsCompat, local, additionalModules = [], + defineNavigatorUserAgent, }: RawOptions) { const entry: Entry = { file: workerScriptPath, @@ -252,6 +257,7 @@ export function buildRawWorker({ forPages: true, local, projectRoot: getPagesProjectRoot(), + defineNavigatorUserAgent, }); } @@ -259,10 +265,12 @@ export async function traverseAndBuildWorkerJSDirectory({ workerJSDirectory, buildOutputDirectory, nodejsCompat, + defineNavigatorUserAgent, }: { workerJSDirectory: string; buildOutputDirectory: string; nodejsCompat?: boolean; + defineNavigatorUserAgent: boolean; }): Promise { const entrypoint = resolve(join(workerJSDirectory, "index.js")); @@ -297,6 +305,7 @@ export async function traverseAndBuildWorkerJSDirectory({ onEnd: () => {}, nodejsCompat, additionalModules, + defineNavigatorUserAgent, }); return { diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index c0c43af624ca..c7c3058b2ebc 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -23,6 +23,7 @@ import { getMigrationsToUpload } from "../durable"; import { UserError } from "../errors"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; +import { isNavigatorDefined } from "../navigator-user-agent"; import { ParseError } from "../parse"; import { getWranglerTmpDir } from "../paths"; import { getQueue } from "../queues/client"; @@ -294,6 +295,10 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m targetConsumer: "deploy", local: false, projectRoot: props.projectRoot, + defineNavigatorUserAgent: isNavigatorDefined( + props.compatibilityDate ?? config.compatibility_date, + props.compatibilityFlags ?? config.compatibility_flags + ), } ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07c25f28787b..7c3f4829746f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -820,7 +820,7 @@ importers: version: 8.49.0 eslint-config-turbo: specifier: latest - version: 1.12.2(eslint@8.49.0) + version: 1.12.3(eslint@8.49.0) eslint-plugin-import: specifier: 2.26.x version: 2.26.0(@typescript-eslint/parser@6.7.2)(eslint@8.49.0) @@ -10718,13 +10718,13 @@ packages: eslint: 8.49.0 dev: true - /eslint-config-turbo@1.12.2(eslint@8.49.0): - resolution: {integrity: sha512-JHTGtDQuISBEWIorHenu5AeX1nv16NiDgDVRi1i0VyeYw0SiVh+lSQbv4BawXSnG1nOFpjbopAQdZvdB3PwXbQ==} + /eslint-config-turbo@1.12.3(eslint@8.49.0): + resolution: {integrity: sha512-Q46MEOiNJpJWC3Et5/YEuIYYhbOieS04yZwQOinO2hpZw3folEXV+hbwVo8M+ap/q8gtpjIWiRMZ1A4QxmhEqQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.49.0 - eslint-plugin-turbo: 1.12.2(eslint@8.49.0) + eslint-plugin-turbo: 1.12.3(eslint@8.49.0) dev: false /eslint-import-resolver-node@0.3.7: @@ -11151,8 +11151,8 @@ packages: - typescript dev: true - /eslint-plugin-turbo@1.12.2(eslint@8.49.0): - resolution: {integrity: sha512-/l0aGvZRzK1LMRTibRd6ZbEEuD5TtGotDTkZpxSIWA1FI764pWVvQduQMKBaRuz7aTuAo0WxatD8v1scK+qRWw==} + /eslint-plugin-turbo@1.12.3(eslint@8.49.0): + resolution: {integrity: sha512-7hEyxa+oP898EFNoxVenHlH8jtBwV1hbbIkdQWgqDcB0EmVNGVEZkYRo5Hm6BuMAjR433B+NISBJdj0bQo4/Lg==} peerDependencies: eslint: '>6.6.0' dependencies: