diff --git a/.changeset/eight-melons-rule.md b/.changeset/eight-melons-rule.md new file mode 100644 index 000000000000..7b2546676407 --- /dev/null +++ b/.changeset/eight-melons-rule.md @@ -0,0 +1,18 @@ +--- +"wrangler": patch +--- + +Add back support for wrangler d1 exports with multiple tables. + +Example: + +```bash +# All tables (default) +wrangler d1 export db --output all-tables.sql + +# Single table (unchanged) +wrangler d1 export db --output single-table.sql --table foo + +# Multiple tables (new) +wrangler d1 export db --output multiple-tables.sql --table foo --table bar +``` diff --git a/.changeset/fix-c3-dashboard-url.md b/.changeset/fix-c3-dashboard-url.md new file mode 100644 index 000000000000..6e726e18cf88 --- /dev/null +++ b/.changeset/fix-c3-dashboard-url.md @@ -0,0 +1,7 @@ +--- +"create-cloudflare": patch +--- + +Fix C3 success summary dashboard link to point to Workers service production view + +The "Dash:" URL now includes `/production` so it opens the correct Workers & Pages service view in the Cloudflare dashboard. diff --git a/.changeset/floppy-webs-smell.md b/.changeset/floppy-webs-smell.md new file mode 100644 index 000000000000..cb0abe279a41 --- /dev/null +++ b/.changeset/floppy-webs-smell.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Warn when the `assets` field is provided for auxiliary Workers + +Auxiliary Workers do not support static assets. Previously, the `assets` field was silently ignored but we now warn if it is used. diff --git a/.changeset/migrate-workers-playground.md b/.changeset/migrate-workers-playground.md new file mode 100644 index 000000000000..54adfc165d49 --- /dev/null +++ b/.changeset/migrate-workers-playground.md @@ -0,0 +1,14 @@ +--- +"@cloudflare/workers-playground": patch +"@cloudflare/playground-preview-worker": patch +--- + +Migrate workers-playground from Cloudflare Pages to Cloudflare Workers + +Replace the Cloudflare Pages deployment with a Workers + static assets deployment. + +In production (`wrangler.jsonc`), this is an assets-only Worker with no code entry point — the `playground-preview-worker` handles all routing and proxying in front of it. + +For local development, a separate config (`wrangler.dev.jsonc`) adds a Worker entry point (`src/worker.ts`) that replicates the proxying behavior of the production `playground-preview-worker`. It proxies `/playground/api/*` requests to the testing `playground-preview-worker`, and for the `/playground` route it fetches an auth cookie from the testing endpoint, transforms it for local use (stripping `SameSite`/`Secure` directives and replacing the testing origin with `localhost`), and injects it into the response so the preview iframe can authenticate. + +The `playground-preview-worker` referer allowlist is updated to also accept requests from `*.workers-playground.workers.dev` (in addition to the existing `*.workers-playground.pages.dev`). diff --git a/.changeset/quiet-foxes-grow.md b/.changeset/quiet-foxes-grow.md new file mode 100644 index 000000000000..86a5dca81cfe --- /dev/null +++ b/.changeset/quiet-foxes-grow.md @@ -0,0 +1,17 @@ +--- +"wrangler": patch +--- + +fix: `vectorize` commands now output valid json + +This fixes: + +- `wrangler vectorize create` +- `wrangler vectorize info` +- `wrangler vectorize insert` +- `wrangler vectorize upsert` +- `wrangler vectorize list` +- `wrangler vectorize list-vectors` +- `wrangler vectorize list-metadata-index` + +Also, `wrangler vectorize create --json` now also includes the `created_at`, `modified_on` and `description` fields. diff --git a/.changeset/ten-dancers-know.md b/.changeset/ten-dancers-know.md new file mode 100644 index 000000000000..11d03a1faa4c --- /dev/null +++ b/.changeset/ten-dancers-know.md @@ -0,0 +1,5 @@ +--- +"miniflare": patch +--- + +Add support for worker connect handler in miniflare diff --git a/.changeset/vpc-hostname-validation.md b/.changeset/vpc-hostname-validation.md new file mode 100644 index 000000000000..017f6997553f --- /dev/null +++ b/.changeset/vpc-hostname-validation.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Add client-side validation for VPC service host flags + +The `--hostname`, `--ipv4`, and `--ipv6` flags on `wrangler vpc service create` and `wrangler vpc service update` now validate input before sending requests to the API. Previously, invalid values were accepted by the CLI and only rejected by the API with opaque error messages. Now users get clear, actionable error messages for common mistakes like passing a URL instead of a hostname, using an IP address in the `--hostname` flag, or providing malformed IP addresses. diff --git a/.github/workflows/deploy-pages-previews.yml b/.github/workflows/deploy-previews.yml similarity index 64% rename from .github/workflows/deploy-pages-previews.yml rename to .github/workflows/deploy-previews.yml index 1618aaaa9fc6..27812207a20f 100644 --- a/.github/workflows/deploy-pages-previews.yml +++ b/.github/workflows/deploy-previews.yml @@ -1,20 +1,19 @@ -name: Deploy Pages Previews +name: Deploy Previews -# This workflow is designed to deploy a "preview" version of a Pages project based on PR labels. +# This workflow is designed to deploy a "preview" version of a project based on PR labels. # Triggers: # - update to a PR that has one of the `preview:...` labels # # Actions: -# - deploy the matching Pages project to Cloudflare. +# - deploy the matching project to Cloudflare. # -# PR Label | Pages Project +# PR Label | Project # --------------------------------------------------------- # preview:chrome-devtools-patches | packages/chrome-devtools-patches -# preview:quick-edit | packages/quick-edit -# preview:workers-playground | packages/workers-playground +# preview:quick-edit | packages/quick-edit # -# Note: this workflow does not run tests against these packages, only for deploys previews. +# Note: this workflow does not run tests against these packages, only deploys previews. on: pull_request: @@ -26,11 +25,11 @@ permissions: pull-requests: write jobs: - deploy-pages-projects: + deploy-projects: # Only run this on PRs that are for the "cloudflare" org and not "from" `main` # - non-Cloudflare PRs will not have the secrets needed # - PRs "from" main would accidentally do a production deployment - if: github.repository_owner == 'cloudflare' && github.head_ref != 'main' && (contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches') || contains(github.event.*.labels.*.name, 'preview:quick-edit') || contains(github.event.*.labels.*.name, 'preview:workers-playground')) + if: github.repository_owner == 'cloudflare' && github.head_ref != 'main' && (contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches') || contains(github.event.*.labels.*.name, 'preview:quick-edit')) timeout-minutes: 60 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-app-previews @@ -69,23 +68,11 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} NODE_OPTIONS: "--max_old_space_size=30000" - - name: Deploy Workers Playground preview - if: contains(github.event.*.labels.*.name, 'preview:workers-playground') - run: | - output=$(pnpm --filter workers-playground run build:testing && pnpm --filter workers-playground run deploy) - echo "Extracting deployed URL from command output" - url=$(echo "$output" | sed -nE 's/.*Take a peek over at (https?:\/\/[^ ]+).*/\1/p') - echo "Extracted URL: $url" - echo "PLAYGROUND_URL=$url" >> $GITHUB_ENV - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - VITE_DEVTOOLS_PREVIEW_URL: ${{ env.VITE_DEVTOOLS_PREVIEW_URL }} - - name: "Comment on PR with Devtools Link" if: contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches') uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 with: - header: ${{ steps.finder.outputs.pr }} + header: chrome-devtools-preview message: | The Wrangler DevTools preview is now live. You can access it directly at: ${{ env.VITE_DEVTOOLS_PREVIEW_URL }}/js_app @@ -99,16 +86,3 @@ jobs: - https://devtools.devprod.cloudflare.dev/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true + https://8afc7d3d.cloudflare-devtools.pages.dev/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true ``` - - - name: "Comment on PR with Combined Link" - if: contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches') && contains(github.event.*.labels.*.name, 'preview:workers-playground') - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 - with: - header: ${{ steps.finder.outputs.pr }} - append: true - message: | - - --- - - The Workers Playground preview is also now live. The Playground preview embeds the above DevTools preview, so you can see them working together at: - ${{ env.PLAYGROUND_URL }}/playground diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f2d925a3a47..afd7ce468d2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -312,10 +312,9 @@ Every PR will have an associated pre-release build for all releasable packages w It's also possible to generate preview builds for the applications in the repository. These aren't generated automatically because they're pretty slow CI jobs, but you can trigger preview builds by adding one of the following labels to your PR: - `preview:chrome-devtools-patches` for deploying [chrome-devtools-patches](packages/chrome-devtools-patches) -- `preview:workers-playground` for deploying [workers-playground](packages/workers-playground) - `preview:quick-edit` for deploying [quick-edit](packages/quick-edit) -Once built, you can find the preview link for these applications in the [Deploy Pages Previews](.github/workflows/deploy-pages-previews.yml) action output +Once built, you can find the preview link for these applications in the [Deploy Previews](.github/workflows/deploy-previews.yml) action output ## PR Tests diff --git a/fixtures/get-platform-proxy-remote-bindings/turbo.json b/fixtures/get-platform-proxy-remote-bindings/turbo.json index 005c6c3ab415..57926cddb348 100644 --- a/fixtures/get-platform-proxy-remote-bindings/turbo.json +++ b/fixtures/get-platform-proxy-remote-bindings/turbo.json @@ -4,14 +4,12 @@ "tasks": { "test:e2e": { "env": [ + "$TURBO_EXTENDS$", "VITEST", - "NODE_DEBUG", "MINIFLARE_WORKERD_PATH", "WRANGLER", "WRANGLER_IMPORT", "MINIFLARE_IMPORT", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN", "TEST_CLOUDFLARE_ACCOUNT_ID", "TEST_CLOUDFLARE_API_TOKEN", "WRANGLER_E2E_TEST_FILE" diff --git a/fixtures/vitest-pool-workers-remote-bindings/turbo.json b/fixtures/vitest-pool-workers-remote-bindings/turbo.json index 8b48b186b967..789f1de68764 100644 --- a/fixtures/vitest-pool-workers-remote-bindings/turbo.json +++ b/fixtures/vitest-pool-workers-remote-bindings/turbo.json @@ -10,8 +10,6 @@ "WRANGLER", "WRANGLER_IMPORT", "MINIFLARE_IMPORT", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN", "TEST_CLOUDFLARE_ACCOUNT_ID", "TEST_CLOUDFLARE_API_TOKEN", "WRANGLER_E2E_TEST_FILE" diff --git a/fixtures/worker-logs/turbo.json b/fixtures/worker-logs/turbo.json index 8b48b186b967..789f1de68764 100644 --- a/fixtures/worker-logs/turbo.json +++ b/fixtures/worker-logs/turbo.json @@ -10,8 +10,6 @@ "WRANGLER", "WRANGLER_IMPORT", "MINIFLARE_IMPORT", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN", "TEST_CLOUDFLARE_ACCOUNT_ID", "TEST_CLOUDFLARE_API_TOKEN", "WRANGLER_E2E_TEST_FILE" diff --git a/lint-turbo.mjs b/lint-turbo.mjs index 1cb6000979e0..c53f6dfc2c4e 100644 --- a/lint-turbo.mjs +++ b/lint-turbo.mjs @@ -2,11 +2,16 @@ import assert from "assert"; import { execSync } from "child_process"; import { existsSync, readFileSync } from "fs"; import path from "path"; +import { parse } from "jsonc-parser"; function readJson(filePath) { return JSON.parse(readFileSync(filePath, "utf8")); } +function readJsonc(filePath) { + return parse(readFileSync(filePath, "utf8")); +} + const listResult = execSync( "pnpm --filter=!@cloudflare/workers-sdk list --recursive --depth -1 --parseable" ); @@ -36,7 +41,7 @@ for (const p of paths) { console.log(pkg.name, "has build script. Checking turbo.json"); let turboConfig; try { - turboConfig = readJson(path.join(p, "turbo.json")); + turboConfig = readJsonc(path.join(p, "turbo.json")); } catch { console.log("Failed to read turbo.json for", pkg.name); process.exit(1); diff --git a/package.json b/package.json index edfa3a820761..31cae1ca62a4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dotenv-cli": "^7.3.0", "esbuild": "catalog:default", "esbuild-register": "^3.5.0", + "jsonc-parser": "catalog:default", "prettier": "^3.2.5", "prettier-plugin-packagejson": "^2.2.18", "tree-kill": "^1.2.2", diff --git a/packages/chrome-devtools-patches/README.md b/packages/chrome-devtools-patches/README.md index 4ade8e8be8b0..e20e4e8ed264 100644 --- a/packages/chrome-devtools-patches/README.md +++ b/packages/chrome-devtools-patches/README.md @@ -52,16 +52,9 @@ Two methods are available for testing updates: **Preview Builds:** -On any pull request to the repo on GitHub, you can add labels to trigger preview builds of both the DevTools frontend, and the Playground. This is useful because it will allow you to manually test your changes in a live environment, and with one-click. +On any pull request to the repo on GitHub, you can add the `preview:chrome-devtools-patches` label to trigger a preview build of the DevTools frontend. This is useful because it will allow you to manually test your changes in a live environment, and with one-click. -There are two labels you can use: - -- `preview:chrome-devtools-patches` - this will trigger the DevTools preview -- `preview:workers-playground` - this will trigger the Playground preview - -If you add **both** labels, Playground will embed the DevTools preview, so you can test them together. - -Once the previews are built, you will see a comment on the PR with links to the live URLs. +Once the preview is built, you will see a comment on the PR with a link to the live URL. ## Acceptance Criteria diff --git a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts index 084a0cd5ea68..d4e3e6e70d69 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts @@ -985,6 +985,7 @@ function getExperimentalFrameworkTestConfig( // this test creates an R2 bucket, so it requires a Cloudflare API token // and needs to be skipped on forks quarantine: !CLOUDFLARE_API_TOKEN, + timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", expectedText: "Generated by create next app", diff --git a/packages/create-cloudflare/src/__tests__/dialog.test.ts b/packages/create-cloudflare/src/__tests__/dialog.test.ts index 95c4521347af..f79ae4128990 100644 --- a/packages/create-cloudflare/src/__tests__/dialog.test.ts +++ b/packages/create-cloudflare/src/__tests__/dialog.test.ts @@ -122,7 +122,7 @@ describe("dialog helpers", () => { 🔍 View Project Visit: https://example.test.workers.dev - Dash: https://dash.cloudflare.com/?to=/:account/workers/services/view/test-project + Dash: https://dash.cloudflare.com/?to=/:account/workers/services/view/test-project/production 💻 Continue Developing Deploy again: pnpm run deploy diff --git a/packages/create-cloudflare/src/dialog.ts b/packages/create-cloudflare/src/dialog.ts index 48b7e0dc6056..d1a9fd82b117 100644 --- a/packages/create-cloudflare/src/dialog.ts +++ b/packages/create-cloudflare/src/dialog.ts @@ -60,7 +60,7 @@ export function printWelcomeMessage( export const printSummary = (ctx: C3Context) => { // Prepare relevant information const dashboardUrl = ctx.account - ? `https://dash.cloudflare.com/?to=/:account/workers/services/view/${ctx.project.name}` + ? `https://dash.cloudflare.com/?to=/:account/workers/services/view/${ctx.project.name}/production` : null; const relativePath = relative(ctx.originalCWD, ctx.project.path); const cdCommand = relativePath ? `cd ${relativePath}` : null; diff --git a/packages/create-cloudflare/turbo.json b/packages/create-cloudflare/turbo.json index b4dd2e78ebb4..04e2b5ebf540 100644 --- a/packages/create-cloudflare/turbo.json +++ b/packages/create-cloudflare/turbo.json @@ -18,9 +18,7 @@ "test:e2e": { "passThroughEnv": ["GITHUB_TOKEN"], "env": [ - "NODE_DEBUG", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN", + "$TURBO_EXTENDS$", "E2E_EXPERIMENTAL", "E2E_TEST_FILTER", "E2E_TEST_PM", diff --git a/packages/miniflare/src/shared/external-service.ts b/packages/miniflare/src/shared/external-service.ts index bf35008e01d8..33e540769cab 100644 --- a/packages/miniflare/src/shared/external-service.ts +++ b/packages/miniflare/src/shared/external-service.ts @@ -354,6 +354,7 @@ const PROXY_ENTRYPOINT_HEADER = "X-Miniflare-Proxy-Entrypoint"; const CREATE_PROXY_PROTOTYPE_CLASS_HELPER_SCRIPT = ` const HANDLER_RESERVED_KEYS = new Set([ "alarm", + "connect", "scheduled", "self", "tail", diff --git a/packages/playground-preview-worker/src/index.ts b/packages/playground-preview-worker/src/index.ts index 1e6c24f1a24f..76e63197eba7 100644 --- a/packages/playground-preview-worker/src/index.ts +++ b/packages/playground-preview-worker/src/index.ts @@ -224,7 +224,10 @@ app.get(`${previewDomain}/.update-preview-token`, (c) => { !( referer.hostname === "workers.cloudflare.com" || referer.hostname === "localhost" || - referer.hostname.endsWith("workers-playground.pages.dev") + referer.hostname.endsWith(".workers-playground.pages.dev") || + referer.hostname === "workers-playground.pages.dev" || + referer.hostname.endsWith(".workers-playground.workers.dev") || + referer.hostname === "workers-playground.workers.dev" ) ) { throw new PreviewRequestForbidden(); diff --git a/packages/playground-preview-worker/tests/index.test.ts b/packages/playground-preview-worker/tests/index.test.ts index 06adbcc07190..3bdd3da1429b 100644 --- a/packages/playground-preview-worker/tests/index.test.ts +++ b/packages/playground-preview-worker/tests/index.test.ts @@ -93,11 +93,9 @@ describe("Preview Worker", () => { }, } ); - expect(resp.headers.get("location")).toMatchInlineSnapshot( - '"/hello?world"' - ); - expect(resp.headers.get("set-cookie") ?? "").toMatchInlineSnapshot( - `"token=${defaultUserToken}; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=random-data.playground-testing.devprod.cloudflare.dev"` + expect(resp.headers.get("location")).toEqual("/hello?world"); + expect(resp.headers.get("set-cookie") ?? "").toEqual( + `token=${defaultUserToken}; Domain=random-data.playground-testing.devprod.cloudflare.dev; Path=/; HttpOnly; Secure; SameSite=None; Partitioned` ); }); it("shouldn't be redirected with no token", async ({ expect }) => { @@ -343,8 +341,8 @@ describe("Preview Worker", () => { expect(resp.headers.get("location")).toMatchInlineSnapshot( '"/hello?world"' ); - expect(resp.headers.get("set-cookie") ?? "").toMatchInlineSnapshot( - `"token=${defaultUserToken}; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=random-data.playground-testing.devprod.cloudflare.dev"` + expect(resp.headers.get("set-cookie") ?? "").toEqual( + `token=${defaultUserToken}; Domain=random-data.playground-testing.devprod.cloudflare.dev; Path=/; HttpOnly; Secure; SameSite=None; Partitioned` ); }); it("should allow workers.cloudflare.com", async ({ expect }) => { @@ -365,8 +363,8 @@ describe("Preview Worker", () => { expect(resp.headers.get("location")).toMatchInlineSnapshot( '"/hello?world"' ); - expect(resp.headers.get("set-cookie") ?? "").toMatchInlineSnapshot( - `"token=${defaultUserToken}; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=random-data.playground-testing.devprod.cloudflare.dev"` + expect(resp.headers.get("set-cookie") ?? "").toEqual( + `token=${defaultUserToken}; Domain=random-data.playground-testing.devprod.cloudflare.dev; Path=/; HttpOnly; Secure; SameSite=None; Partitioned` ); }); it("should allow workers-playground.pages.dev", async ({ expect }) => { @@ -388,11 +386,11 @@ describe("Preview Worker", () => { expect(resp.headers.get("location")).toMatchInlineSnapshot( '"/hello?world"' ); - expect(resp.headers.get("set-cookie") ?? "").toMatchInlineSnapshot( - `"token=${defaultUserToken}; HttpOnly; SameSite=None; Partitioned; Secure; Path=/; Domain=random-data.playground-testing.devprod.cloudflare.dev"` + expect(resp.headers.get("set-cookie") ?? "").toEqual( + `token=${defaultUserToken}; Domain=random-data.playground-testing.devprod.cloudflare.dev; Path=/; HttpOnly; Secure; SameSite=None; Partitioned` ); }); - it("should reject unknown referer", async ({ expect }) => { + it("should allow workers-playground.workers.dev", async ({ expect }) => { const resp = await fetch( `${PREVIEW_REMOTE}/.update-preview-token?token=${defaultUserToken}&suffix=${encodeURIComponent( "/hello?world" @@ -403,17 +401,37 @@ describe("Preview Worker", () => { // These are forbidden headers, but undici currently allows setting them headers: { "Sec-Fetch-Dest": "iframe", - Referer: "https://example.com/some/path", + Referer: "https://workers-playground.workers.dev/some/path", }, } ); - expect(await resp.json()).toMatchInlineSnapshot(` + expect(resp.headers.get("location")).toMatchInlineSnapshot( + '"/hello?world"' + ); + expect(resp.headers.get("set-cookie") ?? "").toEqual( + `token=${defaultUserToken}; Domain=random-data.playground-testing.devprod.cloudflare.dev; Path=/; HttpOnly; Secure; SameSite=None; Partitioned` + ); + }); + it("should reject unknown referer", async ({ expect }) => { + const resp = await fetch( + `${PREVIEW_REMOTE}/.update-preview-token?token=${defaultUserToken}&suffix=${encodeURIComponent( + "/hello?world" + )}`, { - "data": {}, - "error": "PreviewRequestForbidden", - "message": "Preview request forbidden", + method: "GET", + redirect: "manual", + // These are forbidden headers, but undici currently allows setting them + headers: { + "Sec-Fetch-Dest": "iframe", + Referer: "https://example.com/some/path", + }, } - `); + ); + expect(await resp.json()).toEqual({ + data: {}, + error: "PreviewRequestForbidden", + message: "Preview request forbidden", + }); }); it("should reject unknown referer with pages.dev in path", async ({ expect, @@ -432,13 +450,76 @@ describe("Preview Worker", () => { }, } ); - expect(await resp.json()).toMatchInlineSnapshot(` + expect(await resp.json()).toEqual({ + data: {}, + error: "PreviewRequestForbidden", + message: "Preview request forbidden", + }); + }); + it("should reject unknown referer with workers.dev in path", async ({ + expect, + }) => { + const resp = await fetch( + `${PREVIEW_REMOTE}/.update-preview-token?token=${defaultUserToken}&suffix=${encodeURIComponent( + "/hello?world" + )}`, { - "data": {}, - "error": "PreviewRequestForbidden", - "message": "Preview request forbidden", + method: "GET", + redirect: "manual", + // These are forbidden headers, but undici currently allows setting them + headers: { + "Sec-Fetch-Dest": "iframe", + Referer: "https://example.com/workers-playground.workers.dev", + }, } - `); + ); + expect(await resp.json()).toEqual({ + data: {}, + error: "PreviewRequestForbidden", + message: "Preview request forbidden", + }); + }); + it("should reject spoofed pages.dev hostname", async ({ expect }) => { + const resp = await fetch( + `${PREVIEW_REMOTE}/.update-preview-token?token=${defaultUserToken}&suffix=${encodeURIComponent( + "/hello?world" + )}`, + { + method: "GET", + redirect: "manual", + // These are forbidden headers, but undici currently allows setting them + headers: { + "Sec-Fetch-Dest": "iframe", + Referer: "https://evil-workers-playground.pages.dev/some/path", + }, + } + ); + expect(await resp.json()).toEqual({ + data: {}, + error: "PreviewRequestForbidden", + message: "Preview request forbidden", + }); + }); + it("should reject spoofed workers.dev hostname", async ({ expect }) => { + const resp = await fetch( + `${PREVIEW_REMOTE}/.update-preview-token?token=${defaultUserToken}&suffix=${encodeURIComponent( + "/hello?world" + )}`, + { + method: "GET", + redirect: "manual", + // These are forbidden headers, but undici currently allows setting them + headers: { + "Sec-Fetch-Dest": "iframe", + Referer: "https://evil-workers-playground.workers.dev/some/path", + }, + } + ); + expect(await resp.json()).toEqual({ + data: {}, + error: "PreviewRequestForbidden", + message: "Preview request forbidden", + }); }); }); }); @@ -457,7 +538,7 @@ describe("Upload Worker", () => { }, body: TEST_WORKER, }); - expect(w.status).toMatchInlineSnapshot("200"); + expect(w.status).toBe(200); }); it("should upload valid worker and return tail url", async ({ expect }) => { const w = await fetch(`${REMOTE}/api/worker`, { @@ -481,19 +562,15 @@ describe("Upload Worker", () => { }, body: TEST_WORKER.replace("fetch(request)", "fetch(request"), }).then((response) => response.json()); - expect(w).toMatchInlineSnapshot(` - { - "data": { - "error": "Uncaught SyntaxError: Unexpected token '{' - at index.js:2:15 - ", - }, - "error": "PreviewError", - "message": "Uncaught SyntaxError: Unexpected token '{' - at index.js:2:15 - ", - } - `); + expect(w).toEqual({ + data: { + error: + "Uncaught SyntaxError: Unexpected token '{'\n at index.js:2:15\n", + }, + error: "PreviewError", + message: + "Uncaught SyntaxError: Unexpected token '{'\n at index.js:2:15\n", + }); }); it("should reject no token", async ({ expect }) => { const w = await fetch(`${REMOTE}/api/worker`, { @@ -504,8 +581,8 @@ describe("Upload Worker", () => { body: TEST_WORKER, }); expect(w.status).toBe(401); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"UploadFailed","message":"Valid token not provided","data":{}}"` + expect(await w.text()).toEqual( + `{"error":"UploadFailed","message":"Valid token not provided","data":{}}` ); }); it("should reject invalid token", async ({ expect }) => { @@ -518,8 +595,8 @@ describe("Upload Worker", () => { body: TEST_WORKER, }); expect(w.status).toBe(401); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"UploadFailed","message":"Valid token not provided","data":{}}"` + expect(await w.text()).toEqual( + `{"error":"UploadFailed","message":"Valid token not provided","data":{}}` ); }); it("should reject invalid form data", async ({ expect }) => { @@ -532,8 +609,8 @@ describe("Upload Worker", () => { body: "not a form", }); expect(w.status).toBe(400); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"BadUpload","message":"Expected valid form data","data":{"error":"TypeError: Unrecognized Content-Type header value. FormData can only parse the following MIME types: multipart/form-data, application/x-www-form-urlencoded"}}"` + expect(await w.text()).toEqual( + `{"error":"BadUpload","message":"Expected valid form data","data":{"error":"TypeError: Unrecognized Content-Type header value. FormData can only parse the following MIME types: multipart/form-data, application/x-www-form-urlencoded"}}` ); }); it("should reject missing metadata", async ({ expect }) => { @@ -554,8 +631,8 @@ export default { --${TEST_WORKER_BOUNDARY}--`, }); expect(w.status).toBe(400); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"BadUpload","message":"Expected metadata file to be defined","data":{}}"` + expect(await w.text()).toEqual( + `{"error":"BadUpload","message":"Expected metadata file to be defined","data":{}}` ); }); it("should reject invalid metadata json", async ({ expect }) => { @@ -573,8 +650,8 @@ Content-Type: application/json --${TEST_WORKER_BOUNDARY}--`, }); expect(w.status).toBe(400); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"BadUpload","message":"Expected metadata file to be valid","data":{}}"` + expect(await w.text()).toEqual( + `{"error":"BadUpload","message":"Expected metadata file to be valid","data":{}}` ); }); it("should reject invalid metadata", async ({ expect }) => { @@ -592,8 +669,8 @@ Content-Type: application/json --${TEST_WORKER_BOUNDARY}--`, }); expect(w.status).toBe(400); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"BadUpload","message":"Expected metadata file to be valid","data":{}}"` + expect(await w.text()).toEqual( + `{"error":"BadUpload","message":"Expected metadata file to be valid","data":{}}` ); }); it("should reject service worker", async ({ expect }) => { @@ -616,8 +693,8 @@ Content-Type: application/json --${TEST_WORKER_BOUNDARY}--`, }); expect(w.status).toBe(400); - expect(await w.text()).toMatchInlineSnapshot( - `"{"error":"ServiceWorkerNotSupported","message":"Service Workers are not supported in the Workers Playground","data":{}}"` + expect(await w.text()).toEqual( + `{"error":"ServiceWorkerNotSupported","message":"Service Workers are not supported in the Workers Playground","data":{}}` ); }); }); @@ -675,9 +752,7 @@ describe("Raw HTTP preview", () => { }, }); - expect(resp.headers.get("cf-ew-raw-set-cookie")).toMatchInlineSnapshot( - `"foo=1, bar=2"` - ); + expect(resp.headers.get("cf-ew-raw-set-cookie")).toEqual(`foo=1, bar=2`); }); it("should pass headers to the user-worker", async ({ expect }) => { @@ -699,18 +774,10 @@ describe("Raw HTTP preview", () => { ); // This contains some-custom-header & accept, as expected - expect(headers).toMatchInlineSnapshot(` - [ - [ - "accept", - "application/json", - ], - [ - "some-custom-header", - "custom", - ], - ] - `); + expect(headers).toEqual([ + ["accept", "application/json"], + ["some-custom-header", "custom"], + ]); }); it("should strip cf-ew-raw- prefix from headers which have it before hitting the user-worker", async ({ @@ -737,17 +804,9 @@ describe("Raw HTTP preview", () => { ); // This contains some-custom-header & accept, as expected, and does not contain cf-ew-raw-some-custom-header or cf-ew-raw-accept - expect(headers).toMatchInlineSnapshot(` - [ - [ - "accept", - "application/json", - ], - [ - "some-custom-header", - "custom", - ], - ] - `); + expect(headers).toEqual([ + ["accept", "application/json"], + ["some-custom-header", "custom"], + ]); }); }); diff --git a/packages/quick-edit/src/index.ts b/packages/quick-edit/src/index.ts index 3b6b40765b6b..cf31dd05754d 100644 --- a/packages/quick-edit/src/index.ts +++ b/packages/quick-edit/src/index.ts @@ -6,12 +6,14 @@ const ALLOWED_PARENT_ORIGINS = [ "https://dash.cloudflare.com", "https://workers.cloudflare.com", "https://workers-playground.pages.dev", + "https://workers-playground.workers.dev", ]; -// Origin patterns using wildcards, for Pages preview deployments etc. +// Origin patterns using wildcards, for preview deployments etc. // Supported in CSP frame-ancestors and matched manually in the client. const ALLOWED_PARENT_ORIGIN_WILDCARDS = [ "https://*.workers-playground.pages.dev", + "https://*.workers-playground.workers.dev", ]; // During local development (wrangler dev), the playground runs on localhost. diff --git a/packages/vite-plugin-cloudflare/src/__tests__/get-warning-for-workers-resolved-configs.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/get-warning-for-workers-resolved-configs.spec.ts index 78e9e090afae..e4212efc826f 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/get-warning-for-workers-resolved-configs.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/get-warning-for-workers-resolved-configs.spec.ts @@ -72,6 +72,7 @@ describe("getWarningForWorkersConfigs", () => { "no_bundle", "rules", ]), + notSupportedOnAuxiliary: new Set(), }, raw: getEmptyRawConfig(), }, @@ -100,6 +101,7 @@ describe("getWarningForWorkersConfigs", () => { nonApplicable: { replacedByVite: new Set(["alias"]), notRelevant: new Set(["build"]), + notSupportedOnAuxiliary: new Set(), }, raw: getEmptyRawConfig(), }, @@ -113,6 +115,7 @@ describe("getWarningForWorkersConfigs", () => { nonApplicable: { replacedByVite: new Set([]), notRelevant: new Set(["find_additional_modules", "no_bundle"]), + notSupportedOnAuxiliary: new Set(), }, raw: getEmptyRawConfig(), }, @@ -124,6 +127,7 @@ describe("getWarningForWorkersConfigs", () => { nonApplicable: { replacedByVite: new Set([]), notRelevant: new Set(["site"]), + notSupportedOnAuxiliary: new Set(), }, raw: getEmptyRawConfig(), }, @@ -147,12 +151,69 @@ describe("getWarningForWorkersConfigs", () => { " `); }); + test("auxiliary worker with assets", ({ expect }) => { + const warning = getWarningForWorkersConfigs({ + entryWorker: { + type: "worker", + config: { + name: "entry-worker", + configPath: "./wrangler.json", + } as Partial as ResolvedWorkerConfig, + nonApplicable: getEmptyNotApplicableMap(), + raw: getEmptyRawConfig(), + }, + auxiliaryWorkers: [ + { + type: "worker", + config: { + name: "worker-a", + configPath: "./a/wrangler.json", + } as Partial as ResolvedWorkerConfig, + nonApplicable: { + replacedByVite: new Set([]), + notRelevant: new Set([]), + notSupportedOnAuxiliary: new Set(["assets"]), + }, + raw: getEmptyRawConfig(), + }, + ], + }); + const normalizedWarning = warning?.replaceAll( + "\\wrangler.json", + "/wrangler.json" + ); + expect(normalizedWarning).toMatchInlineSnapshot(` + " + WARNING: your workers configs contain configuration options which are ignored since they are not applicable when using Vite: + - (auxiliary) worker "worker-a" (config at \`a/wrangler.json\`) + - \`assets\` which is not supported for auxiliary workers + " + `); + }); + + test("entry worker with assets does not warn", ({ expect }) => { + const warning = getWarningForWorkersConfigs({ + entryWorker: { + type: "worker", + config: { + name: "entry-worker", + configPath: "./wrangler.json", + assets: { directory: "./public" }, + } as Partial as ResolvedWorkerConfig, + nonApplicable: getEmptyNotApplicableMap(), + raw: getEmptyRawConfig(), + }, + auxiliaryWorkers: [], + }); + expect(warning).toBeUndefined(); + }); }); function getEmptyNotApplicableMap() { return { replacedByVite: new Set([]), notRelevant: new Set([]), + notSupportedOnAuxiliary: new Set([]), }; } diff --git a/packages/vite-plugin-cloudflare/src/__tests__/get-worker-config.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/get-worker-config.spec.ts index dd88729108b0..5e9f4f3acb7f 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/get-worker-config.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/get-worker-config.spec.ts @@ -56,6 +56,7 @@ describe("readWorkerConfigFromFile", () => { expect(nonApplicable).toEqual({ replacedByVite: new Set(), notRelevant: new Set(), + notSupportedOnAuxiliary: new Set(), }); }); @@ -85,6 +86,7 @@ describe("readWorkerConfigFromFile", () => { expect(nonApplicable).toEqual({ replacedByVite: new Set(), notRelevant: new Set(), + notSupportedOnAuxiliary: new Set(), }); }); diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index 139a0f852235..e86e928e0630 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -227,6 +227,7 @@ function resolveWorkerConfig( nonApplicable = { replacedByVite: new Set(), notRelevant: new Set(), + notSupportedOnAuxiliary: new Set(), }; } @@ -438,6 +439,10 @@ export function resolvePluginConfig( visitedConfigPaths: configPaths, }); + if (workerResolvedConfig.config.assets) { + workerResolvedConfig.nonApplicable.notSupportedOnAuxiliary.add("assets"); + } + auxiliaryWorkersResolvedConfigs.push(workerResolvedConfig); const workerEnvironmentName = diff --git a/packages/vite-plugin-cloudflare/src/workers-configs.ts b/packages/vite-plugin-cloudflare/src/workers-configs.ts index fc5927a5bfb4..c0cda76adeb7 100644 --- a/packages/vite-plugin-cloudflare/src/workers-configs.ts +++ b/packages/vite-plugin-cloudflare/src/workers-configs.ts @@ -44,6 +44,12 @@ export type NonApplicableConfigMap = { NonApplicableWorkerConfigsInfo["notRelevant"][number] > >; + notSupportedOnAuxiliary: Set< + Extract< + keyof RawWorkerConfig, + NonApplicableWorkerConfigsInfo["notSupportedOnAuxiliary"][number] + > + >; }; type NonApplicableWorkerConfigsInfo = typeof nonApplicableWorkerConfigs; @@ -93,6 +99,10 @@ export const nonApplicableWorkerConfigs = { "site", "tsconfig", ], + /** + * Configs that are only supported on the entry worker and will be ignored on auxiliary workers + */ + notSupportedOnAuxiliary: ["assets"], } as const; /** @@ -120,6 +130,7 @@ function readWorkerConfig( const nonApplicable: NonApplicableConfigMap = { replacedByVite: new Set(), notRelevant: new Set(), + notSupportedOnAuxiliary: new Set(), }; const config: Optional = wrangler.unstable_readConfig( @@ -209,7 +220,8 @@ export function getWarningForWorkersConfigs( ) => { const nonApplicableLines = getWorkerNonApplicableWarnLines( workerConfig, - ` - ` + ` - `, + { isAuxiliary: !isEntryWorker } ); if (nonApplicableLines.length > 0) { @@ -236,11 +248,13 @@ export function getWarningForWorkersConfigs( function getWorkerNonApplicableWarnLines( workerConfig: WorkerResolvedConfig, - linePrefix: string + linePrefix: string, + options?: { isAuxiliary?: boolean } ): string[] { const lines: string[] = []; - const { replacedByVite, notRelevant } = workerConfig.nonApplicable; + const { replacedByVite, notRelevant, notSupportedOnAuxiliary } = + workerConfig.nonApplicable; for (const config of replacedByVite) { lines.push( @@ -254,6 +268,12 @@ function getWorkerNonApplicableWarnLines( ); } + if (options?.isAuxiliary && notSupportedOnAuxiliary.size > 0) { + lines.push( + `${linePrefix}${[...notSupportedOnAuxiliary].map((config) => `\`${config}\``).join(", ")} which ${notSupportedOnAuxiliary.size > 1 ? "are" : "is"} not supported for auxiliary workers` + ); + } + return lines; } diff --git a/packages/vite-plugin-cloudflare/turbo.json b/packages/vite-plugin-cloudflare/turbo.json index 0cb9b2caf243..9bbafe8d5e7e 100644 --- a/packages/vite-plugin-cloudflare/turbo.json +++ b/packages/vite-plugin-cloudflare/turbo.json @@ -6,12 +6,7 @@ "outputs": ["dist/**"] }, "test:e2e": { - "env": [ - "CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS", - "NODE_DEBUG", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN" - ], + "env": ["$TURBO_EXTENDS$", "CLOUDFLARE_VITE_E2E_KEEP_TEMP_DIRS"], "dependsOn": ["build"], "inputs": ["e2e/**"] } diff --git a/packages/vitest-pool-workers/turbo.json b/packages/vitest-pool-workers/turbo.json index adaf4792e614..5b31d9562a54 100644 --- a/packages/vitest-pool-workers/turbo.json +++ b/packages/vitest-pool-workers/turbo.json @@ -6,6 +6,7 @@ "outputs": ["dist/**"] }, "test": { + // Only needed for test/remote-proxy-cleanup.test.ts "env": ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"] } } diff --git a/packages/workers-playground/.env b/packages/workers-playground/.env index b4977e6abfa2..56e3dd4f5d8b 100644 --- a/packages/workers-playground/.env +++ b/packages/workers-playground/.env @@ -1,2 +1 @@ -VITE_PLAYGROUND_ROOT=playground.devprod.cloudflare.dev VITE_PLAYGROUND_PREVIEW=cloudflarepreviews.com \ No newline at end of file diff --git a/packages/workers-playground/.env.development b/packages/workers-playground/.env.development index f6c6a5f5ebf2..7aeefb5b3f41 100644 --- a/packages/workers-playground/.env.development +++ b/packages/workers-playground/.env.development @@ -1,2 +1 @@ -VITE_PLAYGROUND_ROOT=playground-testing.devprod.cloudflare.dev VITE_PLAYGROUND_PREVIEW=playground-testing.devprod.cloudflare.dev \ No newline at end of file diff --git a/packages/workers-playground/README.md b/packages/workers-playground/README.md index acd1f9311244..f80e39296458 100644 --- a/packages/workers-playground/README.md +++ b/packages/workers-playground/README.md @@ -1,30 +1,30 @@ -# Workers Playground Pages Project +# Workers Playground This package contains the client side assets used in the Workers Playground available in the Cloudflare Dashboard at [https://workers.cloudflare.com/playground]. +It is deployed as a Cloudflare Worker with static assets (assets-only in production). + ## Developing locally > This is intended for internal Cloudflare developers. Currently, it's not possible to contribute to this package as an external contributor - Ensure the rest of the team are aware you're working on the Workers Playground, as there's only one instance of the testing `playground-preview-worker`. -- Run `pnpm run dev` in the root of this package. That will start the local Vite server for the playground frontend, with API calls hitting the testing `playground-preview-worker`. +- Run `pnpm dev -F @cloudflare/workers-playground` in the root of the repository. + That will start the local Vite server for the playground frontend, with API calls hitting the testing `playground-preview-worker`. - To test changes to the playground preview worker, run `pnpm run deploy:testing` in `packages/playground-preview-worker` to deploy it to the test environment. ## Building -1. Run `pnpm -F workers-playground build` +1. Run `pnpm build -F @cloudflare/workers-playground` -This generates the files into the `dist` directory that can then be deployed to Cloudflare Pages. +This generates the files into the `dist` directory that can then be deployed as a Cloudflare Worker with static assets. ## Deployment -Deployments are managed by GitHub Actions: +Production deployments are managed by GitHub Actions via the `changesets.yml` workflow: +when a "Version Packages" PR containing a changeset that touches this package is merged to `main`, +the package is deployed to production via `wrangler deploy`. -- deploy-pages-previews.yml: - - Runs on any PR that has the `preview:workers-playground` label. - - Deploys a preview, which can then be accessed via [https://.workers-playground.pages.dev/]. -- changesets.yml: - - Runs when a "Version Packages" PR, containing a changeset that touches this package, is merged to `main`. - - Deploys this package to production, which can then be accessed via [https://workers-playground.pages.dev/]. +To test changes locally, use `pnpm dev -F @cloudflare/workers-playground` (see "Developing locally" above). diff --git a/packages/workers-playground/functions/playground/[[route]].ts b/packages/workers-playground/functions/playground/[[route]].ts deleted file mode 100644 index bc990fa49bca..000000000000 --- a/packages/workers-playground/functions/playground/[[route]].ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * These functions are purely for local development, and when running a Pages preview. In production, requests go through the following pipeline: - - * eyeball -> playground-preview-worker -> workers-playground - * However, locally, and in a Pages preview, requests go through this pipeline: - - * eyeball -> workers-playground (local or preview) -> playground-preview-worker (staging) - */ -export async function onRequest({ request, env }) { - const url = new URL(request.url); - const cookie = await fetch( - "https://playground-testing.devprod.cloudflare.dev" - ); - const header = cookie.headers.getSetCookie(); - const asset = await env.ASSETS.fetch( - new URL(url.pathname.split("/playground")[1], "http://dummy") - ); - if (url.pathname === "/playground") { - return new Response(asset.body, { - headers: { - "Set-Cookie": header[0].replace( - "playground-testing.devprod.cloudflare.dev", - url.host - ), - ...asset.headers, - }, - }); - } else { - return asset; - } -} diff --git a/packages/workers-playground/functions/playground/api/[[route]].ts b/packages/workers-playground/functions/playground/api/[[route]].ts deleted file mode 100644 index f1901dd999fe..000000000000 --- a/packages/workers-playground/functions/playground/api/[[route]].ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * These functions are purely for local development, and when running a Pages preview. In production, requests go through the following pipeline: - - * eyeball -> playground-preview-worker -> workers-playground - * However, locally, and in a Pages preview, requests go through this pipeline: - - * eyeball -> workers-playground (local or preview) -> playground-preview-worker (staging) - */ -export function onRequest({ request }) { - const url = new URL(request.url); - return fetch( - new URL( - url.pathname.split("/playground")[1], - `https://playground-testing.devprod.cloudflare.dev` - ), - request - ); -} diff --git a/packages/workers-playground/generate-default-hashes.ts b/packages/workers-playground/generate-default-hashes.ts index feda6c5e6ec5..90db50e492ab 100644 --- a/packages/workers-playground/generate-default-hashes.ts +++ b/packages/workers-playground/generate-default-hashes.ts @@ -1,3 +1,4 @@ +import assert from "node:assert"; import { readFile, writeFile } from "node:fs/promises"; import { FormData, Response } from "undici"; @@ -86,11 +87,12 @@ const defaultWorker = async () => { async function serialiseWorker(worker: FormData) { const serialisedWorker = new Response(worker); - const generatedBoundary = serialisedWorker.headers - .get("content-type")! - .split(";")[1] - .split("=")[1] - .trim(); + const generatedBoundary = + // content-type is always set for a FormData response, and always has the boundary as a parameter + (serialisedWorker.headers.get("content-type") as string) + .split(";")[1] + .split("=")[1] + .trim(); // This boundary is arbitrary, it's just specified for stability const fixedBoundary = "----formdata-88e2b909-318c-42df-af0d-9077f33c7988"; @@ -107,24 +109,23 @@ const pythonWorkerContent = await pythonWorker(); const defaultWorkerContent = await defaultWorker(); if (process.argv[2] === "check") { - const currentFile = await import("./src/QuickEditor/defaultHashes.ts"); + const currentFile = await import("./src/QuickEditor/defaultHashes.js"); const generated = await serialiseHashes({ "/python": pythonWorkerContent, "/": defaultWorkerContent, }); - const equal = - currentFile.default["/"] === generated["/"] && - currentFile.default["/python"] === generated["/python"]; - if (!equal) { - console.log("Hash not up to date", equal); + try { + assert.deepEqual(currentFile.default, generated); + } catch (error) { + console.error("Hash not up to date", error); console.log("current.txt", currentFile.default); console.log("gen.txt", generated); + process.exit(1); } - process.exit(equal ? 0 : 1); } else { await writeFile( - "./src/QuickEditor/defaultHashes.ts", + "./src/QuickEditor/defaultHashes.js", await generateFileForWorker({ "/python": pythonWorkerContent, "/": defaultWorkerContent, diff --git a/packages/workers-playground/package.json b/packages/workers-playground/package.json index 18e4ecb2d804..0fdd61040195 100644 --- a/packages/workers-playground/package.json +++ b/packages/workers-playground/package.json @@ -4,14 +4,14 @@ "private": true, "type": "module", "scripts": { - "build": "tsc && vite build", - "build:testing": "tsc && vite build -m development", - "check": "pnpm exec tsx ./generate-default-hashes.ts check", + "build": "git clean -xdf dist && tsc && vite build", + "check": "pnpm run check:default-hashes && pnpm run check:lint && pnpm run check:type", + "check:default-hashes": "node -r esbuild-register ./generate-default-hashes.ts check", "check:lint": "eslint src --max-warnings=0 --cache", "check:type": "tsc", - "deploy": "CLOUDFLARE_ACCOUNT_ID=e35fd947284363a46fd7061634477114 wrangler pages deploy --project-name workers-playground ./dist", + "deploy": "pnpm run build && CLOUDFLARE_ACCOUNT_ID=e35fd947284363a46fd7061634477114 wrangler deploy", "dev": "vite", - "generate:default-hashes": "pnpm exec tsx ./generate-default-hashes.ts && pnpm exec prettier --write ./src/QuickEditor/defaultHashes.ts" + "generate:default-hashes": "node -r esbuild-register ./generate-default-hashes.ts && pnpm exec prettier --write ./src/QuickEditor/defaultHashes.js" }, "dependencies": { "@cloudflare/component-button": "^7.0.11", @@ -44,6 +44,7 @@ }, "devDependencies": { "@cloudflare/eslint-config-shared": "workspace:*", + "@cloudflare/vite-plugin": "workspace:*", "@types/glob-to-regexp": "^0.4.1", "@types/react": "^18.3.3", "@types/react-dom": "^18.2.0", @@ -51,9 +52,8 @@ "@vitejs/plugin-react": "^4.3.3", "eslint": "catalog:default", "react-use-websocket": "^4.13.0", - "tsx": "^3.12.8", "undici": "catalog:default", - "vite": "catalog:default", + "vite": "catalog:vite-plugin", "wrangler": "workspace:^" }, "volta": { diff --git a/packages/workers-playground/src/QuickEditor/defaultHashes.ts b/packages/workers-playground/src/QuickEditor/defaultHashes.js similarity index 98% rename from packages/workers-playground/src/QuickEditor/defaultHashes.ts rename to packages/workers-playground/src/QuickEditor/defaultHashes.js index f6aeab50b93a..9a6690c51d88 100644 --- a/packages/workers-playground/src/QuickEditor/defaultHashes.ts +++ b/packages/workers-playground/src/QuickEditor/defaultHashes.js @@ -3,12 +3,12 @@ export default { contentType: "multipart/form-data; boundary=----formdata-88e2b909-318c-42df-af0d-9077f33c7988", worker: - '------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="metadata"\r\n\r\n{"main_module":"index.py","compatibility_date":"$REPLACE_COMPAT_DATE","compatibility_flags":["python_workers"]}\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="index.py"; filename="index.py"\r\nContent-Type: text/x-python\r\n\r\nfrom js import Response\nimport numpy as np\n\ndef on_fetch(request):\n print("Hi there!")\n arr = np.array([1, 2, 3])\n return Response.new(str(arr))\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="cf-requirements.txt"; filename="cf-requirements.txt"\r\nContent-Type: text/plain\r\n\r\nnumpy\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988--', + '------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="metadata"\r\n\r\n{"main_module":"index.py","compatibility_date":"$REPLACE_COMPAT_DATE","compatibility_flags":["python_workers"]}\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="index.py"; filename="index.py"\r\nContent-Type: text/x-python\r\n\r\nfrom js import Response\nimport numpy as np\n\ndef on_fetch(request):\n print("Hi there!")\n arr = np.array([1, 2, 3])\n return Response.new(str(arr))\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="cf-requirements.txt"; filename="cf-requirements.txt"\r\nContent-Type: text/plain\r\n\r\nnumpy\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988--\r\n', }, "/": { contentType: "multipart/form-data; boundary=----formdata-88e2b909-318c-42df-af0d-9077f33c7988", worker: - '------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="metadata"\r\n\r\n{"main_module":"index.js","compatibility_date":"$REPLACE_COMPAT_DATE","compatibility_flags":["nodejs_compat"]}\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="data.js"; filename="data.js"\r\nContent-Type: application/javascript+module\r\n\r\nexport default {\n\tname: "Example API",\n\ttags: ["example", "api", "response"],\n};\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="index.js"; filename="index.js"\r\nContent-Type: application/javascript+module\r\n\r\nimport welcome from "welcome.html";\n\n/**\n * @typedef {Object} Env\n */\n\nexport default {\n\t/**\n\t * @param {Request} request\n\t * @param {Env} env\n\t * @param {ExecutionContext} ctx\n\t * @returns {Promise}\n\t */\n\tasync fetch(request, env, ctx) {\n\t\tconst url = new URL(request.url);\n\t\tconsole.log(`Hello ${navigator.userAgent} at path ${url.pathname}!`);\n\n\t\tif (url.pathname === "/api") {\n\t\t\t// You could also call a third party API here\n\t\t\tconst data = await import("./data.js");\n\t\t\treturn Response.json(data);\n\t\t}\n\t\treturn new Response(welcome, {\n\t\t\theaders: {\n\t\t\t\t"content-type": "text/html",\n\t\t\t},\n\t\t});\n\t},\n};\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="welcome.html"; filename="welcome.html"\r\nContent-Type: text/plain\r\n\r\n\n\n\t\n\t\t\n\t\t\n\t\t\n\t\tCloudflare Workers Playground\n\t\t\n\t\n\n\t\n\t\t
\n\t\t\t\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

\n\t\t\t\tWelcome! Use this Playground to test drive a Worker, create a demo to\n\t\t\t\tshare online, and when ready deploy directly to the edge by setting up a\n\t\t\t\tCloudflare account.\n\t\t\t

\n\t\t\t

What is a Worker?

\n\t\t\t

\n\t\t\t\tA Cloudflare Worker is JavaScript code you write that handles your web\n\t\t\t\tsite\'s HTTP traffic directly in Cloudflare\'s edge locations around the\n\t\t\t\tworld, allowing you to locate code close to your end users in order to\n\t\t\t\trespond to them more quickly\n\t\t\t

\n\t\t\t

Try it yourself

\n\t\t\t

\n\t\t\t\tOn your left is a sample Worker that is running on this site. You can\n\t\t\t\tedit it live and see the results here. Edit the path above to /api to\n\t\t\t\tsee how the example Worker handles different routes. You can also edit\n\t\t\t\tthe code to see what\'s possible and bring your next idea to life.\n\t\t\t

\n\t\t
\n\t\n\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988--', + '------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="metadata"\r\n\r\n{"main_module":"index.js","compatibility_date":"$REPLACE_COMPAT_DATE","compatibility_flags":["nodejs_compat"]}\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="data.js"; filename="data.js"\r\nContent-Type: application/javascript+module\r\n\r\nexport default {\n\tname: "Example API",\n\ttags: ["example", "api", "response"],\n};\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="index.js"; filename="index.js"\r\nContent-Type: application/javascript+module\r\n\r\nimport welcome from "welcome.html";\n\n/**\n * @typedef {Object} Env\n */\n\nexport default {\n\t/**\n\t * @param {Request} request\n\t * @param {Env} env\n\t * @param {ExecutionContext} ctx\n\t * @returns {Promise}\n\t */\n\tasync fetch(request, env, ctx) {\n\t\tconst url = new URL(request.url);\n\t\tconsole.log(`Hello ${navigator.userAgent} at path ${url.pathname}!`);\n\n\t\tif (url.pathname === "/api") {\n\t\t\t// You could also call a third party API here\n\t\t\tconst data = await import("./data.js");\n\t\t\treturn Response.json(data);\n\t\t}\n\t\treturn new Response(welcome, {\n\t\t\theaders: {\n\t\t\t\t"content-type": "text/html",\n\t\t\t},\n\t\t});\n\t},\n};\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988\r\nContent-Disposition: form-data; name="welcome.html"; filename="welcome.html"\r\nContent-Type: text/plain\r\n\r\n\n\n\t\n\t\t\n\t\t\n\t\t\n\t\tCloudflare Workers Playground\n\t\t\n\t\n\n\t\n\t\t
\n\t\t\t\n\t\t
\n\t\t
\n\t\t\t\n\t\t\t

\n\t\t\t\tWelcome! Use this Playground to test drive a Worker, create a demo to\n\t\t\t\tshare online, and when ready deploy directly to the edge by setting up a\n\t\t\t\tCloudflare account.\n\t\t\t

\n\t\t\t

What is a Worker?

\n\t\t\t

\n\t\t\t\tA Cloudflare Worker is JavaScript code you write that handles your web\n\t\t\t\tsite\'s HTTP traffic directly in Cloudflare\'s edge locations around the\n\t\t\t\tworld, allowing you to locate code close to your end users in order to\n\t\t\t\trespond to them more quickly\n\t\t\t

\n\t\t\t

Try it yourself

\n\t\t\t

\n\t\t\t\tOn your left is a sample Worker that is running on this site. You can\n\t\t\t\tedit it live and see the results here. Edit the path above to /api to\n\t\t\t\tsee how the example Worker handles different routes. You can also edit\n\t\t\t\tthe code to see what\'s possible and bring your next idea to life.\n\t\t\t

\n\t\t
\n\t\n\n\r\n------formdata-88e2b909-318c-42df-af0d-9077f33c7988--\r\n', }, }; diff --git a/packages/workers-playground/src/worker.ts b/packages/workers-playground/src/worker.ts new file mode 100644 index 000000000000..45c11087f75d --- /dev/null +++ b/packages/workers-playground/src/worker.ts @@ -0,0 +1,79 @@ +interface Env { + ASSETS: { fetch(input: RequestInfo, init?: RequestInit): Promise }; +} + +const TESTING_ORIGIN = "playground-testing.devprod.cloudflare.dev"; + +export default { + /** + * This Worker entry point is only used in the dev environment (wrangler.dev.jsonc). + * + * In production, workers-playground is deployed as an assets-only Worker, + * and the playground-preview-worker handles all routing and proxying. + * + * eyeball -> playground-preview-worker (production) -> workers-playground (production, wrangler.jsonc) + * + * Locally, this Worker replicates the proxying behavior so that the + * playground can communicate with the testing playground-preview-worker: + * + * eyeball -> workers-playground (local, wrangler.dev.jsonc) -> playground-preview-worker (testing) + */ + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // For the root `/playground` path, fetch a cookie from the testing endpoint and inject it into + // the response so that the preview iframe can authenticate with the testing preview worker. + if (url.pathname === "/playground") { + const tokenCookie = transformTokenCookie( + await fetchTokenCookieFromTestingEndpoint() + ); + url.pathname = "/"; // Rewrite to fetch the index.html asset + const asset = await env.ASSETS.fetch(url.href); + const response = new Response(asset.body, asset); + if (tokenCookie) { + response.headers.set("Set-Cookie", tokenCookie); + } + return response; + } + + // Proxy API requests to the testing playground-preview-worker + if (url.pathname.startsWith("/playground/api")) { + const apiPath = url.pathname.replace(/^\/playground/, ""); + const targetUrl = new URL(apiPath, `https://${TESTING_ORIGIN}`); + targetUrl.search = url.search; + return fetch(targetUrl, request as RequestInit); + } + + // All other requests fall through to static assets + return env.ASSETS.fetch(request); + }, +}; + +/** + * Transforms the cookie fetched from the testing endpoint + * + * - Replaces the testing origin with localhost so that the cookie is sent in subsequent requests from the playground to the local workers-playground. + * - Strips out cookie directives that can cause issues in the local dev environment + */ +function transformTokenCookie(cookie: string | undefined): string | undefined { + return ( + cookie + ?.split(";") + // Strip out SameSite and Secure directives, which can cause issues in the local dev environment without HTTPS + ?.filter((part) => !part.trim().match(/^SameSite=|^Secure$/)) + // Replace the testing origin with localhost so that the cookie is sent in subsequent requests from the playground to the local workers-playground + ?.map((part) => part.replace(TESTING_ORIGIN, "localhost")) + ?.join("; ") + ); +} + +/** + * Fetches the token cookie from the testing endpoint. + */ +async function fetchTokenCookieFromTestingEndpoint(): Promise< + string | undefined +> { + const cookieRequest = await fetch(`https://${TESTING_ORIGIN}`); + const cookies = cookieRequest.headers.getSetCookie(); + return cookies.find((cookie) => cookie.includes("user=")); +} diff --git a/packages/workers-playground/tsconfig.node.json b/packages/workers-playground/tsconfig.node.json index eb2904a7e816..543cd5fe4249 100644 --- a/packages/workers-playground/tsconfig.node.json +++ b/packages/workers-playground/tsconfig.node.json @@ -3,10 +3,10 @@ "composite": true, "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "target": "ES2021", "lib": ["ES2021"] }, - "include": ["vite.config.ts", "eslintrc.cjs", "generate-default-hash.ts"] + "include": ["vite.config.ts", "eslintrc.cjs", "generate-default-hashes.ts"] } diff --git a/packages/workers-playground/turbo.json b/packages/workers-playground/turbo.json index 0ac034929407..207223552e4e 100644 --- a/packages/workers-playground/turbo.json +++ b/packages/workers-playground/turbo.json @@ -6,9 +6,7 @@ "outputs": ["dist/**"], "env": [ "NODE_ENV", - "VITE_PLAYGROUND_PREVIEW", - "VITE_PLAYGROUND_ROOT", - "VITE_DEVTOOLS_PREVIEW_URL" + "VITE_PLAYGROUND_PREVIEW" // used to determine the URL where the preview of the Worker being edited in the playground is hosted. ] } } diff --git a/packages/workers-playground/vite.config.ts b/packages/workers-playground/vite.config.ts index 993da515632f..b096a4b20636 100644 --- a/packages/workers-playground/vite.config.ts +++ b/packages/workers-playground/vite.config.ts @@ -1,20 +1,16 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; -import { defineConfig, loadEnv } from "vite"; +import { defineConfig } from "vite"; -// https://vitejs.dev/config/ export default defineConfig(({ mode }) => { - const playgroundHost = loadEnv(mode, process.cwd())["VITE_PLAYGROUND_ROOT"]; return { - plugins: [react()], - server: { - proxy: { - "/playground/api": { - target: `https://${playgroundHost}`, - changeOrigin: true, - rewrite: (path) => path.replace(/^\/playground/, ""), - }, - }, - }, + plugins: [ + react(), + cloudflare({ + configPath: + mode === "development" ? "./wrangler.dev.jsonc" : "./wrangler.jsonc", + }), + ], resolve: { alias: { "react/jsx-runtime.js": "react/jsx-runtime", diff --git a/packages/workers-playground/welcome/requirements.txt b/packages/workers-playground/welcome/cf-requirements.txt similarity index 100% rename from packages/workers-playground/welcome/requirements.txt rename to packages/workers-playground/welcome/cf-requirements.txt diff --git a/packages/workers-playground/wrangler.dev.jsonc b/packages/workers-playground/wrangler.dev.jsonc new file mode 100644 index 000000000000..81a43ff58866 --- /dev/null +++ b/packages/workers-playground/wrangler.dev.jsonc @@ -0,0 +1,16 @@ +// In production, this is an assets-only Worker. +// The playground-preview-worker handles all routing and proxying. +// In local development we add a Worker entry point to replicate that proxying behavior for local development. +// Keep this in sync with `wrangler.jsonc`. +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "workers-playground", + "compatibility_date": "2025-07-01", + "main": "src/worker.ts", + "assets": { + "directory": "./dist", + "binding": "ASSETS", + "not_found_handling": "single-page-application", + "run_worker_first": ["/playground/api/*", "/playground"], + }, +} diff --git a/packages/workers-playground/wrangler.jsonc b/packages/workers-playground/wrangler.jsonc new file mode 100644 index 000000000000..36a89b4185dd --- /dev/null +++ b/packages/workers-playground/wrangler.jsonc @@ -0,0 +1,14 @@ +// In production, this is an assets-only Worker. +// The playground-preview-worker handles all routing and proxying. +// Keep this in sync with `wrangler.dev.jsonc`. +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "workers-playground", + "compatibility_date": "2025-07-01", + "assets": { + "directory": "./dist", + "not_found_handling": "single-page-application", + }, + "preview_urls": true, + "workers_dev": true, +} diff --git a/packages/wrangler/src/__tests__/d1/export.test.ts b/packages/wrangler/src/__tests__/d1/export.test.ts index 938e0a618574..496e39fe6aa0 100644 --- a/packages/wrangler/src/__tests__/d1/export.test.ts +++ b/packages/wrangler/src/__tests__/d1/export.test.ts @@ -191,6 +191,46 @@ describe("export", () => { `[Error: Found a database with name or binding D1 but it is missing a database_id, which is needed for operations on remote resources. Please create the remote D1 database by deploying your project or running 'wrangler d1 create D1'.]` ); }); + + it("should handle multiple tables", async ({ expect }) => { + setIsTTY(false); + writeWranglerConfig({ + d1_databases: [ + { binding: "DATABASE", database_name: "db", database_id: "xxxx" }, + ], + }); + + // Fill with data + fs.writeFileSync( + "data.sql", + ` + CREATE TABLE foo(id INTEGER PRIMARY KEY, value TEXT); + CREATE TABLE bar(id INTEGER PRIMARY KEY, value TEXT); + CREATE TABLE baz(id INTEGER PRIMARY KEY, value TEXT); + INSERT INTO foo (value) VALUES ('xxx'),('yyy'),('zzz'); + INSERT INTO bar (value) VALUES ('aaa'),('bbb'),('ccc'); + INSERT INTO baz (value) VALUES ('111'),('222'),('333'); + ` + ); + await runWrangler("d1 execute db --file data.sql"); + + await runWrangler( + "d1 export db --output test-multiple.sql --table foo --table baz" + ); + expect(fs.readFileSync("test-multiple.sql", "utf8")).toBe( + [ + "PRAGMA defer_foreign_keys=TRUE;", + "CREATE TABLE foo(id INTEGER PRIMARY KEY, value TEXT);", + "INSERT INTO \"foo\" VALUES(1,'xxx');", + "INSERT INTO \"foo\" VALUES(2,'yyy');", + "INSERT INTO \"foo\" VALUES(3,'zzz');", + "CREATE TABLE baz(id INTEGER PRIMARY KEY, value TEXT);", + "INSERT INTO \"baz\" VALUES(1,'111');", + "INSERT INTO \"baz\" VALUES(2,'222');", + "INSERT INTO \"baz\" VALUES(3,'333');", + ].join("\n") + ); + }); }); function mockResponses() { diff --git a/packages/wrangler/src/__tests__/vectorize/vectorize.test.ts b/packages/wrangler/src/__tests__/vectorize/vectorize.test.ts index e7664c614ac5..22cb90b53741 100644 --- a/packages/wrangler/src/__tests__/vectorize/vectorize.test.ts +++ b/packages/wrangler/src/__tests__/vectorize/vectorize.test.ts @@ -369,14 +369,108 @@ describe("vectorize commands", () => { expect(std.warn).toMatchInlineSnapshot(` "▲ [WARNING]  - You haven't created any indexes on this account. + You haven't created any indexes on this account. - Use 'wrangler vectorize create ' to create one, or visit - https://developers.cloudflare.com/vectorize/ to get started. + Use 'wrangler vectorize create ' to create one, or visit + https://developers.cloudflare.com/vectorize/ to get started. -" + " + `); + }); + + it("should return empty array JSON when there are no vectorize indexes with --json flag", async () => { + mockVectorizeV2RequestError(); + await runWrangler("vectorize list --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(`[]`); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + + it("should handle listing vectorize indexes with valid JSON output", async () => { + mockVectorizeV2Request(); + await runWrangler("vectorize list --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + [ + { + "config": { + "dimensions": 1536, + "metric": "euclidean", + }, + "created_on": "2024-07-11T13:02:18.00268Z", + "description": "test-desc", + "modified_on": "2024-07-11T13:02:18.00268Z", + "name": "test-index", + }, + { + "config": { + "dimensions": 32, + "metric": "dot-product", + }, + "created_on": "2024-07-11T13:02:18.00268Z", + "description": "another-desc", + "modified_on": "2024-07-11T13:02:18.00268Z", + "name": "another-index", + }, + ] + `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + + it("should handle creating a vectorize index with valid JSON output", async () => { + mockVectorizeV2Request(); + await runWrangler( + "vectorize create test-index --dimensions=1536 --metric=euclidean --json" + ); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + { + "config": { + "dimensions": 1536, + "metric": "euclidean", + }, + "created_on": "2024-07-11T13:02:18.00268Z", + "description": "test-desc", + "modified_on": "2024-07-11T13:02:18.00268Z", + "name": "test-index", + } + `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + + it("should handle get on a vectorize index with valid JSON output", async () => { + mockVectorizeV2Request(); + await runWrangler("vectorize get test-index --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + { + "config": { + "dimensions": 1536, + "metric": "euclidean", + }, + "created_on": "2024-07-11T13:02:18.00268Z", + "description": "test-desc", + "modified_on": "2024-07-11T13:02:18.00268Z", + "name": "test-index", + } `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + + it("should handle info on a vectorize index with valid JSON output", async () => { + mockVectorizeV2Request(); + await runWrangler("vectorize info test-index --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + { + "dimensions": 1024, + "processedUpToDatetime": "2024-07-19T13:11:44.064Z", + "processedUpToMutation": "7f11d6e5-d126-4f76-936e-fbfec079e0be", + "vectorCount": 1000, + } + `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); }); it("should handle a get on a vectorize V1 index", async () => { @@ -869,14 +963,45 @@ describe("vectorize commands", () => { expect(std.warn).toMatchInlineSnapshot(` "▲ [WARNING]  - You haven't created any metadata indexes on this account. + You haven't created any metadata indexes on this account. - Use 'wrangler vectorize create-metadata-index ' to create one, or visit - https://developers.cloudflare.com/vectorize/ to get started. + Use 'wrangler vectorize create-metadata-index ' to create one, or visit + https://developers.cloudflare.com/vectorize/ to get started. -" + " + `); + }); + + it("should return empty array JSON when list metadata indexes returns empty with --json flag", async () => { + mockVectorizeV2RequestError(); + await runWrangler("vectorize list-metadata-index test-index --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(`[]`); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + + it("should handle list-metadata-index with valid JSON output", async () => { + mockVectorizeV2Request(); + await runWrangler("vectorize list-metadata-index test-index --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + [ + { + "indexType": "string", + "propertyName": "string-prop", + }, + { + "indexType": "number", + "propertyName": "num-prop", + }, + { + "indexType": "boolean", + "propertyName": "bool-prop", + }, + ] `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); }); it("should handle delete metadata index", async () => { @@ -1025,6 +1150,8 @@ describe("vectorize commands", () => { ], } `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); }); it("should warn when list-vectors returns no vectors", async () => { @@ -1040,8 +1167,25 @@ describe("vectorize commands", () => { expect(std.warn).toMatchInlineSnapshot(` "▲ [WARNING] No vectors found in this index. -" + " + `); + }); + + it("should return valid JSON when list-vectors returns no vectors with --json flag", async () => { + mockVectorizeV2RequestError(); + await runWrangler("vectorize list-vectors test-index --json"); + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + { + "count": 0, + "cursorExpirationTimestamp": null, + "isTruncated": false, + "nextCursor": null, + "totalCount": 0, + "vectors": [], + } `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); }); }); diff --git a/packages/wrangler/src/__tests__/vectorize/vectorize.upsert.test.ts b/packages/wrangler/src/__tests__/vectorize/vectorize.upsert.test.ts index 92f7ad901e08..965c26b57199 100644 --- a/packages/wrangler/src/__tests__/vectorize/vectorize.upsert.test.ts +++ b/packages/wrangler/src/__tests__/vectorize/vectorize.upsert.test.ts @@ -239,6 +239,78 @@ describe("dataset upsert", () => { `); }); + it("should output valid JSON for insert with --json flag", async () => { + writeFileSync( + "vectors.ndjson", + testVectors.map((v) => JSON.stringify(v)).join(`\n`) + ); + + const mutationId = crypto.randomUUID(); + + msw.use( + http.post("*/vectorize/v2/indexes/:indexName/insert", async () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { mutationId: mutationId }, + }, + { status: 200 } + ); + }) + ); + + await runWrangler( + `vectorize insert my-index --file vectors.ndjson --batch-size 10 --json` + ); + + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + { + "count": 5, + "index": "my-index", + } + `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + + it("should output valid JSON for upsert with --json flag", async () => { + writeFileSync( + "vectors.ndjson", + testVectors.map((v) => JSON.stringify(v)).join(`\n`) + ); + + const mutationId = crypto.randomUUID(); + + msw.use( + http.post("*/vectorize/v2/indexes/:indexName/upsert", async () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { mutationId: mutationId }, + }, + { status: 200 } + ); + }) + ); + + await runWrangler( + `vectorize upsert my-index --file vectors.ndjson --batch-size 10 --json` + ); + + expect(JSON.parse(std.out)).toMatchInlineSnapshot(` + { + "count": 5, + "index": "my-index", + } + `); + expect(std.warn).toBe(""); + expect(std.err).toBe(""); + }); + it("should reject an invalid file param", async () => { await expect( runWrangler("vectorize upsert my-index --file invalid_vectors.ndjson") diff --git a/packages/wrangler/src/__tests__/vpc.test.ts b/packages/wrangler/src/__tests__/vpc.test.ts index 1e8c314c1bcb..052585c67d05 100644 --- a/packages/wrangler/src/__tests__/vpc.test.ts +++ b/packages/wrangler/src/__tests__/vpc.test.ts @@ -3,6 +3,7 @@ import { http, HttpResponse } from "msw"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; /* eslint-enable workers-sdk/no-vitest-import-expect */ import { ServiceType } from "../vpc/index"; +import { validateHostname, validateRequest } from "../vpc/validation"; import { endEventLoop } from "./helpers/end-event-loop"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { mockConsoleMethods } from "./helpers/mock-console"; @@ -15,6 +16,7 @@ import type { ConnectivityService, ConnectivityServiceRequest, } from "../vpc/index"; +import type { ServiceArgs } from "../vpc/validation"; describe("vpc help", () => { const std = mockConsoleMethods(); @@ -325,6 +327,189 @@ describe("vpc service commands", () => { }); }); +describe("hostname validation", () => { + it("should accept valid hostnames", () => { + expect(() => validateHostname("api.example.com")).not.toThrow(); + expect(() => validateHostname("localhost")).not.toThrow(); + expect(() => validateHostname("my-service.internal.local")).not.toThrow(); + expect(() => validateHostname("sub.domain.example.co.uk")).not.toThrow(); + }); + + it("should reject empty hostname", () => { + expect(() => validateHostname("")).toThrow("Hostname cannot be empty."); + expect(() => validateHostname(" ")).toThrow("Hostname cannot be empty."); + }); + + it("should reject hostname exceeding 253 characters", () => { + const longHostname = "a".repeat(254); + expect(() => validateHostname(longHostname)).toThrow( + "Hostname is too long. Maximum length is 253 characters." + ); + }); + + it("should accept hostname at exactly 253 characters", () => { + const label = "a".repeat(63); + const hostname = `${label}.${label}.${label}.${label.slice(0, 61)}`; + expect(hostname.length).toBe(253); + expect(() => validateHostname(hostname)).not.toThrow(); + }); + + it("should reject hostname with URL scheme", () => { + expect(() => validateHostname("https://example.com")).toThrow( + "Hostname must not include a URL scheme" + ); + expect(() => validateHostname("http://example.com")).toThrow( + "Hostname must not include a URL scheme" + ); + }); + + it("should reject hostname with path", () => { + expect(() => validateHostname("example.com/path")).toThrow( + "Hostname must not include a path" + ); + }); + + it("should reject bare IPv4 address", () => { + expect(() => validateHostname("192.168.1.1")).toThrow( + "Hostname must not be an IP address. Use --ipv4 or --ipv6 instead." + ); + expect(() => validateHostname("10.0.0.1")).toThrow( + "Hostname must not be an IP address" + ); + }); + + it("should reject bare IPv6 address", () => { + expect(() => validateHostname("::1")).toThrow( + "Hostname must not be an IP address" + ); + expect(() => validateHostname("2001:db8::1")).toThrow( + "Hostname must not be an IP address" + ); + expect(() => validateHostname("[::1]")).toThrow( + "Hostname must not be an IP address" + ); + }); + + it("should reject hostname with port", () => { + expect(() => validateHostname("example.com:8080")).toThrow( + "Hostname must not include a port number" + ); + }); + + it("should reject hostname with whitespace", () => { + expect(() => validateHostname("bad host.com")).toThrow( + "Hostname must not contain whitespace" + ); + }); + + it("should accept hostnames with underscores", () => { + expect(() => validateHostname("_dmarc.example.com")).not.toThrow(); + expect(() => validateHostname("my_service.internal")).not.toThrow(); + }); + + it("should report all applicable errors at once", () => { + // "https://example.com/path" has a scheme AND a path + expect(() => validateHostname("https://example.com/path")).toThrow( + /URL scheme.*\n.*path/s + ); + }); + + it("should reject invalid hostname via wrangler service create", async () => { + await expect(() => + runWrangler( + "vpc service create test-bad-hostname --type http --hostname https://example.com --tunnel-id 550e8400-e29b-41d4-a716-446655440000" + ) + ).rejects.toThrow("Hostname must not include a URL scheme"); + }); + + it("should reject IP address as hostname via wrangler service create", async () => { + await expect(() => + runWrangler( + "vpc service create test-ip-hostname --type http --hostname 192.168.1.1 --tunnel-id 550e8400-e29b-41d4-a716-446655440000" + ) + ).rejects.toThrow("Hostname must not be an IP address"); + }); +}); + +describe("IP address validation", () => { + const baseArgs: ServiceArgs = { + name: "test", + type: ServiceType.Http, + tunnelId: "550e8400-e29b-41d4-a716-446655440000", + }; + + it("should accept valid IPv4 addresses", () => { + expect(() => + validateRequest({ ...baseArgs, ipv4: "192.168.1.1" }) + ).not.toThrow(); + expect(() => + validateRequest({ ...baseArgs, ipv4: "10.0.0.1" }) + ).not.toThrow(); + }); + + it("should reject invalid IPv4 addresses", () => { + expect(() => validateRequest({ ...baseArgs, ipv4: "not-an-ip" })).toThrow( + "Invalid IPv4 address" + ); + expect(() => + validateRequest({ ...baseArgs, ipv4: "999.999.999.999" }) + ).toThrow("Invalid IPv4 address"); + expect(() => validateRequest({ ...baseArgs, ipv4: "example.com" })).toThrow( + "Invalid IPv4 address" + ); + }); + + it("should accept valid IPv6 addresses", () => { + expect(() => validateRequest({ ...baseArgs, ipv6: "::1" })).not.toThrow(); + expect(() => + validateRequest({ ...baseArgs, ipv6: "2001:db8::1" }) + ).not.toThrow(); + }); + + it("should reject invalid IPv6 addresses", () => { + expect(() => validateRequest({ ...baseArgs, ipv6: "not-an-ip" })).toThrow( + "Invalid IPv6 address" + ); + expect(() => validateRequest({ ...baseArgs, ipv6: "192.168.1.1" })).toThrow( + "Invalid IPv6 address" + ); + }); + + it("should accept valid resolver IPs", () => { + expect(() => + validateRequest({ + ...baseArgs, + hostname: "example.com", + resolverIps: "8.8.8.8,8.8.4.4", + }) + ).not.toThrow(); + expect(() => + validateRequest({ + ...baseArgs, + hostname: "example.com", + resolverIps: "2001:db8::1", + }) + ).not.toThrow(); + }); + + it("should reject invalid resolver IPs", () => { + expect(() => + validateRequest({ + ...baseArgs, + hostname: "example.com", + resolverIps: "not-an-ip", + }) + ).toThrow("Invalid resolver IP address(es): 'not-an-ip'"); + expect(() => + validateRequest({ + ...baseArgs, + hostname: "example.com", + resolverIps: "8.8.8.8,bad-ip,1.1.1.1", + }) + ).toThrow("Invalid resolver IP address(es): 'bad-ip'"); + }); +}); + const mockService: ConnectivityService = { service_id: "service-uuid", type: ServiceType.Http, diff --git a/packages/wrangler/src/d1/export.ts b/packages/wrangler/src/d1/export.ts index bab3fa5129ed..ebb5a50dac6b 100644 --- a/packages/wrangler/src/d1/export.ts +++ b/packages/wrangler/src/d1/export.ts @@ -50,6 +50,7 @@ export const d1ExportCommand = createCommand({ table: { type: "string", description: "Specify which tables to include in export", + array: true, }, "no-schema": { type: "boolean", @@ -91,11 +92,7 @@ export const d1ExportCommand = createCommand({ } // Allow multiple --table x --table y flags or none - const tables: string[] = table - ? Array.isArray(table) - ? table - : [table] - : []; + const tables = table ?? []; if (remote) { return await exportRemotely(config, name, output, tables, !schema, !data); diff --git a/packages/wrangler/src/vectorize/create.ts b/packages/wrangler/src/vectorize/create.ts index 061c9615b6ae..d045939c932e 100644 --- a/packages/wrangler/src/vectorize/create.ts +++ b/packages/wrangler/src/vectorize/create.ts @@ -71,9 +71,11 @@ export const vectorizeCreateCommand = createCommand({ let indexConfig; if (args.preset) { indexConfig = { preset: args.preset }; - logger.log( - `Configuring index based for the embedding model ${args.preset}.` - ); + if (!args.json) { + logger.log( + `Configuring index based for the embedding model ${args.preset}.` + ); + } } else if (args.dimensions && args.metric) { // We let the server validate the supported (maximum) dimensions so that we // don't have to keep wrangler in sync with server-side changes @@ -87,7 +89,7 @@ export const vectorizeCreateCommand = createCommand({ ); } - if (args.deprecatedV1) { + if (args.deprecatedV1 && !args.json) { logger.warn( "Creation of legacy Vectorize indexes will be blocked by December 2024" ); @@ -99,7 +101,9 @@ export const vectorizeCreateCommand = createCommand({ config: indexConfig, }; - logger.log(`🚧 Creating index: '${args.name}'`); + if (!args.json) { + logger.log(`🚧 Creating index: '${args.name}'`); + } const indexResult = await createIndex(config, index, args.deprecatedV1); let bindingName: string; @@ -110,7 +114,7 @@ export const vectorizeCreateCommand = createCommand({ } if (args.json) { - logger.log(JSON.stringify(index, null, 2)); + logger.log(JSON.stringify(indexResult, null, 2)); return; } diff --git a/packages/wrangler/src/vectorize/info.ts b/packages/wrangler/src/vectorize/info.ts index 5ccd679f9263..d15d0d3be154 100644 --- a/packages/wrangler/src/vectorize/info.ts +++ b/packages/wrangler/src/vectorize/info.ts @@ -25,7 +25,9 @@ export const vectorizeInfoCommand = createCommand({ }, positionalArgs: ["name"], async handler(args, { config }) { - logger.log(`📋 Fetching index info...`); + if (!args.json) { + logger.log(`📋 Fetching index info...`); + } const info = await indexInfo(config, args.name); if (args.json) { diff --git a/packages/wrangler/src/vectorize/insert.ts b/packages/wrangler/src/vectorize/insert.ts index 9a2f52202316..0b13959e1a3b 100644 --- a/packages/wrangler/src/vectorize/insert.ts +++ b/packages/wrangler/src/vectorize/insert.ts @@ -90,21 +90,27 @@ export const vectorizeInsertCommand = createCommand({ }) ); if (args.deprecatedV1) { - logger.log(`✨ Uploading vector batch (${batch.length} vectors)`); + if (!args.json) { + logger.log(`✨ Uploading vector batch (${batch.length} vectors)`); + } const idxPart = await insertIntoIndexV1(config, args.name, formData); vectorInsertCount += idxPart.count; } else { const mutation = await insertIntoIndex(config, args.name, formData); vectorInsertCount += batch.length; - logger.log( - `✨ Enqueued ${batch.length} vectors into index '${args.name}' for insertion. Mutation changeset identifier: ${mutation.mutationId}` - ); + if (!args.json) { + logger.log( + `✨ Enqueued ${batch.length} vectors into index '${args.name}' for insertion. Mutation changeset identifier: ${mutation.mutationId}` + ); + } } if (vectorInsertCount > VECTORIZE_MAX_UPSERT_VECTOR_RECORDS) { - logger.warn( - `🚧 While Vectorize is in beta, we've limited uploads to 100k vectors per run. You may run this again with another batch to upload further` - ); + if (!args.json) { + logger.warn( + `🚧 While Vectorize is in beta, we've limited uploads to 100k vectors per run. You may run this again with another batch to upload further` + ); + } break; } } diff --git a/packages/wrangler/src/vectorize/list.ts b/packages/wrangler/src/vectorize/list.ts index ebe1a5be8d5a..0baed0250b40 100644 --- a/packages/wrangler/src/vectorize/list.ts +++ b/packages/wrangler/src/vectorize/list.ts @@ -25,9 +25,16 @@ export const vectorizeListCommand = createCommand({ }, }, async handler(args, { config }) { - logger.log(`📋 Listing Vectorize indexes...`); + if (!args.json) { + logger.log(`📋 Listing Vectorize indexes...`); + } const indexes = await listIndexes(config, args.deprecatedV1); + if (args.json) { + logger.log(JSON.stringify(indexes, null, 2)); + return; + } + if (indexes.length === 0) { logger.warn(` You haven't created any indexes on this account. @@ -38,11 +45,6 @@ https://developers.cloudflare.com/vectorize/ to get started. return; } - if (args.json) { - logger.log(JSON.stringify(indexes, null, 2)); - return; - } - logger.table( indexes.map((index) => ({ name: index.name, diff --git a/packages/wrangler/src/vectorize/listMetadataIndex.ts b/packages/wrangler/src/vectorize/listMetadataIndex.ts index 5568650ceeb5..7ff426293f3c 100644 --- a/packages/wrangler/src/vectorize/listMetadataIndex.ts +++ b/packages/wrangler/src/vectorize/listMetadataIndex.ts @@ -26,9 +26,16 @@ export const vectorizeListMetadataIndexCommand = createCommand({ }, positionalArgs: ["name"], async handler(args, { config }) { - logger.log(`📋 Fetching metadata indexes...`); + if (!args.json) { + logger.log(`📋 Fetching metadata indexes...`); + } const res = await listMetadataIndex(config, args.name); + if (args.json) { + logger.log(JSON.stringify(res.metadataIndexes, null, 2)); + return; + } + if (res.metadataIndexes.length === 0) { logger.warn(` You haven't created any metadata indexes on this account. @@ -39,11 +46,6 @@ https://developers.cloudflare.com/vectorize/ to get started. return; } - if (args.json) { - logger.log(JSON.stringify(res.metadataIndexes, null, 2)); - return; - } - logger.table( res.metadataIndexes.map((index) => ({ propertyName: index.propertyName, diff --git a/packages/wrangler/src/vectorize/listVectors.ts b/packages/wrangler/src/vectorize/listVectors.ts index da7c738c2041..02280132a4f0 100644 --- a/packages/wrangler/src/vectorize/listVectors.ts +++ b/packages/wrangler/src/vectorize/listVectors.ts @@ -61,13 +61,13 @@ export const vectorizeListVectorsCommand = createCommand({ const result = await listVectors(config, args.name, options); - if (result.vectors.length === 0) { - logger.warn("No vectors found in this index."); + if (args.json) { + logger.log(JSON.stringify(result, null, 2)); return; } - if (args.json) { - logger.log(JSON.stringify(result, null, 2)); + if (result.vectors.length === 0) { + logger.warn("No vectors found in this index."); return; } diff --git a/packages/wrangler/src/vectorize/upsert.ts b/packages/wrangler/src/vectorize/upsert.ts index 10c372cd067f..dfcdae8eea7a 100644 --- a/packages/wrangler/src/vectorize/upsert.ts +++ b/packages/wrangler/src/vectorize/upsert.ts @@ -73,15 +73,19 @@ export const vectorizeUpsertCommand = createCommand({ { const mutation = await upsertIntoIndex(config, args.name, formData); vectorUpsertCount += batch.length; - logger.log( - `✨ Enqueued ${batch.length} vectors into index '${args.name}' for upsertion. Mutation changeset identifier: ${mutation.mutationId}` - ); + if (!args.json) { + logger.log( + `✨ Enqueued ${batch.length} vectors into index '${args.name}' for upsertion. Mutation changeset identifier: ${mutation.mutationId}` + ); + } } if (vectorUpsertCount > VECTORIZE_MAX_UPSERT_VECTOR_RECORDS) { - logger.warn( - `🚧 While Vectorize is in beta, we've limited uploads to 100k vectors per run. You may run this again with another batch to upload further` - ); + if (!args.json) { + logger.warn( + `🚧 While Vectorize is in beta, we've limited uploads to 100k vectors per run. You may run this again with another batch to upload further` + ); + } break; } } diff --git a/packages/wrangler/src/vpc/validation.ts b/packages/wrangler/src/vpc/validation.ts index 2a0e06fec500..e0b9cb76645e 100644 --- a/packages/wrangler/src/vpc/validation.ts +++ b/packages/wrangler/src/vpc/validation.ts @@ -1,3 +1,4 @@ +import net from "node:net"; import { UserError } from "@cloudflare/workers-utils"; import { ServiceType } from "./index"; import type { ConnectivityServiceRequest, ServiceHost } from "./index"; @@ -14,6 +15,63 @@ export interface ServiceArgs { resolverIps?: string; } +export function validateHostname(hostname: string): void { + const trimmed = hostname.trim(); + + if (trimmed.length === 0) { + throw new UserError("Hostname cannot be empty."); + } + + const errors: string[] = []; + + if (trimmed.length > 253) { + errors.push("Hostname is too long. Maximum length is 253 characters."); + } + + const hasScheme = trimmed.includes("://"); + if (hasScheme) { + errors.push( + "Hostname must not include a URL scheme (e.g., remove 'https://')." + ); + } + + const afterScheme = hasScheme + ? trimmed.slice(trimmed.indexOf("://") + 3) + : trimmed; + if (afterScheme.includes("/")) { + errors.push( + "Hostname must not include a path. Provide only the hostname (e.g., 'api.example.com')." + ); + } + + // Check for bare IP addresses using Node.js built-in validation + const bareValue = trimmed.replace(/^\[|\]$/g, ""); + const isIpAddress = net.isIPv4(trimmed) || net.isIPv6(bareValue); + if (isIpAddress) { + errors.push( + "Hostname must not be an IP address. Use --ipv4 or --ipv6 instead." + ); + } + + // Only check for port numbers when the colon isn't already explained by + // an IPv6 address or a URL scheme, to avoid misleading error messages. + if (!isIpAddress && !hasScheme && trimmed.includes(":")) { + errors.push( + "Hostname must not include a port number. Provide only the hostname and use --http-port or --https-port for ports." + ); + } + + if (/\s/.test(trimmed)) { + errors.push("Hostname must not contain whitespace."); + } + + if (errors.length > 0) { + throw new UserError( + `Invalid hostname '${trimmed}':\n${errors.map((e) => ` - ${e}`).join("\n")}` + ); + } +} + export function validateRequest(args: ServiceArgs) { // Validate host configuration - must have either IP addresses or hostname, not both const hasIpAddresses = Boolean(args.ipv4 || args.ipv6); @@ -24,13 +82,44 @@ export function validateRequest(args: ServiceArgs) { "Must specify either IP addresses (--ipv4/--ipv6) or hostname (--hostname)" ); } + + if (args.ipv4 && !net.isIPv4(args.ipv4)) { + throw new UserError( + `Invalid IPv4 address: '${args.ipv4}'. Provide a valid IPv4 address (e.g., '192.168.1.1').` + ); + } + + if (args.ipv6 && !net.isIPv6(args.ipv6)) { + throw new UserError( + `Invalid IPv6 address: '${args.ipv6}'. Provide a valid IPv6 address (e.g., '2001:db8::1').` + ); + } + + if (hasHostname && args.hostname) { + validateHostname(args.hostname); + } + + if (args.resolverIps) { + const ips = args.resolverIps.split(",").map((ip) => ip.trim()); + const invalidIps = ips.filter( + (ip) => ip.length > 0 && !net.isIPv4(ip) && !net.isIPv6(ip) + ); + if (invalidIps.length > 0) { + throw new UserError( + `Invalid resolver IP address(es): ${invalidIps.map((ip) => `'${ip}'`).join(", ")}. Provide valid IPv4 or IPv6 addresses.` + ); + } + } } export function buildRequest(args: ServiceArgs): ConnectivityServiceRequest { // Parse resolver IPs if provided let resolverIpsList: string[] | undefined = undefined; if (args.resolverIps) { - resolverIpsList = args.resolverIps.split(",").map((ip) => ip.trim()); + resolverIpsList = args.resolverIps + .split(",") + .map((ip) => ip.trim()) + .filter((ip) => ip.length > 0); } // Build the host configuration diff --git a/packages/wrangler/turbo.json b/packages/wrangler/turbo.json index d1e923e77f33..8e480e2740a7 100644 --- a/packages/wrangler/turbo.json +++ b/packages/wrangler/turbo.json @@ -23,8 +23,6 @@ "CF_PAGES_UPLOAD_JWT", "CF_PAGES", "CI", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN", "CUSTOM_BUILD_VAR", "EXPERIMENTAL_MIDDLEWARE", "FORMAT_WRANGLER_ERRORS", @@ -67,14 +65,12 @@ "inputs": ["e2e/**"], "dependsOn": ["build", "create-cloudflare#build"], "env": [ + "$TURBO_EXTENDS$", "VITEST", - "NODE_DEBUG", "MINIFLARE_WORKERD_PATH", "WRANGLER", "WRANGLER_IMPORT", "MINIFLARE_IMPORT", - "CLOUDFLARE_ACCOUNT_ID", - "CLOUDFLARE_API_TOKEN", "WRANGLER_E2E_TEST_FILE" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08defe62922a..77d88ca4282f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,6 +157,9 @@ importers: esbuild-register: specifier: ^3.5.0 version: 3.5.0(esbuild@0.27.3) + jsonc-parser: + specifier: catalog:default + version: 3.2.0 prettier: specifier: ^3.2.5 version: 3.2.5 @@ -4041,6 +4044,9 @@ importers: '@cloudflare/eslint-config-shared': specifier: workspace:* version: link:../eslint-config-shared + '@cloudflare/vite-plugin': + specifier: workspace:* + version: link:../vite-plugin-cloudflare '@types/glob-to-regexp': specifier: ^0.4.1 version: 0.4.1 @@ -4055,22 +4061,19 @@ importers: version: 9.0.4 '@vitejs/plugin-react': specifier: ^4.3.3 - version: 4.3.3(vite@7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@3.12.10)(yaml@2.8.1)) + version: 4.3.3(vite@7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1)) eslint: specifier: catalog:default version: 9.39.1(jiti@2.6.1) react-use-websocket: specifier: ^4.13.0 version: 4.13.0 - tsx: - specifier: ^3.12.8 - version: 3.12.10 undici: specifier: catalog:default version: 7.18.2 vite: - specifier: catalog:default - version: 7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@3.12.10)(yaml@2.8.1) + specifier: catalog:vite-plugin + version: 7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1) wrangler: specifier: workspace:^ version: link:../wrangler @@ -11302,10 +11305,6 @@ packages: expect-type@0.15.0: resolution: {integrity: sha512-yWnriYB4e8G54M5/fAFj7rCIBiKs1HAACaY13kCz6Ku0dezjS9aMcfcdVK2X8Tv2tEV1BPz/wKfQ7WA4S/d8aA==} - expect-type@1.2.1: - resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} - engines: {node: '>=12.0.0'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -11492,9 +11491,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} - flatted@3.4.1: resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} @@ -15629,46 +15625,6 @@ packages: yaml: optional: true - vite@7.2.7: - resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.9 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -21160,14 +21116,14 @@ snapshots: dependencies: vite: 7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1) - '@vitejs/plugin-react@4.3.3(vite@7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@3.12.10)(yaml@2.8.1))': + '@vitejs/plugin-react@4.3.3(vite@7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@3.12.10)(yaml@2.8.1) + vite: 7.1.12(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -22054,7 +22010,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.3 + loupe: 3.2.1 pathval: 2.0.0 chai@6.2.2: {} @@ -23443,8 +23399,6 @@ snapshots: expect-type@0.15.0: {} - expect-type@1.2.1: {} - expect-type@1.3.0: {} expect@29.7.0: @@ -23728,13 +23682,11 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.1 keyv: 4.5.4 flatted@3.3.3: {} - flatted@3.3.4: {} - flatted@3.4.1: {} flow-parser@0.304.0: {} @@ -27948,7 +27900,7 @@ snapshots: uri-js@4.4.1: dependencies: - punycode: 2.1.1 + punycode: 2.3.1 use-sync-external-store@1.2.0(react@18.3.1): dependencies: @@ -28069,7 +28021,7 @@ snapshots: debug: 4.4.1(supports-color@9.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.7(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -28152,38 +28104,6 @@ snapshots: tsx: 4.21.0 yaml: 2.8.1 - vite@7.2.7(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1): - dependencies: - esbuild: 0.25.4 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.9 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - tsx: 4.21.0 - yaml: 2.8.1 - - vite@7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@3.12.10)(yaml@2.8.1): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.9 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - tsx: 3.12.10 - yaml: 2.8.1 - vite@7.3.1(@types/node@20.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.1): dependencies: esbuild: 0.27.3 @@ -28218,11 +28138,11 @@ snapshots: '@vitest/utils': 3.2.4 chai: 5.2.0 debug: 4.4.1(supports-color@9.2.2) - expect-type: 1.2.1 + expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.9.0 + std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 @@ -28261,11 +28181,11 @@ snapshots: '@vitest/utils': 3.2.4 chai: 5.2.0 debug: 4.4.1(supports-color@9.2.2) - expect-type: 1.2.1 + expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.9.0 + std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 diff --git a/tools/turbo.json b/tools/turbo.json index bdbd7e0a2caf..62200801fb45 100644 --- a/tools/turbo.json +++ b/tools/turbo.json @@ -7,11 +7,7 @@ "dependsOn": ["build"] }, "build": { - "env": [ - "CLOUDFLARE_API_TOKEN", - "PUBLISHED_PACKAGES", - "CLOUDFLARE_ACCOUNT_ID" - ] + "env": ["PUBLISHED_PACKAGES"] } } } diff --git a/turbo.json b/turbo.json index 5a82773fed83..70c14f0d94da 100644 --- a/turbo.json +++ b/turbo.json @@ -19,7 +19,9 @@ "MINIFLARE_CONTAINER_EGRESS_IMAGE", "WRANGLER_DOCKER_HOST", "WRANGLER_LOG_PATH", - "WRANGLER_LOG" + "WRANGLER_LOG", + "CLOUDFLARE_API_TOKEN", + "CLOUDFLARE_ACCOUNT_ID" ], "tasks": { "dev": { @@ -50,7 +52,8 @@ }, "test:e2e": { "dependsOn": ["build"], - "outputLogs": "new-only" + "outputLogs": "new-only", + "env": ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN", "NODE_DEBUG"] }, "//#check:format": { "cache": true