From 676b7a8b6babe8e884509145ac24c8ce92f8d620 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Thu, 1 Feb 2024 15:52:11 -0600 Subject: [PATCH 01/13] Split package.json update into multiple phases --- packages/create-cloudflare/src/cli.ts | 7 +++++-- packages/create-cloudflare/src/templates.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index a81694925331..08e087be787d 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -29,7 +29,8 @@ import { createProject } from "./pages"; import { copyTemplateFiles, selectTemplate, - updatePackageJson, + updatePackageName, + updatePackageScripts, } from "./templates"; import { installWorkersTypes, updateWranglerToml } from "./workers"; import type { C3Args, C3Context } from "types"; @@ -122,7 +123,7 @@ const create = async (ctx: C3Context) => { } await copyTemplateFiles(ctx); - await updatePackageJson(ctx); + await updatePackageName(ctx); chdir(ctx.project.path); await npmInstall(ctx); @@ -144,6 +145,8 @@ const configure = async (ctx: C3Context) => { await template.configure({ ...ctx }); } + await updatePackageScripts(ctx); + await offerGit(ctx); await gitCommit(ctx); diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index 3c6d4a9ca27c..16a7b57f3336 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -367,26 +367,36 @@ const downloadRemoteTemplate = async (src: string) => { } }; -export const updatePackageJson = async (ctx: C3Context) => { +export const updatePackageName = async (ctx: C3Context) => { const s = spinner(); - s.start("Updating `package.json`"); + s.start("Updating name in `package.json`"); // Update package.json with project name const placeholderNames = ["", "TBD", ""]; const pkgJsonPath = resolve(ctx.project.path, "package.json"); - let pkgJson = readJSON(pkgJsonPath); + const pkgJson = readJSON(pkgJsonPath); if (placeholderNames.includes(pkgJson.name)) { pkgJson.name = ctx.project.name; } + writeJSON(pkgJsonPath, pkgJson); + s.stop(`${brandColor("updated")} ${dim("`package.json`")}`); +}; + +export const updatePackageScripts = async (ctx: C3Context) => { + const s = spinner(); + s.start("Updating `package.json` scripts"); + + const pkgJsonPath = resolve(ctx.project.path, "package.json"); + let pkgJson = readJSON(pkgJsonPath); + // Run any transformers defined by the template if (ctx.template.transformPackageJson) { const transformed = await ctx.template.transformPackageJson(pkgJson); pkgJson = deepmerge(pkgJson, transformed); } - // Write the finalized package.json to disk writeJSON(pkgJsonPath, pkgJson); s.stop(`${brandColor("updated")} ${dim("`package.json`")}`); }; From 1d19e73f2467e75bff5012f9439f0479d1c65443 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Thu, 1 Feb 2024 15:54:24 -0600 Subject: [PATCH 02/13] Add getBindingsProxy to qwik template --- .../create-cloudflare/src/helpers/codemod.ts | 21 ++++++ .../create-cloudflare/templates/qwik/c3.ts | 64 +++++++++++++++++-- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/create-cloudflare/src/helpers/codemod.ts b/packages/create-cloudflare/src/helpers/codemod.ts index 36a0df18a0de..d1e924e1afcd 100644 --- a/packages/create-cloudflare/src/helpers/codemod.ts +++ b/packages/create-cloudflare/src/helpers/codemod.ts @@ -6,8 +6,28 @@ import * as typescriptParser from "recast/parsers/typescript"; import { readFile, writeFile } from "./files"; import type { Program } from "esprima"; +/* + CODEMOD TIPS & TRICKS + ===================== + + More info about parsing and transforming can be found in the `recast` docs: + https://github.com/benjamn/recast + + `recast` uses the `ast-types` library under the hood for basic AST operations + and defining node types. If you need to manipulate or manually construct AST nodes as + part of a code mod operation, be sure to check the `ast-types` documentation: + https://github.com/benjamn/ast-types + + Last but not least, AST viewers can be extremely helpful when trying to write + a transformer: + - https://astexplorer.net/ + - https://ts-ast-viewer.com/# + +*/ + // Parse an input string as javascript and return an ast export const parseJs = (src: string) => { + src = src.trim(); try { return recast.parse(src, { parser: esprimaParser }); } catch (error) { @@ -17,6 +37,7 @@ export const parseJs = (src: string) => { // Parse an input string as typescript and return an ast export const parseTs = (src: string) => { + src = src.trim(); try { return recast.parse(src, { parser: typescriptParser }); } catch (error) { diff --git a/packages/create-cloudflare/templates/qwik/c3.ts b/packages/create-cloudflare/templates/qwik/c3.ts index 04764717d670..0256e90dc917 100644 --- a/packages/create-cloudflare/templates/qwik/c3.ts +++ b/packages/create-cloudflare/templates/qwik/c3.ts @@ -1,9 +1,13 @@ import { endSection } from "@cloudflare/cli"; +import { brandColor } from "@cloudflare/cli/colors"; +import { spinner } from "@cloudflare/cli/interactive"; +import { parseTs, transformFile } from "helpers/codemod"; import { runCommand, runFrameworkGenerator } from "helpers/command"; -import { compatDateFlag } from "helpers/files"; +import { usesTypescript } from "helpers/files"; import { detectPackageManager } from "helpers/packages"; import { quoteShellArgs } from "../../src/common"; import type { TemplateConfig } from "../../src/templates"; +import type * as recast from "recast"; import type { C3Context } from "types"; const { npm, npx } = detectPackageManager(); @@ -12,11 +16,62 @@ const generate = async (ctx: C3Context) => { await runFrameworkGenerator(ctx, ["basic", ctx.project.name]); }; -const configure = async () => { +const configure = async (ctx: C3Context) => { // Add the pages integration const cmd = [npx, "qwik", "add", "cloudflare-pages"]; endSection(`Running ${quoteShellArgs(cmd)}`); await runCommand(cmd); + + addBindingsProxy(ctx); +}; + +const addBindingsProxy = (ctx: C3Context) => { + // Qwik only has a typescript template atm. + // This check is an extra precaution + if (!usesTypescript(ctx)) { + return; + } + + const s = spinner(); + s.start("Updating `vite.config.ts`"); + + // Insert the env declaration after the last import (but before the rest of the body) + const envDeclaration = ` +let env = {}; + +if(process.env.NODE_ENV === 'development') { + const { getBindingsProxy } = await import('wrangler'); + const { bindings } = await getBindingsProxy(); + env = bindings; +} +`; + + transformFile("vite.config.ts", { + visitProgram: function (n) { + const lastImportIndex = n.node.body.findLastIndex( + (t) => t.type === "ImportDeclaration" + ); + n.get("body").insertAt(lastImportIndex + 1, envDeclaration); + + return false; + }, + }); + + // Populate the `qwikCity` plugin with the platform object containing the `env` defined above. + const platformObject = parseTs(`{ platform: { env } }`); + + transformFile("vite.config.ts", { + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name === "qwikCity") { + n.node.arguments = [platformObject]; + } + + this.traverse(n); + }, + }); + + s.stop(`${brandColor("updated")} \`vite.config.ts\``); }; const config: TemplateConfig = { @@ -24,12 +79,13 @@ const config: TemplateConfig = { id: "qwik", displayName: "Qwik", platform: "pages", + devScript: "dev", + deployScript: "deploy", generate, configure, transformPackageJson: async () => ({ scripts: { - "pages:dev": `wrangler pages dev ${await compatDateFlag()} -- ${npm} run dev`, - "pages:deploy": `${npm} run build && wrangler pages deploy ./dist`, + deploy: `${npm} run build && wrangler pages deploy ./dist`, }, }), }; From c6fde13324a0e3b6e554f499d8d2c102789e43fb Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Fri, 2 Feb 2024 23:46:27 -0600 Subject: [PATCH 03/13] Add support for testing dev scripts in c3 e2e tests --- packages/create-cloudflare/.eslintrc.js | 1 + .../fixtures/qwik/src/routes/test/index.ts | 10 + .../e2e-tests/fixtures/qwik/wrangler.toml | 2 + .../e2e-tests/frameworks.test.ts | 480 ++++++++++-------- .../create-cloudflare/e2e-tests/helpers.ts | 84 ++- packages/create-cloudflare/tsconfig.json | 1 + 6 files changed, 349 insertions(+), 229 deletions(-) create mode 100644 packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts create mode 100644 packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml diff --git a/packages/create-cloudflare/.eslintrc.js b/packages/create-cloudflare/.eslintrc.js index 1e2096d54a14..1057fd4b8aa3 100644 --- a/packages/create-cloudflare/.eslintrc.js +++ b/packages/create-cloudflare/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { ignorePatterns: [ "dist", "scripts", + "e2e-tests/fixtures/*", // template files are ignored by the eslint-config-worker configuration // we do however want the c3 files to be linted "!**/templates/**/c3.ts", diff --git a/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts b/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts new file mode 100644 index 000000000000..fa70a4bd09d8 --- /dev/null +++ b/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts @@ -0,0 +1,10 @@ +import type { RequestHandler } from "@builder.io/qwik-city"; + +export const onGet: RequestHandler = async ({ platform, json }) => { + if (!platform.env) { + json(500, "Platform object not defined"); + return; + } + + json(200, { value: platform.env["TEST"] || "wtf undefined?", version: 1 }); +}; diff --git a/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml b/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml new file mode 100644 index 000000000000..179e2d29b8fb --- /dev/null +++ b/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml @@ -0,0 +1,2 @@ +[vars] +TEST = "C3_TEST" \ No newline at end of file diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index f8096e3a82b9..ce206db577a8 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -1,5 +1,8 @@ +import { cp } from "fs/promises"; import { join } from "path"; import { retry } from "helpers/command"; +import { sleep } from "helpers/common"; +import { detectPackageManager } from "helpers/packages"; import { fetch } from "undici"; import { beforeAll, describe, expect, test } from "vitest"; import { deleteProject, deleteWorker } from "../scripts/common"; @@ -10,6 +13,7 @@ import { keys, recreateLogFolder, runC3, + spawnWithLogging, testDeploymentCommitMessage, testProjectDir, } from "./helpers"; @@ -26,225 +30,140 @@ type FrameworkTestConfig = Omit & { timeout?: number; unsupportedPms?: string[]; unsupportedOSs?: string[]; + shouldTestDevScript?: boolean; }; let frameworkMap: FrameworkMap; -describe.concurrent(`E2E: Web frameworks`, () => { - // These are ordered based on speed and reliability for ease of debugging - const frameworkTests: Record = { - astro: { - expectResponseToContain: "Hello, Astronaut!", - testCommitMessage: true, - unsupportedOSs: ["win32"], - }, - docusaurus: { - expectResponseToContain: "Dinosaurs are cool", - unsupportedPms: ["bun"], - testCommitMessage: true, - unsupportedOSs: ["win32"], - timeout: LONG_TIMEOUT, - }, - angular: { - expectResponseToContain: "Congratulations! Your app is running.", - testCommitMessage: true, - timeout: LONG_TIMEOUT, - }, - gatsby: { - expectResponseToContain: "Gatsby!", - unsupportedPms: ["bun", "pnpm"], - promptHandlers: [ - { - matcher: /Would you like to use a template\?/, - input: ["n"], - }, - ], - testCommitMessage: true, - timeout: LONG_TIMEOUT, - }, - hono: { - expectResponseToContain: "Hello Hono!", - testCommitMessage: false, - }, - qwik: { - expectResponseToContain: "Welcome to Qwik", - promptHandlers: [ - { - matcher: /Yes looks good, finish update/, - input: [keys.enter], - }, - ], - testCommitMessage: true, - unsupportedOSs: ["win32"], - unsupportedPms: ["yarn"], - }, - remix: { - expectResponseToContain: "Welcome to Remix", - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedPms: ["yarn"], - }, - next: { - expectResponseToContain: "Create Next App", - promptHandlers: [ - { - matcher: /Do you want to use the next-on-pages eslint-plugin\?/, - input: ["y"], - }, - ], - testCommitMessage: true, - quarantine: true, - }, - nuxt: { - expectResponseToContain: "Welcome to Nuxt!", - testCommitMessage: true, - timeout: LONG_TIMEOUT, - }, - react: { - expectResponseToContain: "React App", - testCommitMessage: true, - unsupportedOSs: ["win32"], - timeout: LONG_TIMEOUT, - }, - solid: { - expectResponseToContain: "Hello world", - promptHandlers: [ - { - matcher: /Which template do you want to use/, - input: [keys.enter], - }, - { - matcher: /Server Side Rendering/, - input: [keys.enter], - }, - { - matcher: /Use TypeScript/, - input: [keys.enter], - }, - ], - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedOSs: ["win32"], - }, - svelte: { - expectResponseToContain: "SvelteKit app", - promptHandlers: [ - { - matcher: /Which Svelte app template/, - input: [keys.enter], - }, - { - matcher: /Add type checking with TypeScript/, - input: [keys.down, keys.enter], - }, - { - matcher: /Select additional options/, - input: [keys.enter], - }, - ], - testCommitMessage: true, - unsupportedOSs: ["win32"], - unsupportedPms: ["npm"], - }, - vue: { - expectResponseToContain: "Vite App", - testCommitMessage: true, - unsupportedOSs: ["win32"], - }, - }; +// These are ordered based on speed and reliability for ease of debugging +const frameworkTests: Record = { + astro: { + expectResponseToContain: "Hello, Astronaut!", + testCommitMessage: true, + unsupportedOSs: ["win32"], + }, + docusaurus: { + expectResponseToContain: "Dinosaurs are cool", + unsupportedPms: ["bun"], + testCommitMessage: true, + unsupportedOSs: ["win32"], + timeout: LONG_TIMEOUT, + }, + angular: { + expectResponseToContain: "Congratulations! Your app is running.", + testCommitMessage: true, + timeout: LONG_TIMEOUT, + }, + gatsby: { + expectResponseToContain: "Gatsby!", + unsupportedPms: ["bun", "pnpm"], + promptHandlers: [ + { + matcher: /Would you like to use a template\?/, + input: ["n"], + }, + ], + testCommitMessage: true, + timeout: LONG_TIMEOUT, + }, + hono: { + expectResponseToContain: "Hello Hono!", + testCommitMessage: false, + }, + qwik: { + expectResponseToContain: "Welcome to Qwik", + promptHandlers: [ + { + matcher: /Yes looks good, finish update/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + unsupportedOSs: ["win32"], + unsupportedPms: ["yarn"], + shouldTestDevScript: true, + }, + remix: { + expectResponseToContain: "Welcome to Remix", + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedPms: ["yarn"], + }, + next: { + expectResponseToContain: "Create Next App", + promptHandlers: [ + { + matcher: /Do you want to use the next-on-pages eslint-plugin\?/, + input: ["y"], + }, + ], + testCommitMessage: true, + quarantine: true, + }, + nuxt: { + expectResponseToContain: "Welcome to Nuxt!", + testCommitMessage: true, + timeout: LONG_TIMEOUT, + }, + react: { + expectResponseToContain: "React App", + testCommitMessage: true, + unsupportedOSs: ["win32"], + timeout: LONG_TIMEOUT, + }, + solid: { + expectResponseToContain: "Hello world", + promptHandlers: [ + { + matcher: /Which template do you want to use/, + input: [keys.enter], + }, + { + matcher: /Server Side Rendering/, + input: [keys.enter], + }, + { + matcher: /Use TypeScript/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedOSs: ["win32"], + }, + svelte: { + expectResponseToContain: "SvelteKit app", + promptHandlers: [ + { + matcher: /Which Svelte app template/, + input: [keys.enter], + }, + { + matcher: /Add type checking with TypeScript/, + input: [keys.down, keys.enter], + }, + { + matcher: /Select additional options/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + unsupportedOSs: ["win32"], + unsupportedPms: ["npm"], + }, + vue: { + expectResponseToContain: "Vite App", + testCommitMessage: true, + unsupportedOSs: ["win32"], + }, +}; +describe.concurrent(`E2E: Web frameworks`, () => { beforeAll(async (ctx) => { frameworkMap = await getFrameworkMap(); recreateLogFolder(ctx as Suite); }); - const runCli = async ( - framework: string, - projectPath: string, - { ctx, argv = [], promptHandlers = [] }: RunnerConfig - ) => { - const args = [ - projectPath, - "--type", - "webFramework", - "--framework", - framework, - "--deploy", - "--no-open", - "--no-git", - ]; - - args.push(...argv); - - const { output } = await runC3({ - ctx, - argv: args, - promptHandlers, - outputPrefix: `[${framework}]`, - }); - - // Relevant project files should have been created - expect(projectPath).toExist(); - const pkgJsonPath = join(projectPath, "package.json"); - expect(pkgJsonPath).toExist(); - - // Wrangler should be installed - const wranglerPath = join(projectPath, "node_modules/wrangler"); - expect(wranglerPath).toExist(); - - // TODO: Before the refactor introduced in https://github.com/cloudflare/workers-sdk/pull/4754 - // we used to test the packageJson scripts transformations here, try to re-implement such - // checks (might be harder given the switch to a transform function compared to the old - // object based substitution) - - return { output }; - }; - - const runCliWithDeploy = async ( - framework: string, - projectName: string, - projectPath: string, - ctx: TestContext, - testCommitMessage: boolean - ) => { - const { argv, overrides, promptHandlers, expectResponseToContain } = - frameworkTests[framework]; - - const { output } = await runCli(framework, projectPath, { - ctx, - overrides, - promptHandlers, - argv: [...(argv ?? [])], - }); - - // Verify deployment - const deployedUrlRe = - /deployment is ready at: (https:\/\/.+\.(pages|workers)\.dev)/; - - const match = output.match(deployedUrlRe); - if (!match || !match[1]) { - expect(false, "Couldn't find deployment url in C3 output").toBe(true); - return; - } - - const projectUrl = match[1]; - - await retry({ times: 5 }, async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second - const res = await fetch(projectUrl); - const body = await res.text(); - if (!body.includes(expectResponseToContain)) { - throw new Error( - `(${framework}) Deployed page (${projectUrl}) didn't contain expected string: "${expectResponseToContain}"` - ); - } - }); - - if (testCommitMessage) { - await testDeploymentCommitMessage(projectName, framework); - } - }; - Object.keys(frameworkTests).forEach((framework) => { const { quarantine, @@ -252,6 +171,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { testCommitMessage, unsupportedPms, unsupportedOSs, + shouldTestDevScript, } = frameworkTests[framework]; const quarantineModeMatch = isQuarantineMode() == (quarantine ?? false); @@ -275,13 +195,16 @@ describe.concurrent(`E2E: Web frameworks`, () => { const projectName = getName(framework); const frameworkConfig = frameworkMap[framework as FrameworkName]; try { - await runCliWithDeploy( + await verifyDeployment( framework, projectName, projectPath, ctx, testCommitMessage ); + if (shouldTestDevScript) { + await testDevScript(framework, projectPath, ctx); + } } finally { clean(framework); // Cleanup the project in case we need to retry it @@ -292,7 +215,8 @@ describe.concurrent(`E2E: Web frameworks`, () => { } } }, - { retry: 1, timeout: timeout || TEST_TIMEOUT } + // { retry: 1, timeout: timeout || TEST_TIMEOUT } + { retry: 0, timeout: timeout || TEST_TIMEOUT } ); }); @@ -300,3 +224,137 @@ describe.concurrent(`E2E: Web frameworks`, () => { // await runCli("hono", { ctx, argv: ["--wrangler-defaults"] }); // }); }); + +// TODO: Refactor to a function that returns the deployment URL so expectation can be +// done in the actual test +const verifyDeployment = async ( + framework: string, + projectName: string, + projectPath: string, + ctx: TestContext, + testCommitMessage: boolean +) => { + const { argv, overrides, promptHandlers, expectResponseToContain } = + frameworkTests[framework]; + + const { output } = await runCli(framework, projectPath, { + ctx, + overrides, + promptHandlers, + argv: [...(argv ?? [])], + }); + + // Verify deployment + const deployedUrlRe = + /deployment is ready at: (https:\/\/.+\.(pages|workers)\.dev)/; + + const match = output.match(deployedUrlRe); + if (!match || !match[1]) { + expect(false, "Couldn't find deployment url in C3 output").toBe(true); + return; + } + + const projectUrl = match[1]; + + await retry({ times: 5 }, async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second + const res = await fetch(projectUrl); + const body = await res.text(); + if (!body.includes(expectResponseToContain)) { + throw new Error( + `(${framework}) Deployed page (${projectUrl}) didn't contain expected string: "${expectResponseToContain}"` + ); + } + }); + + if (testCommitMessage) { + await testDeploymentCommitMessage(projectName, framework); + } +}; + +const runCli = async ( + framework: string, + projectPath: string, + { ctx, argv = [], promptHandlers = [] }: RunnerConfig +) => { + const args = [ + projectPath, + "--type", + "webFramework", + "--framework", + framework, + "--deploy", + "--no-open", + "--no-git", + ]; + + args.push(...argv); + + const { output } = await runC3({ + ctx, + argv: args, + promptHandlers, + outputPrefix: `[${framework}]`, + }); + + // Relevant project files should have been created + expect(projectPath).toExist(); + const pkgJsonPath = join(projectPath, "package.json"); + expect(pkgJsonPath).toExist(); + + // Wrangler should be installed + const wranglerPath = join(projectPath, "node_modules/wrangler"); + expect(wranglerPath).toExist(); + + // TODO: Before the refactor introduced in https://github.com/cloudflare/workers-sdk/pull/4754 + // we used to test the packageJson scripts transformations here, try to re-implement such + // checks (might be harder given the switch to a transform function compared to the old + // object based substitution) + + return { output }; +}; + +const testDevScript = async ( + framework: string, + projectPath: string, + ctx: TestContext +) => { + const template = frameworkMap[framework as FrameworkName]; + + // Copy over any test fixture files + const fixturePath = join(__dirname, "fixtures", framework); + await cp(fixturePath, projectPath, { recursive: true, force: true }); + + // Run the devserver on a random port to avoid colliding with other tests + const TEST_PORT = Math.ceil(Math.random() * 1000) + 20000; + + const { name: pm } = detectPackageManager(); + const proc = spawnWithLogging( + pm, + ["run", template.devScript as string, "--port", `${TEST_PORT}`], + { + cwd: projectPath, + env: { + NODE_ENV: "development", + }, + }, + ctx + ); + + // Wait a few seconds for dev server to spin up + await sleep(14000); + + // By convention and for simplicity of testing, each test fixture will + // make a page or a simple api route on `/test` that will print a bound + // environment variable set to the value "C3_TEST" + const res = await fetch(`http://localhost:${TEST_PORT}/test`); + const body = await res.text(); + expect(body).toContain("C3_TEST"); + + // Kill the process gracefully so ports can be cleaned up + proc.kill("SIGINT"); + + // Wait for a second to allow process to exit cleanly. Otherwise, the port might + // end up camped and cause future runs to fail + await sleep(1000); +}; diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index 1f839e88ed52..39573550bb2c 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -1,5 +1,11 @@ +import { + ProcessEnvOptions, + SpawnOptions, + SpawnOptionsWithoutStdio, +} from "child_process"; import { createWriteStream, + existsSync, mkdirSync, mkdtempSync, realpathSync, @@ -47,6 +53,24 @@ export type RunnerConfig = { ctx: TestContext; }; +const testEnv = { + ...process.env, + // The following env vars are set to ensure that package managers + // do not use the same global cache and accidentally hit race conditions. + YARN_CACHE_FOLDER: "./.yarn/cache", + YARN_ENABLE_GLOBAL_CACHE: "false", + PNPM_HOME: "./.pnpm", + npm_config_cache: "./.npm/cache", +}; + +const createTestLogStream = (ctx: TestContext) => { + // The .ansi extension allows for editor extensions that format ansi terminal codes + const fileName = `${normalizeTestName(ctx)}.ansi`; + return createWriteStream(join(getLogPath(ctx.task.suite), fileName), { + flags: "a", + }); +}; + export const runC3 = async ({ argv = [], promptHandlers = [], @@ -54,19 +78,7 @@ export const runC3 = async ({ }: RunnerConfig) => { const cmd = "node"; const args = ["./dist/cli.js", ...argv]; - const proc = spawn(cmd, args, { - env: { - ...process.env, - // The following env vars are set to ensure that package managers - // do not use the same global cache and accidentally hit race conditions. - YARN_CACHE_FOLDER: "./.yarn/cache", - YARN_ENABLE_GLOBAL_CACHE: "false", - PNPM_HOME: "./.pnpm", - npm_config_cache: "./.npm/cache", - }, - }); - - promptHandlers = [...promptHandlers]; + const proc = spawn(cmd, args, { env: testEnv }); const { name: pm } = detectPackageManager(); @@ -75,11 +87,7 @@ export const runC3 = async ({ promptHandlers = promptHandlers && [...promptHandlers]; - // The .ansi extension allows for editor extensions that format ansi terminal codes - const logFilename = `${normalizeTestName(ctx)}.ansi`; - const logStream = createWriteStream( - join(getLogPath(ctx.task.suite), logFilename) - ); + const logStream = createTestLogStream(ctx); logStream.write( `Running C3 with command: \`${quoteShellArgs([ @@ -151,6 +159,46 @@ export const runC3 = async ({ }; }; +export const spawnWithLogging = ( + cmd: string, + argv: string[] = [], + opts: SpawnOptionsWithoutStdio, + ctx: TestContext +) => { + const proc = spawn(cmd, argv, { + ...opts, + env: { + ...testEnv, + ...opts.env, + }, + }); + + const logStream = createTestLogStream(ctx); + + logStream.write(`Running command: ${[cmd, ...argv]}\n\n`); + + proc.stdout.on("data", (data) => { + const lines: string[] = data.toString().split("\n"); + + lines.forEach(async (line) => { + const stripped = stripAnsi(line).trim(); + if (stripped.length > 0) { + logStream.write(`${stripped}\n`); + } + }); + }); + + proc.stderr.on("data", (data) => { + logStream.write(data); + }); + + proc.on("close", () => { + logStream.close(); + }); + + return proc; +}; + export const recreateLogFolder = (suite: Suite) => { // Clean the old folder if exists (useful for dev) rmSync(getLogPath(suite), { diff --git a/packages/create-cloudflare/tsconfig.json b/packages/create-cloudflare/tsconfig.json index 465ea5cdbcab..0168b9a96931 100644 --- a/packages/create-cloudflare/tsconfig.json +++ b/packages/create-cloudflare/tsconfig.json @@ -5,6 +5,7 @@ "exclude": [ "node_modules", "dist", + "e2e-tests/fixtures/*", // exclude all template files other than the top level ones so // that we can catch `c3.ts`. For example, any top level files in // templates/angular/ will be included, but any directories will not From bc51957ba09a2f4570b779affafd7b3d42992337 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Sat, 3 Feb 2024 00:29:47 -0600 Subject: [PATCH 04/13] Refactor framework e2e verification helpers --- .../e2e-tests/frameworks.test.ts | 129 ++++++++---------- .../create-cloudflare/e2e-tests/helpers.ts | 9 +- 2 files changed, 59 insertions(+), 79 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index ce206db577a8..e667835423b5 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -33,8 +33,6 @@ type FrameworkTestConfig = Omit & { shouldTestDevScript?: boolean; }; -let frameworkMap: FrameworkMap; - // These are ordered based on speed and reliability for ease of debugging const frameworkTests: Record = { astro: { @@ -159,6 +157,8 @@ const frameworkTests: Record = { }; describe.concurrent(`E2E: Web frameworks`, () => { + let frameworkMap: FrameworkMap; + beforeAll(async (ctx) => { frameworkMap = await getFrameworkMap(); recreateLogFolder(ctx as Suite); @@ -194,16 +194,37 @@ describe.concurrent(`E2E: Web frameworks`, () => { const projectPath = getPath(framework); const projectName = getName(framework); const frameworkConfig = frameworkMap[framework as FrameworkName]; + + const { argv, overrides, promptHandlers, expectResponseToContain } = + frameworkTests[framework]; + try { - await verifyDeployment( - framework, - projectName, - projectPath, + const deploymentUrl = await runC3WithDeploy(framework, projectPath, { ctx, - testCommitMessage - ); + overrides, + promptHandlers, + argv: [...(argv ?? [])], + }); + + // Relevant project files should have been created + expect(projectPath).toExist(); + const pkgJsonPath = join(projectPath, "package.json"); + expect(pkgJsonPath).toExist(); + + // Wrangler should be installed + const wranglerPath = join(projectPath, "node_modules/wrangler"); + expect(wranglerPath).toExist(); + + if (testCommitMessage) { + await testDeploymentCommitMessage(projectName, framework); + } + + // Make a request to the deployed project and verify it was successful + await verifyDeployment(deploymentUrl, expectResponseToContain); + + // If configured, run the dev script and verify bindings work if (shouldTestDevScript) { - await testDevScript(framework, projectPath, ctx); + await verifyDevScript(framework, projectPath, ctx); } } finally { clean(framework); @@ -215,8 +236,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { } } }, - // { retry: 1, timeout: timeout || TEST_TIMEOUT } - { retry: 0, timeout: timeout || TEST_TIMEOUT } + { retry: 1, timeout: timeout || TEST_TIMEOUT } ); }); @@ -225,54 +245,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { // }); }); -// TODO: Refactor to a function that returns the deployment URL so expectation can be -// done in the actual test -const verifyDeployment = async ( - framework: string, - projectName: string, - projectPath: string, - ctx: TestContext, - testCommitMessage: boolean -) => { - const { argv, overrides, promptHandlers, expectResponseToContain } = - frameworkTests[framework]; - - const { output } = await runCli(framework, projectPath, { - ctx, - overrides, - promptHandlers, - argv: [...(argv ?? [])], - }); - - // Verify deployment - const deployedUrlRe = - /deployment is ready at: (https:\/\/.+\.(pages|workers)\.dev)/; - - const match = output.match(deployedUrlRe); - if (!match || !match[1]) { - expect(false, "Couldn't find deployment url in C3 output").toBe(true); - return; - } - - const projectUrl = match[1]; - - await retry({ times: 5 }, async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second - const res = await fetch(projectUrl); - const body = await res.text(); - if (!body.includes(expectResponseToContain)) { - throw new Error( - `(${framework}) Deployed page (${projectUrl}) didn't contain expected string: "${expectResponseToContain}"` - ); - } - }); - - if (testCommitMessage) { - await testDeploymentCommitMessage(projectName, framework); - } -}; - -const runCli = async ( +const runC3WithDeploy = async ( framework: string, projectPath: string, { ctx, argv = [], promptHandlers = [] }: RunnerConfig @@ -297,28 +270,40 @@ const runCli = async ( outputPrefix: `[${framework}]`, }); - // Relevant project files should have been created - expect(projectPath).toExist(); - const pkgJsonPath = join(projectPath, "package.json"); - expect(pkgJsonPath).toExist(); + const deployedUrlRe = + /deployment is ready at: (https:\/\/.+\.(pages|workers)\.dev)/; - // Wrangler should be installed - const wranglerPath = join(projectPath, "node_modules/wrangler"); - expect(wranglerPath).toExist(); + const match = output.match(deployedUrlRe); + if (!match || !match[1]) { + expect(false, "Couldn't find deployment url in C3 output").toBe(true); + return ""; + } - // TODO: Before the refactor introduced in https://github.com/cloudflare/workers-sdk/pull/4754 - // we used to test the packageJson scripts transformations here, try to re-implement such - // checks (might be harder given the switch to a transform function compared to the old - // object based substitution) + return match[1]; +}; - return { output }; +const verifyDeployment = async ( + deploymentUrl: string, + expectedToken: string +) => { + await retry({ times: 5 }, async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second + const res = await fetch(deploymentUrl); + const body = await res.text(); + if (!body.includes(expectedToken)) { + throw new Error( + `Deployed page (${deploymentUrl}) didn't contain expected string: "${expectedToken}"` + ); + } + }); }; -const testDevScript = async ( +const verifyDevScript = async ( framework: string, projectPath: string, ctx: TestContext ) => { + const frameworkMap = await getFrameworkMap(); const template = frameworkMap[framework as FrameworkName]; // Copy over any test fixture files diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index 39573550bb2c..9b04e403c3a1 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -1,11 +1,5 @@ -import { - ProcessEnvOptions, - SpawnOptions, - SpawnOptionsWithoutStdio, -} from "child_process"; import { createWriteStream, - existsSync, mkdirSync, mkdtempSync, realpathSync, @@ -23,6 +17,7 @@ import { fetch } from "undici"; import { expect } from "vitest"; import { version } from "../package.json"; import { quoteShellArgs } from "../src/common"; +import type { SpawnOptionsWithoutStdio } from "child_process"; import type { Suite, TestContext } from "vitest"; export const C3_E2E_PREFIX = "c3-e2e-"; @@ -175,7 +170,7 @@ export const spawnWithLogging = ( const logStream = createTestLogStream(ctx); - logStream.write(`Running command: ${[cmd, ...argv]}\n\n`); + logStream.write(`\nRunning command: ${[cmd, ...argv].join(" ")}\n\n`); proc.stdout.on("data", (data) => { const lines: string[] = data.toString().split("\n"); From 08d89fbc7b66bee59d24beb36976ef0a7763e8af Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Sun, 4 Feb 2024 11:19:05 -0600 Subject: [PATCH 05/13] Add support for verifying build script in framework e2es --- .../e2e-tests/frameworks.test.ts | 97 ++++++++++++++++--- .../create-cloudflare/e2e-tests/helpers.ts | 47 +++++++++ 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index e667835423b5..8844f64e15d6 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -14,6 +14,7 @@ import { recreateLogFolder, runC3, spawnWithLogging, + spawnWithLoggingAwaitable, testDeploymentCommitMessage, testProjectDir, } from "./helpers"; @@ -30,7 +31,11 @@ type FrameworkTestConfig = Omit & { timeout?: number; unsupportedPms?: string[]; unsupportedOSs?: string[]; - shouldTestDevScript?: boolean; + testBindings?: boolean; + build?: { + outputDir: string; + script: string; + }; }; // These are ordered based on speed and reliability for ease of debugging @@ -79,7 +84,11 @@ const frameworkTests: Record = { testCommitMessage: true, unsupportedOSs: ["win32"], unsupportedPms: ["yarn"], - shouldTestDevScript: true, + testBindings: true, + build: { + outputDir: "./dist", + script: "build", + }, }, remix: { expectResponseToContain: "Welcome to Remix", @@ -171,7 +180,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { testCommitMessage, unsupportedPms, unsupportedOSs, - shouldTestDevScript, + testBindings, } = frameworkTests[framework]; const quarantineModeMatch = isQuarantineMode() == (quarantine ?? false); @@ -185,6 +194,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { // Skip if the package manager is unsupported shouldRun &&= !unsupportedPms?.includes(process.env.TEST_PM ?? ""); + // Skip if the OS is unsupported shouldRun &&= !unsupportedOSs?.includes(process.platform); test.runIf(shouldRun)( @@ -223,8 +233,16 @@ describe.concurrent(`E2E: Web frameworks`, () => { await verifyDeployment(deploymentUrl, expectResponseToContain); // If configured, run the dev script and verify bindings work - if (shouldTestDevScript) { + if (testBindings) { + // Copy over any test fixture files + const fixturePath = join(__dirname, "fixtures", framework); + await cp(fixturePath, projectPath, { + recursive: true, + force: true, + }); + await verifyDevScript(framework, projectPath, ctx); + await verifyBuildScript(framework, projectPath, ctx); } } finally { clean(framework); @@ -239,10 +257,6 @@ describe.concurrent(`E2E: Web frameworks`, () => { { retry: 1, timeout: timeout || TEST_TIMEOUT } ); }); - - // test.skip("Hono (wrangler defaults)", async (ctx) => { - // await runCli("hono", { ctx, argv: ["--wrangler-defaults"] }); - // }); }); const runC3WithDeploy = async ( @@ -306,10 +320,6 @@ const verifyDevScript = async ( const frameworkMap = await getFrameworkMap(); const template = frameworkMap[framework as FrameworkName]; - // Copy over any test fixture files - const fixturePath = join(__dirname, "fixtures", framework); - await cp(fixturePath, projectPath, { recursive: true, force: true }); - // Run the devserver on a random port to avoid colliding with other tests const TEST_PORT = Math.ceil(Math.random() * 1000) + 20000; @@ -327,14 +337,13 @@ const verifyDevScript = async ( ); // Wait a few seconds for dev server to spin up - await sleep(14000); + await sleep(4000); // By convention and for simplicity of testing, each test fixture will // make a page or a simple api route on `/test` that will print a bound // environment variable set to the value "C3_TEST" const res = await fetch(`http://localhost:${TEST_PORT}/test`); const body = await res.text(); - expect(body).toContain("C3_TEST"); // Kill the process gracefully so ports can be cleaned up proc.kill("SIGINT"); @@ -342,4 +351,64 @@ const verifyDevScript = async ( // Wait for a second to allow process to exit cleanly. Otherwise, the port might // end up camped and cause future runs to fail await sleep(1000); + + expect(body).toContain("C3_TEST"); +}; + +const verifyBuildScript = async ( + framework: string, + projectPath: string, + ctx: TestContext +) => { + const { build } = frameworkTests[framework]; + + if (!build) { + throw Error( + "`build` must be specified in test config when verifying bindings" + ); + } + + const { outputDir, script } = build; + + // Run the build script + const { name: pm, npx } = detectPackageManager(); + await spawnWithLoggingAwaitable( + pm, + ["run", script], + { + cwd: projectPath, + }, + ctx + ); + + // Run wrangler dev on a random port to avoid colliding with other tests + const TEST_PORT = Math.ceil(Math.random() * 1000) + 20000; + + const devProc = spawnWithLogging( + npx, + ["wrangler", "pages", "dev", outputDir, "--port", `${TEST_PORT}`], + { + cwd: projectPath, + }, + ctx + ); + + // Wait a few seconds for dev server to spin up + await sleep(4000); + + // By convention and for simplicity of testing, each test fixture will + // make a page or a simple api route on `/test` that will print a bound + // environment variable set to the value "C3_TEST" + const res = await fetch(`http://localhost:${TEST_PORT}/test`); + const body = await res.text(); + + // Kill the process gracefully so ports can be cleaned up + devProc.kill("SIGINT"); + + // Wait for a second to allow process to exit cleanly. Otherwise, the port might + // end up camped and cause future runs to fail + await sleep(1000); + + // Verify expectation after killing the process so that it exits cleanly in case of failure + expect(body).toContain("C3_TEST"); }; diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index 9b04e403c3a1..bb2af0a4bc6b 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -194,6 +194,53 @@ export const spawnWithLogging = ( return proc; }; +export const spawnWithLoggingAwaitable = async ( + cmd: string, + argv: string[] = [], + opts: SpawnOptionsWithoutStdio, + ctx: TestContext +) => { + const proc = spawnWithLogging(cmd, argv, opts, ctx); + + const stdout: string[] = []; + const stderr: string[] = []; + + await new Promise((resolve, rejects) => { + proc.stdout.on("data", (data) => { + const lines: string[] = data.toString().split("\n"); + + lines.forEach(async (line) => { + stdout.push(line); + }); + }); + + proc.stderr.on("data", (data) => { + stderr.push(data); + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(null); + } else { + rejects(code); + } + }); + + proc.on("error", (exitCode) => { + rejects({ + exitCode, + output: stdout.join("\n").trim(), + errors: stderr.join("\n").trim(), + }); + }); + }); + + return { + output: stdout.join("\n").trim(), + errors: stderr.join("\n").trim(), + }; +}; + export const recreateLogFolder = (suite: Suite) => { // Clean the old folder if exists (useful for dev) rmSync(getLogPath(suite), { From f3323dda214f087f0fad7a1671261dce81846ff9 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Sun, 4 Feb 2024 12:46:15 -0600 Subject: [PATCH 06/13] Refactor e2e helpers --- .../e2e-tests/frameworks.test.ts | 67 +++--- .../create-cloudflare/e2e-tests/helpers.ts | 191 +++++++----------- .../e2e-tests/workers.test.ts | 29 +-- 3 files changed, 127 insertions(+), 160 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 8844f64e15d6..cf91a50d8de9 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -4,23 +4,25 @@ import { retry } from "helpers/command"; import { sleep } from "helpers/common"; import { detectPackageManager } from "helpers/packages"; import { fetch } from "undici"; -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import { deleteProject, deleteWorker } from "../scripts/common"; import { getFrameworkMap } from "../src/templates"; import { frameworkToTest } from "./frameworkToTest"; import { + createTestLogStream, isQuarantineMode, keys, recreateLogFolder, runC3, spawnWithLogging, - spawnWithLoggingAwaitable, testDeploymentCommitMessage, testProjectDir, + waitForExit, } from "./helpers"; import type { FrameworkMap, FrameworkName } from "../src/templates"; import type { RunnerConfig } from "./helpers"; -import type { Suite, TestContext } from "vitest"; +import type { WriteStream } from "fs"; +import type { Suite } from "vitest"; const TEST_TIMEOUT = 1000 * 60 * 5; const LONG_TIMEOUT = 1000 * 60 * 10; @@ -167,12 +169,17 @@ const frameworkTests: Record = { describe.concurrent(`E2E: Web frameworks`, () => { let frameworkMap: FrameworkMap; + let logStream: WriteStream; beforeAll(async (ctx) => { frameworkMap = await getFrameworkMap(); recreateLogFolder(ctx as Suite); }); + beforeEach(async (ctx) => { + logStream = createTestLogStream(ctx); + }); + Object.keys(frameworkTests).forEach((framework) => { const { quarantine, @@ -199,7 +206,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { shouldRun &&= !unsupportedOSs?.includes(process.platform); test.runIf(shouldRun)( framework, - async (ctx) => { + async () => { const { getPath, getName, clean } = testProjectDir("pages"); const projectPath = getPath(framework); const projectName = getName(framework); @@ -209,12 +216,16 @@ describe.concurrent(`E2E: Web frameworks`, () => { frameworkTests[framework]; try { - const deploymentUrl = await runC3WithDeploy(framework, projectPath, { - ctx, - overrides, - promptHandlers, - argv: [...(argv ?? [])], - }); + const deploymentUrl = await runC3WithDeploy( + framework, + projectPath, + logStream, + { + argv: [...(argv ?? [])], + overrides, + promptHandlers, + } + ); // Relevant project files should have been created expect(projectPath).toExist(); @@ -241,8 +252,8 @@ describe.concurrent(`E2E: Web frameworks`, () => { force: true, }); - await verifyDevScript(framework, projectPath, ctx); - await verifyBuildScript(framework, projectPath, ctx); + await verifyDevScript(framework, projectPath, logStream); + await verifyBuildScript(framework, projectPath, logStream); } } finally { clean(framework); @@ -262,7 +273,8 @@ describe.concurrent(`E2E: Web frameworks`, () => { const runC3WithDeploy = async ( framework: string, projectPath: string, - { ctx, argv = [], promptHandlers = [] }: RunnerConfig + logStream: WriteStream, + { argv = [], promptHandlers = [] }: RunnerConfig ) => { const args = [ projectPath, @@ -277,12 +289,7 @@ const runC3WithDeploy = async ( args.push(...argv); - const { output } = await runC3({ - ctx, - argv: args, - promptHandlers, - outputPrefix: `[${framework}]`, - }); + const { output } = await runC3(args, promptHandlers, logStream); const deployedUrlRe = /deployment is ready at: (https:\/\/.+\.(pages|workers)\.dev)/; @@ -315,7 +322,7 @@ const verifyDeployment = async ( const verifyDevScript = async ( framework: string, projectPath: string, - ctx: TestContext + logStream: WriteStream ) => { const frameworkMap = await getFrameworkMap(); const template = frameworkMap[framework as FrameworkName]; @@ -325,15 +332,14 @@ const verifyDevScript = async ( const { name: pm } = detectPackageManager(); const proc = spawnWithLogging( - pm, - ["run", template.devScript as string, "--port", `${TEST_PORT}`], + [pm, "run", template.devScript as string, "--port", `${TEST_PORT}`], { cwd: projectPath, env: { NODE_ENV: "development", }, }, - ctx + logStream ); // Wait a few seconds for dev server to spin up @@ -358,7 +364,7 @@ const verifyDevScript = async ( const verifyBuildScript = async ( framework: string, projectPath: string, - ctx: TestContext + logStream: WriteStream ) => { const { build } = frameworkTests[framework]; @@ -372,25 +378,24 @@ const verifyBuildScript = async ( // Run the build script const { name: pm, npx } = detectPackageManager(); - await spawnWithLoggingAwaitable( - pm, - ["run", script], + const buildProc = spawnWithLogging( + [pm, "run", script], { cwd: projectPath, }, - ctx + logStream ); + await waitForExit(buildProc); // Run wrangler dev on a random port to avoid colliding with other tests const TEST_PORT = Math.ceil(Math.random() * 1000) + 20000; const devProc = spawnWithLogging( - npx, - ["wrangler", "pages", "dev", outputDir, "--port", `${TEST_PORT}`], + [npx, "wrangler", "pages", "dev", outputDir, "--port", `${TEST_PORT}`], { cwd: projectPath, }, - ctx + logStream ); // Wait a few seconds for dev server to spin up diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index bb2af0a4bc6b..4a64f307769c 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -12,12 +12,14 @@ import { stripAnsi } from "@cloudflare/cli"; import { spawn } from "cross-spawn"; import { retry } from "helpers/command"; import { sleep } from "helpers/common"; -import { detectPackageManager } from "helpers/packages"; import { fetch } from "undici"; import { expect } from "vitest"; import { version } from "../package.json"; -import { quoteShellArgs } from "../src/common"; -import type { SpawnOptionsWithoutStdio } from "child_process"; +import type { + ChildProcessWithoutNullStreams, + SpawnOptionsWithoutStdio, +} from "child_process"; +import type { WriteStream } from "fs"; import type { Suite, TestContext } from "vitest"; export const C3_E2E_PREFIX = "c3-e2e-"; @@ -32,6 +34,16 @@ export const keys = { left: "\x1b\x5b\x44", }; +const testEnv = { + ...process.env, + // The following env vars are set to ensure that package managers + // do not use the same global cache and accidentally hit race conditions. + YARN_CACHE_FOLDER: "./.yarn/cache", + YARN_ENABLE_GLOBAL_CACHE: "false", + PNPM_HOME: "./.pnpm", + npm_config_cache: "./.npm/cache", +}; + export type PromptHandler = { matcher: RegExp; input: string[]; @@ -43,123 +55,64 @@ export type RunnerConfig = { }; promptHandlers?: PromptHandler[]; argv?: string[]; - outputPrefix?: string; quarantine?: boolean; - ctx: TestContext; -}; - -const testEnv = { - ...process.env, - // The following env vars are set to ensure that package managers - // do not use the same global cache and accidentally hit race conditions. - YARN_CACHE_FOLDER: "./.yarn/cache", - YARN_ENABLE_GLOBAL_CACHE: "false", - PNPM_HOME: "./.pnpm", - npm_config_cache: "./.npm/cache", }; -const createTestLogStream = (ctx: TestContext) => { - // The .ansi extension allows for editor extensions that format ansi terminal codes - const fileName = `${normalizeTestName(ctx)}.ansi`; - return createWriteStream(join(getLogPath(ctx.task.suite), fileName), { - flags: "a", - }); -}; - -export const runC3 = async ({ - argv = [], - promptHandlers = [], - ctx, -}: RunnerConfig) => { - const cmd = "node"; - const args = ["./dist/cli.js", ...argv]; - const proc = spawn(cmd, args, { env: testEnv }); - - const { name: pm } = detectPackageManager(); - - const stdout: string[] = []; - const stderr: string[] = []; +export const runC3 = async ( + argv: string[] = [], + promptHandlers: PromptHandler[] = [], + logStream: WriteStream +) => { + const cmd = ["node", "./dist/cli.js", ...argv]; + const proc = spawnWithLogging(cmd, { env: testEnv }, logStream); + // Clone the prompt handlers so we can consume them destructively promptHandlers = promptHandlers && [...promptHandlers]; - const logStream = createTestLogStream(ctx); - - logStream.write( - `Running C3 with command: \`${quoteShellArgs([ - cmd, - ...args, - ])}\` (using ${pm})\n\n` - ); - - await new Promise((resolve, rejects) => { - proc.stdout.on("data", (data) => { - const lines: string[] = data.toString().split("\n"); - const currentDialog = promptHandlers[0]; - - lines.forEach(async (line) => { - stdout.push(line); - - const stripped = stripAnsi(line).trim(); - if (stripped.length > 0) { - logStream.write(`${stripped}\n`); - } + const onData = (data: string) => { + const lines: string[] = data.toString().split("\n"); + const currentDialog = promptHandlers[0]; - if (currentDialog && currentDialog.matcher.test(line)) { - // Add a small sleep to avoid input race - await sleep(1000); + lines.forEach(async (line) => { + if (currentDialog && currentDialog.matcher.test(line)) { + // Add a small sleep to avoid input race + await sleep(1000); - currentDialog.input.forEach((keystroke) => { - proc.stdin.write(keystroke); - }); + currentDialog.input.forEach((keystroke) => { + proc.stdin.write(keystroke); + }); - // Consume the handler once we've used it - promptHandlers.shift(); + // Consume the handler once we've used it + promptHandlers.shift(); - // If we've consumed the last prompt handler, close the input stream - // Otherwise, the process wont exit properly - if (promptHandlers[0] === undefined) { - proc.stdin.end(); - } + // If we've consumed the last prompt handler, close the input stream + // Otherwise, the process wont exit properly + if (promptHandlers[0] === undefined) { + proc.stdin.end(); } - }); - }); - - proc.stderr.on("data", (data) => { - logStream.write(data); - stderr.push(data); - }); - - proc.on("close", (code) => { - logStream.close(); - - if (code === 0) { - resolve(null); - } else { - rejects(code); } }); - - proc.on("error", (exitCode) => { - rejects({ - exitCode, - output: stdout.join("\n").trim(), - errors: stderr.join("\n").trim(), - }); - }); - }); - - return { - output: stdout.join("\n").trim(), - errors: stderr.join("\n").trim(), }; + + return waitForExit(proc, onData); }; +/** + * Spawn a child process and attach a handler that will log any output from + * `stdout` or errors from `stderror` to a dedicated log file. + * + * @param args The command and arguments as an array + * @param opts Additional options to be passed to the `spawn` call + * @param logStream A write stream to the log file for the test + * @returns the child process that was created + */ export const spawnWithLogging = ( - cmd: string, - argv: string[] = [], + args: string[], opts: SpawnOptionsWithoutStdio, - ctx: TestContext + logStream: WriteStream ) => { + const [cmd, ...argv] = args; + const proc = spawn(cmd, argv, { ...opts, env: { @@ -168,8 +121,6 @@ export const spawnWithLogging = ( }, }); - const logStream = createTestLogStream(ctx); - logStream.write(`\nRunning command: ${[cmd, ...argv].join(" ")}\n\n`); proc.stdout.on("data", (data) => { @@ -194,24 +145,26 @@ export const spawnWithLogging = ( return proc; }; -export const spawnWithLoggingAwaitable = async ( - cmd: string, - argv: string[] = [], - opts: SpawnOptionsWithoutStdio, - ctx: TestContext +/** + * An async function that waits on a spawned process to run to completion, collecting + * any output or errors from `stdout` and `stderr`, respectively. + * + * @param proc The child process to wait for + * @param onData An optional handler to be called on `stdout.on('data')` + */ +export const waitForExit = async ( + proc: ChildProcessWithoutNullStreams, + onData?: (chunk: string) => void ) => { - const proc = spawnWithLogging(cmd, argv, opts, ctx); - const stdout: string[] = []; const stderr: string[] = []; await new Promise((resolve, rejects) => { proc.stdout.on("data", (data) => { - const lines: string[] = data.toString().split("\n"); - - lines.forEach(async (line) => { - stdout.push(line); - }); + stdout.push(data); + if (onData) { + onData(data); + } }); proc.stderr.on("data", (data) => { @@ -241,6 +194,14 @@ export const spawnWithLoggingAwaitable = async ( }; }; +export const createTestLogStream = (ctx: TestContext) => { + // The .ansi extension allows for editor extensions that format ansi terminal codes + const fileName = `${normalizeTestName(ctx)}.ansi`; + return createWriteStream(join(getLogPath(ctx.task.suite), fileName), { + flags: "a", + }); +}; + export const recreateLogFolder = (suite: Suite) => { // Clean the old folder if exists (useful for dev) rmSync(getLogPath(suite), { diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index cc63b176d5c5..35a0b2123938 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -2,17 +2,19 @@ import { join } from "path"; import { retry } from "helpers/command"; import { readToml } from "helpers/files"; import { fetch } from "undici"; -import { beforeAll, describe, expect, test } from "vitest"; +import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import { deleteWorker } from "../scripts/common"; import { frameworkToTest } from "./frameworkToTest"; import { + createTestLogStream, isQuarantineMode, recreateLogFolder, runC3, testProjectDir, } from "./helpers"; import type { RunnerConfig } from "./helpers"; -import type { Suite, TestContext } from "vitest"; +import type { WriteStream } from "fs"; +import type { Suite } from "vitest"; const TEST_TIMEOUT = 1000 * 60 * 5; @@ -51,25 +53,26 @@ describe }, ]; + let logStream: WriteStream; + beforeAll((ctx) => { recreateLogFolder(ctx as Suite); }); + beforeEach(async (ctx) => { + logStream = createTestLogStream(ctx); + }); + const runCli = async ( template: string, projectPath: string, - { ctx, argv = [], promptHandlers = [] }: RunnerConfig + { argv = [], promptHandlers = [] }: RunnerConfig ) => { const args = [projectPath, "--type", template, "--no-open", "--no-git"]; args.push(...argv); - const { output } = await runC3({ - ctx, - argv: args, - promptHandlers, - outputPrefix: `[${template}]`, - }); + const { output } = await runC3(args, promptHandlers, logStream); // Relevant project files should have been created expect(projectPath).toExist(); @@ -95,14 +98,12 @@ describe const runCliWithDeploy = async ( template: WorkerTestConfig, - projectPath: string, - ctx: TestContext + projectPath: string ) => { const { argv, overrides, promptHandlers, expectResponseToContain } = template; const { output } = await runCli(template.template, projectPath, { - ctx, overrides, promptHandlers, argv: [ @@ -170,12 +171,12 @@ describe const name = template.name ?? template.template; test( name, - async (ctx) => { + async () => { const { getPath, getName, clean } = testProjectDir("workers"); const projectPath = getPath(name); const projectName = getName(name); try { - await runCliWithDeploy(template, projectPath, ctx); + await runCliWithDeploy(template, projectPath); } finally { clean(name); await deleteWorker(projectName); From 1d4286bd250241ad3324d9b9a65665f701681086 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Mon, 5 Feb 2024 10:34:07 -0600 Subject: [PATCH 07/13] Refactor workers e2e tests to re-align with frameworks tests --- .../e2e-tests/frameworks.test.ts | 4 +- .../e2e-tests/workers.test.ts | 214 +++++++++--------- 2 files changed, 113 insertions(+), 105 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index cf91a50d8de9..96c6c41db45f 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -216,7 +216,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { frameworkTests[framework]; try { - const deploymentUrl = await runC3WithDeploy( + const deploymentUrl = await runCli( framework, projectPath, logStream, @@ -270,7 +270,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { }); }); -const runC3WithDeploy = async ( +const runCli = async ( framework: string, projectPath: string, logStream: WriteStream, diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index 35a0b2123938..705a0d54beb9 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -24,35 +24,36 @@ type WorkerTestConfig = Omit & { name?: string; template: string; }; + +const workerTemplates: WorkerTestConfig[] = [ + { + expectResponseToContain: "Hello World!", + template: "hello-world", + }, + { + template: "common", + expectResponseToContain: "Try making requests to:", + }, + { + template: "queues", + // Skipped for now, since C3 does not yet support resource creation + // expectResponseToContain: + }, + { + template: "scheduled", + // Skipped for now, since it's not possible to test scheduled events on deployed Workers + // expectResponseToContain: + }, + { + template: "openapi", + expectResponseToContain: "SwaggerUI", + promptHandlers: [], + }, +]; + describe .skipIf(frameworkToTest || isQuarantineMode() || process.platform === "win32") .concurrent(`E2E: Workers templates`, () => { - const workerTemplates: WorkerTestConfig[] = [ - { - expectResponseToContain: "Hello World!", - template: "hello-world", - }, - { - template: "common", - expectResponseToContain: "Try making requests to:", - }, - { - template: "queues", - // Skipped for now, since C3 does not yet support resource creation - // expectResponseToContain: - }, - { - template: "scheduled", - // Skipped for now, since it's not possible to test scheduled events on deployed Workers - // expectResponseToContain: - }, - { - template: "openapi", - expectResponseToContain: "SwaggerUI", - promptHandlers: [], - }, - ]; - let logStream: WriteStream; beforeAll((ctx) => { @@ -63,82 +64,6 @@ describe logStream = createTestLogStream(ctx); }); - const runCli = async ( - template: string, - projectPath: string, - { argv = [], promptHandlers = [] }: RunnerConfig - ) => { - const args = [projectPath, "--type", template, "--no-open", "--no-git"]; - - args.push(...argv); - - const { output } = await runC3(args, promptHandlers, logStream); - - // Relevant project files should have been created - expect(projectPath).toExist(); - - const gitignorePath = join(projectPath, ".gitignore"); - expect(gitignorePath).toExist(); - - const pkgJsonPath = join(projectPath, "package.json"); - expect(pkgJsonPath).toExist(); - - const wranglerPath = join(projectPath, "node_modules/wrangler"); - expect(wranglerPath).toExist(); - - const tomlPath = join(projectPath, "wrangler.toml"); - expect(tomlPath).toExist(); - - const config = readToml(tomlPath) as { main: string }; - - expect(join(projectPath, config.main)).toExist(); - - return { output }; - }; - - const runCliWithDeploy = async ( - template: WorkerTestConfig, - projectPath: string - ) => { - const { argv, overrides, promptHandlers, expectResponseToContain } = - template; - - const { output } = await runCli(template.template, projectPath, { - overrides, - promptHandlers, - argv: [ - // Skip deployment if the test config has no response expectation - expectResponseToContain ? "--deploy" : "--no-deploy", - ...(argv ?? []), - ], - }); - - if (expectResponseToContain) { - // Verify deployment - const deployedUrlRe = - /deployment is ready at: (https:\/\/.+\.(workers)\.dev)/; - - const match = output.match(deployedUrlRe); - if (!match || !match[1]) { - expect(false, "Couldn't find deployment url in C3 output").toBe(true); - return; - } - - const projectUrl = match[1]; - - await retry({ times: 5 }, async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second - const res = await fetch(projectUrl); - const body = await res.text(); - if (!body.includes(expectResponseToContain)) { - throw new Error( - `(${template}) Deployed page (${projectUrl}) didn't contain expected string: "${expectResponseToContain}"` - ); - } - }); - } - }; - workerTemplates .flatMap((template) => template.promptHandlers @@ -176,7 +101,36 @@ describe const projectPath = getPath(name); const projectName = getName(name); try { - await runCliWithDeploy(template, projectPath); + const deployedUrl = await runCli( + template, + projectPath, + logStream + ); + + // Relevant project files should have been created + expect(projectPath).toExist(); + + const gitignorePath = join(projectPath, ".gitignore"); + expect(gitignorePath).toExist(); + + const pkgJsonPath = join(projectPath, "package.json"); + expect(pkgJsonPath).toExist(); + + const wranglerPath = join(projectPath, "node_modules/wrangler"); + expect(wranglerPath).toExist(); + + const tomlPath = join(projectPath, "wrangler.toml"); + expect(tomlPath).toExist(); + + const config = readToml(tomlPath) as { main: string }; + expect(join(projectPath, config.main)).toExist(); + + if (deployedUrl) { + await verifyDeployment( + deployedUrl, + template.expectResponseToContain as string + ); + } } finally { clean(name); await deleteWorker(projectName); @@ -186,3 +140,57 @@ describe ); }); }); + +const runCli = async ( + template: WorkerTestConfig, + projectPath: string, + logStream: WriteStream +) => { + const { argv, promptHandlers, expectResponseToContain } = template; + + const deploy = Boolean(expectResponseToContain); + + const args = [ + projectPath, + "--type", + template.template, + "--no-open", + "--no-git", + deploy ? "--deploy" : "--no-deploy", + ...(argv ?? []), + ]; + + const { output } = await runC3(args, promptHandlers, logStream); + + if (!deploy) { + return null; + } + + // Verify deployment + const deployedUrlRe = + /deployment is ready at: (https:\/\/.+\.(workers)\.dev)/; + + const match = output.match(deployedUrlRe); + if (!match || !match[1]) { + expect(false, "Couldn't find deployment url in C3 output").toBe(true); + return; + } + + return match[1]; +}; + +const verifyDeployment = async ( + deploymentUrl: string, + expectedString: string +) => { + await retry({ times: 5 }, async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second + const res = await fetch(deploymentUrl); + const body = await res.text(); + if (!body.includes(expectedString)) { + throw new Error( + `(Deployed page (${deploymentUrl}) didn't contain expected string: "${expectedString}"` + ); + } + }); +}; From 4a85b4d12f48bb9cf7f2a93b19b61a774d4dadf4 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Mon, 5 Feb 2024 14:44:00 -0600 Subject: [PATCH 08/13] Refactor RunnerConfig in e2e tests --- .../e2e-tests/frameworks.test.ts | 149 ++++++++++++------ .../create-cloudflare/e2e-tests/helpers.ts | 8 +- .../e2e-tests/workers.test.ts | 37 ++--- 3 files changed, 126 insertions(+), 68 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 96c6c41db45f..2239c4b13076 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -1,3 +1,4 @@ +import { existsSync } from "fs"; import { cp } from "fs/promises"; import { join } from "path"; import { retry } from "helpers/command"; @@ -27,40 +28,51 @@ import type { Suite } from "vitest"; const TEST_TIMEOUT = 1000 * 60 * 5; const LONG_TIMEOUT = 1000 * 60 * 10; -type FrameworkTestConfig = Omit & { - expectResponseToContain: string; +type FrameworkTestConfig = RunnerConfig & { testCommitMessage: boolean; - timeout?: number; unsupportedPms?: string[]; unsupportedOSs?: string[]; - testBindings?: boolean; - build?: { + verifyDev?: { + route: string; + expectedToken: string; + }; + verifyBuild?: { outputDir: string; script: string; + route: string; + expectedToken: string; }; }; // These are ordered based on speed and reliability for ease of debugging const frameworkTests: Record = { astro: { - expectResponseToContain: "Hello, Astronaut!", testCommitMessage: true, unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedToken: "Hello, Astronaut!", + }, }, docusaurus: { - expectResponseToContain: "Dinosaurs are cool", unsupportedPms: ["bun"], testCommitMessage: true, unsupportedOSs: ["win32"], timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedToken: "Dinosaurs are cool", + }, }, angular: { - expectResponseToContain: "Congratulations! Your app is running.", testCommitMessage: true, timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedToken: "Congratulations! Your app is running.", + }, }, gatsby: { - expectResponseToContain: "Gatsby!", unsupportedPms: ["bun", "pnpm"], promptHandlers: [ { @@ -70,13 +82,19 @@ const frameworkTests: Record = { ], testCommitMessage: true, timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedToken: "Gatsby!", + }, }, hono: { - expectResponseToContain: "Hello Hono!", testCommitMessage: false, + verifyDeploy: { + route: "/", + expectedToken: "Hello Hono!", + }, }, qwik: { - expectResponseToContain: "Welcome to Qwik", promptHandlers: [ { matcher: /Yes looks good, finish update/, @@ -86,20 +104,31 @@ const frameworkTests: Record = { testCommitMessage: true, unsupportedOSs: ["win32"], unsupportedPms: ["yarn"], - testBindings: true, - build: { + verifyDeploy: { + route: "/", + expectedToken: "Welcome to Qwik", + }, + verifyDev: { + route: "/test", + expectedToken: "C3_TEST", + }, + verifyBuild: { outputDir: "./dist", script: "build", + route: "/test", + expectedToken: "C3_TEST", }, }, remix: { - expectResponseToContain: "Welcome to Remix", testCommitMessage: true, timeout: LONG_TIMEOUT, unsupportedPms: ["yarn"], + verifyDeploy: { + route: "/", + expectedToken: "Welcome to Remix", + }, }, next: { - expectResponseToContain: "Create Next App", promptHandlers: [ { matcher: /Do you want to use the next-on-pages eslint-plugin\?/, @@ -108,20 +137,29 @@ const frameworkTests: Record = { ], testCommitMessage: true, quarantine: true, + verifyDeploy: { + route: "/", + expectedToken: "Create Next App", + }, }, nuxt: { - expectResponseToContain: "Welcome to Nuxt!", testCommitMessage: true, timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedToken: "Welcome to Nuxt!", + }, }, react: { - expectResponseToContain: "React App", testCommitMessage: true, unsupportedOSs: ["win32"], timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedToken: "React App", + }, }, solid: { - expectResponseToContain: "Hello world", promptHandlers: [ { matcher: /Which template do you want to use/, @@ -139,9 +177,12 @@ const frameworkTests: Record = { testCommitMessage: true, timeout: LONG_TIMEOUT, unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedToken: "Hello world", + }, }, svelte: { - expectResponseToContain: "SvelteKit app", promptHandlers: [ { matcher: /Which Svelte app template/, @@ -159,11 +200,18 @@ const frameworkTests: Record = { testCommitMessage: true, unsupportedOSs: ["win32"], unsupportedPms: ["npm"], + verifyDeploy: { + route: "/", + expectedToken: "SvelteKit app", + }, }, vue: { - expectResponseToContain: "Vite App", testCommitMessage: true, unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedToken: "Vite App", + }, }, }; @@ -187,7 +235,6 @@ describe.concurrent(`E2E: Web frameworks`, () => { testCommitMessage, unsupportedPms, unsupportedOSs, - testBindings, } = frameworkTests[framework]; const quarantineModeMatch = isQuarantineMode() == (quarantine ?? false); @@ -212,9 +259,17 @@ describe.concurrent(`E2E: Web frameworks`, () => { const projectName = getName(framework); const frameworkConfig = frameworkMap[framework as FrameworkName]; - const { argv, overrides, promptHandlers, expectResponseToContain } = + const { argv, promptHandlers, verifyDeploy } = frameworkTests[framework]; + if (!verifyDeploy) { + expect( + true, + "A `deploy` configuration must be defined for all framework tests" + ).toBe(false); + return; + } + try { const deploymentUrl = await runCli( framework, @@ -222,7 +277,6 @@ describe.concurrent(`E2E: Web frameworks`, () => { logStream, { argv: [...(argv ?? [])], - overrides, promptHandlers, } ); @@ -241,20 +295,22 @@ describe.concurrent(`E2E: Web frameworks`, () => { } // Make a request to the deployed project and verify it was successful - await verifyDeployment(deploymentUrl, expectResponseToContain); + await verifyDeployment( + `${deploymentUrl}${verifyDeploy.route}`, + verifyDeploy.expectedToken + ); - // If configured, run the dev script and verify bindings work - if (testBindings) { - // Copy over any test fixture files - const fixturePath = join(__dirname, "fixtures", framework); + // Copy over any test fixture files + const fixturePath = join(__dirname, "fixtures", framework); + if (existsSync(fixturePath)) { await cp(fixturePath, projectPath, { recursive: true, force: true, }); - - await verifyDevScript(framework, projectPath, logStream); - await verifyBuildScript(framework, projectPath, logStream); } + + await verifyDevScript(framework, projectPath, logStream); + await verifyBuildScript(framework, projectPath, logStream); } finally { clean(framework); // Cleanup the project in case we need to retry it @@ -324,6 +380,11 @@ const verifyDevScript = async ( projectPath: string, logStream: WriteStream ) => { + const { verifyDev } = frameworkTests[framework]; + if (!verifyDev) { + return; + } + const frameworkMap = await getFrameworkMap(); const template = frameworkMap[framework as FrameworkName]; @@ -345,10 +406,8 @@ const verifyDevScript = async ( // Wait a few seconds for dev server to spin up await sleep(4000); - // By convention and for simplicity of testing, each test fixture will - // make a page or a simple api route on `/test` that will print a bound - // environment variable set to the value "C3_TEST" - const res = await fetch(`http://localhost:${TEST_PORT}/test`); + // Make a request to the specified test route + const res = await fetch(`http://localhost:${TEST_PORT}${verifyDev.route}`); const body = await res.text(); // Kill the process gracefully so ports can be cleaned up @@ -358,7 +417,7 @@ const verifyDevScript = async ( // end up camped and cause future runs to fail await sleep(1000); - expect(body).toContain("C3_TEST"); + expect(body).toContain(verifyDev.expectedToken); }; const verifyBuildScript = async ( @@ -366,15 +425,13 @@ const verifyBuildScript = async ( projectPath: string, logStream: WriteStream ) => { - const { build } = frameworkTests[framework]; + const { verifyBuild } = frameworkTests[framework]; - if (!build) { - throw Error( - "`build` must be specified in test config when verifying bindings" - ); + if (!verifyBuild) { + return; } - const { outputDir, script } = build; + const { outputDir, script, route, expectedToken } = verifyBuild; // Run the build script const { name: pm, npx } = detectPackageManager(); @@ -401,10 +458,8 @@ const verifyBuildScript = async ( // Wait a few seconds for dev server to spin up await sleep(4000); - // By convention and for simplicity of testing, each test fixture will - // make a page or a simple api route on `/test` that will print a bound - // environment variable set to the value "C3_TEST" - const res = await fetch(`http://localhost:${TEST_PORT}/test`); + // Make a request to the specified test route + const res = await fetch(`http://localhost:${TEST_PORT}${route}`); const body = await res.text(); // Kill the process gracefully so ports can be cleaned up @@ -415,5 +470,5 @@ const verifyBuildScript = async ( await sleep(1000); // Verify expectation after killing the process so that it exits cleanly in case of failure - expect(body).toContain("C3_TEST"); + expect(body).toContain(expectedToken); }; diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index 4a64f307769c..a7134d02a42c 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -50,12 +50,14 @@ export type PromptHandler = { }; export type RunnerConfig = { - overrides?: { - packageScripts?: Record; - }; promptHandlers?: PromptHandler[]; argv?: string[]; quarantine?: boolean; + timeout?: number; + verifyDeploy?: { + route: string; + expectedToken: string; + }; }; export const runC3 = async ( diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index 705a0d54beb9..d2b835e964c3 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -18,36 +18,41 @@ import type { Suite } from "vitest"; const TEST_TIMEOUT = 1000 * 60 * 5; -type WorkerTestConfig = Omit & { - expectResponseToContain?: string; - timeout?: number; +type WorkerTestConfig = RunnerConfig & { name?: string; template: string; }; const workerTemplates: WorkerTestConfig[] = [ { - expectResponseToContain: "Hello World!", template: "hello-world", + verifyDeploy: { + route: "/", + expectedToken: "Hello World!", + }, }, { template: "common", - expectResponseToContain: "Try making requests to:", + verifyDeploy: { + route: "/", + expectedToken: "Try making requests to:", + }, }, { template: "queues", // Skipped for now, since C3 does not yet support resource creation - // expectResponseToContain: }, { template: "scheduled", // Skipped for now, since it's not possible to test scheduled events on deployed Workers - // expectResponseToContain: }, { template: "openapi", - expectResponseToContain: "SwaggerUI", promptHandlers: [], + verifyDeploy: { + route: "/", + expectedToken: "SwaggerUI", + }, }, ]; @@ -125,11 +130,9 @@ describe const config = readToml(tomlPath) as { main: string }; expect(join(projectPath, config.main)).toExist(); - if (deployedUrl) { - await verifyDeployment( - deployedUrl, - template.expectResponseToContain as string - ); + const { verifyDeploy } = template; + if (verifyDeploy && deployedUrl) { + await verifyDeployment(deployedUrl, verifyDeploy.expectedToken); } } finally { clean(name); @@ -146,9 +149,7 @@ const runCli = async ( projectPath: string, logStream: WriteStream ) => { - const { argv, promptHandlers, expectResponseToContain } = template; - - const deploy = Boolean(expectResponseToContain); + const { argv, promptHandlers, verifyDeploy } = template; const args = [ projectPath, @@ -156,13 +157,13 @@ const runCli = async ( template.template, "--no-open", "--no-git", - deploy ? "--deploy" : "--no-deploy", + verifyDeploy ? "--deploy" : "--no-deploy", ...(argv ?? []), ]; const { output } = await runC3(args, promptHandlers, logStream); - if (!deploy) { + if (!verifyDeploy) { return null; } From d20b3920befd5a0473d075b8539b95f7cddee445 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Mon, 5 Feb 2024 14:59:30 -0600 Subject: [PATCH 09/13] Fixing cli e2e tests --- .../create-cloudflare/e2e-tests/cli.test.ts | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/cli.test.ts b/packages/create-cloudflare/e2e-tests/cli.test.ts index 174acf2cdb61..7a26171b2450 100644 --- a/packages/create-cloudflare/e2e-tests/cli.test.ts +++ b/packages/create-cloudflare/e2e-tests/cli.test.ts @@ -11,7 +11,14 @@ import { } from "vitest"; import { version } from "../package.json"; import { frameworkToTest } from "./frameworkToTest"; -import { isQuarantineMode, keys, recreateLogFolder, runC3 } from "./helpers"; +import { + createTestLogStream, + isQuarantineMode, + keys, + recreateLogFolder, + runC3, +} from "./helpers"; +import type { WriteStream } from "fs"; import type { Suite } from "vitest"; // Note: skipIf(frameworkToTest) makes it so that all the basic C3 functionality @@ -21,13 +28,15 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( () => { const tmpDirPath = realpathSync(mkdtempSync(join(tmpdir(), "c3-tests"))); const projectPath = join(tmpDirPath, "basic-tests"); + let logStream: WriteStream; beforeAll((ctx) => { recreateLogFolder(ctx as Suite); }); - beforeEach(() => { + beforeEach((ctx) => { rmSync(projectPath, { recursive: true, force: true }); + logStream = createTestLogStream(ctx); }); afterEach(() => { @@ -36,18 +45,18 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( } }); - test("--version", async (ctx) => { - const { output } = await runC3({ ctx, argv: ["--version"] }); + test("--version", async () => { + const { output } = await runC3(["--version"], [], logStream); expect(output).toEqual(version); }); - test("--version with positionals", async (ctx) => { + test("--version with positionals", async () => { const argv = ["foo", "bar", "baz", "--version"]; - const { output } = await runC3({ ctx, argv }); + const { output } = await runC3(argv, [], logStream); expect(output).toEqual(version); }); - test("--version with flags", async (ctx) => { + test("--version with flags", async () => { const argv = [ "foo", "--type", @@ -55,17 +64,16 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( "--no-deploy", "--version", ]; - const { output } = await runC3({ ctx, argv }); + const { output } = await runC3(argv, [], logStream); expect(output).toEqual(version); }); test.skipIf(process.platform === "win32")( "Using arrow keys + enter", - async (ctx) => { - const { output } = await runC3({ - ctx, - argv: [projectPath], - promptHandlers: [ + async () => { + const { output } = await runC3( + [projectPath], + [ { matcher: /What type of application do you want to create/, input: [keys.enter], @@ -83,7 +91,8 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( input: [keys.left, keys.enter], }, ], - }); + logStream + ); expect(projectPath).toExist(); expect(output).toContain(`type "Hello World" Worker`); @@ -95,11 +104,10 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( test.skipIf(process.platform === "win32")( "Typing custom responses", - async (ctx) => { - const { output } = await runC3({ - argv: [], - ctx, - promptHandlers: [ + async () => { + const { output } = await runC3( + [], + [ { matcher: /In which directory do you want to create your application/, @@ -122,7 +130,8 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( input: ["n"], }, ], - }); + logStream + ); expect(projectPath).toExist(); expect(output).toContain(`type Example router & proxy Worker`); @@ -134,11 +143,10 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( test.skipIf(process.platform === "win32")( "Mixed args and interactive", - async (ctx) => { - const { output } = await runC3({ - ctx, - argv: [projectPath, "--ts", "--no-deploy"], - promptHandlers: [ + async () => { + const { output } = await runC3( + [projectPath, "--ts", "--no-deploy"], + [ { matcher: /What type of application do you want to create/, input: [keys.enter], @@ -148,7 +156,8 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( input: ["n"], }, ], - }); + logStream + ); expect(projectPath).toExist(); expect(output).toContain(`type "Hello World" Worker`); From ee2e7c3808ffb236088d0f51769418bf389cbf0c Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Mon, 5 Feb 2024 15:03:43 -0600 Subject: [PATCH 10/13] changeset --- .changeset/ten-parents-double.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/ten-parents-double.md diff --git a/.changeset/ten-parents-double.md b/.changeset/ten-parents-double.md new file mode 100644 index 000000000000..3f7a4058b812 --- /dev/null +++ b/.changeset/ten-parents-double.md @@ -0,0 +1,8 @@ +--- +"create-cloudflare": patch +--- + +feature: Add `getBindingsProxy` support to `qwik` template + +The `qwik` template now uses `getBindingsProxy` for handling requests for bound resources +in dev. This allows projects to use `vite` for dev instead of `wrangler pages dev` on built output. From f7b6da1a754b223b57c926d58784c9d0308a0b68 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Mon, 5 Feb 2024 15:05:21 -0600 Subject: [PATCH 11/13] remove leftover test value --- .../e2e-tests/fixtures/qwik/src/routes/test/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts b/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts index fa70a4bd09d8..c31b601352fb 100644 --- a/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts +++ b/packages/create-cloudflare/e2e-tests/fixtures/qwik/src/routes/test/index.ts @@ -6,5 +6,5 @@ export const onGet: RequestHandler = async ({ platform, json }) => { return; } - json(200, { value: platform.env["TEST"] || "wtf undefined?", version: 1 }); + json(200, { value: platform.env["TEST"], version: 1 }); }; From 35a1a7e446cd9e5a757a8ae9c00b508dd120ab6a Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Mon, 5 Feb 2024 19:56:02 -0600 Subject: [PATCH 12/13] Fix issue with npm tests & fix e2e logging --- .../e2e-tests/frameworks.test.ts | 22 +++++++++++++++++-- .../create-cloudflare/e2e-tests/helpers.ts | 6 +---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 2239c4b13076..9c336736f381 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -5,7 +5,14 @@ import { retry } from "helpers/command"; import { sleep } from "helpers/common"; import { detectPackageManager } from "helpers/packages"; import { fetch } from "undici"; -import { beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "vitest"; import { deleteProject, deleteWorker } from "../scripts/common"; import { getFrameworkMap } from "../src/templates"; import { frameworkToTest } from "./frameworkToTest"; @@ -228,6 +235,10 @@ describe.concurrent(`E2E: Web frameworks`, () => { logStream = createTestLogStream(ctx); }); + afterEach(async () => { + logStream.close(); + }); + Object.keys(frameworkTests).forEach((framework) => { const { quarantine, @@ -393,7 +404,14 @@ const verifyDevScript = async ( const { name: pm } = detectPackageManager(); const proc = spawnWithLogging( - [pm, "run", template.devScript as string, "--port", `${TEST_PORT}`], + [ + pm, + "run", + template.devScript as string, + pm === "npm" ? "--" : "", + "--port", + `${TEST_PORT}`, + ], { cwd: projectPath, env: { diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index a7134d02a42c..899b716dd765 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -140,10 +140,6 @@ export const spawnWithLogging = ( logStream.write(data); }); - proc.on("close", () => { - logStream.close(); - }); - return proc; }; @@ -258,7 +254,7 @@ export const testProjectDir = (suite: string) => { } catch (e) { if (typeof e === "object" && e !== null && "code" in e) { const code = e.code; - if (code === "EBUSY" || code === "ENOENT") { + if (code === "EBUSY" || code === "ENOENT" || code === "ENOTEMPTY") { return; } } From 0535c9e51e47d20beb54c499d6cf9468aa4f6830 Mon Sep 17 00:00:00 2001 From: James Culveyhouse Date: Tue, 6 Feb 2024 11:32:39 -0600 Subject: [PATCH 13/13] Addressing PR feedback --- .../e2e-tests/fixtures/qwik/wrangler.toml | 2 +- .../e2e-tests/frameworks.test.ts | 50 +++++++++---------- .../create-cloudflare/e2e-tests/helpers.ts | 2 +- .../e2e-tests/workers.test.ts | 11 ++-- packages/create-cloudflare/src/templates.ts | 22 ++++---- 5 files changed, 46 insertions(+), 41 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml b/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml index 179e2d29b8fb..4679b8cbbddd 100644 --- a/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml +++ b/packages/create-cloudflare/e2e-tests/fixtures/qwik/wrangler.toml @@ -1,2 +1,2 @@ [vars] -TEST = "C3_TEST" \ No newline at end of file +TEST = "C3_TEST" diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 9c336736f381..5433e81276d1 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -41,13 +41,13 @@ type FrameworkTestConfig = RunnerConfig & { unsupportedOSs?: string[]; verifyDev?: { route: string; - expectedToken: string; + expectedText: string; }; verifyBuild?: { outputDir: string; script: string; route: string; - expectedToken: string; + expectedText: string; }; }; @@ -58,7 +58,7 @@ const frameworkTests: Record = { unsupportedOSs: ["win32"], verifyDeploy: { route: "/", - expectedToken: "Hello, Astronaut!", + expectedText: "Hello, Astronaut!", }, }, docusaurus: { @@ -68,7 +68,7 @@ const frameworkTests: Record = { timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", - expectedToken: "Dinosaurs are cool", + expectedText: "Dinosaurs are cool", }, }, angular: { @@ -76,7 +76,7 @@ const frameworkTests: Record = { timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", - expectedToken: "Congratulations! Your app is running.", + expectedText: "Congratulations! Your app is running.", }, }, gatsby: { @@ -91,14 +91,14 @@ const frameworkTests: Record = { timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", - expectedToken: "Gatsby!", + expectedText: "Gatsby!", }, }, hono: { testCommitMessage: false, verifyDeploy: { route: "/", - expectedToken: "Hello Hono!", + expectedText: "Hello Hono!", }, }, qwik: { @@ -113,17 +113,17 @@ const frameworkTests: Record = { unsupportedPms: ["yarn"], verifyDeploy: { route: "/", - expectedToken: "Welcome to Qwik", + expectedText: "Welcome to Qwik", }, verifyDev: { route: "/test", - expectedToken: "C3_TEST", + expectedText: "C3_TEST", }, verifyBuild: { outputDir: "./dist", script: "build", route: "/test", - expectedToken: "C3_TEST", + expectedText: "C3_TEST", }, }, remix: { @@ -132,7 +132,7 @@ const frameworkTests: Record = { unsupportedPms: ["yarn"], verifyDeploy: { route: "/", - expectedToken: "Welcome to Remix", + expectedText: "Welcome to Remix", }, }, next: { @@ -146,7 +146,7 @@ const frameworkTests: Record = { quarantine: true, verifyDeploy: { route: "/", - expectedToken: "Create Next App", + expectedText: "Create Next App", }, }, nuxt: { @@ -154,7 +154,7 @@ const frameworkTests: Record = { timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", - expectedToken: "Welcome to Nuxt!", + expectedText: "Welcome to Nuxt!", }, }, react: { @@ -163,7 +163,7 @@ const frameworkTests: Record = { timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", - expectedToken: "React App", + expectedText: "React App", }, }, solid: { @@ -186,7 +186,7 @@ const frameworkTests: Record = { unsupportedOSs: ["win32"], verifyDeploy: { route: "/", - expectedToken: "Hello world", + expectedText: "Hello world", }, }, svelte: { @@ -209,7 +209,7 @@ const frameworkTests: Record = { unsupportedPms: ["npm"], verifyDeploy: { route: "/", - expectedToken: "SvelteKit app", + expectedText: "SvelteKit app", }, }, vue: { @@ -217,7 +217,7 @@ const frameworkTests: Record = { unsupportedOSs: ["win32"], verifyDeploy: { route: "/", - expectedToken: "Vite App", + expectedText: "Vite App", }, }, }; @@ -308,7 +308,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { // Make a request to the deployed project and verify it was successful await verifyDeployment( `${deploymentUrl}${verifyDeploy.route}`, - verifyDeploy.expectedToken + verifyDeploy.expectedText ); // Copy over any test fixture files @@ -372,15 +372,15 @@ const runCli = async ( const verifyDeployment = async ( deploymentUrl: string, - expectedToken: string + expectedText: string ) => { await retry({ times: 5 }, async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second + await sleep(1000); const res = await fetch(deploymentUrl); const body = await res.text(); - if (!body.includes(expectedToken)) { + if (!body.includes(expectedText)) { throw new Error( - `Deployed page (${deploymentUrl}) didn't contain expected string: "${expectedToken}"` + `Deployed page (${deploymentUrl}) didn't contain expected string: "${expectedText}"` ); } }); @@ -435,7 +435,7 @@ const verifyDevScript = async ( // end up camped and cause future runs to fail await sleep(1000); - expect(body).toContain(verifyDev.expectedToken); + expect(body).toContain(verifyDev.expectedText); }; const verifyBuildScript = async ( @@ -449,7 +449,7 @@ const verifyBuildScript = async ( return; } - const { outputDir, script, route, expectedToken } = verifyBuild; + const { outputDir, script, route, expectedText } = verifyBuild; // Run the build script const { name: pm, npx } = detectPackageManager(); @@ -488,5 +488,5 @@ const verifyBuildScript = async ( await sleep(1000); // Verify expectation after killing the process so that it exits cleanly in case of failure - expect(body).toContain(expectedToken); + expect(body).toContain(expectedText); }; diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index 899b716dd765..1005c1b1578c 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -56,7 +56,7 @@ export type RunnerConfig = { timeout?: number; verifyDeploy?: { route: string; - expectedToken: string; + expectedText: string; }; }; diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index d2b835e964c3..63877bbbbc05 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -1,5 +1,6 @@ import { join } from "path"; import { retry } from "helpers/command"; +import { sleep } from "helpers/common"; import { readToml } from "helpers/files"; import { fetch } from "undici"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; @@ -28,14 +29,14 @@ const workerTemplates: WorkerTestConfig[] = [ template: "hello-world", verifyDeploy: { route: "/", - expectedToken: "Hello World!", + expectedText: "Hello World!", }, }, { template: "common", verifyDeploy: { route: "/", - expectedToken: "Try making requests to:", + expectedText: "Try making requests to:", }, }, { @@ -51,7 +52,7 @@ const workerTemplates: WorkerTestConfig[] = [ promptHandlers: [], verifyDeploy: { route: "/", - expectedToken: "SwaggerUI", + expectedText: "SwaggerUI", }, }, ]; @@ -132,7 +133,7 @@ describe const { verifyDeploy } = template; if (verifyDeploy && deployedUrl) { - await verifyDeployment(deployedUrl, verifyDeploy.expectedToken); + await verifyDeployment(deployedUrl, verifyDeploy.expectedText); } } finally { clean(name); @@ -185,7 +186,7 @@ const verifyDeployment = async ( expectedString: string ) => { await retry({ times: 5 }, async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); // wait a second + await sleep(1000); const res = await fetch(deploymentUrl); const body = await res.text(); if (!body.includes(expectedString)) { diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index 16a7b57f3336..78d25f36d070 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -368,23 +368,29 @@ const downloadRemoteTemplate = async (src: string) => { }; export const updatePackageName = async (ctx: C3Context) => { - const s = spinner(); - s.start("Updating name in `package.json`"); - // Update package.json with project name const placeholderNames = ["", "TBD", ""]; const pkgJsonPath = resolve(ctx.project.path, "package.json"); const pkgJson = readJSON(pkgJsonPath); - if (placeholderNames.includes(pkgJson.name)) { - pkgJson.name = ctx.project.name; + if (!placeholderNames.includes(pkgJson.name)) { + return; } + const s = spinner(); + s.start("Updating name in `package.json`"); + + pkgJson.name = ctx.project.name; + writeJSON(pkgJsonPath, pkgJson); s.stop(`${brandColor("updated")} ${dim("`package.json`")}`); }; export const updatePackageScripts = async (ctx: C3Context) => { + if (!ctx.template.transformPackageJson) { + return; + } + const s = spinner(); s.start("Updating `package.json` scripts"); @@ -392,10 +398,8 @@ export const updatePackageScripts = async (ctx: C3Context) => { let pkgJson = readJSON(pkgJsonPath); // Run any transformers defined by the template - if (ctx.template.transformPackageJson) { - const transformed = await ctx.template.transformPackageJson(pkgJson); - pkgJson = deepmerge(pkgJson, transformed); - } + const transformed = await ctx.template.transformPackageJson(pkgJson); + pkgJson = deepmerge(pkgJson, transformed); writeJSON(pkgJsonPath, pkgJson); s.stop(`${brandColor("updated")} ${dim("`package.json`")}`);