From 205f4ad3a65471903abc4117fbc628822181b86d Mon Sep 17 00:00:00 2001 From: Samuel Macleod Date: Wed, 7 Feb 2024 02:38:00 +0000 Subject: [PATCH] Add tests --- .../__tests__/navigator-user-agent.test.ts | 193 ++++++++++++++++++ .../esbuild-plugins/nodejs-compat.ts | 4 +- packages/wrangler/src/navigator-user-agent.ts | 8 +- packages/wrangler/src/pages/build.ts | 2 +- 4 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 packages/wrangler/src/__tests__/navigator-user-agent.test.ts 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..87cae03ef0c2 --- /dev/null +++ b/packages/wrangler/src/__tests__/navigator-user-agent.test.ts @@ -0,0 +1,193 @@ +import assert from "node:assert"; +import { mkdir, writeFile, readFile } 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/deployment-bundle/esbuild-plugins/nodejs-compat.ts b/packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts index 86574b3e3a9b..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,7 +1,7 @@ +import { relative } from "path"; import chalk from "chalk"; import { logger } from "../../logger"; import type { Plugin } from "esbuild"; -import { relative } from "path"; // Infinite loop detection const seen = new Set(); @@ -17,6 +17,8 @@ export const nodejsCompatPlugin: (silenceWarnings: boolean) => Plugin = ( ) => ({ name: "nodejs_compat imports plugin", setup(pluginBuild) { + seen.clear(); + warnedPackaged.clear(); pluginBuild.onResolve( { filter: /node:.*/ }, async ({ path, kind, resolveDir, ...opts }) => { diff --git a/packages/wrangler/src/navigator-user-agent.ts b/packages/wrangler/src/navigator-user-agent.ts index 2c359b4ef5ff..b4f15a0d451a 100644 --- a/packages/wrangler/src/navigator-user-agent.ts +++ b/packages/wrangler/src/navigator-user-agent.ts @@ -5,8 +5,10 @@ export function isNavigatorDefined( compatibility_flags: string[] = [] ) { assert( - compatibility_flags.includes("global_navigator") && - compatibility_flags.includes("no_global_navigator"), + !( + 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")) { @@ -15,5 +17,5 @@ export function isNavigatorDefined( if (compatibility_flags.includes("no_global_navigator")) { return false; } - return !compatibility_date || compatibility_date >= "2022-03-21"; + 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 f8badceab179..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, @@ -21,7 +22,6 @@ import type { CommonYargsArgv, StrictYargsOptionsToInterface, } from "../yargs-types"; -import { isNavigatorDefined } from "../navigator-user-agent"; export type PagesBuildArgs = StrictYargsOptionsToInterface;