From 059fd886a34b7ba641133ab34d7c1b0bf8c2c33c Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 25 Sep 2024 15:35:41 +0100 Subject: [PATCH 1/4] enable `getCloudflareContext` to also work with `next dev` --- packages/cloudflare/package.json | 3 ++ .../src/api/get-cloudflare-context.ts | 48 +++++++++++++++---- packages/cloudflare/tsup.config.ts | 2 + pnpm-lock.yaml | 3 ++ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 439af866..a78d963d 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -48,5 +48,8 @@ }, "dependencies": { "ts-morph": "catalog:" + }, + "peerDependencies": { + "wrangler": "catalog:" } } diff --git a/packages/cloudflare/src/api/get-cloudflare-context.ts b/packages/cloudflare/src/api/get-cloudflare-context.ts index b9afec87..c68bd207 100644 --- a/packages/cloudflare/src/api/get-cloudflare-context.ts +++ b/packages/cloudflare/src/api/get-cloudflare-context.ts @@ -37,17 +37,49 @@ export async function getCloudflareContext< CfProperties extends Record = IncomingRequestCfProperties, Context = ExecutionContext, >(): Promise> { - const cloudflareContext = ( - globalThis as unknown as { - [cloudflareContextSymbol]: CloudflareContext | undefined; - } - )[cloudflareContextSymbol]; + const global = globalThis as unknown as { + [cloudflareContextSymbol]: CloudflareContext | undefined; + }; + + const cloudflareContext = global[cloudflareContextSymbol]; if (!cloudflareContext) { - // TODO: cloudflareContext should always be present in production/preview, if not it means that this - // is running under `next dev`, in this case use `getPlatformProxy` to return local proxies - throw new Error("Cloudflare context is not defined!"); + // the cloudflare context is initialized by the worker and is always present in production/preview, + // so, it not being present means that the application is running under `next dev` + return getCloudflareContextInNextDev(); } return cloudflareContext; } + +const cloudflareContextInNextDevSymbol = Symbol.for("__next-dev/cloudflare-context__"); + +/** + * Gets a local proxy version of the cloudflare context (created using `getPlatformProxy`) when + * running in the standard next dev server (via `next dev`) + * + * @returns the local proxy version of the cloudflare context + */ +async function getCloudflareContextInNextDev< + CfProperties extends Record = IncomingRequestCfProperties, + Context = ExecutionContext, +>(): Promise> { + const global = globalThis as unknown as { + [cloudflareContextInNextDevSymbol]: CloudflareContext | undefined; + }; + + if (!global[cloudflareContextInNextDevSymbol]) { + // Note: we need to add the webpackIgnore comment to the dynamic import because + // the next dev server transpiled modules on the fly but we don't want it to try + // to also transpile the wrangler code + const { getPlatformProxy } = await import(/* webpackIgnore: true */ "wrangler"); + const { env, cf, ctx } = await getPlatformProxy(); + global[cloudflareContextInNextDevSymbol] = { + env, + cf: cf as unknown as CfProperties, + ctx: ctx as Context, + }; + } + + return global[cloudflareContextInNextDevSymbol]!; +} diff --git a/packages/cloudflare/tsup.config.ts b/packages/cloudflare/tsup.config.ts index 367faabe..e69b049b 100644 --- a/packages/cloudflare/tsup.config.ts +++ b/packages/cloudflare/tsup.config.ts @@ -8,6 +8,7 @@ const cliConfig = defineConfig({ format: ["esm"], platform: "node", external: ["esbuild"], + clean: true, onSuccess: async () => { await cp(`${__dirname}/src/cli/templates`, `${__dirname}/dist/cli/templates`, { recursive: true, @@ -22,6 +23,7 @@ const apiConfig = defineConfig({ format: ["esm"], platform: "node", external: ["server-only"], + clean: true, }); export default [cliConfig, apiConfig]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 335379d9..32cd52f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: ts-morph: specifier: 'catalog:' version: 23.0.0 + wrangler: + specifier: 'catalog:' + version: 3.78.6(@cloudflare/workers-types@4.20240919.0) devDependencies: '@cloudflare/workers-types': specifier: 'catalog:' From 26bc5bb24dd7e16f589a8efac7faf95c0505c5de Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 25 Sep 2024 16:32:56 +0100 Subject: [PATCH 2/4] add hack to get standard build to work --- packages/cloudflare/src/api/get-cloudflare-context.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cloudflare/src/api/get-cloudflare-context.ts b/packages/cloudflare/src/api/get-cloudflare-context.ts index c68bd207..095f6d01 100644 --- a/packages/cloudflare/src/api/get-cloudflare-context.ts +++ b/packages/cloudflare/src/api/get-cloudflare-context.ts @@ -69,10 +69,10 @@ async function getCloudflareContextInNextDev< }; if (!global[cloudflareContextInNextDevSymbol]) { - // Note: we need to add the webpackIgnore comment to the dynamic import because - // the next dev server transpiled modules on the fly but we don't want it to try - // to also transpile the wrangler code - const { getPlatformProxy } = await import(/* webpackIgnore: true */ "wrangler"); + // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does + const { getPlatformProxy } = await import( + /* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}` + ); const { env, cf, ctx } = await getPlatformProxy(); global[cloudflareContextInNextDevSymbol] = { env, From a88b42e5b85c3a769c5774a356530b65758af01a Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 25 Sep 2024 16:54:59 +0100 Subject: [PATCH 3/4] add playwright test for testing dev mode too --- .github/workflows/playwright.yml | 8 +-- examples/api/package.json | 1 + examples/api/playwright.dev.config.ts | 80 +++++++++++++++++++++++++++ package.json | 3 +- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 examples/api/playwright.dev.config.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3c20c4fd..66dbdc94 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -17,7 +17,7 @@ jobs: run: npm install -g pnpm && pnpm install - name: Install Playwright browsers run: pnpm run install-playwright - - name: Run playwright tests (api) - run: pnpm -F api run e2e - - name: Run playwright tests (create-next-app) - run: pnpm -F create-next-app run e2e + - name: Run playwright tests + run: pnpm e2e + - name: Run playwright dev tests + run: pnpm e2e:dev diff --git a/examples/api/package.json b/examples/api/package.json index 54bb2149..15ce8261 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -11,6 +11,7 @@ "dev:worker": "wrangler dev --port 8770", "preview:worker": "pnpm build:worker && pnpm dev:worker", "e2e": "playwright test", + "e2e:dev": "playwright test -c playwright.dev.config.ts", "cf-typegen": "wrangler types --env-interface CloudflareEnv" }, "dependencies": { diff --git a/examples/api/playwright.dev.config.ts b/examples/api/playwright.dev.config.ts new file mode 100644 index 00000000..e3cc777b --- /dev/null +++ b/examples/api/playwright.dev.config.ts @@ -0,0 +1,80 @@ +import { defineConfig, devices } from "@playwright/test"; + +declare var process: { env: Record }; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e-tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3333", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "pnpm dev --port 3333", + url: "http://localhost:3333", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/package.json b/package.json index 7bae7dd2..fdffc8c9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "prettier:fix": "prettier --write .", "postinstall": "pnpm --filter cloudflare build", "install-playwright": "playwright install --with-deps", - "e2e": "pnpm -r e2e" + "e2e": "pnpm -r e2e", + "e2e:dev": "pnpm -r e2e:dev" } } From f8ba2359321814d64b46f040d87c1fa93a824b50 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 25 Sep 2024 17:03:33 +0100 Subject: [PATCH 4/4] remove outdated comment --- packages/cloudflare/src/api/get-cloudflare-context.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cloudflare/src/api/get-cloudflare-context.ts b/packages/cloudflare/src/api/get-cloudflare-context.ts index 095f6d01..a2baac0d 100644 --- a/packages/cloudflare/src/api/get-cloudflare-context.ts +++ b/packages/cloudflare/src/api/get-cloudflare-context.ts @@ -29,8 +29,6 @@ const cloudflareContextSymbol = Symbol.for("__cloudflare-context__"); /** * Utility to get the current Cloudflare context * - * Throws an error if the context could not be retrieved - * * @returns the cloudflare context */ export async function getCloudflareContext<