From 01956212410d6cd43419754028b018cfa5039add Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 26 Sep 2024 15:00:20 +0100 Subject: [PATCH] feat: add experimental Next.js, with Workers assets, template (#6830) * feat: add experimental Next.js, with Workers assets, template This template uses the new `@opennextjs/cloudflare` adaptor. * test: add support for e2e testing of experimental frameworks and workers * ci: Run C3 experimental e2e tests in CI * ci: remove unnecessary build step from C3 e2e job Not only is this unnecessary, the cached does not even get used by the e2e step that follows (probably because the env vars change) so it adds extra time to these jobs. * add logging to c3 e2e test failures * ci: write json test results to the correct directory * test: do not run experimental next.js template e2e tests on Windows --- .changeset/quiet-eels-rest.md | 5 + .github/actions/run-c3-e2e/action.yml | 21 +- .github/workflows/c3-e2e-experimental.yml | 64 ++ .prettierignore | 2 +- packages/create-cloudflare/.env.example | 1 + packages/create-cloudflare/.gitignore | 4 +- .../create-cloudflare/e2e-tests/cli.test.ts | 7 +- .../e2e-tests/frameworks.test.ts | 880 +++++++++--------- .../create-cloudflare/e2e-tests/helpers.ts | 37 +- .../e2e-tests/workers.test.ts | 90 +- packages/create-cloudflare/src/templates.ts | 2 + .../templates-experimental/next/c3.ts | 52 ++ .../next/templates/.gitignore | 42 + .../next/templates/env.d.ts | 5 + .../next/templates/wrangler.toml | 12 + packages/create-cloudflare/turbo.json | 3 +- .../create-cloudflare/vitest-e2e.config.mts | 2 +- 17 files changed, 733 insertions(+), 496 deletions(-) create mode 100644 .changeset/quiet-eels-rest.md create mode 100644 .github/workflows/c3-e2e-experimental.yml create mode 100644 packages/create-cloudflare/templates-experimental/next/c3.ts create mode 100644 packages/create-cloudflare/templates-experimental/next/templates/.gitignore create mode 100644 packages/create-cloudflare/templates-experimental/next/templates/env.d.ts create mode 100644 packages/create-cloudflare/templates-experimental/next/templates/wrangler.toml diff --git a/.changeset/quiet-eels-rest.md b/.changeset/quiet-eels-rest.md new file mode 100644 index 000000000000..674f75c154e6 --- /dev/null +++ b/.changeset/quiet-eels-rest.md @@ -0,0 +1,5 @@ +--- +"create-cloudflare": patch +--- + +feat: add experimental Next.js, with Workers assets, template diff --git a/.github/actions/run-c3-e2e/action.yml b/.github/actions/run-c3-e2e/action.yml index 39901629bdf3..fa421e0c18d9 100644 --- a/.github/actions/run-c3-e2e/action.yml +++ b/.github/actions/run-c3-e2e/action.yml @@ -10,6 +10,9 @@ inputs: quarantine: description: "Whether to run the tests in quarantine mode" required: false + experimental: + description: "Whether to run the tests in experimental mode" + required: false framework: description: "When specified, will only run tests for this framework" required: false @@ -39,14 +42,7 @@ runs: git config --global user.email wrangler@cloudflare.com git config --global user.name 'Wrangler automated PR updater' - - name: Build - shell: bash - run: pnpm run build - env: - NODE_ENV: "production" - CI_OS: ${{ runner.os }} - - - name: E2E Tests + - name: E2E Tests ${{inputs.experimental && '(experimental)' || ''}} id: run-e2e shell: bash run: pnpm run test:e2e --filter create-cloudflare @@ -54,6 +50,7 @@ runs: CLOUDFLARE_API_TOKEN: ${{ inputs.apiToken }} CLOUDFLARE_ACCOUNT_ID: ${{ inputs.accountId }} E2E_QUARANTINE: ${{ inputs.quarantine }} + E2E_EXPERIMENTAL: ${{ inputs.experimental }} FRAMEWORK_CLI_TO_TEST: ${{ inputs.framework }} TEST_PM: ${{ inputs.packageManager }} TEST_PM_VERSION: ${{ inputs.packageManagerVersion }} @@ -64,15 +61,15 @@ runs: uses: actions/upload-artifact@v3 if: always() with: - name: e2e-logs-${{matrix.os}} - path: packages/create-cloudflare/.e2e-logs + name: e2e-logs${{inputs.experimental && '-experimental' || ''}}-${{matrix.os}} + path: packages/create-cloudflare/.e2e-logs${{inputs.experimental && '-experimental' || ''}} - name: Upload Framework Diffs if: ${{ steps.run-e2e.outcome == 'success' && inputs.saveDiffs == 'true' }} uses: actions/upload-artifact@v3 with: - name: e2e-framework-diffs - path: packages/create-cloudflare/.e2e-diffs + name: e2e-framework-diffs${{inputs.experimental && '-experimental' || ''}} + path: packages/create-cloudflare/.e2e-diffs${{inputs.experimental && '-experimental' || ''}} overwrite: true - name: Fail if errors detected diff --git a/.github/workflows/c3-e2e-experimental.yml b/.github/workflows/c3-e2e-experimental.yml new file mode 100644 index 000000000000..4a43ed41f758 --- /dev/null +++ b/.github/workflows/c3-e2e-experimental.yml @@ -0,0 +1,64 @@ +# Runs c3 e2e tests on pull requests with c3 changes + +name: C3 E2E Tests (experimental) +on: + pull_request: + paths: + - packages/create-cloudflare/** + push: + branches: + - main + paths: + - packages/create-cloudflare/** + +jobs: + e2e: + # Note: please keep this job in sync with the e2e-only-dependabot-bumped-framework one + #  in .github/workflows/c3-e2e-dependabot.yml + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}-${{ matrix.pm.name }}-${{ matrix.pm.version }} + cancel-in-progress: true + name: ${{ format('Run experimental tests for {0}@{1} on {2}', matrix.pm.name, matrix.pm.version, matrix.os) }} + if: github.repository_owner == 'cloudflare' && github.event.pull_request.user.login != 'dependabot[bot]' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + pm: + [ + { name: npm, version: "0.0.0" }, + { name: pnpm, version: "9.10.0" }, + { name: bun, version: "1.0.3" }, + { name: yarn, version: "1.0.0" }, + ] + # include a single windows test with pnpm + include: + - os: windows-latest + pm: { name: pnpm, version: "9.10.0" } + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: E2E Tests + uses: ./.github/actions/run-c3-e2e + with: + packageManager: ${{ matrix.pm.name }} + packageManagerVersion: ${{ matrix.pm.version }} + quarantine: false + experimental: true + accountId: ${{ secrets.C3_TEST_CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.C3_TEST_CLOUDFLARE_API_TOKEN }} + # We only need to do this once per-framework per-run, so avoid re-running for each package manager and os + saveDiffs: ${{ github.head_ref == 'changeset-release/main' && matrix.pm.name == 'pnpm' && matrix.os == 'ubuntu-latest'}} diff --git a/.prettierignore b/.prettierignore index 21ec43abe695..933b3b5e4736 100644 --- a/.prettierignore +++ b/.prettierignore @@ -31,7 +31,7 @@ dist-functions vscode.d.ts vscode.*.d.ts -.e2e-logs +.e2e-logs* templates/*/build templates/*/dist diff --git a/packages/create-cloudflare/.env.example b/packages/create-cloudflare/.env.example index 9e1b2466451e..2e5478ed5cbd 100644 --- a/packages/create-cloudflare/.env.example +++ b/packages/create-cloudflare/.env.example @@ -8,3 +8,4 @@ # E2E_RETRIES=0 # the number of retries for framework e2e tests # E2E_NO_DEPLOY=true # flag to skip the deployment step in the e2es (for easier debugging, where the deployment is not relevant to current changes) # SAVE_DIFFS=true # flag to trigger the diffs saving during the e2es process +# E2E_EXPERIMENTAL=true # flag to run only experimental framework e2e tests diff --git a/packages/create-cloudflare/.gitignore b/packages/create-cloudflare/.gitignore index d7d418bf4d9f..ae10202c35a5 100644 --- a/packages/create-cloudflare/.gitignore +++ b/packages/create-cloudflare/.gitignore @@ -1,8 +1,8 @@ node_modules /dist create-cloudflare-*.tgz -/.e2e-logs/* -/.e2e-diffs/* +/.e2e-logs*/* +/.e2e-diffs*/* .DS_Store diff --git a/packages/create-cloudflare/e2e-tests/cli.test.ts b/packages/create-cloudflare/e2e-tests/cli.test.ts index 31bd2ce2663c..5b4e25aa1945 100644 --- a/packages/create-cloudflare/e2e-tests/cli.test.ts +++ b/packages/create-cloudflare/e2e-tests/cli.test.ts @@ -21,11 +21,12 @@ import { import type { WriteStream } from "fs"; import type { Suite } from "vitest"; +const experimental = Boolean(process.env.E2E_EXPERIMENTAL); const frameworkToTest = getFrameworkToTest({ experimental: false }); // Note: skipIf(frameworkToTest) makes it so that all the basic C3 functionality // tests are skipped in case we are testing a specific framework -describe.skipIf(frameworkToTest || isQuarantineMode())( +describe.skipIf(experimental || frameworkToTest || isQuarantineMode())( "E2E: Basic C3 functionality ", () => { const tmpDirPath = realpathSync(mkdtempSync(join(tmpdir(), "c3-tests"))); @@ -33,12 +34,12 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( let logStream: WriteStream; beforeAll((ctx) => { - recreateLogFolder(ctx as Suite); + recreateLogFolder({ experimental }, ctx as Suite); }); beforeEach((ctx) => { rmSync(projectPath, { recursive: true, force: true }); - logStream = createTestLogStream(ctx); + logStream = createTestLogStream({ experimental }, ctx); }); afterEach(() => { diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 46c1e854dde7..43730ddbd2bf 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -1,3 +1,4 @@ +import assert from "assert"; import { existsSync } from "fs"; import { cp } from "fs/promises"; import { join } from "path"; @@ -31,7 +32,7 @@ import { testProjectDir, waitForExit, } from "./helpers"; -import type { TemplateMap } from "../src/templates"; +import type { TemplateConfig } from "../src/templates"; import type { RunnerConfig } from "./helpers"; import type { WriteStream } from "fs"; import type { Suite } from "vitest"; @@ -67,432 +68,470 @@ type FrameworkTestConfig = RunnerConfig & { const { name: pm, npx } = detectPackageManager(); -// These are ordered based on speed and reliability for ease of debugging -const frameworkTests: Record = { - astro: { - testCommitMessage: true, - quarantine: true, - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Hello, Astronaut!", - }, - verifyDev: { - route: "/test", - expectedText: "C3_TEST", - }, - verifyBuild: { - outputDir: "./dist", - script: "build", - route: "/test", - expectedText: "C3_TEST", - }, - flags: [ - "--skip-houston", - "--no-install", - "--no-git", - "--template", - "blog", - "--typescript", - "strict", - ], - }, - docusaurus: { - unsupportedPms: ["bun"], - testCommitMessage: true, - unsupportedOSs: ["win32"], - timeout: LONG_TIMEOUT, - verifyDeploy: { - route: "/", - expectedText: "Dinosaurs are cool", - }, - flags: [`--package-manager`, pm], - promptHandlers: [ - { - matcher: /Which language do you want to use\?/, - input: [keys.enter], +function getFrameworkTests(opts: { + experimental: boolean; +}): Record { + if (opts.experimental) { + return { + next: { + testCommitMessage: false, + verifyBuildCfTypes: { + outputFile: "env.d.ts", + envInterfaceName: "CloudflareEnv", + }, + verifyDeploy: { + route: "/", + expectedText: "Create Next App", + }, + unsupportedOSs: ["win32"], }, - ], - }, - analog: { - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedOSs: ["win32"], - // The analog template works with yarn, but the build takes so long that it - // becomes flaky in CI - unsupportedPms: ["yarn"], - verifyDeploy: { - route: "/", - expectedText: "The fullstack meta-framework for Angular!", - }, - verifyDev: { - route: "/api/v1/test", - expectedText: "C3_TEST", - }, - verifyBuildCfTypes: { - outputFile: "worker-configuration.d.ts", - envInterfaceName: "Env", - }, - verifyBuild: { - outputDir: "./dist/analog/public", - script: "build", - route: "/api/v1/test", - expectedText: "C3_TEST", - }, - flags: ["--skipTailwind"], - quarantine: true, - }, - angular: { - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Congratulations! Your app is running.", - }, - flags: ["--style", "sass"], - }, - gatsby: { - unsupportedPms: ["bun", "pnpm"], - promptHandlers: [ - { - matcher: /Would you like to use a template\?/, - input: ["n"], + }; + } else { + // These are ordered based on speed and reliability for ease of debugging + return { + astro: { + testCommitMessage: true, + quarantine: true, + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Hello, Astronaut!", + }, + verifyDev: { + route: "/test", + expectedText: "C3_TEST", + }, + verifyBuild: { + outputDir: "./dist", + script: "build", + route: "/test", + expectedText: "C3_TEST", + }, + flags: [ + "--skip-houston", + "--no-install", + "--no-git", + "--template", + "blog", + "--typescript", + "strict", + ], }, - ], - testCommitMessage: true, - timeout: LONG_TIMEOUT, - verifyDeploy: { - route: "/", - expectedText: "Gatsby!", - }, - }, - hono: { - testCommitMessage: false, - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Hello Hono!", - }, - promptHandlers: [ - { - matcher: /Do you want to install project dependencies\?/, - input: [keys.enter], + docusaurus: { + unsupportedPms: ["bun"], + testCommitMessage: true, + unsupportedOSs: ["win32"], + timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedText: "Dinosaurs are cool", + }, + flags: [`--package-manager`, pm], + promptHandlers: [ + { + matcher: /Which language do you want to use\?/, + input: [keys.enter], + }, + ], }, - ], - }, - qwik: { - promptHandlers: [ - { - matcher: /Yes looks good, finish update/, - input: [keys.enter], + analog: { + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedOSs: ["win32"], + // The analog template works with yarn, but the build takes so long that it + // becomes flaky in CI + unsupportedPms: ["yarn"], + verifyDeploy: { + route: "/", + expectedText: "The fullstack meta-framework for Angular!", + }, + verifyDev: { + route: "/api/v1/test", + expectedText: "C3_TEST", + }, + verifyBuildCfTypes: { + outputFile: "worker-configuration.d.ts", + envInterfaceName: "Env", + }, + verifyBuild: { + outputDir: "./dist/analog/public", + script: "build", + route: "/api/v1/test", + expectedText: "C3_TEST", + }, + flags: ["--skipTailwind"], + quarantine: true, }, - ], - testCommitMessage: true, - unsupportedOSs: ["win32"], - unsupportedPms: ["yarn"], - verifyDeploy: { - route: "/", - expectedText: "Welcome to Qwik", - }, - verifyDev: { - route: "/test", - expectedText: "C3_TEST", - }, - verifyBuildCfTypes: { - outputFile: "worker-configuration.d.ts", - envInterfaceName: "Env", - }, - verifyBuild: { - outputDir: "./dist", - script: "build", - route: "/test", - expectedText: "C3_TEST", - }, - }, - remix: { - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedPms: ["yarn"], - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Welcome to Remix", - }, - verifyDev: { - route: "/test", - expectedText: "C3_TEST", - }, - verifyBuildCfTypes: { - outputFile: "worker-configuration.d.ts", - envInterfaceName: "Env", - }, - verifyBuild: { - outputDir: "./build/client", - script: "build", - route: "/test", - expectedText: "C3_TEST", - }, - flags: ["--typescript", "--no-install", "--no-git-init"], - }, - next: { - promptHandlers: [ - { - matcher: /Do you want to use the next-on-pages eslint-plugin\?/, - input: ["y"], + angular: { + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Congratulations! Your app is running.", + }, + flags: ["--style", "sass"], }, - ], - testCommitMessage: true, - quarantine: true, - verifyBuildCfTypes: { - outputFile: "env.d.ts", - envInterfaceName: "CloudflareEnv", - }, - verifyDeploy: { - route: "/", - expectedText: "Create Next App", - }, - flags: [ - "--typescript", - "--no-install", - "--eslint", - "--tailwind", - "--src-dir", - "--app", - "--import-alias", - "@/*", - ], - }, - nuxt: { - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Welcome to Nuxt!", - }, - verifyDev: { - route: "/test", - expectedText: "C3_TEST", - }, - verifyBuildCfTypes: { - outputFile: "worker-configuration.d.ts", - envInterfaceName: "Env", - }, - verifyBuild: { - outputDir: "./dist", - script: "build", - route: "/test", - expectedText: "C3_TEST", - }, - }, - react: { - promptHandlers: [ - { - matcher: /Select a variant:/, - input: [keys.enter], + gatsby: { + unsupportedPms: ["bun", "pnpm"], + promptHandlers: [ + { + matcher: /Would you like to use a template\?/, + input: ["n"], + }, + ], + testCommitMessage: true, + timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedText: "Gatsby!", + }, }, - ], - testCommitMessage: true, - unsupportedOSs: ["win32"], - unsupportedPms: ["yarn"], - timeout: LONG_TIMEOUT, - verifyDeploy: { - route: "/", - expectedText: "Vite + React", - }, - }, - solid: { - promptHandlers: [ - { - matcher: /Which template would you like to use/, - input: [keys.enter], + hono: { + testCommitMessage: false, + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Hello Hono!", + }, + promptHandlers: [ + { + matcher: /Do you want to install project dependencies\?/, + input: [keys.enter], + }, + ], }, - { - matcher: /Use Typescript/, - input: [keys.enter], + qwik: { + promptHandlers: [ + { + matcher: /Yes looks good, finish update/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + unsupportedOSs: ["win32"], + unsupportedPms: ["yarn"], + verifyDeploy: { + route: "/", + expectedText: "Welcome to Qwik", + }, + verifyDev: { + route: "/test", + expectedText: "C3_TEST", + }, + verifyBuildCfTypes: { + outputFile: "worker-configuration.d.ts", + envInterfaceName: "Env", + }, + verifyBuild: { + outputDir: "./dist", + script: "build", + route: "/test", + expectedText: "C3_TEST", + }, }, - ], - testCommitMessage: true, - timeout: LONG_TIMEOUT, - unsupportedPms: ["npm", "yarn"], - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Hello world", - }, - }, - svelte: { - promptHandlers: [ - { - matcher: /Which Svelte app template/, - input: [keys.enter], + remix: { + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedPms: ["yarn"], + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Welcome to Remix", + }, + verifyDev: { + route: "/test", + expectedText: "C3_TEST", + }, + verifyBuildCfTypes: { + outputFile: "worker-configuration.d.ts", + envInterfaceName: "Env", + }, + verifyBuild: { + outputDir: "./build/client", + script: "build", + route: "/test", + expectedText: "C3_TEST", + }, + flags: ["--typescript", "--no-install", "--no-git-init"], }, - { - matcher: /Add type checking with TypeScript/, - input: [keys.down, keys.enter], + next: { + promptHandlers: [ + { + matcher: /Do you want to use the next-on-pages eslint-plugin\?/, + input: ["y"], + }, + ], + testCommitMessage: true, + quarantine: true, + verifyBuildCfTypes: { + outputFile: "env.d.ts", + envInterfaceName: "CloudflareEnv", + }, + verifyDeploy: { + route: "/", + expectedText: "Create Next App", + }, + flags: [ + "--typescript", + "--no-install", + "--eslint", + "--tailwind", + "--src-dir", + "--app", + "--import-alias", + "@/*", + ], }, - { - matcher: /Select additional options/, - input: [keys.enter], + nuxt: { + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Welcome to Nuxt!", + }, + verifyDev: { + route: "/test", + expectedText: "C3_TEST", + }, + verifyBuildCfTypes: { + outputFile: "worker-configuration.d.ts", + envInterfaceName: "Env", + }, + verifyBuild: { + outputDir: "./dist", + script: "build", + route: "/test", + expectedText: "C3_TEST", + }, }, - ], - testCommitMessage: true, - unsupportedOSs: ["win32"], - unsupportedPms: ["npm"], - verifyDeploy: { - route: "/", - expectedText: "SvelteKit app", - }, - verifyDev: { - route: "/test", - expectedText: "C3_TEST", - }, - verifyBuild: { - outputDir: ".svelte-kit/cloudflare", - script: "build", - route: "/test", - expectedText: "C3_TEST", - }, - }, - vue: { - testCommitMessage: true, - unsupportedOSs: ["win32"], - verifyDeploy: { - route: "/", - expectedText: "Vite App", - }, - flags: ["--ts"], - quarantine: true, - }, -}; - -describe.concurrent(`E2E: Web frameworks`, () => { - let frameworkMap: TemplateMap; - let logStream: WriteStream; - - beforeAll(async (ctx) => { - frameworkMap = getFrameworkMap({ experimental: false }); - recreateLogFolder(ctx as Suite); - recreateDiffsFolder(); - }); - - beforeEach(async (ctx) => { - logStream = createTestLogStream(ctx); - }); - - afterEach(async () => { - logStream.close(); - }); - - Object.keys(frameworkTests).forEach((framework) => { - const { quarantine, timeout, unsupportedPms, unsupportedOSs } = - frameworkTests[framework]; - - const quarantineModeMatch = isQuarantineMode() == (quarantine ?? false); - - // If the framework in question is being run in isolation, always run it. - // Otherwise, only run the test if it's configured `quarantine` value matches - // what is set in E2E_QUARANTINE - const frameworkToTest = getFrameworkToTest({ experimental: false }); - let shouldRun = frameworkToTest - ? frameworkToTest === framework - : quarantineModeMatch; - - // Skip if the package manager is unsupported - shouldRun &&= !unsupportedPms?.includes(TEST_PM); - - // Skip if the OS is unsupported - shouldRun &&= !unsupportedOSs?.includes(process.platform); - test.runIf(shouldRun)( - framework, - async () => { - const { getPath, getName, clean } = testProjectDir("pages"); - const projectPath = getPath(framework); - const projectName = getName(framework); - const frameworkConfig = frameworkMap[framework]; - - const { promptHandlers, verifyDeploy, flags } = - 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, - projectPath, - logStream, - { - argv: [...(flags ? ["--", ...flags] : [])], - promptHandlers, - }, - ); - - // 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(); - - // Make a request to the deployed project and verify it was successful - await verifyDeployment( - framework, - projectName, - `${deploymentUrl}${verifyDeploy.route}`, - verifyDeploy.expectedText, - ); - - // Copy over any test fixture files - const fixturePath = join(__dirname, "fixtures", framework); - if (existsSync(fixturePath)) { - await cp(fixturePath, projectPath, { - recursive: true, - force: true, - }); + react: { + promptHandlers: [ + { + matcher: /Select a variant:/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + unsupportedOSs: ["win32"], + unsupportedPms: ["yarn"], + timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedText: "Vite + React", + }, + }, + solid: { + promptHandlers: [ + { + matcher: /Which template would you like to use/, + input: [keys.enter], + }, + { + matcher: /Use Typescript/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + timeout: LONG_TIMEOUT, + unsupportedPms: ["npm", "yarn"], + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Hello world", + }, + }, + svelte: { + 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"], + verifyDeploy: { + route: "/", + expectedText: "SvelteKit app", + }, + verifyDev: { + route: "/test", + expectedText: "C3_TEST", + }, + verifyBuild: { + outputDir: ".svelte-kit/cloudflare", + script: "build", + route: "/test", + expectedText: "C3_TEST", + }, + }, + vue: { + testCommitMessage: true, + unsupportedOSs: ["win32"], + verifyDeploy: { + route: "/", + expectedText: "Vite App", + }, + flags: ["--ts"], + quarantine: true, + }, + }; + } +} + +const experimental = Boolean(process.env.E2E_EXPERIMENTAL); +const frameworkMap = getFrameworkMap({ experimental }); +const frameworkTests = getFrameworkTests({ experimental }); + +describe.concurrent( + `E2E: Web frameworks (experimental:${experimental})`, + () => { + let logStream: WriteStream; + + beforeAll(async (ctx) => { + recreateLogFolder({ experimental }, ctx as Suite); + recreateDiffsFolder({ experimental }); + }); + + beforeEach(async (ctx) => { + logStream = createTestLogStream({ experimental }, ctx); + }); + + afterEach(async () => { + logStream.close(); + }); + + test("dummy in case there are no frameworks to test", () => {}); + + Object.keys(frameworkTests).forEach((frameworkId) => { + const frameworkConfig = frameworkMap[frameworkId]; + const testConfig = frameworkTests[frameworkId]; + + const quarantineModeMatch = + isQuarantineMode() == (testConfig.quarantine ?? false); + + // If the framework in question is being run in isolation, always run it. + // Otherwise, only run the test if it's configured `quarantine` value matches + // what is set in E2E_QUARANTINE + const frameworkToTest = getFrameworkToTest({ experimental }); + let shouldRun = frameworkToTest + ? frameworkToTest === frameworkId + : quarantineModeMatch; + + // Skip if the package manager is unsupported + shouldRun &&= !testConfig.unsupportedPms?.includes(TEST_PM); + + // Skip if the OS is unsupported + shouldRun &&= !testConfig.unsupportedOSs?.includes(process.platform); + test.runIf(shouldRun)( + frameworkId, + async () => { + const { getPath, getName, clean } = testProjectDir("pages"); + const projectPath = getPath(frameworkId); + const projectName = getName(frameworkId); + + if (!testConfig.verifyDeploy) { + expect( + true, + "A `deploy` configuration must be defined for all framework tests", + ).toBe(false); + return; } - await verifyDevScript(framework, projectPath, logStream); - await verifyBuildCfTypesScript(framework, projectPath, logStream); - await verifyBuildScript(framework, projectPath, logStream); - await storeDiff(framework, projectPath); - } catch (e) { - console.error("ERROR", e); - expect.fail( - "Failed due to an exception while running C3. See logs for more details", - ); - } finally { - clean(framework); - // Cleanup the project in case we need to retry it - if (frameworkConfig.platform === "workers") { - await deleteWorker(projectName); - } else { - await deleteProject(projectName); + try { + const deploymentUrl = await runCli( + frameworkId, + projectPath, + logStream, + { + argv: [ + ...(experimental ? ["--experimental"] : []), + ...(testConfig.flags ? ["--", ...testConfig.flags] : []), + ], + promptHandlers: testConfig.promptHandlers, + }, + ); + + // 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(); + + // Make a request to the deployed project and verify it was successful + await verifyDeployment( + testConfig, + frameworkId, + projectName, + `${deploymentUrl}${testConfig.verifyDeploy.route}`, + testConfig.verifyDeploy.expectedText, + ); + + // Copy over any test fixture files + const fixturePath = join(__dirname, "fixtures", frameworkId); + if (existsSync(fixturePath)) { + await cp(fixturePath, projectPath, { + recursive: true, + force: true, + }); + } + + await verifyDevScript( + testConfig, + frameworkConfig, + projectPath, + logStream, + ); + await verifyBuildCfTypesScript(testConfig, projectPath, logStream); + await verifyBuildScript(testConfig, projectPath, logStream); + await storeDiff(frameworkId, projectPath, { experimental }); + } catch (e) { + console.error("ERROR", e); + expect.fail( + "Failed due to an exception while running C3. See logs for more details", + ); + } finally { + clean(frameworkId); + // Cleanup the project in case we need to retry it + if (frameworkConfig.platform === "workers") { + await deleteWorker(projectName); + } else { + await deleteProject(projectName); + } } - } - }, - { - retry: TEST_RETRIES, - timeout: timeout || TEST_TIMEOUT, - }, - ); - }); -}); + }, + { + retry: TEST_RETRIES, + timeout: testConfig.timeout || TEST_TIMEOUT, + }, + ); + }); + }, +); -const storeDiff = async (framework: string, projectPath: string) => { +const storeDiff = async ( + framework: string, + projectPath: string, + opts: { experimental: boolean }, +) => { if (!process.env.SAVE_DIFFS) { return; } - const outputPath = join(getDiffsPath(), `${framework}.diff`); + const outputPath = join(getDiffsPath(opts), `${framework}.diff`); const output = await runCommand(["git", "diff"], { silent: true, @@ -531,6 +570,7 @@ const runCli = async ( const match = output.replaceAll("\n", "").match(deployedUrlRe); if (!match || !match[1]) { + console.error(output); expect(false, "Couldn't find deployment url in C3 output").toBe(true); return ""; } @@ -539,7 +579,8 @@ const runCli = async ( }; const verifyDeployment = async ( - framework: string, + { testCommitMessage }: FrameworkTestConfig, + frameworkId: string, projectName: string, deploymentUrl: string, expectedText: string, @@ -548,10 +589,8 @@ const verifyDeployment = async ( return; } - const { testCommitMessage } = frameworkTests[framework]; - if (testCommitMessage) { - await testDeploymentCommitMessage(projectName, framework); + await testDeploymentCommitMessage(projectName, frameworkId); } await retry({ times: 5 }, async () => { @@ -567,17 +606,16 @@ const verifyDeployment = async ( }; const verifyDevScript = async ( - framework: string, + { verifyDev }: FrameworkTestConfig, + { devScript }: TemplateConfig, projectPath: string, logStream: WriteStream, ) => { - const { verifyDev } = frameworkTests[framework]; if (!verifyDev) { return; } - const frameworkMap = getFrameworkMap({ experimental: false }); - const template = frameworkMap[framework]; + assert(devScript !== undefined, "Expected `devScript` to be defined"); // Run the devserver on a random port to avoid colliding with other tests const TEST_PORT = Math.ceil(Math.random() * 1000) + 20000; @@ -586,7 +624,7 @@ const verifyDevScript = async ( [ pm, "run", - template.devScript as string, + devScript, ...(pm === "npm" ? ["--"] : []), "--port", `${TEST_PORT}`, @@ -626,12 +664,10 @@ const verifyDevScript = async ( }; const verifyBuildCfTypesScript = async ( - framework: string, + { verifyBuildCfTypes }: FrameworkTestConfig, projectPath: string, logStream: WriteStream, ) => { - const { verifyBuildCfTypes } = frameworkTests[framework]; - if (!verifyBuildCfTypes) { return; } @@ -670,12 +706,10 @@ const verifyBuildCfTypesScript = async ( }; const verifyBuildScript = async ( - framework: string, + { verifyBuild }: FrameworkTestConfig, projectPath: string, logStream: WriteStream, ) => { - const { verifyBuild } = frameworkTests[framework]; - if (!verifyBuild) { return; } diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index c608f02ce9ce..f91737a11faa 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -282,18 +282,24 @@ export const waitForExit = async ( }; }; -export const createTestLogStream = (ctx: TaskContext) => { +export const createTestLogStream = ( + opts: { experimental: boolean }, + ctx: TaskContext, +) => { // The .ansi extension allows for editor extensions that format ansi terminal codes const fileName = `${normalizeTestName(ctx)}.ansi`; assert(ctx.task.suite, "Suite must be defined"); - return createWriteStream(path.join(getLogPath(ctx.task.suite), fileName), { - flags: "a", - }); + return createWriteStream( + path.join(getLogPath(opts, ctx.task.suite), fileName), + { + flags: "a", + }, + ); }; -export const recreateDiffsFolder = () => { +export const recreateDiffsFolder = (opts: { experimental: boolean }) => { // Recreate the diffs folder - const diffsPath = getDiffsPath(); + const diffsPath = getDiffsPath(opts); rmSync(diffsPath, { recursive: true, force: true, @@ -301,21 +307,26 @@ export const recreateDiffsFolder = () => { mkdirSync(diffsPath, { recursive: true }); }; -export const getDiffsPath = () => { - return path.resolve("./.e2e-diffs"); +export const getDiffsPath = (opts: { experimental: boolean }) => { + return path.resolve( + "./.e2e-diffs" + (opts.experimental ? "-experimental" : ""), + ); }; -export const recreateLogFolder = (suite: Suite) => { +export const recreateLogFolder = ( + opts: { experimental: boolean }, + suite: Suite, +) => { // Clean the old folder if exists (useful for dev) - rmSync(getLogPath(suite), { + rmSync(getLogPath(opts, suite), { recursive: true, force: true, }); - mkdirSync(getLogPath(suite), { recursive: true }); + mkdirSync(getLogPath(opts, suite), { recursive: true }); }; -const getLogPath = (suite: Suite) => { +const getLogPath = (opts: { experimental: boolean }, suite: Suite) => { const { file } = suite; const suiteFilename = file @@ -323,7 +334,7 @@ const getLogPath = (suite: Suite) => { : "unknown"; return path.join( - "./.e2e-logs/", + "./.e2e-logs" + (opts.experimental ? "-experimental" : ""), process.env.TEST_PM as string, suiteFilename, ); diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index 62d180073966..d6259f37b7d9 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -25,46 +25,56 @@ type WorkerTestConfig = RunnerConfig & { variants: string[]; }; -const workerTemplates: WorkerTestConfig[] = [ - { - template: "hello-world", - variants: ["TypeScript", "JavaScript", "Python"], - verifyDeploy: { - route: "/", - expectedText: "Hello World!", - }, - }, - { - template: "common", - variants: ["TypeScript", "JavaScript"], - verifyDeploy: { - route: "/", - expectedText: "Try making requests to:", - }, - }, - { - template: "queues", - variants: ["TypeScript", "JavaScript"], - // Skipped for now, since C3 does not yet support resource creation - }, - { - template: "scheduled", - variants: ["TypeScript", "JavaScript"], - // Skipped for now, since it's not possible to test scheduled events on deployed Workers - }, - { - template: "openapi", - variants: [], - verifyDeploy: { - route: "/", - expectedText: "SwaggerUI", - }, - }, -]; +function getWorkerTests(opts: { experimental: boolean }): WorkerTestConfig[] { + if (opts.experimental) { + return []; + } else { + return [ + { + template: "hello-world", + variants: ["TypeScript", "JavaScript", "Python"], + verifyDeploy: { + route: "/", + expectedText: "Hello World!", + }, + }, + { + template: "common", + variants: ["TypeScript", "JavaScript"], + verifyDeploy: { + route: "/", + expectedText: "Try making requests to:", + }, + }, + { + template: "queues", + variants: ["TypeScript", "JavaScript"], + // Skipped for now, since C3 does not yet support resource creation + }, + { + template: "scheduled", + variants: ["TypeScript", "JavaScript"], + // Skipped for now, since it's not possible to test scheduled events on deployed Workers + }, + { + template: "openapi", + variants: [], + verifyDeploy: { + route: "/", + expectedText: "SwaggerUI", + }, + }, + ]; + } +} + +const experimental = Boolean(process.env.E2E_EXPERIMENTAL); +const workerTests = getWorkerTests({ experimental }); describe .skipIf( - getFrameworkToTest({ experimental: false }) || + experimental || // skip until we add tests for experimental workers templates + getFrameworkToTest({ experimental }) || isQuarantineMode() || process.platform === "win32", ) @@ -72,14 +82,14 @@ describe let logStream: WriteStream; beforeAll((ctx) => { - recreateLogFolder(ctx as Suite); + recreateLogFolder({ experimental }, ctx as Suite); }); beforeEach(async (ctx) => { - logStream = createTestLogStream(ctx); + logStream = createTestLogStream({ experimental }, ctx); }); - workerTemplates + workerTests .flatMap((template) => template.variants.length > 0 ? template.variants.map((variant) => { diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index a19b914c5731..5c73a4647883 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -26,6 +26,7 @@ import assetsOnlyTemplateExperimental from "templates-experimental/hello-world-a import helloWorldWithDurableObjectAssetsTemplateExperimental from "templates-experimental/hello-world-durable-object-with-assets/c3"; import helloWorldWithAssetsTemplateExperimental from "templates-experimental/hello-world-with-assets/c3"; import honoTemplateExperimental from "templates-experimental/hono/c3"; +import nextTemplateExperimental from "templates-experimental/next/c3"; import nuxtTemplateExperimental from "templates-experimental/nuxt/c3"; import qwikTemplateExperimental from "templates-experimental/qwik/c3"; import remixTemplateExperimental from "templates-experimental/remix/c3"; @@ -167,6 +168,7 @@ export function getFrameworkMap({ experimental = false }): TemplateMap { docusaurus: docusaurusTemplateExperimental, gatsby: gatsbyTemplateExperimental, hono: honoTemplateExperimental, + next: nextTemplateExperimental, nuxt: nuxtTemplateExperimental, qwik: qwikTemplateExperimental, remix: remixTemplateExperimental, diff --git a/packages/create-cloudflare/templates-experimental/next/c3.ts b/packages/create-cloudflare/templates-experimental/next/c3.ts new file mode 100644 index 000000000000..94e6274c00e8 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/next/c3.ts @@ -0,0 +1,52 @@ +import { brandColor, dim } from "@cloudflare/cli/colors"; +import { runFrameworkGenerator } from "frameworks/index"; +import { installPackages } from "helpers/packages"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--ts", + "--tailwind", + "--eslint", + "--app", + "--import-alias", + "@/*", + "--src-dir", + ]); +}; + +const configure = async () => { + const packages = ["@opennextjs/cloudflare", "@cloudflare/workers-types"]; + await installPackages(packages, { + dev: true, + startText: "Adding the Cloudflare adapter", + doneText: `${brandColor(`installed`)} ${dim(packages.join(", "))}`, + }); +}; + +export default { + configVersion: 1, + id: "next", + frameworkCli: "create-next-app", + platform: "workers", + displayName: "Next (using Node.js compat + Workers Assets)", + path: "templates-experimental/next", + copyFiles: { + path: "./templates", + }, + generate, + configure, + transformPackageJson: async () => ({ + scripts: { + deploy: `cloudflare && wrangler deploy`, + preview: `cloudflare && wrangler dev`, + "cf-typegen": `wrangler types --env-interface CloudflareEnv env.d.ts`, + }, + }), + devScript: "dev", + previewScript: "preview", + deployScript: "deploy", + compatibilityFlags: ["nodejs_compat"], +} as TemplateConfig; diff --git a/packages/create-cloudflare/templates-experimental/next/templates/.gitignore b/packages/create-cloudflare/templates-experimental/next/templates/.gitignore new file mode 100644 index 000000000000..0b418115f12a --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/next/templates/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + + +# Cloudflare related +/.save.next +/.worker-next +/.wrangler diff --git a/packages/create-cloudflare/templates-experimental/next/templates/env.d.ts b/packages/create-cloudflare/templates-experimental/next/templates/env.d.ts new file mode 100644 index 000000000000..68a2a989df06 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/next/templates/env.d.ts @@ -0,0 +1,5 @@ +// Generated by Wrangler +// by running `wrangler types --env-interface CloudflareEnv env.d.ts` + +interface CloudflareEnv { +} diff --git a/packages/create-cloudflare/templates-experimental/next/templates/wrangler.toml b/packages/create-cloudflare/templates-experimental/next/templates/wrangler.toml new file mode 100644 index 000000000000..4f7e9f1324c4 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/next/templates/wrangler.toml @@ -0,0 +1,12 @@ +#:schema node_modules/wrangler/config-schema.json +name = "" +main = ".worker-next/index.mjs" + +compatibility_date = "2024-09-26" +compatibility_flags = ["nodejs_compat"] + +# Minification helps to keep the Worker bundle size down and improve start up time. +minify = true + +# Use the new Workers + Assets to host the static frontend files +assets = { directory = ".worker-next/assets", binding = "ASSETS" } diff --git a/packages/create-cloudflare/turbo.json b/packages/create-cloudflare/turbo.json index a3479eb8dd53..0302314d9ad9 100644 --- a/packages/create-cloudflare/turbo.json +++ b/packages/create-cloudflare/turbo.json @@ -23,7 +23,8 @@ "E2E_QUARANTINE", "E2E_PROJECT_PATH", "E2E_RETRIES", - "E2E_NO_DEPLOY" + "E2E_NO_DEPLOY", + "E2E_EXPERIMENTAL" ], "dependsOn": ["build"] } diff --git a/packages/create-cloudflare/vitest-e2e.config.mts b/packages/create-cloudflare/vitest-e2e.config.mts index b704f9493305..d4f8b39580fb 100644 --- a/packages/create-cloudflare/vitest-e2e.config.mts +++ b/packages/create-cloudflare/vitest-e2e.config.mts @@ -12,7 +12,7 @@ export default defineConfig({ setupFiles: ["e2e-tests/setup.ts", "dotenv/config"], reporters: ["json", "verbose", "hanging-process"], outputFile: { - json: "./.e2e-logs/" + process.env.TEST_PM + "/results.json", + json: `./.e2e-logs${process.env.E2E_EXPERIMENTAL ? "-experimental" : ""}/${process.env.TEST_PM}/results.json`, }, }, });