From 70bf21e064dcea4aaf94b04aae91a0b87592e471 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:30:11 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .changeset/README.md | 127 + .changeset/bright-doors-evict.md | 7 + .changeset/config.json | 21 + .changeset/fix-schema-editor-dirty-state.md | 9 + .changeset/fix-tanstack-no-install.md | 7 + .changeset/gentle-lions-list-do-ids.md | 5 + .changeset/green-badgers-evict.md | 12 + .changeset/lazy-register-hooks.md | 7 + .changeset/pages-delegation-force-notice.md | 7 + .changeset/remove-service-environments.md | 9 + .../suggest-similar-commands-on-typo.md | 7 + .codeowners | 81 + .editorconfig | 10 + .git-blame-ignore-revs | 4 + .gitattributes | 3 + .gitguardian.yml | 4 + .github/ISSUE_TEMPLATE/bug-template.yaml | 35 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/actions/check-remote-tests/action.yml | 33 + .../expose-actions-variables/action.yml | 5 + .../expose-actions-variables/index.mjs | 7 + .../actions/install-dependencies/action.yml | 62 + .github/actions/install-python-uv/action.yml | 9 + .github/bonk_reviewer.md | 60 + .github/changeset-version.js | 147 + .github/dependabot.yml | 48 + .github/extract-pr-and-workflow-id.js | 21 + .../get-c3-dependabot-bumped-framework.cjs | 26 + .github/holopin.yml | 9 + .github/opencode.json | 26 + .github/prereleases/upload.mjs | 50 + .github/pull_request_template.md | 25 + .github/skills/issue-review.md | 376 + .github/version-script.js | 35 + .github/workflows/README.md | 151 + .github/workflows/bonk-pr-review.yml | 69 + .github/workflows/bonk.yml | 80 + .../c3-dependabot-versioning-prs.yml | 39 + .github/workflows/c3-e2e.yml | 311 + .github/workflows/changeset-review.yml | 92 + .github/workflows/changesets.yml | 108 + .github/workflows/codeowners.yml | 54 + .github/workflows/dependabot-auto-merge.yml | 141 + .github/workflows/deploy-previews.yml | 90 + .github/workflows/e2e-local-explorer-ui.yml | 69 + .github/workflows/e2e-project-cleanup.yml | 33 + .github/workflows/e2e-vite.yml | 73 + .github/workflows/e2e-wrangler.yml | 88 + .github/workflows/hotfix-release.yml | 82 + .github/workflows/issues.yml | 41 + .../miniflare-dependabot-versioning-prs.yml | 51 + .github/workflows/opencode-issue-solver.yml | 19 + .github/workflows/prerelease.yml | 53 + .../workflows/rerun-codeowners-privileged.yml | 67 + .github/workflows/rerun-codeowners.yml | 35 + .github/workflows/rerun-remote-tests.yml | 86 + .../workflows/run-ci-for-external-forks.yml | 100 + .github/workflows/semgrep.yml | 31 + .../workflows/test-and-check-other-node.yml | 118 + .github/workflows/test-and-check.yml | 202 + .github/workflows/triage-issue.yml | 235 + .github/workflows/validate-pr-description.yml | 68 + .github/workflows/vite-plugin-playgrounds.yml | 100 + ...nd-preview-testing-env-deploy-and-test.yml | 60 + .../workers-shared-deploy-production.yml | 34 + .../workers-shared-deploy-staging.yml | 31 + .gitignore | 241 + .markdownlint.json | 4 + .opencode/agents/bonk.md | 221 + .opencode/skills/local-explorer/SKILL.md | 74 + .../assets/local-explorer-diagram.md | 57 + .oxfmtrc.jsonc | 64 + .oxlintrc.jsonc | 217 + .vscode/extensions.json | 3 + .vscode/launch.json | 16 + .vscode/settings.json | 112 + AGENTS.md | 247 + CLAUDE.md | 5 + CODEOWNERS | 5 + CODEOWNERS.md | 194 + CODE_OF_CONDUCT.md | 128 + CONTRIBUTING.md | 495 + LICENSE-APACHE | 176 + LICENSE-MIT | 25 + README.md | 160 + README.wehub.md | 7 + REVIEW.md | 34 + SECURITY.md | 3 + STYLEGUIDE.md | 158 + cloudflare-workers-outline.png | Bin 0 -> 5526 bytes codeowners.toml | 28 + fixtures/additional-modules/package.json | 22 + fixtures/additional-modules/src/common.cjs | 1 + fixtures/additional-modules/src/dep.ts | 1 + fixtures/additional-modules/src/dynamic.js | 1 + fixtures/additional-modules/src/example.sql | 1 + fixtures/additional-modules/src/index.ts | 32 + fixtures/additional-modules/src/lang/en.js | 1 + fixtures/additional-modules/src/lang/fr.js | 1 + fixtures/additional-modules/src/text.d.ts | 9 + fixtures/additional-modules/src/text.txt | 1 + .../additional-modules/test/index.test.ts | 216 + .../additional-modules/test/tsconfig.json | 7 + fixtures/additional-modules/tsconfig.json | 14 + fixtures/additional-modules/turbo.json | 9 + fixtures/additional-modules/vitest.config.mts | 9 + fixtures/additional-modules/wrangler.jsonc | 16 + fixtures/browser-run/package.json | 21 + fixtures/browser-run/src/index.ts | 15 + fixtures/browser-run/src/playwright.ts | 74 + fixtures/browser-run/src/puppeteer.ts | 73 + fixtures/browser-run/test/index.spec.ts | 97 + fixtures/browser-run/test/tsconfig.json | 7 + fixtures/browser-run/tsconfig.json | 13 + fixtures/browser-run/vitest.config.mts | 9 + .../browser-run/worker-configuration.d.ts | 13 + fixtures/browser-run/wrangler.jsonc | 24 + fixtures/container-app/.gitignore | 168 + fixtures/container-app/Dockerfile | 8 + fixtures/container-app/README.md | 3 + .../container/simple-node-app.js | 49 + fixtures/container-app/package.json | 20 + fixtures/container-app/src/index.ts | 68 + fixtures/container-app/tsconfig.json | 13 + fixtures/container-app/wrangler.jsonc | 20 + .../container-app/wrangler.registry.jsonc | 28 + .../create-test-harness-example/.gitignore | 2 + .../create-test-harness-example/README.md | 66 + .../create-test-harness-example/package.json | 35 + .../playwright.config.ts | 9 + .../tests/playwright.test.ts | 107 + .../tests/tsconfig.json | 14 + .../tests/vitest.test.ts | 160 + .../create-test-harness-example/tsconfig.json | 17 + .../create-test-harness-example/turbo.json | 9 + .../vite.config.ts | 16 + .../vitest.config.mts | 11 + .../workers/api/index.ts | 97 + .../workers/api/migrations/0001_reports.sql | 4 + .../workers/api/worker-configuration.d.ts | 13 + .../workers/api/wrangler.jsonc | 14 + .../workers/mock-browser/index.ts | 19 + .../workers/mock-browser/wrangler.jsonc | 5 + .../workers/web/index.ts | 42 + .../workers/web/worker-configuration.d.ts | 13 + .../workers/web/wrangler.jsonc | 8 + fixtures/d1-read-replication-app/.gitignore | 2 + fixtures/d1-read-replication-app/README.md | 3 + fixtures/d1-read-replication-app/package.json | 19 + fixtures/d1-read-replication-app/src/index.ts | 41 + .../tests/index.test.ts | 82 + .../tests/tsconfig.json | 7 + .../d1-read-replication-app/tsconfig.json | 13 + .../d1-read-replication-app/vitest.config.mts | 9 + .../d1-read-replication-app/wrangler.jsonc | 18 + fixtures/d1-worker-app/.gitignore | 2 + fixtures/d1-worker-app/README.md | 3 + fixtures/d1-worker-app/package.json | 22 + fixtures/d1-worker-app/schema.sql | 11 + fixtures/d1-worker-app/src/index.ts | 12 + fixtures/d1-worker-app/tests/index.test.ts | 15 + fixtures/d1-worker-app/tests/tsconfig.json | 7 + fixtures/d1-worker-app/tsconfig.json | 13 + fixtures/d1-worker-app/vitest.config.mts | 9 + fixtures/d1-worker-app/wrangler.jsonc | 28 + fixtures/dev-registry/assets/example.txt | 1 + fixtures/dev-registry/package.json | 21 + .../dev-registry/tests/dev-registry.test.ts | 1381 + fixtures/dev-registry/tests/tsconfig.json | 8 + fixtures/dev-registry/tsconfig.json | 7 + fixtures/dev-registry/tsconfig.node.json | 10 + fixtures/dev-registry/tsconfig.worker.json | 6 + ...ite.exported-handler-with-assets.config.ts | 15 + .../vite.exported-handler.config.ts | 12 + ...te.worker-entrypoint-with-assets.config.ts | 20 + .../vite.worker-entrypoint.config.ts | 17 + fixtures/dev-registry/vitest.config.mts | 9 + .../dev-registry/workers/durable-object.ts | 17 + .../dev-registry/workers/exported-handler.ts | 126 + .../dev-registry/workers/service-worker.ts | 3 + .../dev-registry/workers/worker-entrypoint.ts | 148 + ...rangler.exported-handler-with-assets.jsonc | 42 + .../wrangler.exported-handler.jsonc | 43 + .../wrangler.external-durable-object.jsonc | 20 + .../wrangler.internal-durable-object.jsonc | 20 + .../wrangler.nonexistent-durable-object.jsonc | 15 + .../wrangler.nonexistent-entrypoint.jsonc | 13 + .../wrangler.service-worker.jsonc | 6 + ...angler.worker-entrypoint-with-assets.jsonc | 37 + .../wrangler.worker-entrypoint.jsonc | 43 + fixtures/durable-objects-app/package.json | 20 + fixtures/durable-objects-app/src/index.js | 5 + .../durable-objects-app/tests/index.test.ts | 26 + .../durable-objects-app/tests/tsconfig.json | 7 + fixtures/durable-objects-app/tsconfig.json | 12 + .../durable-objects-app/vitest.config.mts | 9 + fixtures/durable-objects-app/wrangler.jsonc | 19 + .../durable-objects-exports-app/package.json | 17 + .../durable-objects-exports-app/src/index.ts | 93 + .../tests/index.test.ts | 85 + .../tests/tsconfig.json | 7 + .../durable-objects-exports-app/tsconfig.json | 13 + .../vitest.config.mts | 9 + .../worker-configuration.d.ts | 14 + .../wrangler.jsonc | 23 + fixtures/dynamic-worker-loading/package.json | 16 + fixtures/dynamic-worker-loading/src/index.ts | 34 + .../tests/index.test.ts | 36 + .../tests/tsconfig.json | 7 + fixtures/dynamic-worker-loading/tsconfig.json | 13 + .../dynamic-worker-loading/vitest.config.mts | 9 + .../dynamic-worker-loading/wrangler.jsonc | 10 + fixtures/email-worker/README.md | 119 + fixtures/email-worker/package.json | 14 + fixtures/email-worker/src/index.ts | 255 + fixtures/email-worker/tsconfig.json | 14 + .../email-worker/worker-configuration.d.ts | 7 + fixtures/email-worker/wrangler.jsonc | 22 + fixtures/entrypoints-rpc-tests/package.json | 16 + .../tests/entrypoints.spec.ts | 951 + fixtures/entrypoints-rpc-tests/tsconfig.json | 4 + .../entrypoints-rpc-tests/vitest.config.mts | 9 + fixtures/entrypoints-rpc-tests/wrangler.jsonc | 4 + .../cloudflare.config.ts | 11 + fixtures/experimental-new-config/package.json | 21 + fixtures/experimental-new-config/src/index.ts | 7 + .../test/index.test.ts | 210 + .../experimental-new-config/tsconfig.json | 7 + .../tsconfig.node.json | 4 + .../tsconfig.worker.json | 13 + fixtures/experimental-new-config/turbo.json | 9 + .../experimental-new-config/vitest.config.mts | 9 + .../worker-configuration.d.ts | 17 + .../wrangler.config.ts | 6 + .../package.json | 17 + .../remote-worker.js | 7 + .../remote-worker.staging.js | 7 + .../tests/index.test.ts | 360 + .../tests/remote-bindings-false.test.ts | 24 + .../tests/tsconfig.json | 7 + .../turbo.json | 19 + .../wrangler.remote-bindings-false.jsonc | 8 + .../custom-toml/path/test.toml | 7 + fixtures/get-platform-proxy/package.json | 20 + fixtures/get-platform-proxy/public/test.txt | 1 + ...ains-functioning-image-bindings-2-snap.png | Bin 0 -> 4092 bytes .../tests/get-platform-proxy.caches.test.ts | 71 + .../tests/get-platform-proxy.cf.test.ts | 43 + .../tests/get-platform-proxy.ctx.test.ts | 148 + .../tests/get-platform-proxy.env.test.ts | 384 + fixtures/get-platform-proxy/tests/shared.ts | 13 + .../get-platform-proxy/tests/tsconfig.json | 7 + fixtures/get-platform-proxy/vitest.config.mts | 11 + fixtures/get-platform-proxy/wrangler.json | 11 + fixtures/get-platform-proxy/wrangler.jsonc | 64 + .../wrangler_external_do.jsonc | 24 + .../wrangler_internal_do.jsonc | 23 + .../wrangler_workflow.jsonc | 22 + fixtures/import-npm/.gitignore | 1 + fixtures/import-npm/README.md | 3 + fixtures/import-npm/package.json | 16 + .../packages/import-example/package.json | 21 + .../packages/import-example/src/index.js | 19 + .../import-example/tests/index.test.ts | 25 + .../import-example/tests/tsconfig.json | 7 + .../packages/import-example/tsconfig.json | 13 + .../packages/import-example/vitest.config.mts | 9 + .../packages/import-example/wrangler.jsonc | 5 + fixtures/import-npm/turbo.json | 21 + fixtures/import-wasm-example/.gitignore | 1 + fixtures/import-wasm-example/README.md | 4 + fixtures/import-wasm-example/package.json | 23 + fixtures/import-wasm-example/src/index.js | 19 + .../import-wasm-example/tests/index.test.ts | 25 + .../import-wasm-example/tests/tsconfig.json | 7 + fixtures/import-wasm-example/tsconfig.json | 13 + .../import-wasm-example/vitest.config.mts | 9 + fixtures/import-wasm-example/wrangler.jsonc | 5 + fixtures/import-wasm-static/README.md | 5 + fixtures/import-wasm-static/package.json | 8 + .../import-wasm-static/wasm/multiply.wasm | Bin 0 -> 46 bytes fixtures/import-wasm-static/wasm/multiply.wat | 7 + .../import-wasm-static/wasm/not-exported.wasm | Bin 0 -> 46 bytes .../import-wasm-static/wasm/not-exported.wat | 7 + fixtures/interactive-dev-tests/README.md | 3 + .../container-app/Dockerfile | 8 + .../container-app/DockerfileWithLongSleep | 6 + .../container/simple-node-app.js | 49 + .../container-app/src/index.ts | 67 + .../container-app/src/tsconfig.json | 12 + .../src/worker-configuration.d.ts | 13 + .../container-app/wrangler.jsonc | 27 + fixtures/interactive-dev-tests/index.js | 1 + .../multi-containers-app/DockerfileA | 20 + .../multi-containers-app/DockerfileB | 20 + .../multi-containers-app/src/index.ts | 49 + .../multi-containers-app/src/tsconfig.json | 12 + .../src/worker-configuration.d.ts | 14 + .../multi-containers-app/wrangler.jsonc | 41 + .../tsconfig.json | 13 + .../workerA/Dockerfile | 18 + .../workerA/index.ts | 38 + .../workerA/worker-configuration.d.ts | 14 + .../workerA/wrangler.jsonc | 28 + .../workerB/Dockerfile | 18 + .../workerB/index.ts | 35 + .../workerB/worker-configuration.d.ts | 13 + .../workerB/wrangler.jsonc | 27 + fixtures/interactive-dev-tests/package.json | 21 + .../interactive-dev-tests/public/index.html | 1 + fixtures/interactive-dev-tests/src/index.mjs | 5 + .../src/startup-error.ts | 5 + .../interactive-dev-tests/src/worker-a.mjs | 7 + .../interactive-dev-tests/src/worker-b.mjs | 5 + .../interactive-dev-tests/tests/index.test.ts | 943 + fixtures/interactive-dev-tests/tsconfig.json | 12 + .../interactive-dev-tests/vitest.config.mts | 12 + .../interactive-dev-tests/wrangler.a.jsonc | 11 + .../interactive-dev-tests/wrangler.b.jsonc | 5 + fixtures/interactive-dev-tests/wrangler.jsonc | 4 + fixtures/isomorphic-random-example/README.md | 7 + .../isomorphic-random-example/package.json | 11 + .../isomorphic-random-example/src/default.js | 4 + .../isomorphic-random-example/src/node.js | 5 + .../isomorphic-random-example/src/other.js | 4 + .../isomorphic-random-example/src/workerd.mjs | 3 + fixtures/legacy-site-app/package.json | 9 + fixtures/legacy-site-app/public/404.html | 50 + fixtures/legacy-site-app/public/favicon.ico | Bin 0 -> 1150 bytes .../public/img/200-wrangler-ferris.gif | Bin 0 -> 45598 bytes .../public/img/404-wrangler-ferris.gif | Bin 0 -> 31606 bytes fixtures/legacy-site-app/public/index.html | 50 + .../legacy-site-app/workers-site/.gitignore | 3 + .../legacy-site-app/workers-site/index.js | 87 + .../legacy-site-app/workers-site/package.json | 9 + fixtures/legacy-site-app/wrangler.jsonc | 10 + fixtures/miniflare-node-test/package.json | 19 + .../src/index-build.test.js | 41 + .../src/index-with-bindings.test.js | 51 + .../src/index-with-imports.js | 12 + .../src/index-with-imports.test.js | 34 + fixtures/miniflare-node-test/src/index.js | 10 + .../miniflare-node-test/src/index.test.js | 33 + fixtures/miniflare-node-test/src/index.ts | 10 + fixtures/miniflare-node-test/src/say-hello.js | 8 + fixtures/miniflare-node-test/tsconfig.json | 8 + .../miniflare-node-test/wrangler-build.json | 5 + fixtures/miniflare-node-test/wrangler.jsonc | 8 + fixtures/multi-worker/package.json | 21 + .../multi-worker/tests/multi-worker.test.ts | 21 + fixtures/multi-worker/vitest.config.mts | 9 + .../multi-worker/workers/default/src/index.ts | 6 + .../workers/default/tsconfig.json | 43 + .../workers/default/worker-configuration.d.ts | 10 + .../workers/default/wrangler.jsonc | 14 + .../multi-worker/workers/sentry/src/index.ts | 13 + .../multi-worker/workers/sentry/tsconfig.json | 43 + .../workers/sentry/worker-configuration.d.ts | 10 + .../workers/sentry/wrangler.jsonc | 14 + fixtures/no-bundle-import/CHANGELOG.md | 77 + fixtures/no-bundle-import/README.md | 3 + fixtures/no-bundle-import/package.json | 15 + fixtures/no-bundle-import/src/data.bin | Bin 0 -> 4 bytes fixtures/no-bundle-import/src/data.txt | 1 + fixtures/no-bundle-import/src/dynamic.cjs | 1 + fixtures/no-bundle-import/src/dynamic.js | 1 + fixtures/no-bundle-import/src/index.js | 74 + fixtures/no-bundle-import/src/index.test.ts | 88 + fixtures/no-bundle-import/src/lang/en.json | 3 + fixtures/no-bundle-import/src/lang/fr.json | 3 + fixtures/no-bundle-import/src/nested/index.js | 36 + .../no-bundle-import/src/nested/say-hello.js | 1 + .../no-bundle-import/src/nested/simple.wasm | Bin 0 -> 78 bytes fixtures/no-bundle-import/src/say-hello.cjs | 3 + fixtures/no-bundle-import/src/say-hello.js | 1 + fixtures/no-bundle-import/src/simple.wasm | Bin 0 -> 78 bytes fixtures/no-bundle-import/vitest.config.mts | 9 + fixtures/no-bundle-import/wrangler.jsonc | 21 + fixtures/node-app-pages/.gitignore | 1 + fixtures/node-app-pages/functions/stripe.ts | 12 + fixtures/node-app-pages/package.json | 24 + fixtures/node-app-pages/public/index.html | 6 + fixtures/node-app-pages/tests/index.test.ts | 30 + fixtures/node-app-pages/tests/tsconfig.json | 7 + fixtures/node-app-pages/tsconfig.json | 13 + fixtures/node-app-pages/vitest.config.mts | 9 + fixtures/node-env/package.json | 25 + fixtures/node-env/src/index.ts | 20 + fixtures/node-env/tests/node-env.test.ts | 137 + fixtures/node-env/tsconfig.json | 7 + fixtures/node-env/vitest.config.mts | 9 + fixtures/node-env/wrangler.jsonc | 5 + fixtures/nodejs-als-app/package.json | 17 + fixtures/nodejs-als-app/src/index.ts | 24 + fixtures/nodejs-als-app/tests/index.test.ts | 25 + fixtures/nodejs-als-app/tests/tsconfig.json | 7 + fixtures/nodejs-als-app/tsconfig.json | 14 + fixtures/nodejs-als-app/turbo.json | 9 + fixtures/nodejs-als-app/vitest.config.mts | 9 + fixtures/nodejs-als-app/wrangler.jsonc | 6 + fixtures/nodejs-hybrid-app/.env | 1 + fixtures/nodejs-hybrid-app/.gitignore | 1 + fixtures/nodejs-hybrid-app/package.json | 22 + fixtures/nodejs-hybrid-app/src/dep.cjs | 4 + fixtures/nodejs-hybrid-app/src/index.ts | 354 + .../nodejs-hybrid-app/tests/index.test.ts | 140 + .../nodejs-hybrid-app/tests/tsconfig.json | 7 + fixtures/nodejs-hybrid-app/tsconfig.json | 17 + fixtures/nodejs-hybrid-app/turbo.json | 9 + fixtures/nodejs-hybrid-app/vitest.config.mts | 9 + .../worker-configuration.d.ts | 9 + fixtures/nodejs-hybrid-app/wrangler.jsonc | 18 + .../pages-dev-proxy-with-script/_worker.js | 5 + .../custom/script/path/index.js | 5 + .../pages-dev-proxy-with-script/package.json | 19 + .../tests/index.test.ts | 75 + .../tests/tsconfig.json | 7 + .../pages-dev-proxy-with-script/tsconfig.json | 12 + .../vitest.config.mts | 9 + fixtures/pages-functions-app/.gitignore | 1 + .../functions/_middleware.ts | 5 + .../functions/blog/[slug].ts | 6 + .../pages-functions-app/functions/date.ts | 3 + .../functions/import-html.ts | 5 + .../functions/intercept.ts | 10 + .../middleware-data/additional-data.ts | 9 + .../functions/middleware-data/bad-data.ts | 4 + .../functions/middleware-data/merge-data.ts | 13 + .../functions/middleware-data/mutate-data.ts | 13 + .../functions/mounted-plugin/_middleware.ts | 5 + .../[p]/plugin/_middleware.ts | 3 + .../pages-functions-app/functions/next.ts | 1 + .../passThroughOnException/_middleware.ts | 5 + .../passThroughOnException/nested.ts | 4 + .../functions/passThroughOnExceptionClosed.ts | 4 + .../functions/passThroughOnExceptionOpen.ts | 5 + .../_middleware.ts | 13 + .../nested.ts | 4 + .../functions/r2/create.ts | 8 + .../pages-functions-app/functions/r2/get.ts | 8 + .../functions/regex_chars/my-file.ts | 1 + .../functions/static/_middleware.ts | 1 + .../functions/variables.ts | 5 + fixtures/pages-functions-app/index.d.ts | 4 + fixtures/pages-functions-app/package.json | 25 + fixtures/pages-functions-app/public/[id].js | 1 + fixtures/pages-functions-app/public/_headers | 6 + .../pages-functions-app/public/_redirects | 2 + fixtures/pages-functions-app/public/a.b.html | 6 + fixtures/pages-functions-app/public/here.html | 1 + .../pages-functions-app/public/index.html | 6 + .../public/some-asset.html | 6 + .../public/users/[id].html | 6 + .../static-assets/index.html | 9 + .../pages-functions-app/tests/index.test.ts | 358 + .../pages-functions-app/tests/tsconfig.json | 8 + fixtures/pages-functions-app/tsconfig.json | 12 + .../pages-functions-app/vitest.config.mts | 9 + .../functions/[[path]].ts | 10 + .../pages-functions-unenv-alias/package.json | 20 + .../tests/index.test.ts | 20 + .../tests/tsconfig.json | 7 + .../pages-functions-unenv-alias/tsconfig.json | 19 + .../vitest.config.mts | 9 + .../wrangler.jsonc | 5 + fixtures/pages-functions-wasm-app/README.md | 41 + .../external-modules/add.wasm | Bin 0 -> 41 bytes .../external-modules/add.wat | 7 + .../external-modules/meaning-of-life.html | 5 + .../external-modules/meaning-of-life.txt | 1 + .../functions/meaning-of-life-html.js | 7 + .../functions/meaning-of-life-text.js | 7 + .../functions/meaning-of-life-wasm.js | 8 + .../pages-functions-wasm-app/package.json | 20 + .../public/index.html | 6 + .../tests/index.test.ts | 50 + .../tests/tsconfig.json | 7 + .../pages-functions-wasm-app/tsconfig.json | 13 + .../vitest.config.mts | 9 + .../README.md | 29 + .../functions/celebrate.ts | 11 + .../functions/holiday.ts | 11 + .../package.json | 20 + .../public/_routes.json | 5 + .../public/index.html | 6 + .../tests/index.test.ts | 48 + .../tests/tsconfig.json | 8 + .../tsconfig.json | 11 + .../vitest.config.mts | 9 + .../wrangler.jsonc | 9 + .../CHANGELOG.md | 18 + .../pages-functions-with-routes-app/README.md | 31 + .../functions/bye.ts | 3 + .../functions/date.ts | 3 + .../functions/greeting/bye.ts | 3 + .../functions/greeting/hello.ts | 3 + .../functions/greeting/index.ts | 3 + .../functions/greetings.ts | 3 + .../functions/index.ts | 3 + .../package.json | 20 + .../public/_routes.json | 5 + .../public/greeting/test.json | 3 + .../public/index.html | 8 + .../tests/index.test.ts | 76 + .../tests/tsconfig.json | 8 + .../tsconfig.json | 11 + .../vitest.config.mts | 9 + .../apps/functions/README.md | 5 + .../apps/workerjs-directory/README.md | 1 + .../workerjs-directory/_worker.js/index.js | 12 + .../apps/workerjs-file/README.md | 1 + .../apps/workerjs-file/_worker.js | 12 + .../functions/[[path]].ts | 10 + fixtures/pages-nodejs-v2-compat/package.json | 22 + .../tests/index.test.ts | 54 + .../tests/tsconfig.json | 7 + fixtures/pages-nodejs-v2-compat/tsconfig.json | 19 + .../pages-nodejs-v2-compat/vitest.config.mts | 9 + .../pages-nodejs-v2-compat/wrangler.jsonc | 5 + fixtures/pages-plugin-example/.gitignore | 1 + .../functions/_middleware.ts | 32 + .../functions/api/v1/instance.ts | 1 + .../pages-plugin-example/functions/ext.ts | 5 + .../pages-plugin-example/functions/fixed.ts | 3 + .../functions/proxy-me-somewhere-else.ts | 1 + .../functions/static/_middleware.ts | 1 + .../pages-plugin-example/functions/status.ts | 3 + fixtures/pages-plugin-example/index.d.ts | 5 + fixtures/pages-plugin-example/package.json | 21 + .../pages-plugin-example/public/dir/bar.html | 9 + fixtures/pages-plugin-example/public/foo.html | 9 + .../pages-plugin-example/public/index.html | 9 + fixtures/pages-plugin-example/tsconfig.json | 11 + fixtures/pages-plugin-example/turbo.json | 9 + .../.gitignore | 1 + .../functions/_middleware.ts | 3 + .../index.d.ts | 4 + .../package.json | 25 + .../public/index.html | 6 + .../tests/index.test.ts | 28 + .../tests/tsconfig.json | 8 + .../tsconfig.json | 11 + .../vitest.config.mts | 9 + fixtures/pages-proxy-app/package.json | 23 + fixtures/pages-proxy-app/server/index.ts | 10 + fixtures/pages-proxy-app/tests/index.test.ts | 34 + fixtures/pages-proxy-app/tests/tsconfig.json | 7 + fixtures/pages-proxy-app/tsconfig.json | 13 + fixtures/pages-proxy-app/turbo.json | 9 + fixtures/pages-proxy-app/vitest.config.mts | 9 + fixtures/pages-redirected-config/.gitignore | 2 + fixtures/pages-redirected-config/package.json | 20 + fixtures/pages-redirected-config/src/index.js | 5 + .../tests/index.test.ts | 52 + .../pages-redirected-config/tools/build.ts | 24 + .../pages-redirected-config/tsconfig.json | 13 + fixtures/pages-redirected-config/turbo.json | 9 + .../pages-redirected-config/vitest.config.mts | 9 + fixtures/pages-simple-assets/index.d.ts | 4 + fixtures/pages-simple-assets/package.json | 21 + .../pages-simple-assets/public/index.html | 1 + .../pages-simple-assets/tests/index.test.ts | 65 + .../pages-simple-assets/tests/tsconfig.json | 8 + fixtures/pages-simple-assets/tsconfig.json | 12 + .../pages-simple-assets/vitest.config.mts | 9 + .../CHANGELOG.md | 10 + .../README.md | 32 + .../functions/date.ts | 3 + .../functions/greeting/hello.ts | 3 + .../package.json | 20 + .../public/_worker.js | 24 + .../public/index.html | 8 + .../tests/index.test.ts | 50 + .../tests/tsconfig.json | 7 + .../tsconfig.json | 12 + .../vitest.config.mts | 9 + fixtures/pages-workerjs-app/.env | 1 + fixtures/pages-workerjs-app/.gitignore | 1 + fixtures/pages-workerjs-app/package.json | 19 + .../pages-workerjs-app/tests/index.test.ts | 201 + .../pages-workerjs-app/tests/tsconfig.json | 7 + fixtures/pages-workerjs-app/tsconfig.json | 13 + fixtures/pages-workerjs-app/vitest.config.mts | 9 + .../workerjs-test/_routes.json | 6 + .../workerjs-test/_worker.js | 11 + .../workerjs-test/other-script.js | 1 + .../pages-workerjs-directory/CHANGELOG.md | 7 + .../pages-workerjs-directory/package.json | 18 + .../public/_worker.js/add.wasm | Bin 0 -> 41 bytes .../public/_worker.js/index.js | 59 + .../public/_worker.js/other-other-script.mjs | 1 + .../public/_worker.js/other-script.js | 1 + .../public/_worker.js/static.js | 1 + .../public/_worker.js/static.mjs | 1 + .../public/index.html | 1 + .../tests/index.test.ts | 92 + .../tests/tsconfig.json | 7 + .../pages-workerjs-directory/tsconfig.json | 13 + .../vitest.config.mts | 9 + fixtures/pages-workerjs-wasm-app/README.md | 41 + .../external-modules/meaning-of-life.html | 5 + fixtures/pages-workerjs-wasm-app/package.json | 20 + .../pages-workerjs-wasm-app/public/_worker.js | 28 + .../pages-workerjs-wasm-app/public/index.html | 6 + .../tests/index.test.ts | 42 + .../tests/tsconfig.json | 7 + .../pages-workerjs-wasm-app/tsconfig.json | 13 + .../pages-workerjs-wasm-app/vitest.config.mts | 9 + .../wasm/multiply.wasm | Bin 0 -> 46 bytes .../pages-workerjs-wasm-app/wasm/multiply.wat | 7 + .../README.md | 27 + .../package.json | 19 + .../public/_routes.json | 5 + .../public/_worker.js | 41 + .../public/index.html | 6 + .../tests/index.test.ts | 74 + .../tests/tsconfig.json | 7 + .../tsconfig.json | 13 + .../vitest.config.mts | 9 + .../wrangler.jsonc | 14 + .../CHANGELOG.md | 18 + .../pages-workerjs-with-routes-app/README.md | 31 + .../package.json | 19 + .../public/_routes.json | 5 + .../public/_worker.js | 41 + .../public/index.html | 6 + .../tests/index.test.ts | 70 + .../tests/tsconfig.json | 7 + .../tsconfig.json | 13 + .../vitest.config.mts | 9 + fixtures/python-worker/requirements.txt | 1 + fixtures/python-worker/src/arith.py | 2 + fixtures/python-worker/src/index.py | 8 + fixtures/python-worker/src/other.py | 2 + fixtures/python-worker/wrangler.jsonc | 6 + fixtures/ratelimit-app/.gitignore | 1 + fixtures/ratelimit-app/package.json | 21 + fixtures/ratelimit-app/src/index.js | 27 + fixtures/ratelimit-app/tests/index.test.ts | 51 + fixtures/ratelimit-app/tests/tsconfig.json | 7 + fixtures/ratelimit-app/tsconfig.json | 13 + fixtures/ratelimit-app/vitest.config.mts | 9 + fixtures/ratelimit-app/wrangler.jsonc | 28 + .../.gitignore | 2 + .../package.json | 20 + .../src/index.js | 5 + .../tests/index.test.ts | 28 + .../tools/build.ts | 36 + .../tsconfig.json | 13 + .../turbo.json | 9 + .../vitest.config.mts | 9 + .../wrangler.jsonc | 5 + fixtures/redirected-config-worker/.gitignore | 2 + .../redirected-config-worker/package.json | 20 + .../redirected-config-worker/src/index.js | 5 + .../tests/index.test.ts | 140 + .../redirected-config-worker/tools/build.ts | 24 + .../redirected-config-worker/tsconfig.json | 13 + fixtures/redirected-config-worker/turbo.json | 9 + .../vitest.config.mts | 9 + .../redirected-config-worker/wrangler.jsonc | 17 + fixtures/routing-app/functions/[default].js | 13 + fixtures/routing-app/functions/_middleware.js | 15 + fixtures/routing-app/functions/bar/[stuff].js | 13 + .../routing-app/functions/bar/baz/[[all]].js | 13 + fixtures/routing-app/functions/bar/foo.js | 13 + fixtures/routing-app/functions/blah.js | 13 + fixtures/routing-app/functions/foo.js | 13 + fixtures/routing-app/functions/index.ts | 15 + fixtures/routing-app/package.json | 11 + fixtures/routing-app/public/_routes.json | 5 + fixtures/routing-app/public/index.html | 1 + fixtures/routing-app/tsconfig.json | 11 + fixtures/rules-app/md.d.ts | 4 + fixtures/rules-app/package.json | 14 + fixtures/rules-app/src/content.md | 1 + fixtures/rules-app/src/index.ts | 17 + fixtures/rules-app/tsconfig.json | 11 + fixtures/rules-app/wrangler.jsonc | 12 + fixtures/secrets-store/package.json | 12 + fixtures/secrets-store/src/index.ts | 23 + fixtures/secrets-store/tsconfig.json | 13 + fixtures/secrets-store/wrangler.jsonc | 13 + fixtures/shared/package.json | 9 + fixtures/shared/src/fs-helpers.ts | 3 + fixtures/shared/src/mock-console.ts | 60 + fixtures/shared/src/mock-postgres-server.ts | 212 + .../shared/src/run-wrangler-long-lived.ts | 296 + fixtures/sites-app/package.json | 12 + fixtures/sites-app/public/404.html | 50 + fixtures/sites-app/public/favicon.ico | Bin 0 -> 1150 bytes .../public/img/200-wrangler-ferris.gif | Bin 0 -> 45598 bytes .../public/img/404-wrangler-ferris.gif | Bin 0 -> 31606 bytes fixtures/sites-app/public/index.html | 50 + fixtures/sites-app/src/modules.js | 88 + fixtures/sites-app/src/service-worker.js | 66 + fixtures/sites-app/wrangler.jsonc | 7 + fixtures/start-worker-node-test/package.json | 20 + .../src/config-errors.test.js | 102 + .../start-worker-node-test/src/index.test.js | 28 + fixtures/start-worker-node-test/src/index.ts | 13 + .../start-worker-node-test/src/say-hello.ts | 3 + fixtures/start-worker-node-test/tsconfig.json | 8 + fixtures/start-worker-node-test/wrangler.json | 5 + fixtures/unbound-durable-object/package.json | 15 + fixtures/unbound-durable-object/src/index.ts | 60 + .../tests/index.test.ts | 30 + .../tests/tsconfig.json | 7 + fixtures/unbound-durable-object/tsconfig.json | 13 + .../unbound-durable-object/vitest.config.mts | 9 + .../unbound-durable-object/wrangler.jsonc | 21 + fixtures/unsafe-external-plugin/README.md | 3 + fixtures/unsafe-external-plugin/package.json | 19 + fixtures/unsafe-external-plugin/src/index.ts | 8 + .../src/plugins/unsafe-service.ts | 149 + .../src/plugins/worker-shim.ts | 7 + .../src/workers/binding.worker.ts | 61 + .../src/workers/object.worker.ts | 11 + .../unsafe-external-plugin/tools/build.ts | 110 + fixtures/unsafe-external-plugin/tsconfig.json | 14 + fixtures/unsafe-external-plugin/turbo.json | 9 + .../vitest-pool-workers-examples/README.md | 26 + .../ai-vectorize/README.md | 7 + .../ai-vectorize/global-setup.ts | 9 + .../ai-vectorize/src/env.d.ts | 13 + .../ai-vectorize/src/index.ts | 31 + .../ai-vectorize/src/tsconfig.json | 4 + .../ai-vectorize/test/index.spec.ts | 80 + .../ai-vectorize/test/tsconfig.json | 4 + .../ai-vectorize/tsconfig.json | 4 + .../ai-vectorize/vitest.config.ts | 18 + .../ai-vectorize/wrangler.jsonc | 15 + .../basics-integration-auxiliary/README.md | 8 + .../global-setup.ts | 9 + .../basics-integration-auxiliary/src/index.ts | 8 + .../src/tsconfig.json | 4 + .../test/env.d.ts | 5 + .../test/fetch-integration-auxiliary.test.ts | 7 + .../scheduled-integration-auxiliary.test.ts | 12 + .../test/tsconfig.json | 4 + .../tsconfig.json | 4 + .../vitest.config.ts | 46 + .../wrangler.jsonc | 6 + .../basics-unit-integration-self/README.md | 10 + .../basics-unit-integration-self/src/env.d.ts | 10 + .../basics-unit-integration-self/src/greet.ts | 3 + .../basics-unit-integration-self/src/index.ts | 13 + .../src/tsconfig.json | 4 + .../test/fetch-integration-self.test.ts | 11 + .../test/fetch-unit.test.ts | 26 + .../test/scheduled-integration-self.test.ts | 16 + .../test/scheduled-unit.test.ts | 18 + .../test/tsconfig.json | 4 + .../tsconfig.json | 4 + .../vitest.config.ts | 21 + .../wrangler.jsonc | 5 + .../container-app/Dockerfile | 9 + .../container-app/README.md | 3 + .../container/simple-node-app.js | 12 + .../container-app/src/env.d.ts | 13 + .../container-app/src/index.ts | 57 + .../container-app/src/tsconfig.json | 10 + .../container-app/test/container.test.ts | 16 + .../container-app/test/container.unit.test.ts | 9 + .../container-app/test/tsconfig.json | 4 + .../container-app/tsconfig.json | 10 + .../container-app/vitest.config.ts | 14 + .../container-app/wrangler.jsonc | 27 + .../context-exports/README.md | 9 + .../context-exports/auxiliary-worker/index.ts | 13 + .../auxiliary-worker/tsconfig.json | 4 + .../worker-configuration.d.ts | 9 + .../auxiliary-worker/wrangler.jsonc | 6 + .../context-exports/global-setup.ts | 11 + .../context-exports/src/index.ts | 79 + .../context-exports/src/tsconfig.json | 11 + .../context-exports/src/virtual.ts | 29 + .../src/worker-configuration.d.ts | 11 + .../context-exports/src/wrangler.jsonc | 22 + .../context-exports/test/auxiliary.test.ts | 11 + .../test/durable-objects.test.ts | 40 + .../test/integration-self.test.ts | 85 + .../context-exports/test/tsconfig.json | 11 + .../context-exports/test/unit.test.ts | 38 + .../context-exports/tsconfig.json | 4 + .../context-exports/vitest.config.ts | 44 + .../vitest-pool-workers-examples/d1/README.md | 8 + .../d1/migrations/0000_initial.sql | 13 + .../d1/migrations/0001_admin.sql | 2 + .../d1/src/env.d.ts | 15 + .../d1/src/index.ts | 39 + .../d1/src/tsconfig.json | 4 + .../d1/src/utils.ts | 92 + .../d1/test/apply-migrations.ts | 7 + .../d1/test/env.d.ts | 6 + .../d1/test/queries.test.ts | 33 + .../d1/test/routes.test.ts | 61 + .../d1/test/tsconfig.json | 4 + .../d1/tsconfig.json | 4 + .../d1/vitest.config.ts | 35 + .../d1/wrangler.jsonc | 16 + .../durable-objects-exports/README.md | 9 + .../durable-objects-exports/src/env.d.ts | 13 + .../durable-objects-exports/src/index.ts | 52 + .../durable-objects-exports/src/tsconfig.json | 4 + .../test/exports.test.ts | 45 + .../test/tsconfig.json | 4 + .../durable-objects-exports/tsconfig.json | 4 + .../durable-objects-exports/vitest.config.ts | 19 + .../durable-objects-exports/wrangler.jsonc | 20 + .../durable-objects/README.md | 9 + .../durable-objects/src/env.d.ts | 14 + .../durable-objects/src/index.ts | 78 + .../durable-objects/src/tsconfig.json | 4 + .../durable-objects/test/alarm.test.ts | 30 + .../test/direct-access.test.ts | 48 + .../durable-objects/test/eviction.test.ts | 121 + .../durable-objects/test/sqlite-in-do.test.ts | 7 + .../durable-objects/test/tsconfig.json | 4 + .../durable-objects/test/websockets.test.ts | 79 + .../durable-objects/tsconfig.json | 4 + .../durable-objects/vitest.config.ts | 19 + .../durable-objects/wrangler.jsonc | 24 + .../dynamic-import/src/greeting.ts | 3 + .../dynamic-import/src/index.ts | 25 + .../dynamic-import/src/tsconfig.json | 4 + .../src/worker-configuration.d.ts | 13 + .../test/dynamic-import.test.ts | 29 + .../dynamic-import/test/tsconfig.json | 4 + .../dynamic-import/tsconfig.json | 4 + .../dynamic-import/vitest.config.ts | 16 + .../dynamic-import/wrangler.jsonc | 9 + .../hyperdrive/README.md | 7 + .../hyperdrive/env.d.ts | 7 + .../hyperdrive/global-setup.ts | 40 + .../hyperdrive/src/env.d.ts | 12 + .../hyperdrive/src/index.ts | 17 + .../hyperdrive/src/tsconfig.json | 4 + .../hyperdrive/test/echo.test.ts | 11 + .../hyperdrive/test/tsconfig.json | 4 + .../hyperdrive/tsconfig.json | 4 + .../hyperdrive/vitest.config.ts | 29 + .../hyperdrive/wrangler.jsonc | 12 + .../images/README.md | 3 + .../images/src/env.d.ts | 12 + .../images/src/index.ts | 8 + .../images/src/tsconfig.json | 4 + .../images/test/images.test.ts | 100 + .../images/test/tsconfig.json | 4 + .../images/tsconfig.json | 4 + .../images/vitest.config.ts | 16 + .../images/wrangler.jsonc | 8 + .../kv-r2-caches/README.md | 8 + .../kv-r2-caches/src/env.d.ts | 13 + .../kv-r2-caches/src/helpers.ts | 39 + .../kv-r2-caches/src/index.ts | 14 + .../kv-r2-caches/src/tsconfig.json | 4 + .../kv-r2-caches/test/kv.test.ts | 14 + .../kv-r2-caches/test/r2.test.ts | 32 + .../kv-r2-caches/test/tsconfig.json | 4 + .../kv-r2-caches/tsconfig.json | 4 + .../kv-r2-caches/vitest.config.ts | 16 + .../kv-r2-caches/wrangler.jsonc | 17 + .../misc/README.md | 3 + .../misc/global-setup.ts | 12 + .../misc/public/test.txt | 1 + .../misc/src/other-worker.mjs | 5 + .../misc/test/bare-specifiers.test.ts | 26 + .../misc/test/defines.test.ts | 13 + .../misc/test/durable-objects.test.ts | 23 + .../misc/test/env.d.ts | 7 + .../misc/test/fake-timers.test.ts | 23 + .../misc/test/global.d.ts | 9 + .../misc/test/large-provide.test.ts | 12 + .../misc/test/module-mocking.test.ts | 14 + .../misc/test/msw.test.ts | 61 + .../misc/test/nodejs.test.ts | 8 + .../misc/test/pages-functions.test.ts | 183 + .../misc/test/server.ts | 3 + .../misc/test/setup.ts | 10 + .../misc/test/tsconfig.json | 4 + .../misc/tsconfig.json | 4 + .../misc/vitest.assets.config.ts | 27 + .../misc/vitest.config.ts | 46 + .../misc/vitest.nodejs.config.ts | 21 + .../misc/wrangler.assets.jsonc | 10 + .../misc/wrangler.jsonc | 17 + .../misc/wrangler.nodejs.jsonc | 5 + .../module-resolution/README.md | 36 + .../module-resolution/src/env.d.ts | 10 + .../module-resolution/src/index.ts | 41 + .../module-resolution/src/test.sql | 1 + .../module-resolution/src/tsconfig.json | 9 + .../module-resolution/test/index.d.ts | 10 + .../module-resolution/test/index.spec.ts | 63 + .../module-resolution/test/tsconfig.json | 4 + .../module-resolution/tsconfig.json | 4 + .../vendor/cjs-wasm-module-dep/add.wasm | Bin 0 -> 53 bytes .../vendor/cjs-wasm-module-dep/index.d.ts | 3 + .../vendor/cjs-wasm-module-dep/index.js | 5 + .../vendor/cjs-wasm-module-dep/package.json | 5 + .../vendor/esm-dep/index.d.mts | 1 + .../vendor/esm-dep/index.mjs | 5 + .../vendor/esm-dep/package.json | 5 + .../vendor/esm-dep/value.mjs | 1 + .../module-resolution/vendor/ext-dep/index.js | 1 + .../vendor/ext-dep/package.json | 5 + .../vendor/ext-dep/subfolder/index.js | 1 + .../vendor/wasm-module-dep/add.wasm | Bin 0 -> 53 bytes .../vendor/wasm-module-dep/index.d.mts | 3 + .../vendor/wasm-module-dep/index.mjs | 7 + .../vendor/wasm-module-dep/package.json | 6 + .../module-resolution/vitest.config.ts | 14 + .../module-resolution/wrangler.jsonc | 7 + .../multiple-workers/README.md | 13 + .../multiple-workers/api-service/src/env.d.ts | 13 + .../multiple-workers/api-service/src/index.ts | 23 + .../api-service/src/tsconfig.json | 4 + .../api-service/wrangler.jsonc | 19 + .../auth-service/src/env.d.ts | 12 + .../auth-service/src/index.ts | 45 + .../auth-service/src/tsconfig.json | 4 + .../auth-service/wrangler.jsonc | 13 + .../database-service/src/env.d.ts | 12 + .../database-service/src/index.ts | 14 + .../database-service/src/tsconfig.json | 4 + .../database-service/wrangler.jsonc | 12 + .../multiple-workers/global-setup.ts | 21 + .../multiple-workers/test/env.d.ts | 5 + .../multiple-workers/test/integration.test.ts | 65 + .../multiple-workers/test/tsconfig.json | 4 + .../multiple-workers/tsconfig.json | 4 + .../multiple-workers/vitest.config.ts | 123 + .../vitest-pool-workers-examples/package.json | 62 + .../.gitignore | 1 + .../README.md | 8 + .../functions/api/_middleware.ts | 11 + .../functions/api/kv/[key].ts | 11 + .../functions/api/ping.ts | 3 + .../functions/env.d.ts | 6 + .../functions/tsconfig.json | 4 + .../global-setup.ts | 25 + .../public/404.html | 1 + .../public/_headers | 4 + .../public/_redirects | 1 + .../public/index.html | 1 + .../public/secure.html | 1 + .../test/env.d.ts | 10 + .../test/integration-self.test.ts | 71 + .../test/tsconfig.json | 4 + .../test/unit.test.ts | 66 + .../tsconfig.json | 4 + .../vitest.config.ts | 32 + .../pages-with-config/README.md | 3 + .../pages-with-config/pages-config.test.ts | 7 + .../pages-with-config/tsconfig.json | 4 + .../pages-with-config/vitest.config.ts | 14 + .../pages-with-config/wrangler.jsonc | 5 + .../pipelines/README.md | 8 + .../pipelines/src/env.d.ts | 14 + .../pipelines/src/index.ts | 11 + .../pipelines/src/tsconfig.json | 4 + .../pipeline-send-integration-self.test.ts | 12 + .../pipelines/test/pipeline-send-unit.test.ts | 45 + .../pipelines/test/tsconfig.json | 4 + .../pipelines/tsconfig.json | 4 + .../pipelines/vitest.config.ts | 16 + .../pipelines/wrangler.jsonc | 11 + .../queues/README.md | 10 + .../queues/src/env.d.ts | 13 + .../queues/src/index.ts | 34 + .../queues/src/tsconfig.json | 4 + .../queue-consumer-integration-self.test.ts | 36 + .../queues/test/queue-consumer-unit.test.ts | 42 + .../queue-producer-integration-self.test.ts | 20 + .../queues/test/queue-producer-unit.test.ts | 79 + .../queues/test/tsconfig.json | 4 + .../queues/tsconfig.json | 4 + .../queues/vitest.config.ts | 25 + .../queues/wrangler.jsonc | 25 + .../request-mocking/README.md | 8 + .../request-mocking/src/env.d.ts | 10 + .../request-mocking/src/index.ts | 11 + .../request-mocking/src/tsconfig.json | 4 + .../request-mocking/test/declarative.test.ts | 65 + .../request-mocking/test/imperative.test.ts | 104 + .../request-mocking/test/server.ts | 3 + .../request-mocking/test/setup.ts | 10 + .../request-mocking/test/tsconfig.json | 4 + .../request-mocking/tsconfig.json | 4 + .../request-mocking/vitest.config.ts | 19 + .../request-mocking/wrangler.jsonc | 5 + .../reset/src/env.d.ts | 18 + .../reset/src/index.ts | 27 + .../reset/src/tsconfig.json | 4 + .../reset/test/reset.test.ts | 105 + .../reset/test/tsconfig.json | 4 + .../reset/tsconfig.json | 4 + .../reset/vitest.config.ts | 19 + .../reset/wrangler.jsonc | 51 + .../rpc/README.md | 8 + .../rpc/src/counter.ts | 26 + .../rpc/src/env.d.ts | 19 + .../rpc/src/index.ts | 188 + .../rpc/src/tsconfig.json | 4 + .../rpc/test/integration-self.test.ts | 78 + .../rpc/test/tsconfig.json | 4 + .../rpc/test/unit.test.ts | 292 + .../rpc/tsconfig.json | 4 + .../rpc/vitest.config.ts | 24 + .../rpc/wrangler.jsonc | 45 + .../vitest-pool-workers-examples/tsc-all.mjs | 32 + .../tsconfig.node.json | 12 + .../tsconfig.workerd-test.json | 11 + .../tsconfig.workerd.json | 7 + .../vitest.config.ts | 22 + .../vitest.global.ts | 3 + .../web-assembly/README.md | 7 + .../web-assembly/src/add.wasm | Bin 0 -> 53 bytes .../web-assembly/src/env.d.ts | 10 + .../web-assembly/src/index.ts | 25 + .../web-assembly/src/tsconfig.json | 4 + .../web-assembly/src/wasm-shim.d.ts | 9 + .../web-assembly/test/add.test.ts | 7 + .../test/not-actually.wasm.test.ts | 10 + .../web-assembly/test/tsconfig.json | 4 + .../web-assembly/tsconfig.json | 4 + .../web-assembly/vitest.config.ts | 20 + .../web-assembly/wrangler.jsonc | 5 + .../workers-assets-no-dir/src/env.d.ts | 12 + .../workers-assets-no-dir/src/index.ts | 5 + .../workers-assets-no-dir/src/tsconfig.json | 4 + .../test/assets-no-dir.test.ts | 19 + .../workers-assets-no-dir/test/tsconfig.json | 4 + .../workers-assets-no-dir/tsconfig.json | 4 + .../workers-assets-no-dir/vitest.config.ts | 14 + .../workers-assets-no-dir/wrangler.jsonc | 10 + .../workers-assets/README.md | 5 + .../workers-assets/public/binding.html | 1 + .../workers-assets/public/index.html | 11 + .../workers-assets/src/env.d.ts | 12 + .../workers-assets/src/index.ts | 15 + .../workers-assets/src/tsconfig.json | 4 + .../workers-assets/test/assets.test.ts | 34 + .../workers-assets/test/tsconfig.json | 4 + .../workers-assets/tsconfig.json | 4 + .../workers-assets/vitest.config.ts | 14 + .../workers-assets/wrangler.jsonc | 11 + .../workflows/README.md | 9 + .../workflows/src/env.d.ts | 14 + .../workflows/src/index.ts | 89 + .../workflows/src/tsconfig.json | 4 + .../workflows/test/integration.test.ts | 174 + .../workflows/test/tsconfig.json | 4 + .../workflows/test/unit.test.ts | 217 + .../workflows/tsconfig.json | 4 + .../workflows/vitest.config.ts | 16 + .../workflows/wrangler.jsonc | 12 + .../env.d.ts | 4 + .../package.json | 17 + .../remote-worker.js | 7 + .../remote-worker.staging.js | 7 + .../run-tests.mjs | 200 + .../src/index.ts | 7 + .../test-staging/index.spec.ts | 30 + .../test/index.spec.ts | 30 + .../tsconfig.json | 18 + .../turbo.json | 19 + .../vitest.workers.config.staging.ts | 22 + .../vitest.workers.config.ts | 22 + .../worker-configuration.d.ts | 17 + .../wrangler.json | 23 + fixtures/wasm-app/README.md | 17 + fixtures/wasm-app/package.json | 4 + fixtures/wasm-app/worker/index_bg.wasm | Bin 0 -> 563460 bytes .../wasm-app/worker/module/export_wasm.js | 10 + fixtures/wasm-app/worker/module/index.js | 3 + fixtures/wasm-app/worker/module/index_bg.js | 683 + .../service-worker-module/export_wasm.js | 10 + .../worker/service-worker-module/index.js | 5 + .../worker/service-worker-module/index_bg.js | 683 + .../worker/service-worker/export_wasm.js | 11 + .../wasm-app/worker/service-worker/index.js | 5 + .../worker/service-worker/index_bg.js | 683 + .../worker/service-worker/wrangler.jsonc | 5 + fixtures/wildcard-modules/package.json | 20 + fixtures/wildcard-modules/src/common.cjs | 1 + fixtures/wildcard-modules/src/dep.ts | 1 + fixtures/wildcard-modules/src/dynamic.js | 1 + fixtures/wildcard-modules/src/index.ts | 32 + fixtures/wildcard-modules/src/lang/en.js | 1 + fixtures/wildcard-modules/src/lang/fr.js | 1 + fixtures/wildcard-modules/src/text.d.ts | 4 + fixtures/wildcard-modules/src/text.txt | 1 + fixtures/wildcard-modules/test/index.test.ts | 207 + fixtures/wildcard-modules/test/tsconfig.json | 7 + fixtures/wildcard-modules/tsconfig.json | 14 + fixtures/wildcard-modules/turbo.json | 9 + fixtures/wildcard-modules/vitest.config.mts | 9 + fixtures/wildcard-modules/wrangler.jsonc | 5 + fixtures/worker-app/.env | 1 + fixtures/worker-app/.gitignore | 2 + fixtures/worker-app/package.json | 25 + fixtures/worker-app/public/404.html | 50 + fixtures/worker-app/public/favicon.ico | Bin 0 -> 1150 bytes .../public/img/200-wrangler-ferris.gif | Bin 0 -> 45598 bytes .../public/img/404-wrangler-ferris.gif | Bin 0 -> 31606 bytes fixtures/worker-app/public/index.html | 50 + fixtures/worker-app/src/dep.js | 3 + .../src/explicit-resource-management.js | 24 + fixtures/worker-app/src/index.js | 108 + fixtures/worker-app/src/log.ts | 17 + .../worker-app/tests/build-conditions.test.ts | 94 + fixtures/worker-app/tests/https.test.ts | 33 + fixtures/worker-app/tests/index.test.ts | 212 + fixtures/worker-app/tests/tsconfig.json | 7 + .../worker-app/tests/undrained-body.test.ts | 44 + fixtures/worker-app/tsconfig.json | 13 + fixtures/worker-app/vitest.config.mts | 17 + fixtures/worker-app/wrangler.jsonc | 11 + fixtures/worker-logs/package.json | 19 + fixtures/worker-logs/src/module.js | 28 + fixtures/worker-logs/src/service.js | 12 + fixtures/worker-logs/tests/index.test.ts | 345 + fixtures/worker-logs/tsconfig.json | 13 + fixtures/worker-logs/turbo.json | 19 + fixtures/worker-logs/vitest.config.mts | 9 + fixtures/worker-logs/wrangler.module.jsonc | 6 + fixtures/worker-logs/wrangler.service.jsonc | 5 + fixtures/worker-ts/CHANGELOG.md | 7 + fixtures/worker-ts/package.json | 12 + fixtures/worker-ts/src/index.ts | 34 + fixtures/worker-ts/tsconfig.json | 13 + fixtures/worker-ts/wrangler.jsonc | 6 + fixtures/worker-with-resources/package.json | 20 + fixtures/worker-with-resources/src/index.ts | 371 + .../worker-with-resources/tests/index.test.ts | 199 + fixtures/worker-with-resources/tsconfig.json | 17 + .../worker-with-resources/vitest.config.mts | 12 + .../worker-with-resources/worker-b/index.ts | 45 + .../worker-b/tsconfig.json | 7 + .../worker-b/worker-configuration.d.ts | 16 + .../worker-b/wrangler.jsonc | 33 + .../worker-configuration.d.ts | 25 + fixtures/worker-with-resources/wrangler.jsonc | 65 + .../README.md | 26 + .../package.json | 18 + .../src/index.ts | 45 + .../tests/env.d.ts | 3 + .../tests/index.spec.ts | 9 + .../tests/tsconfig.json | 10 + .../tsconfig.json | 8 + .../vitest.config.ts | 13 + .../wrangler.jsonc | 24 + .../workers-shared-asset-config/README.md | 5 + fixtures/workers-shared-asset-config/env.d.ts | 7 + .../html-handling.test.ts | 101 + .../loopback.test.ts | 66 + .../workers-shared-asset-config/package.json | 24 + .../redirects.test.ts | 97 + .../test-cases/encoding-test-cases.ts | 631 + .../test-cases/html-handling-test-cases.ts | 788 + .../workers-shared-asset-config/tsconfig.json | 11 + .../url-normalization.test.ts | 85 + .../vitest.config.ts | 18 + .../README.md | 65 + .../package.json | 18 + .../tests/index.test.ts | 415 + .../tests/tsconfig.json | 7 + .../tsconfig.json | 13 + .../vitest.config.mts | 9 + .../workerA/src/index.ts | 46 + .../workerA/src/workerB.util.ts | 44 + .../workerA/src/workerC.util.ts | 42 + .../workerA/src/workerD.util.ts | 41 + .../workerA/src/workerWS.util.ts | 36 + .../workerA/wrangler.jsonc | 27 + .../public/busy-bee.html | 1 + .../public/index.html | 1 + .../workerB-with-default-export/src/index.ts | 55 + .../wrangler.jsonc | 9 + .../public/busy-bee.html | 1 + .../public/index.html | 1 + .../src/index.ts | 87 + .../wrangler.jsonc | 9 + .../public/busy-bee.html | 1 + .../public/index.html | 1 + .../src/index.ts | 81 + .../wrangler.jsonc | 9 + .../workerWS/public/busy-bee.html | 1 + .../workerWS/public/index.html | 1 + .../workerWS/src/index.ts | 24 + .../workerWS/wrangler.jsonc | 8 + fixtures/workers-with-assets-only/README.md | 17 + .../workers-with-assets-only/package.json | 18 + .../workers-with-assets-only/public/404.html | 1 + .../workers-with-assets-only/public/README.md | 57 + .../public/about/%5Bboop%5D.html | 1 + .../public/about/404.html | 1 + .../public/about/[boop].html | 1 + .../public/about/[fünky].txt | 1 + .../public/about/index.html | 1 + fixtures/workers-with-assets-only/public/bin | 1 + .../workers-with-assets-only/public/bin.html | 1 + .../workers-with-assets-only/public/both.html | 1 + .../public/both/index.html | 1 + .../workers-with-assets-only/public/file-bin | 1 + .../public/file-bin.html | 1 + .../workers-with-assets-only/public/file.html | 1 + .../public/folder-bin | 0 .../public/folder/index.html | 1 + .../public/index-bin/index | 1 + .../public/index-bin/index.html | 1 + .../public/index.html | 1 + .../public/lava-lamps.jpg | Bin 0 -> 337047 bytes .../workers-with-assets-only/public/yay.txt | 12 + .../tests/index.test.ts | 178 + .../tests/tsconfig.json | 7 + .../workers-with-assets-only/tsconfig.json | 13 + .../vitest.config.mts | 9 + .../workers-with-assets-only/wrangler.jsonc | 7 + .../README.md | 17 + .../package.json | 19 + .../public/index.html | 1 + .../src/index.ts | 22 + .../tests/index.test.ts | 37 + .../tests/tsconfig.json | 7 + .../tsconfig.json | 13 + .../vitest.config.mts | 9 + .../wrangler.jsonc | 10 + fixtures/workers-with-assets-spa/README.md | 17 + fixtures/workers-with-assets-spa/package.json | 24 + .../workers-with-assets-spa/public/index.html | 223 + .../public/shadowed-by-asset.txt | 1 + fixtures/workers-with-assets-spa/src/index.ts | 24 + ...homepage-in-a-browser-correctly-1-snap.png | Bin 0 -> 37988 bytes .../tests/index.test.ts | 267 + .../tests/tsconfig.json | 7 + .../workers-with-assets-spa/tsconfig.json | 13 + .../workers-with-assets-spa/vitest.config.mts | 14 + .../workers-with-assets-spa/wrangler.jsonc | 13 + .../package.json | 23 + .../public/static/page.html | 1 + .../public/worker/asset.html | 1 + .../public/worker/worker-runs.html | 1 + .../spa-assets/index.html | 14 + .../spa.wrangler.jsonc | 11 + .../src/index.ts | 31 + .../test/index.test.ts | 177 + .../test/tsconfig.json | 7 + .../tsconfig.json | 13 + .../vitest.config.mts | 9 + .../wrangler.jsonc | 17 + fixtures/workers-with-assets/.gitignore | 2 + fixtures/workers-with-assets/README.md | 17 + fixtures/workers-with-assets/package.json | 19 + .../workers-with-assets/public/.assetsignore | 1 + .../public/.dot/index.html | 1 + .../workers-with-assets/public/.dotfile.html | 1 + fixtures/workers-with-assets/public/README.md | 57 + fixtures/workers-with-assets/public/_headers | 2 + .../workers-with-assets/public/_redirects | 2 + .../workers-with-assets/public/_worker.js | 1 + .../public/about/%5Bboop%5D.html | 1 + .../public/about/%5Bwomp%5D.html | 1 + .../public/about/[boop].html | 1 + .../public/about/[fünky].txt | 1 + .../public/about/index.html | 1 + fixtures/workers-with-assets/public/bar.html | 1 + .../workers-with-assets/public/binding.html | 1 + fixtures/workers-with-assets/public/foo.html | 1 + .../workers-with-assets/public/ignore-me.txt | 1 + .../workers-with-assets/public/index.html | 1 + .../workers-with-assets/public/lava-lamps.jpg | Bin 0 -> 337047 bytes .../public/totallyinvalidextension.greg | 1 + fixtures/workers-with-assets/public/yay.txt | 12 + fixtures/workers-with-assets/src/index.ts | 32 + .../workers-with-assets/tests/index.test.ts | 299 + .../workers-with-assets/tests/tsconfig.json | 7 + fixtures/workers-with-assets/tsconfig.json | 13 + .../workers-with-assets/vitest.config.mts | 9 + fixtures/workers-with-assets/wrangler.jsonc | 17 + fixtures/workflow-multiple/package.json | 16 + fixtures/workflow-multiple/src/index.ts | 139 + .../workflow-multiple/tests/index.test.ts | 244 + .../workflow-multiple/tests/tsconfig.json | 7 + fixtures/workflow-multiple/tsconfig.json | 13 + fixtures/workflow-multiple/vitest.config.mts | 9 + fixtures/workflow-multiple/wrangler.jsonc | 22 + fixtures/workflow/package.json | 17 + fixtures/workflow/src/index.ts | 175 + fixtures/workflow/tests/index.test.ts | 699 + fixtures/workflow/tests/tsconfig.json | 7 + fixtures/workflow/tsconfig.json | 13 + fixtures/workflow/vitest.config.mts | 9 + fixtures/workflow/wrangler.jsonc | 22 + lint-turbo.mjs | 63 + package.json | 66 + packages/autoconfig/CHANGELOG.md | 51 + packages/autoconfig/package.json | 57 + packages/autoconfig/scripts/deps.ts | 12 + packages/autoconfig/src/context.ts | 107 + .../src/details/framework-detection.ts | 338 + packages/autoconfig/src/details/index.ts | 349 + packages/autoconfig/src/errors.ts | 39 + .../src/frameworks/all-frameworks.ts | 276 + packages/autoconfig/src/frameworks/analog.ts | 90 + packages/autoconfig/src/frameworks/angular.ts | 155 + packages/autoconfig/src/frameworks/astro.ts | 305 + .../src/frameworks/framework-class.ts | 119 + packages/autoconfig/src/frameworks/index.ts | 83 + packages/autoconfig/src/frameworks/next.ts | 48 + packages/autoconfig/src/frameworks/no-op.ts | 10 + packages/autoconfig/src/frameworks/nuxt.ts | 89 + packages/autoconfig/src/frameworks/qwik.ts | 135 + .../autoconfig/src/frameworks/react-router.ts | 524 + .../autoconfig/src/frameworks/solid-start.ts | 127 + packages/autoconfig/src/frameworks/static.ts | 17 + .../autoconfig/src/frameworks/sveltekit.ts | 58 + .../autoconfig/src/frameworks/tanstack.ts | 34 + .../src/frameworks/utils/packages.ts | 4 + .../src/frameworks/utils/vite-config.ts | 323 + .../src/frameworks/utils/vite-plugin.ts | 51 + packages/autoconfig/src/frameworks/vike.ts | 293 + packages/autoconfig/src/frameworks/vite.ts | 73 + packages/autoconfig/src/frameworks/waku.ts | 210 + packages/autoconfig/src/index.ts | 39 + packages/autoconfig/src/run.ts | 448 + packages/autoconfig/src/types.ts | 80 + packages/autoconfig/src/uses-typescript.ts | 6 + .../confirm-auto-config-details.test.ts | 248 + .../display-auto-config-details.test.ts | 96 + .../basic-framework-detection.test.ts | 49 + .../lock-file-warning.test.ts | 49 + .../multiple-frameworks-detected.test.ts | 193 + .../package-manager-detection.test.ts | 71 + .../pages-project-detection.test.ts | 110 + .../workspace-root-handling.test.ts | 59 + .../get-details-for-auto-config.test.ts | 562 + .../tests/frameworks/angular.test.ts | 445 + .../config-future-no-middleware.ts | 7 + .../config-middleware-and-split.ts | 8 + .../react-router/config-middleware-false.ts | 7 + .../react-router/config-middleware-true.ts | 7 + .../fixtures/react-router/config-no-future.ts | 4 + .../config-plain-object-middleware.ts | 6 + .../react-router/vite-config-basic.ts | 2 + .../frameworks/get-framework-class.test.ts | 37 + .../frameworks/is-framework-supported.test.ts | 20 + .../frameworks/is-known-framework.test.ts | 22 + .../tests/frameworks/react-router.test.ts | 472 + .../frameworks/utils/vite-plugin.test.ts | 288 + .../validate-framework-version.test.ts | 149 + .../autoconfig/tests/frameworks/vike.test.ts | 326 + .../autoconfig/tests/frameworks/vite.test.ts | 111 + .../get-installed-package-version.test.ts | 81 + .../autoconfig/tests/helpers/mock-context.ts | 34 + packages/autoconfig/tests/run-summary.test.ts | 250 + packages/autoconfig/tests/tsconfig.json | 8 + packages/autoconfig/tests/vite-config.test.ts | 304 + packages/autoconfig/tsconfig.json | 10 + packages/autoconfig/tsup.config.ts | 21 + packages/autoconfig/turbo.json | 16 + packages/autoconfig/vitest.config.mts | 12 + packages/chrome-devtools-patches/CHANGELOG.md | 50 + packages/chrome-devtools-patches/Makefile | 38 + packages/chrome-devtools-patches/README.md | 90 + packages/chrome-devtools-patches/package.json | 26 + ...upport-make-it-work-in-Firefox-Safar.patch | 128 + ...etup-Cloudflare-devtools-target-type.patch | 49 + ...ove-connection-stability.-Without-th.patch | 45 + ...source-files-over-the-network.-This-.patch | 158 + ...the-devtools-theme-via-a-query-param.patch | 41 + .../0006-All-about-the-network-tab.patch | 116 + ...Limit-heap-profiling-modes-available.patch | 31 + ...ame-as-the-title-for-the-Javascript-.patch | 31 + .../chrome-devtools-patches/wrangler.jsonc | 10 + packages/cli/CHANGELOG.md | 201 + packages/cli/README.md | 5 + .../cli/__tests__/check-macos-version.test.ts | 161 + packages/cli/__tests__/cli.test.ts | 10 + packages/cli/__tests__/command.test.ts | 72 + packages/cli/__tests__/gitignore.test.ts | 445 + packages/cli/__tests__/packages.test.ts | 154 + packages/cli/args.ts | 21 + packages/cli/check-macos-version.ts | 109 + packages/cli/colors.ts | 34 + packages/cli/command.ts | 189 + packages/cli/cursor.ts | 13 + packages/cli/error.ts | 11 + packages/cli/gitignore.ts | 151 + packages/cli/index.ts | 280 + packages/cli/interactive.ts | 738 + packages/cli/package.json | 86 + packages/cli/packages.ts | 187 + packages/cli/scripts/deps.ts | 25 + packages/cli/select-list.ts | 82 + packages/cli/streams.ts | 9 + packages/cli/test-util.ts | 45 + packages/cli/tsconfig.json | 13 + packages/cli/tsdown.config.ts | 27 + packages/cli/turbo.json | 11 + packages/cli/vite.setup.ts | 6 + packages/cli/vitest.config.mts | 12 + packages/codemod/.gitignore | 1 + packages/codemod/CHANGELOG.md | 9 + packages/codemod/README.md | 63 + packages/codemod/dev-snippets/test.ts | 5 + packages/codemod/package.json | 55 + packages/codemod/src/dev.ts | 75 + packages/codemod/src/index.ts | 124 + packages/codemod/tests/index.test.ts | 129 + packages/codemod/tests/tsconfig.json | 9 + packages/codemod/tsconfig.json | 16 + packages/codemod/tsup.config.ts | 29 + packages/codemod/turbo.json | 16 + packages/codemod/vitest.config.mts | 12 + packages/config/package.json | 47 + packages/config/src/__tests__/convert.test.ts | 1008 + .../config/src/__tests__/generate.test.ts | 28 + packages/config/src/__tests__/load.test.ts | 253 + packages/config/src/__tests__/schema.test.ts | 591 + packages/config/src/bindings.ts | 890 + packages/config/src/build-output.ts | 86 + packages/config/src/convert.ts | 882 + packages/config/src/exports.ts | 222 + packages/config/src/generate.ts | 60 + packages/config/src/index.ts | 27 + packages/config/src/inference.ts | 335 + packages/config/src/load.ts | 218 + packages/config/src/public.ts | 84 + packages/config/src/schema.ts | 543 + packages/config/src/triggers.ts | 132 + packages/config/src/types.ts | 410 + packages/config/src/utils.ts | 10 + packages/config/src/worker-definition.ts | 129 + packages/config/tsconfig.json | 7 + packages/config/tsdown.config.ts | 16 + packages/config/turbo.json | 9 + packages/config/vitest.config.ts | 8 + packages/containers-shared/CHANGELOG.md | 326 + packages/containers-shared/README.md | 6 + packages/containers-shared/index.ts | 9 + packages/containers-shared/package.json | 38 + packages/containers-shared/src/build.ts | 166 + .../src/client/core/ApiError.ts | 28 + .../src/client/core/ApiRequestOptions.ts | 23 + .../src/client/core/ApiResult.ts | 10 + .../src/client/core/CancelablePromise.ts | 129 + .../src/client/core/OpenAPI.ts | 33 + .../src/client/core/PaginatedResult.ts | 14 + .../src/client/core/request.ts | 526 + .../containers-shared/src/client/index.ts | 225 + .../src/client/models/AccountDefaults.ts | 22 + .../src/client/models/AccountID.ts | 8 + .../src/client/models/AccountLimit.ts | 49 + .../src/client/models/AccountLocation.ts | 12 + .../client/models/AccountLocationLimits.ts | 25 + .../models/AccountLocationLimitsAsProperty.ts | 12 + .../src/client/models/AccountRegistryToken.ts | 18 + .../src/client/models/AddressAssignment.ts | 16 + .../src/client/models/Application.ts | 49 + .../client/models/ApplicationAffinities.ts | 15 + .../models/ApplicationAffinityColocation.ts | 12 + .../ApplicationAffinityHardwareGeneration.ts | 11 + .../client/models/ApplicationConstraintPop.ts | 8 + .../client/models/ApplicationConstraints.ts | 16 + .../src/client/models/ApplicationHealth.ts | 9 + .../models/ApplicationHealthInstances.ts | 32 + .../src/client/models/ApplicationID.ts | 8 + .../src/client/models/ApplicationJob.ts | 76 + .../client/models/ApplicationJobsConfig.ts | 8 + .../client/models/ApplicationMutationError.ts | 21 + .../src/client/models/ApplicationName.ts | 8 + .../client/models/ApplicationNotFoundError.ts | 7 + .../client/models/ApplicationPriorities.ts | 15 + .../src/client/models/ApplicationPriority.ts | 8 + .../src/client/models/ApplicationRollout.ts | 89 + .../ApplicationRolloutActiveGracePeriod.ts | 10 + .../models/ApplicationRolloutProgress.ts | 25 + .../models/ApplicationSchedulingHint.ts | 18 + .../src/client/models/ApplicationStatus.ts | 14 + .../src/client/models/AssignIPv4.ts | 9 + .../src/client/models/AssignIPv6.ts | 9 + .../src/client/models/BadRequestError.ts | 7 + .../client/models/BadRequestWithCodeError.ts | 23 + .../src/client/models/City.ts | 8 + .../src/client/models/Command.ts | 13 + .../client/models/CompleteAccountCustomer.ts | 20 + .../models/CompleteAccountLocationCustomer.ts | 14 + .../src/client/models/ContainerNetworkMode.ts | 13 + .../models/CreateApplicationBadRequest.ts | 13 + .../models/CreateApplicationJobBadRequest.ts | 13 + .../models/CreateApplicationJobRequest.ts | 54 + .../client/models/CreateApplicationRequest.ts | 45 + .../models/CreateApplicationRolloutRequest.ts | 75 + .../models/CreateDeploymentBadRequest.ts | 13 + .../models/CreateDeploymentV2RequestBody.ts | 12 + .../models/CreateImageRegistryRequestBody.ts | 20 + .../client/models/CreateSSHPublicKeyError.ts | 8 + .../models/CreateSSHPublicKeyRequestBody.ts | 13 + .../client/models/CustomerImageRegistry.ts | 26 + .../src/client/models/DNSConfiguration.ts | 27 + .../src/client/models/DashApplication.ts | 24 + .../DashApplicationDurableObjectInstance.ts | 11 + .../client/models/DashApplicationInstance.ts | 18 + .../client/models/DashApplicationInstances.ts | 11 + .../client/models/DefaultImageRegistryKind.ts | 7 + .../client/models/DeleteDeploymentError.ts | 8 + .../models/DeleteImageRegistryResponse.ts | 11 + .../client/models/DeploymentAlreadyExists.ts | 7 + .../src/client/models/DeploymentCheck.ts | 25 + .../src/client/models/DeploymentCheckHTTP.ts | 24 + .../models/DeploymentCheckHTTPRequestBody.ts | 24 + .../src/client/models/DeploymentCheckKind.ts | 8 + .../models/DeploymentCheckRequestBody.ts | 54 + .../src/client/models/DeploymentCheckType.ts | 8 + .../client/models/DeploymentCreationError.ts | 8 + .../src/client/models/DeploymentID.ts | 8 + .../src/client/models/DeploymentListError.ts | 8 + .../src/client/models/DeploymentLocation.ts | 21 + .../models/DeploymentModificationError.ts | 8 + .../client/models/DeploymentMutationError.ts | 15 + .../client/models/DeploymentNotFoundError.ts | 17 + .../client/models/DeploymentPlacementState.ts | 13 + .../client/models/DeploymentQueuedDetails.ts | 16 + .../client/models/DeploymentQueuedReason.ts | 11 + .../models/DeploymentReplacementError.ts | 8 + .../models/DeploymentSchedulingState.ts | 11 + .../src/client/models/DeploymentSecretMap.ts | 20 + .../src/client/models/DeploymentState.ts | 13 + .../src/client/models/DeploymentType.ts | 24 + .../src/client/models/DeploymentV2.ts | 107 + .../src/client/models/DeploymentVersion.ts | 8 + .../src/client/models/Disk.ts | 20 + .../src/client/models/DiskMB.ts | 10 + .../src/client/models/DiskSizeWithUnit.ts | 8 + .../src/client/models/Domain.ts | 8 + .../models/DurableObjectStatusHealth.ts | 14 + .../models/DurableObjectsConfiguration.ts | 15 + .../src/client/models/Duration.ts | 12 + .../src/client/models/EmptyResponse.ts | 8 + .../src/client/models/Entrypoint.ts | 13 + .../src/client/models/EnvironmentVariable.ts | 14 + .../client/models/EnvironmentVariableName.ts | 8 + .../client/models/EnvironmentVariableValue.ts | 8 + .../src/client/models/EventName.ts | 42 + .../src/client/models/EventType.ts | 11 + .../src/client/models/ExecFormParam.ts | 5 + .../src/client/models/ExternalRegistryKind.ts | 12 + .../src/client/models/GenericErrorDetails.ts | 8 + .../GenericErrorResponseWithRequestID.ts | 8 + .../client/models/GenericMessageResponse.ts | 10 + .../src/client/models/GetDeploymentError.ts | 8 + .../src/client/models/GetPlacementError.ts | 8 + .../src/client/models/HTTPMethod.ts | 16 + .../containers-shared/src/client/models/IP.ts | 8 + .../src/client/models/IPAllocation.ts | 30 + .../models/IPAllocationConfiguration.ts | 20 + .../client/models/IPAllocationsWithFilter.ts | 13 + .../src/client/models/IPType.ts | 11 + .../src/client/models/IPV4.ts | 8 + .../src/client/models/ISO8601Timestamp.ts | 8 + .../src/client/models/Identity.ts | 8 + .../src/client/models/Image.ts | 8 + .../models/ImageRegistryAlreadyExistsError.ts | 26 + .../src/client/models/ImageRegistryAuth.ts | 16 + .../ImageRegistryCredentialsConfiguration.ts | 13 + .../client/models/ImageRegistryIsPublic.ts | 16 + .../models/ImageRegistryNotAllowedError.ts | 26 + .../models/ImageRegistryNotFoundError.ts | 16 + .../client/models/ImageRegistryPermissions.ts | 9 + .../client/models/ImageRegistryProtoDomain.ts | 13 + .../client/models/ImageRegistryProtocol.ts | 21 + .../ImageRegistryProtocolAlreadyExists.ts | 13 + .../ImageRegistryProtocolIsReferencedError.ts | 16 + .../models/ImageRegistryProtocolNotFound.ts | 16 + .../client/models/ImageRegistryProtocols.ts | 7 + .../src/client/models/InstanceType.ts | 29 + .../src/client/models/InternalError.ts | 11 + .../src/client/models/JobEvents.ts | 10 + .../src/client/models/JobID.ts | 8 + .../src/client/models/JobNotFoundError.ts | 7 + .../src/client/models/JobSecretMap.ts | 16 + .../src/client/models/JobStatus.ts | 9 + .../src/client/models/JobStatusHealth.ts | 20 + .../src/client/models/JobTimeoutSeconds.ts | 8 + .../src/client/models/Label.ts | 14 + .../src/client/models/LabelName.ts | 8 + .../src/client/models/LabelValue.ts | 8 + .../src/client/models/ListApplications.ts | 10 + .../src/client/models/ListDeploymentsV2.ts | 10 + .../src/client/models/ListIPsIsAllocated.ts | 8 + .../src/client/models/ListPlacements.ts | 10 + .../src/client/models/ListPlacementsError.ts | 8 + .../src/client/models/ListSSHPublicKeys.ts | 10 + .../client/models/ListSSHPublicKeysError.ts | 8 + .../src/client/models/ListSecretsMetadata.ts | 10 + .../src/client/models/Location.ts | 18 + .../src/client/models/LocationID.ts | 8 + .../src/client/models/MemorySizeWithUnit.ts | 8 + .../models/ModifyApplicationBadRequest.ts | 13 + .../models/ModifyApplicationJobBadRequest.ts | 13 + .../models/ModifyApplicationJobRequest.ts | 10 + .../models/ModifyApplicationRequestBody.ts | 43 + .../models/ModifyDeploymentBadRequest.ts | 13 + .../models/ModifyDeploymentV2RequestBody.ts | 72 + .../src/client/models/ModifyMeRequestBody.ts | 21 + .../client/models/ModifySecretRequestBody.ts | 12 + .../ModifyUserDeploymentConfiguration.ts | 80 + .../src/client/models/Network.ts | 15 + .../src/client/models/NetworkMode.ts | 12 + .../src/client/models/NetworkParameters.ts | 21 + .../src/client/models/NodeGroup.ts | 11 + .../src/client/models/Observability.ts | 12 + .../src/client/models/ObservabilityLogs.ts | 10 + .../src/client/models/Placement.ts | 26 + .../src/client/models/PlacementEvent.ts | 20 + .../src/client/models/PlacementEvents.ts | 10 + .../src/client/models/PlacementID.ts | 8 + .../client/models/PlacementNotFoundError.ts | 7 + .../src/client/models/PlacementStatus.ts | 29 + .../client/models/PlacementStatusHealth.ts | 22 + .../src/client/models/PlacementWithEvents.ts | 28 + .../src/client/models/PlainTextSecretValue.ts | 8 + .../src/client/models/Port.ts | 35 + .../src/client/models/PortRange.ts | 14 + .../src/client/models/PortRangeAllocation.ts | 22 + .../client/models/ProvisionerConfiguration.ts | 23 + .../src/client/models/Ref.ts | 8 + .../src/client/models/Region.ts | 10 + .../models/ReplaceDeploymentRequestBody.ts | 10 + .../src/client/models/RolloutID.ts | 8 + .../src/client/models/RolloutStep.ts | 48 + .../src/client/models/RolloutStepRequest.ts | 19 + .../src/client/models/SSHPublicKey.ts | 8 + .../src/client/models/SSHPublicKeyID.ts | 8 + .../src/client/models/SSHPublicKeyItem.ts | 15 + .../models/SSHPublicKeyNotFoundError.ts | 16 + .../SchedulerDeploymentConfiguration.ts | 12 + .../src/client/models/SchedulingPolicy.ts | 14 + .../src/client/models/Secret.ts | 13 + .../src/client/models/SecretAccessType.ts | 10 + .../src/client/models/SecretMap.ts | 14 + .../src/client/models/SecretMetadata.ts | 15 + .../src/client/models/SecretName.ts | 8 + .../client/models/SecretNameAlreadyExists.ts | 25 + .../src/client/models/SecretNotFound.ts | 16 + .../src/client/models/SecretsStoreRef.ts | 17 + .../src/client/models/UnAuthorizedError.ts | 7 + .../src/client/models/UnixTimestamp.ts | 8 + .../src/client/models/UnknownAccount.ts | 7 + .../models/UpdateApplicationRolloutRequest.ts | 32 + .../client/models/UpdateRolloutResponse.ts | 20 + .../models/UserDeploymentConfiguration.ts | 80 + .../src/client/models/UserSSHPublicKey.ts | 13 + .../src/client/models/WranglerSSHConfig.ts | 11 + .../src/client/models/WranglerSSHResponse.ts | 8 + .../src/client/services/AccountService.ts | 50 + .../client/services/ApplicationsService.ts | 593 + .../src/client/services/DeploymentsService.ts | 310 + .../src/client/services/IPsService.ts | 45 + .../client/services/ImageRegistriesService.ts | 194 + .../src/client/services/JobsService.ts | 154 + .../src/client/services/PlacementsService.ts | 93 + .../src/client/services/RolloutsService.ts | 162 + .../src/client/services/SecretsService.ts | 126 + .../client/services/SshPublicKeysService.ts | 75 + packages/containers-shared/src/images.ts | 440 + packages/containers-shared/src/inspect.ts | 35 + packages/containers-shared/src/knobs.ts | 19 + packages/containers-shared/src/login.ts | 65 + packages/containers-shared/src/registry.ts | 13 + packages/containers-shared/src/types.ts | 103 + packages/containers-shared/src/utils.ts | 427 + .../containers-shared/tests/build.test.ts | 87 + .../tests/docker-context.test.ts | 60 + .../containers-shared/tests/images.test.ts | 296 + .../containers-shared/tests/knobs.test.ts | 17 + .../containers-shared/tests/login.test.ts | 33 + .../containers-shared/tests/request.test.ts | 34 + .../containers-shared/tests/tsconfig.json | 8 + .../containers-shared/tests/utils.test.ts | 222 + packages/containers-shared/tsconfig.json | 15 + packages/containers-shared/turbo.json | 5 + packages/containers-shared/vitest.config.mts | 11 + packages/create-cloudflare/.editorconfig | 12 + packages/create-cloudflare/.env.example | 12 + packages/create-cloudflare/.gitignore | 20 + packages/create-cloudflare/AGENTS.md | 31 + packages/create-cloudflare/CHANGELOG.md | 5834 ++++ packages/create-cloudflare/README.md | 10 + packages/create-cloudflare/bin/c3.js | 54 + packages/create-cloudflare/dns2.d.ts | 14 + packages/create-cloudflare/e2e/README.md | 98 + .../e2e/helpers/constants.ts | 49 + .../create-cloudflare/e2e/helpers/debuglog.ts | 3 + .../e2e/helpers/framework-helpers.ts | 554 + .../e2e/helpers/global-setup.ts | 16 + .../create-cloudflare/e2e/helpers/index.ts | 98 + .../e2e/helpers/log-stream.ts | 59 + .../create-cloudflare/e2e/helpers/run-c3.ts | 250 + .../create-cloudflare/e2e/helpers/spawn.ts | 140 + .../create-cloudflare/e2e/helpers/to-exist.ts | 28 + .../e2e/helpers/workers-helpers.ts | 144 + .../e2e/tests/cli/cli.test.ts | 955 + .../index.js | 5 + .../wrangler.jsonc | 8 + .../analog/src/server/routes/v1/test.ts | 8 + .../fixtures/astro/src/pages/test.astro | 8 + .../fixtures/nuxt/server/routes/test.ts | 8 + .../fixtures/qwik/src/routes/test/index.ts | 13 + .../svelte/src/routes/test/+server.ts | 9 + .../e2e/tests/frameworks/frameworks.test.ts | 188 + .../e2e/tests/frameworks/test-config.ts | 1090 + .../e2e/tests/workers/test-config.ts | 198 + .../e2e/tests/workers/workers.test.ts | 105 + packages/create-cloudflare/package.json | 101 + packages/create-cloudflare/scripts/build.ts | 53 + packages/create-cloudflare/scripts/common.ts | 68 + packages/create-cloudflare/scripts/deps.ts | 19 + .../scripts/e2e/run-tests.ts | 70 + .../src/__tests__/check-node.test.ts | 65 + .../src/__tests__/deploy.test.ts | 168 + .../src/__tests__/dialog.test.ts | 178 + .../src/__tests__/git.test.ts | 380 + .../src/__tests__/helpers.ts | 64 + .../src/__tests__/metrics.test.ts | 667 + .../src/__tests__/pre-existing.test.ts | 43 + .../src/__tests__/templates.test.ts | 299 + .../src/__tests__/validators.test.ts | 89 + .../src/__tests__/workers.test.ts | 171 + packages/create-cloudflare/src/agents-md.ts | 50 + packages/create-cloudflare/src/cli.ts | 265 + packages/create-cloudflare/src/deploy.ts | 194 + packages/create-cloudflare/src/dialog.ts | 118 + packages/create-cloudflare/src/event.ts | 525 + .../src/frameworks/__tests__/index.test.ts | 54 + .../create-cloudflare/src/frameworks/index.ts | 57 + .../src/frameworks/package.json | 27 + .../src/frameworks/tsconfig.json | 3 + packages/create-cloudflare/src/git.ts | 261 + packages/create-cloudflare/src/help.ts | 124 + .../src/helpers/__tests__/args.test.ts | 158 + .../src/helpers/__tests__/cli.test.ts | 100 + .../src/helpers/__tests__/command.test.ts | 103 + .../src/helpers/__tests__/compatDate.test.ts | 63 + .../src/helpers/__tests__/json.test.ts | 191 + .../src/helpers/__tests__/mocks.ts | 68 + .../helpers/__tests__/packageManagers.test.ts | 140 + .../src/helpers/__tests__/packages.test.ts | 224 + .../__tests__/pnpmBuildApprovals.test.ts | 356 + .../src/helpers/__tests__/retry.test.ts | 70 + .../create-cloudflare/src/helpers/args.ts | 469 + packages/create-cloudflare/src/helpers/cli.ts | 92 + .../create-cloudflare/src/helpers/codemod.ts | 36 + .../create-cloudflare/src/helpers/command.ts | 26 + .../src/helpers/compatDate.ts | 57 + .../create-cloudflare/src/helpers/files.ts | 145 + .../create-cloudflare/src/helpers/json.ts | 101 + .../src/helpers/metrics-config.ts | 82 + .../src/helpers/packageManagers.ts | 142 + .../create-cloudflare/src/helpers/packages.ts | 216 + .../src/helpers/pnpmBuildApprovals.ts | 221 + .../create-cloudflare/src/helpers/poll.ts | 139 + .../create-cloudflare/src/helpers/retry.ts | 44 + .../create-cloudflare/src/helpers/sparrow.ts | 41 + packages/create-cloudflare/src/metrics.ts | 325 + packages/create-cloudflare/src/pages.ts | 101 + packages/create-cloudflare/src/templates.ts | 1087 + packages/create-cloudflare/src/types.ts | 57 + packages/create-cloudflare/src/validators.ts | 126 + packages/create-cloudflare/src/workers.ts | 210 + .../src/wrangler/__tests__/accounts.test.ts | 190 + .../src/wrangler/__tests__/config.test.ts | 709 + .../src/wrangler/accounts.ts | 133 + .../create-cloudflare/src/wrangler/config.ts | 387 + packages/create-cloudflare/telemetry.md | 95 + .../create-cloudflare/templates/analog/c3.ts | 94 + .../templates/analog/templates/wrangler.jsonc | 12 + .../create-cloudflare/templates/angular/c3.ts | 9 + .../templates/angular/pages/c3.ts | 131 + .../angular/pages/templates/src/_routes.json | 5 + .../angular/pages/templates/src/server.ts | 19 + .../pages/templates/tools/copy-files.mjs | 15 + .../angular/pages/templates/wrangler.jsonc | 5 + .../templates/angular/workers/c3.ts | 127 + .../angular/workers/templates/src/server.ts | 19 + .../angular/workers/templates/wrangler.jsonc | 12 + .../create-cloudflare/templates/astro/c3.ts | 9 + .../templates/astro/pages/c3.ts | 112 + .../astro/pages/templates/js/wrangler.jsonc | 8 + .../astro/pages/templates/ts/src/env.d.ts | 5 + .../templates/ts/worker-configuration.d.ts | 4 + .../astro/pages/templates/ts/wrangler.jsonc | 8 + .../templates/astro/workers/c3.ts | 73 + .../workers/templates/js/public/.assetsignore | 2 + .../workers/templates/ts/public/.assetsignore | 2 + .../astro/workers/templates/ts/src/env.d.ts | 5 + .../create-cloudflare/templates/common/c3.ts | 23 + .../templates/common/js/.editorconfig | 12 + .../templates/common/js/.gitignore | 167 + .../templates/common/js/.prettierrc | 6 + .../templates/common/js/package.json | 14 + .../templates/common/js/src/ab-test.js | 41 + .../templates/common/js/src/index.js | 46 + .../templates/common/js/src/proxy.js | 23 + .../templates/common/js/src/redirect.js | 13 + .../templates/common/js/src/router.js | 22 + .../templates/common/js/wrangler.jsonc | 8 + .../templates/common/ts/.editorconfig | 12 + .../templates/common/ts/.gitignore | 164 + .../templates/common/ts/.prettierrc | 6 + .../templates/common/ts/package.json | 16 + .../templates/common/ts/src/ab-test.ts | 41 + .../templates/common/ts/src/index.ts | 49 + .../templates/common/ts/src/proxy.ts | 23 + .../templates/common/ts/src/redirect.ts | 13 + .../templates/common/ts/src/router.ts | 22 + .../templates/common/ts/tsconfig.json | 40 + .../templates/common/ts/wrangler.jsonc | 8 + .../templates/docusaurus/c3.ts | 9 + .../templates/docusaurus/pages/c3.ts | 32 + .../templates/docusaurus/workers/c3.ts | 34 + .../workers/templates/wrangler.jsonc | 10 + .../create-cloudflare/templates/gatsby/c3.ts | 9 + .../templates/gatsby/pages/c3.ts | 52 + .../templates/gatsby/workers/c3.ts | 54 + .../gatsby/workers/templates/wrangler.jsonc | 10 + .../templates/hello-world-assets-only/c3.ts | 16 + .../templates/.gitignore | 167 + .../templates/package.json | 13 + .../templates/public/index.html | 12 + .../templates/wrangler.jsonc | 11 + .../c3.ts | 26 + .../js/.editorconfig | 12 + .../js/.gitignore | 167 + .../js/.prettierrc | 6 + .../js/package.json | 13 + .../js/public/index.html | 20 + .../js/src/index.js | 68 + .../js/wrangler.jsonc | 28 + .../py/.gitignore | 68 + .../py/.python-version | 1 + .../py/README.md | 23 + .../py/package.json | 13 + .../py/public/index.html | 20 + .../py/pyproject.toml | 13 + .../py/src/entry.py | 65 + .../py/wrangler.jsonc | 31 + .../ts/.editorconfig | 12 + .../ts/.gitignore | 167 + .../ts/.prettierrc | 6 + .../ts/package.json | 15 + .../ts/public/index.html | 20 + .../ts/src/index.ts | 65 + .../ts/tsconfig.json | 40 + .../ts/wrangler.jsonc | 28 + .../hello-world-durable-object/c3.ts | 24 + .../js/.editorconfig | 12 + .../hello-world-durable-object/js/.gitignore | 167 + .../hello-world-durable-object/js/.prettierrc | 6 + .../js/package.json | 13 + .../js/src/index.js | 68 + .../js/wrangler.jsonc | 24 + .../hello-world-durable-object/py/.gitignore | 68 + .../py/.python-version | 1 + .../hello-world-durable-object/py/README.md | 23 + .../py/package.json | 13 + .../py/pyproject.toml | 13 + .../py/src/entry.py | 65 + .../py/wrangler.jsonc | 27 + .../ts/.editorconfig | 12 + .../hello-world-durable-object/ts/.gitignore | 167 + .../hello-world-durable-object/ts/.prettierrc | 6 + .../ts/package.json | 15 + .../ts/src/index.ts | 64 + .../ts/tsconfig.json | 40 + .../ts/wrangler.jsonc | 24 + .../templates/hello-world-with-assets/c3.ts | 25 + .../hello-world-with-assets/js/.editorconfig | 12 + .../hello-world-with-assets/js/.gitignore | 167 + .../hello-world-with-assets/js/.prettierrc | 6 + .../hello-world-with-assets/js/package.json | 16 + .../js/public/index.html | 32 + .../hello-world-with-assets/js/src/index.js | 23 + .../js/test/index.spec.js | 50 + .../js/vitest.config.js | 11 + .../hello-world-with-assets/js/wrangler.jsonc | 16 + .../hello-world-with-assets/py/.gitignore | 68 + .../py/.python-version | 1 + .../hello-world-with-assets/py/README.md | 23 + .../hello-world-with-assets/py/package.json | 13 + .../py/public/index.html | 32 + .../hello-world-with-assets/py/pyproject.toml | 13 + .../hello-world-with-assets/py/src/entry.py | 12 + .../hello-world-with-assets/py/wrangler.jsonc | 15 + .../hello-world-with-assets/ts/.editorconfig | 12 + .../hello-world-with-assets/ts/.gitignore | 167 + .../hello-world-with-assets/ts/.prettierrc | 6 + .../hello-world-with-assets/ts/package.json | 18 + .../ts/public/index.html | 32 + .../hello-world-with-assets/ts/src/index.ts | 26 + .../hello-world-with-assets/ts/test/env.d.ts | 3 + .../ts/test/index.spec.ts | 54 + .../ts/test/tsconfig.json | 8 + .../hello-world-with-assets/ts/tsconfig.json | 42 + .../ts/vitest.config.mts | 11 + .../hello-world-with-assets/ts/wrangler.jsonc | 15 + .../templates/hello-world-workflows/c3.ts | 20 + .../hello-world-workflows/js/package.json | 13 + .../hello-world-workflows/js/src/index.js | 129 + .../hello-world-workflows/js/wrangler.jsonc | 17 + .../hello-world-workflows/ts/package.json | 15 + .../hello-world-workflows/ts/src/index.ts | 111 + .../hello-world-workflows/ts/tsconfig.json | 40 + .../hello-world-workflows/ts/wrangler.jsonc | 17 + .../templates/hello-world/c3.ts | 23 + .../templates/hello-world/js/.editorconfig | 12 + .../templates/hello-world/js/.gitignore | 167 + .../templates/hello-world/js/.prettierrc | 6 + .../templates/hello-world/js/package.json | 16 + .../templates/hello-world/js/src/index.js | 15 + .../hello-world/js/test/index.spec.js | 25 + .../templates/hello-world/js/vitest.config.js | 11 + .../templates/hello-world/js/wrangler.jsonc | 8 + .../templates/hello-world/py/.gitignore | 68 + .../templates/hello-world/py/.python-version | 1 + .../templates/hello-world/py/README.md | 23 + .../templates/hello-world/py/package.json | 13 + .../templates/hello-world/py/pyproject.toml | 10 + .../templates/hello-world/py/src/entry.py | 5 + .../templates/hello-world/py/src/submodule.py | 2 + .../templates/hello-world/py/wrangler.jsonc | 9 + .../templates/hello-world/ts/.editorconfig | 12 + .../templates/hello-world/ts/.gitignore | 167 + .../templates/hello-world/ts/.prettierrc | 6 + .../templates/hello-world/ts/package.json | 18 + .../templates/hello-world/ts/src/index.ts | 18 + .../templates/hello-world/ts/test/env.d.ts | 3 + .../hello-world/ts/test/index.spec.ts | 29 + .../hello-world/ts/test/tsconfig.json | 8 + .../templates/hello-world/ts/tsconfig.json | 42 + .../hello-world/ts/vitest.config.mts | 11 + .../templates/hello-world/ts/wrangler.jsonc | 8 + .../create-cloudflare/templates/hono/c3.ts | 9 + .../templates/hono/pages/c3.ts | 47 + .../hono/pages/templates/src/index.tsx | 12 + .../hono/pages/templates/wrangler.jsonc | 8 + .../templates/hono/workers/c3.ts | 44 + .../hono/workers/templates/public/index.html | 19 + .../hono/workers/templates/src/index.ts | 9 + .../hono/workers/templates/wrangler.jsonc | 9 + .../create-cloudflare/templates/next/c3.ts | 32 + .../templates/next/experimental_c3.ts | 26 + .../create-cloudflare/templates/nuxt/c3.ts | 9 + .../templates/nuxt/pages/c3.ts | 123 + .../nuxt/pages/templates/_cloudflare/env.d.ts | 14 + .../nuxt/pages/templates/app/env.d.ts | 1 + .../nuxt/pages/templates/server/env.d.ts | 1 + .../nuxt/pages/templates/wrangler.jsonc | 5 + .../templates/nuxt/workers/c3.ts | 122 + .../workers/templates/_cloudflare/env.d.ts | 14 + .../nuxt/workers/templates/app/env.d.ts | 1 + .../nuxt/workers/templates/server/env.d.ts | 1 + .../nuxt/workers/templates/wrangler.jsonc | 12 + .../create-cloudflare/templates/openapi/c3.ts | 14 + .../templates/openapi/ts/.gitignore | 167 + .../templates/openapi/ts/README.md | 25 + .../templates/openapi/ts/package.json | 21 + .../openapi/ts/src/endpoints/taskCreate.ts | 57 + .../openapi/ts/src/endpoints/taskDelete.ts | 54 + .../openapi/ts/src/endpoints/taskFetch.ts | 55 + .../openapi/ts/src/endpoints/taskList.ts | 62 + .../templates/openapi/ts/src/index.ts | 26 + .../templates/openapi/ts/src/types.ts | 12 + .../templates/openapi/ts/tsconfig.json | 25 + .../templates/openapi/ts/wrangler.jsonc | 8 + .../templates/pre-existing/c3.ts | 120 + .../templates/pre-existing/js/.editorconfig | 12 + .../templates/pre-existing/js/.gitignore | 167 + .../templates/pre-existing/js/.prettierrc | 6 + .../templates/pre-existing/js/package.json | 13 + .../create-cloudflare/templates/queues/c3.ts | 32 + .../templates/queues/js/.editorconfig | 12 + .../templates/queues/js/.gitignore | 167 + .../templates/queues/js/.prettierrc | 6 + .../templates/queues/js/package.json | 13 + .../templates/queues/js/src/index.js | 38 + .../templates/queues/js/wrangler.jsonc | 21 + .../templates/queues/ts/.editorconfig | 12 + .../templates/queues/ts/.gitignore | 167 + .../templates/queues/ts/.prettierrc | 6 + .../templates/queues/ts/package.json | 15 + .../templates/queues/ts/src/index.ts | 41 + .../templates/queues/ts/tsconfig.json | 40 + .../templates/queues/ts/wrangler.jsonc | 21 + .../create-cloudflare/templates/qwik/c3.ts | 9 + .../templates/qwik/pages/c3.ts | 126 + .../qwik/pages/snippets/getPlatformProxy.ts | 6 + .../pages/templates/worker-configuration.d.ts | 4 + .../qwik/pages/templates/wrangler.jsonc | 8 + .../templates/qwik/workers/c3.ts | 125 + .../qwik/workers/snippets/getPlatformProxy.ts | 6 + .../workers/templates/public/.assetsignore | 4 + .../templates/worker-configuration.d.ts | 4 + .../qwik/workers/templates/wrangler.jsonc | 16 + .../templates/react-router/c3.ts | 82 + .../templates/react-router/ts/.gitignore | 14 + .../templates/react-router/ts/README.md | 79 + .../templates/react-router/ts/app/app.css | 15 + .../react-router/ts/app/entry.server.tsx | 42 + .../react-router/ts/app/routes/home.tsx | 19 + .../react-router/ts/app/welcome/welcome.tsx | 90 + .../react-router/ts/tsconfig.cloudflare.json | 27 + .../templates/react-router/ts/tsconfig.json | 14 + .../react-router/ts/tsconfig.node.json | 13 + .../templates/react-router/ts/vite.config.ts | 15 + .../templates/react-router/ts/workers/app.ts | 12 + .../templates/react-router/ts/wrangler.jsonc | 9 + .../create-cloudflare/templates/react/c3.ts | 9 + .../templates/react/pages/c3.ts | 84 + .../react/pages/templates/wrangler.jsonc | 5 + .../templates/react/workers/c3.ts | 207 + .../templates/react/workers/js/src/App.jsx | 148 + .../workers/js/src/assets/cloudflare.svg | 1 + .../react/workers/js/worker/index.js | 13 + .../templates/react/workers/js/wrangler.jsonc | 9 + .../templates/react/workers/ts/src/App.tsx | 148 + .../workers/ts/src/assets/cloudflare.svg | 1 + .../react/workers/ts/tsconfig.worker.json | 8 + .../react/workers/ts/worker/index.ts | 12 + .../templates/react/workers/ts/wrangler.jsonc | 9 + .../create-cloudflare/templates/redwood/c3.ts | 33 + .../templates/scheduled/c3.ts | 22 + .../templates/scheduled/js/.editorconfig | 12 + .../templates/scheduled/js/.gitignore | 167 + .../templates/scheduled/js/.prettierrc | 6 + .../templates/scheduled/js/package.json | 13 + .../templates/scheduled/js/src/index.js | 37 + .../templates/scheduled/js/wrangler.jsonc | 13 + .../templates/scheduled/ts/.editorconfig | 12 + .../templates/scheduled/ts/.gitignore | 167 + .../templates/scheduled/ts/.prettierrc | 6 + .../templates/scheduled/ts/package.json | 15 + .../templates/scheduled/ts/src/index.ts | 40 + .../templates/scheduled/ts/tsconfig.json | 40 + .../templates/scheduled/ts/wrangler.jsonc | 13 + .../create-cloudflare/templates/solid/c3.ts | 77 + .../templates/solid/templates/wrangler.jsonc | 10 + .../create-cloudflare/templates/svelte/c3.ts | 9 + .../templates/svelte/pages/c3.ts | 163 + .../svelte/pages/templates/wrangler.jsonc | 6 + .../templates/svelte/workers/c3.ts | 132 + .../workers/templates/static/.assetsignore | 2 + .../svelte/workers/templates/wrangler.jsonc | 13 + .../templates/tanstack-start/c3.ts | 45 + .../create-cloudflare/templates/vike/c3.ts | 33 + .../templates/vike/experimental-c3.ts | 30 + .../create-cloudflare/templates/vue/c3.ts | 9 + .../templates/vue/pages/c3.ts | 33 + .../vue/pages/templates/wrangler.jsonc | 5 + .../templates/vue/workers/c3.ts | 101 + .../templates/vue/workers/js/server/index.js | 13 + .../templates/vue/workers/js/src/App.vue | 107 + .../workers/js/src/components/HelloWorld.vue | 47 + .../templates/vue/workers/js/vite.config.js | 21 + .../templates/vue/workers/js/wrangler.jsonc | 11 + .../templates/vue/workers/ts/server/index.ts | 12 + .../templates/vue/workers/ts/src/App.vue | 107 + .../workers/ts/src/components/HelloWorld.vue | 44 + .../vue/workers/ts/tsconfig.worker.json | 8 + .../templates/vue/workers/ts/vite.config.ts | 21 + .../templates/vue/workers/ts/wrangler.jsonc | 11 + .../create-cloudflare/templates/waku/c3.ts | 78 + .../templates/waku/experimental-c3.ts | 30 + .../templates/waku/templates/public/404.html | 1 + .../templates/waku/templates/public/_headers | 4 + .../waku/templates/src/waku.server.tsx | 18 + .../templates/waku/templates/waku.config.ts | 41 + .../templates/waku/templates/wrangler.jsonc | 26 + packages/create-cloudflare/tsconfig.json | 32 + packages/create-cloudflare/turbo.json | 38 + .../create-cloudflare/vitest-e2e.config.mts | 18 + packages/create-cloudflare/vitest.config.mts | 15 + packages/create-cloudflare/vitest.setup.ts | 12 + packages/deploy-helpers/CHANGELOG.md | 190 + packages/deploy-helpers/package.json | 84 + packages/deploy-helpers/scripts/deps.ts | 28 + packages/deploy-helpers/src/config.ts | 18 + packages/deploy-helpers/src/deploy/deploy.ts | 765 + .../src/deploy/helpers/assets.ts | 576 + .../helpers/binding-depends-on-export.ts | 20 + .../src/deploy/helpers/binding-utils.ts | 400 + .../src/deploy/helpers/bundle-reporter.ts | 44 + .../src/deploy/helpers/capnp.ts | 39 + .../helpers/check-remote-secrets-override.ts | 173 + .../helpers/check-workflow-conflicts.ts | 91 + .../src/deploy/helpers/config-diffs.ts | 688 + .../confirm-latest-deployment-overwrite.ts | 102 + .../helpers/create-worker-upload-form.ts | 889 + .../src/deploy/helpers/deploy-confirm.ts | 23 + .../src/deploy/helpers/deploy-wfp.ts | 10 + .../src/deploy/helpers/diff-json.ts | 113 + .../deploy/helpers/download-worker-config.ts | 140 + .../src/deploy/helpers/durable.ts | 122 + .../src/deploy/helpers/environments.ts | 55 + .../src/deploy/helpers/error-codes.ts | 41 + .../deploy/helpers/exports-reconciliation.ts | 139 + .../src/deploy/helpers/exports.ts | 29 + .../helpers/friendly-validator-errors.ts | 171 + .../deploy-helpers/src/deploy/helpers/hash.ts | 13 + .../deploy/helpers/inconsistent-exports.ts | 25 + .../deploy-helpers/src/deploy/helpers/jwt.ts | 26 + .../src/deploy/helpers/match-tag.ts | 90 + .../src/deploy/helpers/node-compat.ts | 54 + .../deploy/helpers/package-dependencies.ts | 106 + .../src/deploy/helpers/parse-bulk-input.ts | 125 + .../src/deploy/helpers/placement.ts | 31 + .../src/deploy/helpers/preview-alias.ts | 107 + .../src/deploy/helpers/print-bindings.ts | 1076 + .../src/deploy/helpers/provision-bindings.ts | 1289 + .../src/deploy/helpers/secrets-validation.ts | 85 + .../src/deploy/helpers/sourcemap.ts | 348 + .../src/deploy/helpers/validate-routes.ts | 71 + .../deploy/helpers/validate-worker-props.ts | 309 + .../src/deploy/helpers/versions-api.ts | 187 + .../src/deploy/helpers/versions-types.ts | 55 + .../deploy/helpers/worker-not-found-error.ts | 31 + .../deploy/helpers/workers-sites-bindings.ts | 32 + .../src/deploy/versions-upload.ts | 434 + packages/deploy-helpers/src/index.ts | 50 + packages/deploy-helpers/src/shared/context.ts | 46 + packages/deploy-helpers/src/shared/types.ts | 169 + .../deploy-helpers/src/triggers/deploy.ts | 614 + .../src/triggers/publish-routes.ts | 355 + .../src/triggers/queue-consumers.ts | 448 + .../deploy-helpers/src/triggers/subdomain.ts | 218 + packages/deploy-helpers/src/triggers/zones.ts | 97 + packages/deploy-helpers/tests/index.test.ts | 26 + .../tests/package-dependencies.test.ts | 343 + .../deploy-helpers/tests/sourcemap.test.ts | 14 + packages/deploy-helpers/tests/tsconfig.json | 8 + packages/deploy-helpers/tsconfig.json | 10 + packages/deploy-helpers/tsup.config.ts | 43 + packages/deploy-helpers/turbo.json | 16 + packages/deploy-helpers/vitest.config.mts | 12 + packages/devprod-status-bot/CHANGELOG.md | 121 + packages/devprod-status-bot/package.json | 22 + packages/devprod-status-bot/src/index.ts | 866 + .../devprod-status-bot/src/status-bot.png | Bin 0 -> 890516 bytes packages/devprod-status-bot/tsconfig.json | 22 + .../worker-configuration.d.ts | 18 + packages/devprod-status-bot/wrangler.jsonc | 22 + .../CHANGELOG.md | 98 + .../README.md | 0 .../package.json | 28 + .../src/index.ts | 453 + .../tests/index.test.ts | 487 + .../tests/tsconfig.json | 22 + .../tsconfig.json | 18 + .../vitest.config.mts | 17 + .../worker-configuration.d.ts | 19 + .../wrangler.jsonc | 30 + packages/format-errors/CHANGELOG.md | 55 + packages/format-errors/README.md | 91 + .../format-errors/images/modules-worker.png | Bin 0 -> 169192 bytes .../format-errors/images/service-worker.png | Bin 0 -> 140634 bytes packages/format-errors/package.json | 28 + packages/format-errors/src/Stacktracey.js | 335 + packages/format-errors/src/Youch.js | 314 + .../format-errors/src/__tests__/index.test.ts | 101 + .../format-errors/src/error.compiled.mustache | 805 + packages/format-errors/src/impl/partition.js | 20 + packages/format-errors/src/index.ts | 199 + packages/format-errors/tsconfig.json | 33 + packages/format-errors/turbo.json | 9 + packages/format-errors/vitest.config.mts | 11 + packages/format-errors/wrangler.jsonc | 26 + packages/kv-asset-handler/CHANGELOG.md | 587 + packages/kv-asset-handler/README.md | 346 + packages/kv-asset-handler/package.json | 59 + packages/kv-asset-handler/src/index.ts | 353 + packages/kv-asset-handler/src/types.ts | 57 + .../test/getAssetFromKV-optional.test.ts | 22 + .../test/getAssetFromKV.test.ts | 574 + .../test/mapRequestToAsset.test.ts | 46 + packages/kv-asset-handler/test/mocks.ts | 158 + .../test/serveSinglePageApp.test.ts | 52 + packages/kv-asset-handler/tsconfig.json | 14 + packages/kv-asset-handler/tsup.config.ts | 20 + packages/kv-asset-handler/turbo.json | 10 + packages/kv-asset-handler/vitest.config.mts | 16 + packages/lint-config-shared/CHANGELOG.md | 30 + packages/lint-config-shared/oxlint-plugin.mjs | 13 + packages/lint-config-shared/package.json | 22 + .../__tests__/no-direct-recursive-rm.test.mjs | 129 + .../no-unsafe-command-execution.test.mjs | 192 + .../rules/no-direct-recursive-rm.mjs | 235 + .../rules/no-unsafe-command-execution.mjs | 268 + .../rules/no-wrangler-named-imports.mjs | 47 + .../require-description-when-disabling.mjs | 83 + packages/local-explorer-ui/.gitignore | 2 + packages/local-explorer-ui/CHANGELOG.md | 255 + packages/local-explorer-ui/README.md | 45 + packages/local-explorer-ui/index.html | 13 + .../local-explorer-ui/openapi-ts.config.ts | 14 + packages/local-explorer-ui/package.json | 63 + packages/local-explorer-ui/public/favicon.svg | 3 + .../src/__e2e__/d1/d1-database.spec.ts | 270 + .../src/__e2e__/do/durable-object.spec.ts | 179 + .../src/__e2e__/global-setup.ts | 71 + .../src/__e2e__/kv/kv-namespace.spec.ts | 266 + .../src/__e2e__/r2/r2-bucket.spec.ts | 753 + .../local-explorer-ui/src/__e2e__/setup.ts | 49 + .../local-explorer-ui/src/__e2e__/utils.ts | 483 + .../src/__e2e__/workflows/workflow.spec.ts | 449 + .../src/__tests__/drivers/common.test.ts | 526 + .../__tests__/drivers/sqlite/driver.test.ts | 206 + .../__tests__/drivers/sqlite/generate.test.ts | 612 + .../__tests__/drivers/sqlite/parsers.test.ts | 338 + .../src/__tests__/utils/agent-prompt.test.ts | 39 + .../src/__tests__/utils/format.test.ts | 48 + .../src/__tests__/utils/kv-validation.test.ts | 42 + .../src/__tests__/utils/sidebar-state.test.ts | 312 + .../src/__tests__/utils/studio.test.ts | 337 + .../src/__tests__/utils/theme-state.test.ts | 157 + .../__tests__/workflows/step-helpers.test.ts | 108 + .../src/api/client-config.ts | 7 + packages/local-explorer-ui/src/api/index.ts | 10 + .../local-explorer-ui/src/assets/icons/d1.svg | 3 + .../src/assets/icons/durable-objects.svg | 3 + .../local-explorer-ui/src/assets/icons/kv.svg | 4 + .../local-explorer-ui/src/assets/icons/r2.svg | 3 + .../src/assets/icons/workflows.svg | 3 + .../src/components/AddKVForm.tsx | 110 + .../src/components/AnimatedCloudflareLogo.tsx | 59 + .../src/components/Breadcrumbs.tsx | 35 + .../src/components/CopyButton.tsx | 34 + .../src/components/KVTable.tsx | 262 + .../src/components/NotFound.tsx | 20 + .../src/components/R2ObjectTable.tsx | 432 + .../src/components/R2UploadDialog.tsx | 350 + .../src/components/ResourceError.tsx | 30 + .../src/components/SearchForm.tsx | 35 + .../src/components/Sidebar.tsx | 321 + .../src/components/SidebarGroupPopup.tsx | 81 + .../src/components/TableSelect.tsx | 82 + .../src/components/WorkerSelector.tsx | 163 + .../src/components/studio/Code/Block.tsx | 23 + .../src/components/studio/Code/Mirror.tsx | 180 + .../src/components/studio/Context.tsx | 58 + .../src/components/studio/ContextMenu.tsx | 193 + .../studio/Explain/SQLiteExplainTab.tsx | 209 + .../src/components/studio/HighlightText.tsx | 56 + .../src/components/studio/Modal.tsx | 109 + .../studio/Modal/CommitConfirmation.tsx | 91 + .../studio/Modal/DeleteConfirmation.tsx | 113 + .../studio/Modal/DropTableConfirmation.tsx | 107 + .../studio/Modal/Export/OptionsEditor.tsx | 294 + .../components/studio/Modal/Export/index.tsx | 109 + .../components/studio/QueryResult/Stats.tsx | 85 + .../components/studio/QueryResult/Summary.tsx | 101 + .../src/components/studio/QueryResult/Tab.tsx | 67 + .../studio/SQLEditor/SQLThemePlugin.tsx | 93 + .../studio/SQLEditor/SQLiteDialect.ts | 291 + .../SQLiteTableNameHighlightPlugin.tsx | 61 + .../SQLEditor/StatementHighlightExtension.ts | 269 + .../src/components/studio/SQLEditor/index.tsx | 159 + .../src/components/studio/SQLWhereEditor.tsx | 88 + .../src/components/studio/SkeletonBlock.tsx | 40 + .../src/components/studio/TabRegister.tsx | 79 + .../studio/Table/ActionsDropdown.tsx | 90 + .../src/components/studio/Table/BaseTable.tsx | 383 + .../src/components/studio/Table/Cell.tsx | 93 + .../components/studio/Table/FakePadding.tsx | 101 + .../components/studio/Table/HeaderList.tsx | 81 + .../components/studio/Table/HeaderResizer.tsx | 99 + .../studio/Table/Result/DisplayCell.tsx | 219 + .../studio/Table/Result/EditableCell.tsx | 298 + .../Table/Result/ResultTableContextMenu.tsx | 190 + .../components/studio/Table/Result/index.tsx | 349 + .../studio/Table/SchemaEditor/Column.tsx | 507 + .../SchemaEditor/ConstraintListEditor.tsx | 202 + .../studio/Table/SchemaEditor/index.tsx | 272 + .../components/studio/Table/State/Helpers.tsx | 360 + .../components/studio/Table/State/index.tsx | 872 + .../src/components/studio/Table/index.tsx | 233 + .../studio/Table/useVisibilityCalculation.ts | 174 + .../studio/Tabs/CreateUpdateTable.tsx | 210 + .../src/components/studio/Tabs/Query.tsx | 342 + .../components/studio/Tabs/TableExplorer.tsx | 536 + .../components/studio/WhereFilterInput.tsx | 169 + .../studio/WindowTab/ContentWrapper.tsx | 85 + .../components/studio/WindowTab/Context.tsx | 21 + .../studio/WindowTab/ItemRenderer.tsx | 75 + .../components/studio/WindowTab/MenuProps.tsx | 20 + .../src/components/studio/WindowTab/Pane.tsx | 42 + .../src/components/studio/WindowTab/index.tsx | 113 + .../src/components/studio/WindowTab/types.ts | 15 + .../src/components/studio/index.tsx | 537 + .../src/components/workflows/CopyButton.tsx | 33 + .../workflows/CreateInstanceDialog.tsx | 185 + .../workflows/ScrollableCodeBlock.tsx | 13 + .../src/components/workflows/StatusBadge.tsx | 52 + .../src/components/workflows/StatusIcon.tsx | 66 + .../src/components/workflows/StepRow.tsx | 315 + .../src/components/workflows/Timestamp.tsx | 61 + .../src/components/workflows/helpers.ts | 66 + .../src/components/workflows/types.ts | 89 + .../local-explorer-ui/src/drivers/common.ts | 570 + packages/local-explorer-ui/src/drivers/d1.ts | 134 + packages/local-explorer-ui/src/drivers/do.ts | 160 + .../src/drivers/sqlite/generate.ts | 263 + .../src/drivers/sqlite/index.ts | 493 + .../src/drivers/sqlite/parsers.ts | 920 + .../src/hooks/leave-guard.ts | 66 + packages/local-explorer-ui/src/main.tsx | 33 + .../local-explorer-ui/src/routeTree.gen.ts | 321 + .../local-explorer-ui/src/routes/__root.tsx | 110 + .../src/routes/d1/$databaseId.tsx | 259 + .../src/routes/do/$className.tsx | 33 + .../src/routes/do/$className/$objectId.tsx | 288 + .../src/routes/do/$className/index.tsx | 261 + .../local-explorer-ui/src/routes/index.tsx | 80 + .../src/routes/kv/$namespaceId.tsx | 550 + .../src/routes/r2/$bucketName.tsx | 10 + .../src/routes/r2/$bucketName/index.tsx | 562 + .../src/routes/r2/$bucketName/object.$.tsx | 317 + .../src/routes/workflows/$workflowName.tsx | 15 + .../workflows/$workflowName/$instanceId.tsx | 848 + .../routes/workflows/$workflowName/index.tsx | 907 + .../local-explorer-ui/src/styles/tailwind.css | 89 + .../local-explorer-ui/src/types/studio.ts | 417 + .../src/utils/agent-prompt.ts | 32 + packages/local-explorer-ui/src/utils/async.ts | 20 + .../local-explorer-ui/src/utils/format.ts | 47 + .../local-explorer-ui/src/utils/is-equal.ts | 51 + .../src/utils/kv-validation.ts | 16 + .../src/utils/sidebar-state.ts | 112 + .../src/utils/studio/commit.ts | 124 + .../src/utils/studio/export.ts | 288 + .../src/utils/studio/formatter.ts | 384 + .../src/utils/studio/index.ts | 218 + .../local-explorer-ui/src/utils/studio/sql.ts | 134 + .../src/utils/studio/where-parser.ts | 355 + .../src/utils/theme-state.ts | 101 + packages/local-explorer-ui/src/vite-env.d.ts | 7 + .../local-explorer-ui/tsconfig.client.json | 9 + packages/local-explorer-ui/tsconfig.e2e.json | 15 + packages/local-explorer-ui/tsconfig.json | 8 + packages/local-explorer-ui/tsconfig.node.json | 9 + packages/local-explorer-ui/turbo.json | 16 + packages/local-explorer-ui/vite.config.ts | 47 + .../local-explorer-ui/vitest.config.e2e.ts | 14 + packages/local-explorer-ui/vitest.config.mts | 11 + packages/miniflare/AGENTS.md | 61 + packages/miniflare/CHANGELOG.md | 4863 +++ packages/miniflare/CONTRIBUTING.md | 344 + packages/miniflare/README.md | 957 + packages/miniflare/bootstrap.js | 11 + packages/miniflare/miniflare.drawio.svg | 4 + packages/miniflare/openapi-ts.config.ts | 31 + packages/miniflare/package.json | 118 + packages/miniflare/scripts/build.mjs | 399 + .../miniflare/scripts/check-generate-api.ts | 76 + packages/miniflare/scripts/common.mjs | 29 + packages/miniflare/scripts/deps.ts | 29 + packages/miniflare/scripts/filter-openapi.ts | 640 + .../scripts/openapi-filter-config.ts | 1728 + packages/miniflare/scripts/types.mjs | 254 + packages/miniflare/src/cf.ts | 222 + packages/miniflare/src/exit-hook.ts | 96 + packages/miniflare/src/http/cert.ts | 95 + packages/miniflare/src/http/fetch.ts | 290 + packages/miniflare/src/http/helpers.ts | 18 + packages/miniflare/src/http/index.ts | 21 + packages/miniflare/src/http/request.ts | 51 + packages/miniflare/src/http/response.ts | 80 + packages/miniflare/src/http/server.ts | 49 + packages/miniflare/src/http/websocket.ts | 296 + packages/miniflare/src/index.ts | 3416 ++ packages/miniflare/src/merge.ts | 110 + .../src/plugins/agent-memory/index.ts | 72 + .../miniflare/src/plugins/ai-search/index.ts | 122 + packages/miniflare/src/plugins/ai/index.ts | 78 + .../src/plugins/analytics-engine/index.ts | 83 + .../miniflare/src/plugins/artifacts/index.ts | 68 + .../miniflare/src/plugins/assets/constants.ts | 5 + .../miniflare/src/plugins/assets/index.ts | 489 + .../miniflare/src/plugins/assets/schema.ts | 26 + .../browser-rendering/browser-version.ts | 14 + .../plugins/browser-rendering/constants.ts | 1 + .../src/plugins/browser-rendering/index.ts | 369 + packages/miniflare/src/plugins/cache/index.ts | 159 + .../miniflare/src/plugins/core/constants.ts | 76 + .../src/plugins/core/errors/callsite.ts | 171 + .../src/plugins/core/errors/index.ts | 392 + .../src/plugins/core/errors/sourcemap.ts | 103 + .../miniflare/src/plugins/core/explorer.ts | 412 + packages/miniflare/src/plugins/core/index.ts | 1482 + .../plugins/core/inspector-proxy/devtools.ts | 52 + .../src/plugins/core/inspector-proxy/index.ts | 1 + .../inspector-proxy-controller.ts | 357 + .../core/inspector-proxy/inspector-proxy.ts | 169 + .../src/plugins/core/module-fallback.ts | 116 + .../miniflare/src/plugins/core/modules.ts | 462 + .../miniflare/src/plugins/core/node-compat.ts | 69 + .../src/plugins/core/proxy/README.md | 49 + .../src/plugins/core/proxy/client.ts | 744 + .../src/plugins/core/proxy/fetch-sync.ts | 171 + .../miniflare/src/plugins/core/proxy/index.ts | 2 + .../miniflare/src/plugins/core/proxy/types.ts | 337 + .../miniflare/src/plugins/core/services.ts | 110 + packages/miniflare/src/plugins/core/types.ts | 56 + packages/miniflare/src/plugins/d1/index.ts | 193 + .../src/plugins/dispatch-namespace/index.ts | 114 + packages/miniflare/src/plugins/do/index.ts | 185 + packages/miniflare/src/plugins/email/index.ts | 148 + .../miniflare/src/plugins/flagship/index.ts | 73 + .../src/plugins/hello-world/index.ts | 162 + .../plugins/hyperdrive/hyperdrive-proxy.ts | 531 + .../miniflare/src/plugins/hyperdrive/index.ts | 210 + .../miniflare/src/plugins/images/fetcher.ts | 443 + .../miniflare/src/plugins/images/index.ts | 195 + packages/miniflare/src/plugins/index.ts | 251 + .../miniflare/src/plugins/kv/constants.ts | 1 + packages/miniflare/src/plugins/kv/index.ts | 204 + packages/miniflare/src/plugins/kv/sites.ts | 146 + packages/miniflare/src/plugins/media/index.ts | 70 + packages/miniflare/src/plugins/mtls/index.ts | 75 + .../miniflare/src/plugins/pipelines/index.ts | 128 + .../miniflare/src/plugins/queues/errors.ts | 6 + .../miniflare/src/plugins/queues/index.ts | 181 + packages/miniflare/src/plugins/r2/index.ts | 196 + .../miniflare/src/plugins/ratelimit/index.ts | 171 + .../src/plugins/secret-store/index.ts | 188 + .../miniflare/src/plugins/shared/constants.ts | 133 + .../miniflare/src/plugins/shared/index.ts | 385 + .../miniflare/src/plugins/shared/routing.ts | 94 + .../miniflare/src/plugins/stream/index.ts | 193 + .../miniflare/src/plugins/vectorize/index.ts | 94 + .../src/plugins/version-metadata/index.ts | 49 + .../src/plugins/vpc-networks/index.ts | 86 + .../src/plugins/vpc-services/index.ts | 75 + .../miniflare/src/plugins/websearch/index.ts | 82 + .../src/plugins/worker-loader/index.ts | 32 + .../miniflare/src/plugins/workflows/index.ts | 216 + .../src/runtime/config/generated/workerd.ts | 4276 +++ .../miniflare/src/runtime/config/index.ts | 63 + .../miniflare/src/runtime/config/workerd.ts | 265 + packages/miniflare/src/runtime/index.ts | 405 + .../miniflare/src/runtime/structured-logs.ts | 198 + packages/miniflare/src/shared/DEV_REGISTRY.md | 141 + packages/miniflare/src/shared/colour.ts | 10 + packages/miniflare/src/shared/dev-control.ts | 22 + .../src/shared/dev-registry-types.ts | 27 + packages/miniflare/src/shared/dev-registry.ts | 291 + packages/miniflare/src/shared/error.ts | 66 + packages/miniflare/src/shared/event.ts | 37 + .../miniflare/src/shared/external-service.ts | 43 + packages/miniflare/src/shared/index.ts | 7 + packages/miniflare/src/shared/log.ts | 193 + packages/miniflare/src/shared/matcher.ts | 33 + packages/miniflare/src/shared/mime-types.ts | 60 + packages/miniflare/src/shared/streams.ts | 61 + packages/miniflare/src/shared/types.ts | 60 + packages/miniflare/src/workers/README.md | 9 + .../analytics-engine.worker.ts | 13 + .../src/workers/assets/assets-kv.worker.ts | 48 + .../src/workers/assets/assets.worker.ts | 8 + .../src/workers/assets/router.worker.ts | 6 + .../src/workers/assets/rpc-proxy.worker.ts | 87 + .../browser-rendering/binding.worker.ts | 693 + .../miniflare/src/workers/cache/README.md | 57 + .../workers/cache/cache-entry-noop.worker.ts | 20 + .../src/workers/cache/cache-entry.worker.ts | 28 + .../src/workers/cache/cache.worker.ts | 401 + .../miniflare/src/workers/cache/constants.ts | 12 + .../src/workers/cache/errors.worker.ts | 56 + packages/miniflare/src/workers/cache/index.ts | 1 + .../miniflare/src/workers/core/constants.ts | 158 + .../src/workers/core/dev-control.worker.ts | 61 + .../core/dev-registry-proxy-shared.worker.ts | 226 + .../workers/core/dev-registry-proxy.worker.ts | 188 + .../miniflare/src/workers/core/devalue.ts | 367 + .../src/workers/core/do-wrapper.worker.ts | 121 + packages/miniflare/src/workers/core/email.ts | 239 + .../src/workers/core/entry.worker.ts | 634 + packages/miniflare/src/workers/core/http.ts | 65 + packages/miniflare/src/workers/core/index.ts | 3 + .../src/workers/core/outbound.worker.ts | 121 + .../src/workers/core/proxy.worker.ts | 405 + .../miniflare/src/workers/core/routing.ts | 34 + .../miniflare/src/workers/core/scheduled.ts | 23 + .../src/workers/d1/database.worker.ts | 258 + packages/miniflare/src/workers/d1/dumpSql.ts | 257 + .../dispatch-namespace-proxy.worker.ts | 33 + .../dispatch-namespace.worker.ts | 21 + .../miniflare/src/workers/email/constants.ts | 1 + .../src/workers/email/email.worker.ts | 34 + .../src/workers/email/send_email.worker.ts | 301 + packages/miniflare/src/workers/email/types.ts | 35 + .../miniflare/src/workers/email/validate.ts | 137 + .../src/workers/hello-world/binding.worker.ts | 32 + .../src/workers/hello-world/object.worker.ts | 11 + .../src/workers/images/images.worker.ts | 284 + packages/miniflare/src/workers/index.ts | 5 + .../miniflare/src/workers/kv/constants.ts | 137 + packages/miniflare/src/workers/kv/index.ts | 1 + .../src/workers/kv/namespace.worker.ts | 315 + .../miniflare/src/workers/kv/sites.worker.ts | 174 + .../src/workers/kv/validator.worker.ts | 152 + .../src/workers/local-explorer/aggregation.ts | 127 + .../src/workers/local-explorer/common.ts | 182 + .../workers/local-explorer/explorer.worker.ts | 403 + .../workers/local-explorer/generated/index.ts | 217 + .../local-explorer/generated/types.gen.ts | 1822 + .../local-explorer/generated/zod.gen.ts | 1129 + .../workers/local-explorer/openapi.local.json | 3290 ++ .../workers/local-explorer/resources/d1.ts | 268 + .../workers/local-explorer/resources/do.ts | 388 + .../workers/local-explorer/resources/kv.ts | 442 + .../workers/local-explorer/resources/r2.ts | 434 + .../local-explorer/resources/workflows.ts | 1081 + .../src/workers/local-explorer/route-names.ts | 49 + .../src/workers/local-explorer/telemetry.ts | 111 + packages/miniflare/src/workers/node.d.ts | 158 + .../src/workers/pipelines/pipeline.worker.ts | 5 + .../src/workers/queues/broker.worker.ts | 550 + .../miniflare/src/workers/queues/constants.ts | 25 + .../miniflare/src/workers/queues/index.ts | 2 + .../miniflare/src/workers/queues/schemas.ts | 78 + .../miniflare/src/workers/r2/bucket.worker.ts | 1092 + .../miniflare/src/workers/r2/constants.ts | 15 + .../miniflare/src/workers/r2/errors.worker.ts | 218 + .../miniflare/src/workers/r2/public.worker.ts | 172 + .../src/workers/r2/r2Object.worker.ts | 125 + .../src/workers/r2/schemas.worker.ts | 330 + .../src/workers/r2/validator.worker.ts | 198 + .../ratelimit/ratelimit-object.worker.ts | 39 + .../src/workers/ratelimit/ratelimit.worker.ts | 81 + .../src/workers/secrets-store/constants.ts | 1 + .../workers/secrets-store/secret.worker.ts | 81 + .../src/workers/shared/blob.worker.ts | 250 + .../miniflare/src/workers/shared/constants.ts | 21 + packages/miniflare/src/workers/shared/data.ts | 55 + .../miniflare/src/workers/shared/index.ts | 6 + .../src/workers/shared/index.worker.ts | 69 + .../src/workers/shared/keyvalue.worker.ts | 293 + .../miniflare/src/workers/shared/matcher.ts | 13 + .../src/workers/shared/object-entry.worker.ts | 18 + .../src/workers/shared/object.worker.ts | 183 + .../src/workers/shared/public-url.ts | 15 + .../miniflare/src/workers/shared/range.ts | 68 + .../workers/shared/remote-bindings-utils.ts | 219 + .../shared/remote-proxy-client.worker.ts | 47 + .../src/workers/shared/router.worker.ts | 97 + .../src/workers/shared/sql.worker.ts | 128 + packages/miniflare/src/workers/shared/sync.ts | 101 + .../src/workers/shared/timers.worker.ts | 96 + .../miniflare/src/workers/shared/types.ts | 27 + .../src/workers/shared/zod.worker.ts | 16 + .../src/workers/stream/binding.worker.ts | 287 + .../miniflare/src/workers/stream/errors.ts | 31 + .../src/workers/stream/object.worker.ts | 688 + .../miniflare/src/workers/stream/schemas.ts | 208 + packages/miniflare/src/workers/tsconfig.json | 19 + .../miniflare/src/workers/workerd-unsafe.d.ts | 14 + .../src/workers/workflows/binding.worker.ts | 6 + .../workflows/wrapped-binding.worker.ts | 146 + packages/miniflare/src/zod-format.ts | 538 + packages/miniflare/test/cf.spec.ts | 139 + packages/miniflare/test/dev-registry.spec.ts | 1790 + .../test/fixtures/cache/parse-http.ts | 153 + .../test/fixtures/crashing-workerd.mjs | 14 + packages/miniflare/test/fixtures/d1/README.md | 13 + .../test/fixtures/d1/worker.dist.mjs | 429 + .../miniflare/test/fixtures/d1/worker.mjs | 94 + .../miniflare/test/fixtures/d1/wrangler.jsonc | 12 + .../test/fixtures/little-workerd.mjs | 21 + .../migrations/3.20230821.0/README.md | 42 + .../3.20230821.0/d1/DATABASE/db.sqlite | Bin 0 -> 12288 bytes ...5c1a39c5baea36f40e912e81e6000637434f5e7544 | 1 + .../3.20230821.0/kv/NAMESPACE/db.sqlite | Bin 0 -> 16384 bytes ...2966feb09ac09ec3cc4b27dcff000637435043236c | 1 + .../3.20230821.0/r2/BUCKET/db.sqlite | Bin 0 -> 28672 bytes .../miniflare/test/fixtures/modules/add.wasm | Bin 0 -> 57 bytes .../test/fixtures/modules/blobs-indirect.mjs | 2 + .../miniflare/test/fixtures/modules/blobs.mjs | 3 + .../test/fixtures/modules/blobs/data.bin | 1 + .../test/fixtures/modules/blobs/text.txt | 1 + .../test/fixtures/modules/cyclic/cyclic1.mjs | 7 + .../test/fixtures/modules/cyclic/cyclic2.mjs | 5 + .../test/fixtures/modules/cyclic/index.mjs | 7 + .../test/fixtures/modules/dynamic-require.cjs | 2 + .../test/fixtures/modules/index-dynamic.mjs | 22 + .../miniflare/test/fixtures/modules/index.cjs | 7 + .../miniflare/test/fixtures/modules/index.mjs | 21 + .../test/fixtures/modules/index.node.cjs | 15 + .../miniflare/test/fixtures/r2/validator.ts | 175 + .../miniflare/test/fixtures/shared/router.ts | 78 + .../test/fixtures/sites/manifest.d.ts | 4 + .../miniflare/test/fixtures/sites/modules.ts | 35 + .../test/fixtures/sites/service-worker.ts | 9 + .../source-maps/inline-sourcemap-worker.js | 5 + .../test/fixtures/source-maps/modules.ts | 11 + .../test/fixtures/source-maps/nested/dep.ts | 9 + .../test/fixtures/source-maps/reduce.ts | 7 + .../fixtures/source-maps/service-worker.ts | 13 + .../miniflare/test/fixtures/tsconfig.json | 19 + .../fixtures/unsafe-plugin-bad/no-export.cjs | 4 + .../unsafe-plugin-bad/not-function.cjs | 2 + .../test/fixtures/unsafe-plugin/index.ts | 165 + .../test/fixtures/unsafe-plugin/tsconfig.json | 4 + .../miniflare/test/fixtures/worker-test.ts | 34 + packages/miniflare/test/http/fetch.spec.ts | 239 + packages/miniflare/test/http/request.spec.ts | 22 + packages/miniflare/test/http/response.spec.ts | 60 + .../miniflare/test/http/websocket.spec.ts | 487 + packages/miniflare/test/index.spec.ts | 4219 +++ packages/miniflare/test/logs.spec.ts | 270 + packages/miniflare/test/merge.spec.ts | 132 + .../test/plugins/assets/index.spec.ts | 100 + .../test/plugins/browser/index.spec.ts | 1065 + .../test/plugins/cache/index.spec.ts | 600 + .../test/plugins/cache/parse-http.spec.ts | 6 + .../test/plugins/core/cf-image.spec.ts | 170 + .../test/plugins/core/errors/index.spec.ts | 481 + .../miniflare/test/plugins/core/index.spec.ts | 95 + .../core/inspector-proxy/index.spec.ts | 791 + .../test/plugins/core/modules.spec.ts | 420 + .../test/plugins/core/proxy/client.spec.ts | 388 + .../miniflare/test/plugins/d1/index.spec.ts | 33 + .../d1/index.with-wrangler-shim.spec.ts | 96 + packages/miniflare/test/plugins/d1/suite.ts | 737 + packages/miniflare/test/plugins/d1/test.ts | 35 + .../miniflare/test/plugins/do/index.spec.ts | 737 + .../test/plugins/email/index.spec.ts | 2039 ++ .../test/plugins/flagship/index.spec.ts | 32 + .../test/plugins/hello-world/index.spec.ts | 57 + .../test/plugins/hyperdrive/index.spec.ts | 340 + .../test/plugins/hyperdrive/proxy.spec.ts | 672 + .../test/plugins/images/index.spec.ts | 301 + .../miniflare/test/plugins/kv/index.spec.ts | 872 + .../miniflare/test/plugins/kv/sites.spec.ts | 256 + .../local-explorer/aggregation.spec.ts | 689 + .../test/plugins/local-explorer/d1.spec.ts | 343 + .../plugins/local-explorer/do-wrapper.spec.ts | 114 + .../test/plugins/local-explorer/do.spec.ts | 905 + .../test/plugins/local-explorer/helpers.ts | 29 + .../test/plugins/local-explorer/index.spec.ts | 744 + .../test/plugins/local-explorer/kv.spec.ts | 516 + .../test/plugins/local-explorer/r2.spec.ts | 397 + .../plugins/local-explorer/telemetry.spec.ts | 38 + .../test/plugins/pipelines/index.spec.ts | 20 + .../test/plugins/queues/cross-process.spec.ts | 211 + .../test/plugins/queues/delay.spec.ts | 186 + .../test/plugins/queues/index.spec.ts | 1075 + .../test/plugins/queues/retry.spec.ts | 120 + .../miniflare/test/plugins/r2/index.spec.ts | 1631 + .../miniflare/test/plugins/r2/public.spec.ts | 701 + .../test/plugins/r2/validator.spec.ts | 6 + .../test/plugins/ratelimit/index.spec.ts | 162 + .../test/plugins/secret-store/index.spec.ts | 119 + .../test/plugins/shared/range.spec.ts | 81 + .../remote-bindings-access-warning.spec.ts | 211 + .../test/plugins/shared/router.spec.ts | 6 + .../test/plugins/shared/routing.spec.ts | 166 + .../test/plugins/stream/index.spec.ts | 1681 + .../test/plugins/unsafe/index.spec.ts | 132 + .../plugins/version-metadata/index.spec.ts | 96 + .../test/plugins/workflows/index.spec.ts | 331 + packages/miniflare/test/public-url.spec.ts | 61 + packages/miniflare/test/setup.mjs | 42 + packages/miniflare/test/shared/blob.spec.ts | 257 + .../miniflare/test/shared/matcher.spec.ts | 62 + packages/miniflare/test/shared/sync.spec.ts | 140 + packages/miniflare/test/shared/types.spec.ts | 25 + .../test/snapshots/zod-format.spec.ts.md | 307 + .../test/snapshots/zod-format.spec.ts.snap | Bin 0 -> 1517 bytes packages/miniflare/test/sourcemap.spec.ts | 31 + packages/miniflare/test/test-shared/http.ts | 34 + packages/miniflare/test/test-shared/index.ts | 7 + packages/miniflare/test/test-shared/log.ts | 31 + .../miniflare/test/test-shared/miniflare.ts | 229 + packages/miniflare/test/test-shared/object.ts | 43 + .../miniflare/test/test-shared/registry.ts | 24 + .../miniflare/test/test-shared/storage.ts | 87 + .../miniflare/test/test-shared/worker-test.ts | 60 + .../test/workers/core/serialize.spec.ts | 68 + packages/miniflare/test/zod-format.spec.ts | 458 + packages/miniflare/tsconfig.bundle.json | 9 + packages/miniflare/tsconfig.json | 37 + packages/miniflare/turbo.json | 15 + packages/miniflare/types/env.d.ts | 10 + packages/miniflare/types/events.d.ts | 38 + packages/miniflare/types/streams.d.ts | 40 + packages/miniflare/types/webassembly.d.ts | 106 + packages/miniflare/types/worker-shim.d.ts | 3 + packages/miniflare/types/workerd.d.ts | 5 + packages/miniflare/vitest.config.mts | 31 + packages/mock-npm-registry/README.md | 35 + packages/mock-npm-registry/package.json | 28 + packages/mock-npm-registry/src/index.ts | 330 + packages/mock-npm-registry/tsconfig.json | 15 + packages/mock-npm-registry/turbo.json | 9 + packages/pages-shared/CHANGELOG.md | 1896 ++ packages/pages-shared/README.md | 9 + .../__tests__/asset-server/handler.test.ts | 1528 + .../__tests__/asset-server/responses.test.ts | 66 + .../createMetadataObject.test.ts | 321 + .../metadata-generator/fixtures/_redirects | 6 + packages/pages-shared/asset-server/handler.ts | 854 + .../pages-shared/asset-server/metadata.ts | 67 + .../pages-shared/asset-server/patchUrl.ts | 21 + .../pages-shared/asset-server/responses.ts | 195 + .../environment-polyfills/html-rewriter.ts | 81 + .../environment-polyfills/index.ts | 14 + .../environment-polyfills/miniflare.ts | 13 + .../environment-polyfills/types.ts | 21 + .../metadata-generator/constants.ts | 15 + .../createMetadataObject.ts | 65 + .../pages-shared/metadata-generator/types.ts | 53 + packages/pages-shared/package.json | 43 + packages/pages-shared/scripts/build.ts | 55 + packages/pages-shared/tsconfig.build.json | 12 + packages/pages-shared/tsconfig.json | 9 + packages/pages-shared/turbo.json | 9 + packages/pages-shared/vitest.config.mts | 16 + packages/playground-preview-worker/.gitignore | 6 + .../playground-preview-worker/CHANGELOG.md | 119 + .../playground-preview-worker/package.json | 35 + .../playground-preview-worker/src/errors.ts | 130 + .../playground-preview-worker/src/index.ts | 331 + .../src/inject-middleware.ts | 102 + .../src/middleware/common.module.template | 23 + .../definitions/json.module.template | 35 + .../src/middleware/loader.module.template | 90 + .../playground-preview-worker/src/realish.ts | 133 + .../playground-preview-worker/src/sentry.ts | 59 + .../playground-preview-worker/src/user.do.ts | 236 + .../tests/index.test.ts | 812 + .../tests/tsconfig.json | 19 + .../playground-preview-worker/tsconfig.json | 15 + .../worker-configuration.d.ts | 20 + .../playground-preview-worker/wrangler.json | 85 + packages/quick-edit-extension/.vscodeignore | 13 + packages/quick-edit-extension/README.md | 4 + packages/quick-edit-extension/package.json | 60 + .../quick-edit-extension/scripts/bundle.ts | 66 + .../scripts/tsconfig.json | 9 + packages/quick-edit-extension/src/cfs.ts | 638 + .../quick-edit-extension/src/extension.ts | 52 + packages/quick-edit-extension/src/ipc.ts | 64 + packages/quick-edit-extension/src/raw.d.ts | 4 + packages/quick-edit-extension/tsconfig.json | 12 + packages/quick-edit-extension/vscode.d.ts | 20792 ++++++++++++ .../vscode.proposed.fileSearchProvider.d.ts | 73 + .../vscode.proposed.ipc.d.ts | 37 + .../vscode.proposed.textSearchProvider.d.ts | 281 + packages/quick-edit/.node-version | 1 + packages/quick-edit/.nvmrc | 1 + packages/quick-edit/CHANGELOG.md | 138 + packages/quick-edit/README.md | 114 + packages/quick-edit/build.sh | 18 + packages/quick-edit/bundle-dts.ts | 34 + packages/quick-edit/editor-files/code.d.ts | 8 + .../quick-edit/editor-files/tsconfig.json | 10 + .../quick-edit/editor-files/workbench.html | 59 + packages/quick-edit/editor-files/workbench.ts | 152 + packages/quick-edit/package.json | 40 + .../quick-edit/patches/hide-unwanted-ui.diff | 4115 +++ ...echecking-without-shared-array-buffer.diff | 969 + packages/quick-edit/setup.sh | 44 + packages/quick-edit/src/index.ts | 129 + packages/quick-edit/tsconfig.json | 18 + packages/quick-edit/worker-configuration.d.ts | 8 + packages/quick-edit/wrangler.jsonc | 10 + packages/runtime-types/CHANGELOG.md | 8 + packages/runtime-types/package.json | 45 + .../src/__tests__/header.test.ts | 22 + .../src/__tests__/runtime.test.ts | 162 + packages/runtime-types/src/header.ts | 25 + packages/runtime-types/src/index.ts | 6 + packages/runtime-types/src/runtime.ts | 132 + packages/runtime-types/src/workerd.d.ts | 6 + packages/runtime-types/tsconfig.json | 7 + packages/runtime-types/tsdown.config.ts | 11 + packages/runtime-types/turbo.json | 9 + packages/runtime-types/vitest.config.ts | 8 + packages/solarflare-theme/.vscode/launch.json | 11 + packages/solarflare-theme/.vscodeignore | 4 + packages/solarflare-theme/CHANGELOG.md | 31 + packages/solarflare-theme/README.md | 3 + packages/solarflare-theme/package.json | 49 + .../solarflare-theme/solarflare-quickstart.md | 30 + .../themes/cloudflare-dark-color-theme.json | 597 + .../themes/cloudflare-light-color-theme.json | 619 + packages/turbo-r2-archive/.gitignore | 8 + packages/turbo-r2-archive/CHANGELOG.md | 27 + packages/turbo-r2-archive/README.md | 93 + packages/turbo-r2-archive/package.json | 34 + .../turbo-r2-archive/src/autoCacheBust.ts | 23 + packages/turbo-r2-archive/src/index.ts | 5 + packages/turbo-r2-archive/src/routes.ts | 120 + packages/turbo-r2-archive/tsconfig.json | 15 + packages/turbo-r2-archive/turbo.json | 9 + .../worker-configuration.d.ts | 7 + packages/turbo-r2-archive/wrangler.json | 12 + packages/unenv-preset/CHANGELOG.md | 500 + packages/unenv-preset/LICENSE-APACHE | 176 + packages/unenv-preset/LICENSE-MIT | 25 + packages/unenv-preset/README.md | 24 + packages/unenv-preset/build.config.ts | 12 + packages/unenv-preset/package.json | 64 + packages/unenv-preset/src/index.ts | 87 + packages/unenv-preset/src/preset.ts | 1035 + .../unenv-preset/src/runtime/node/console.ts | 77 + .../unenv-preset/src/runtime/node/process.ts | 254 + .../src/runtime/polyfill/package.json | 3 + .../src/runtime/polyfill/performance.ts | 47 + packages/unenv-preset/tsconfig.json | 15 + packages/unenv-preset/turbo.json | 9 + packages/vite-plugin-cloudflare/AGENTS.md | 87 + packages/vite-plugin-cloudflare/CHANGELOG.md | 2863 ++ packages/vite-plugin-cloudflare/README.md | 37 + packages/vite-plugin-cloudflare/bin/cf-vite | 2 + packages/vite-plugin-cloudflare/e2e/README.md | 57 + .../vite-plugin-cloudflare/e2e/basic.test.ts | 217 + .../e2e/dynamic.test.ts | 22 + .../e2e/fixtures/basic/.gitignore | 24 + .../e2e/fixtures/basic/README.md | 50 + .../e2e/fixtures/basic/api/index.ts | 64 + .../e2e/fixtures/basic/index.html | 13 + .../e2e/fixtures/basic/package.json | 28 + .../e2e/fixtures/basic/public/vite.svg | 1 + .../e2e/fixtures/basic/src/App.css | 42 + .../e2e/fixtures/basic/src/App.tsx | 53 + .../e2e/fixtures/basic/src/assets/react.svg | 1 + .../e2e/fixtures/basic/src/index.css | 68 + .../e2e/fixtures/basic/src/main.tsx | 13 + .../e2e/fixtures/basic/src/vite-env.d.ts | 1 + .../e2e/fixtures/basic/tsconfig.app.json | 26 + .../e2e/fixtures/basic/tsconfig.json | 8 + .../e2e/fixtures/basic/tsconfig.node.json | 24 + .../e2e/fixtures/basic/tsconfig.worker.json | 8 + .../e2e/fixtures/basic/vite.config.ts | 7 + .../e2e/fixtures/basic/wrangler.jsonc | 13 + .../e2e/fixtures/dynamic/package.json | 21 + .../e2e/fixtures/dynamic/src/dynamic.ts | 8 + .../e2e/fixtures/dynamic/src/index.ts | 8 + .../e2e/fixtures/dynamic/tsconfig.json | 7 + .../e2e/fixtures/dynamic/tsconfig.node.json | 24 + .../e2e/fixtures/dynamic/tsconfig.worker.json | 8 + .../e2e/fixtures/dynamic/vite.config.ts | 6 + .../e2e/fixtures/dynamic/wrangler.jsonc | 6 + .../index.js | 5 + .../package.json | 13 + .../vite.config.ts | 13 + .../wrangler.jsonc | 5 + .../invalid-wrangler-version/package.json | 21 + .../invalid-wrangler-version/src/index.ts | 5 + .../invalid-wrangler-version/tsconfig.json | 7 + .../tsconfig.node.json | 24 + .../tsconfig.worker.json | 8 + .../invalid-wrangler-version/vite.config.ts | 6 + .../invalid-wrangler-version/wrangler.jsonc | 6 + .../index.ts | 5 + .../package.json | 14 + .../vite.config.ts | 10 + .../wrangler.jsonc | 5 + .../dependency-import/package.json | 16 + .../dependency-import/src/index.ts | 8 + .../dependency-import/vite.config.ts | 6 + .../dependency-import/wrangler.jsonc | 5 + .../direct-import/package.json | 13 + .../direct-import/src/index.ts | 10 + .../direct-import/vite.config.ts | 6 + .../direct-import/wrangler.jsonc | 5 + .../package.json | 22 + .../src/index.ts | 5 + .../tsconfig.json | 7 + .../tsconfig.node.json | 24 + .../tsconfig.worker.json | 8 + .../vite.config.ts | 12 + .../wrangler.jsonc | 14 + .../remote-bindings-disabled/package.json | 21 + .../remote-bindings-disabled/src/index.ts | 11 + .../remote-bindings-disabled/tsconfig.json | 7 + .../tsconfig.node.json | 24 + .../tsconfig.worker.json | 8 + .../remote-bindings-disabled/vite.config.ts | 13 + .../remote-bindings-disabled/wrangler.jsonc | 10 + .../package.json | 21 + .../src/index.ts | 5 + .../tsconfig.json | 7 + .../tsconfig.node.json | 24 + .../tsconfig.worker.json | 8 + .../vite.config.ts | 12 + .../wrangler.jsonc | 13 + .../auxiliary-worker/src/index.ts | 9 + .../auxiliary-worker/wrangler.jsonc | 13 + .../remote-bindings/entry-worker/src/index.ts | 33 + .../entry-worker/wrangler.jsonc | 21 + .../e2e/fixtures/remote-bindings/package.json | 21 + .../remote-worker-alt/index.js | 5 + .../remote-worker-alt/wrangler.jsonc | 5 + .../remote-bindings/remote-worker/index.js | 5 + .../remote-worker/wrangler.jsonc | 5 + .../fixtures/remote-bindings/tsconfig.json | 7 + .../remote-bindings/tsconfig.node.json | 24 + .../remote-bindings/tsconfig.worker.json | 8 + .../fixtures/remote-bindings/vite.config.ts | 17 + .../e2e/fixtures/unresolved-main/package.json | 13 + .../fixtures/unresolved-main/vite.config.ts | 6 + .../fixtures/unresolved-main/wrangler.jsonc | 5 + .../generated-wrangler-config-path.test.ts | 35 + .../e2e/global-setup.ts | 48 + .../vite-plugin-cloudflare/e2e/helpers.ts | 350 + ...invalid-worker-environment-options.test.ts | 20 + .../e2e/invalid-wrangler-version.test.ts | 22 + .../e2e/nodejs-compat-warnings.test.ts | 39 + .../e2e/remote-bindings.test.ts | 212 + .../vite-plugin-cloudflare/e2e/tsconfig.json | 13 + .../e2e/unresolved-main.test.ts | 16 + .../e2e/vitest.config.ts | 13 + .../e2e/wrangler-configs-validation.test.ts | 23 + packages/vite-plugin-cloudflare/package.json | 97 + .../playground/__test-utils__/index.ts | 35 + .../playground/__test-utils__/responses.ts | 42 + .../playground/__test-utils__/vite-version.ts | 6 + .../playground/additional-modules/README.md | 18 + .../__tests__/additional-modules.spec.ts | 1 + .../__tests__/base-tests.ts | 70 + .../additional-modules.spec.ts | 1 + .../additional-modules.spec.ts | 1 + .../additional-modules/cloudflare.config.ts | 8 + .../additional-modules/package.json | 27 + .../additional-modules/src/index.ts | 73 + .../src/modules/bin-example.bin | Bin 0 -> 342936 bytes .../src/modules/html-example.html | 6 + .../src/modules/sql-example.sql | 1 + .../src/modules/text-example.txt | 1 + .../src/modules/text__example__2.txt | 1 + .../src/modules/wasm-example.wasm | Bin 0 -> 526 bytes .../additional-modules/src/types.d.ts | 29 + .../additional-modules/tsconfig.json | 7 + .../additional-modules/tsconfig.node.json | 4 + .../additional-modules/tsconfig.worker.json | 7 + .../vite.config.cf-build-output.ts | 17 + .../vite.config.new-module-registry.ts | 15 + .../additional-modules/vite.config.ts | 6 + .../additional-modules/wrangler.jsonc | 5 + .../wrangler.new-module-registry.jsonc | 6 + .../assets/__tests__/assets.spec.ts | 47 + .../playground/assets/__tests__/base-tests.ts | 31 + .../__tests__/cf-build-output/assets.spec.ts | 1 + .../__tests__/no-client-entry/assets.spec.ts | 21 + .../__tests__/public-dir-only/assets.spec.ts | 30 + .../playground/assets/cloudflare.config.ts | 14 + .../playground/assets/html-page.html | 9 + .../playground/assets/package.json | 26 + .../playground/assets/public/.assetsignore | 2 + .../playground/assets/public/public-image.svg | 1 + .../playground/assets/src/imported-image.svg | 27 + .../playground/assets/src/imported-text.txt | 1 + .../playground/assets/src/index.css | 3 + .../playground/assets/src/index.html | 17 + .../playground/assets/src/index.ts | 67 + .../playground/assets/src/inline-image.svg | 8 + .../playground/assets/tsconfig.json | 7 + .../playground/assets/tsconfig.node.json | 4 + .../playground/assets/tsconfig.worker.json | 7 + .../assets/vite.config.cf-build-output.ts | 24 + .../assets/vite.config.no-client-entry.ts | 13 + .../assets/vite.config.public-dir-only.ts | 19 + .../playground/assets/vite.config.ts | 21 + .../playground/assets/wrangler.jsonc | 6 + .../assets/wrangler.public-dir-only.jsonc | 4 + .../bindings/__tests__/worker.spec.ts | 37 + .../playground/bindings/package.json | 20 + .../playground/bindings/src/image.png | Bin 0 -> 96549 bytes .../playground/bindings/src/index.ts | 102 + .../playground/bindings/tsconfig.json | 7 + .../playground/bindings/tsconfig.node.json | 4 + .../playground/bindings/tsconfig.worker.json | 4 + .../playground/bindings/vite.config.ts | 6 + .../bindings/worker-configuration.d.ts | 18 + .../playground/bindings/wrangler.jsonc | 45 + .../__tests__/block-concurrency-while.spec.ts | 9 + .../block-concurrency-while/package.json | 19 + .../block-concurrency-while/src/index.ts | 38 + .../block-concurrency-while/tsconfig.json | 7 + .../tsconfig.node.json | 4 + .../tsconfig.worker.json | 4 + .../block-concurrency-while/vite.config.ts | 6 + .../block-concurrency-while/wrangler.jsonc | 19 + .../__tests__/child-environment.spec.ts | 10 + .../playground/child-environment/package.json | 19 + .../src/child-environment-module.ts | 6 + .../playground/child-environment/src/index.ts | 18 + .../child-environment/tsconfig.json | 7 + .../child-environment/tsconfig.node.json | 4 + .../child-environment/tsconfig.worker.json | 4 + .../child-environment/vite.config.ts | 28 + .../child-environment/wrangler.jsonc | 5 + .../__tests__/cloudflare-env.spec.ts | 8 + .../custom-mode/cloudflare-env.spec.ts | 8 + .../playground/cloudflare-env/package.json | 22 + .../playground/cloudflare-env/src/index.ts | 9 + .../playground/cloudflare-env/tsconfig.json | 7 + .../cloudflare-env/tsconfig.node.json | 4 + .../cloudflare-env/tsconfig.worker.json | 4 + .../cloudflare-env/vite.config.custom-mode.ts | 7 + .../playground/cloudflare-env/vite.config.ts | 6 + .../playground/cloudflare-env/wrangler.jsonc | 15 + .../playground/config-changes/.dev.vars | 1 + .../playground/config-changes/.gitignore | 1 + .../__tests__/config-changes.spec.ts | 73 + .../playground/config-changes/package.json | 19 + .../playground/config-changes/src/index.ts | 12 + .../playground/config-changes/tsconfig.json | 7 + .../config-changes/tsconfig.node.json | 4 + .../config-changes/tsconfig.worker.json | 4 + .../playground/config-changes/vite.config.ts | 7 + .../vite.config.with-cloudflare-env.ts | 7 + .../playground/config-changes/wrangler.json | 7 + .../playground/containers/Dockerfile | 9 + .../containers/__tests__/containers.spec.ts | 63 + .../containers/container/simple-node-app.js | 49 + .../playground/containers/package.json | 19 + .../playground/containers/src/entry.ts | 40 + .../playground/containers/src/index.ts | 77 + .../playground/containers/tsconfig.json | 7 + .../playground/containers/tsconfig.node.json | 4 + .../containers/tsconfig.worker.json | 4 + .../playground/containers/vite.config.ts | 21 + .../containers/wrangler.docker.jsonc | 28 + .../playground/containers/wrangler.jsonc | 15 + .../containers/wrangler.registry.jsonc | 28 + .../__tests__/cf-worker-header.spec.ts | 54 + .../__tests__/cron-triggers.spec.ts | 10 + .../playground/cron-triggers/package.json | 19 + .../playground/cron-triggers/src/index.ts | 25 + .../playground/cron-triggers/tsconfig.json | 7 + .../cron-triggers/tsconfig.node.json | 4 + .../cron-triggers/tsconfig.worker.json | 4 + .../playground/cron-triggers/vite.config.ts | 6 + .../playground/cron-triggers/wrangler.jsonc | 20 + .../ctx-exports/__tests__/ctx-exports.spec.ts | 99 + .../playground/ctx-exports/package.json | 20 + .../playground/ctx-exports/src/index.ts | 37 + .../playground/ctx-exports/tsconfig.json | 7 + .../playground/ctx-exports/tsconfig.node.json | 4 + .../ctx-exports/tsconfig.worker.json | 4 + .../playground/ctx-exports/vite.config.ts | 6 + .../ctx-exports/worker-configuration.d.ts | 12 + .../playground/ctx-exports/wrangler.jsonc | 12 + .../__tests__/custom-build-app.spec.ts | 22 + .../playground/custom-build-app/index.html | 9 + .../playground/custom-build-app/package.json | 19 + .../playground/custom-build-app/src/index.ts | 5 + .../playground/custom-build-app/tsconfig.json | 7 + .../custom-build-app/tsconfig.node.json | 4 + .../custom-build-app/tsconfig.worker.json | 4 + .../custom-build-app/vite.config.ts | 27 + .../custom-build-app/wrangler.jsonc | 5 + ...assets-importing-from-dependencies.spec.ts | 10 + .../deps-assets-importing/package.json | 19 + .../deps-assets-importing/src/index.ts | 11 + .../deps-assets-importing/tsconfig.json | 7 + .../deps-assets-importing/tsconfig.node.json | 4 + .../tsconfig.worker.json | 4 + .../deps-assets-importing/vite.config.ts | 6 + .../deps-assets-importing/wrangler.jsonc | 5 + .../playground/dev-vars/.dev.vars | 4 + .../playground/dev-vars/.dev.vars.staging | 3 + .../playground/dev-vars/.gitignore | 1 + .../__tests__/dev-vars-loading.spec.ts | 51 + .../dev-vars/__tests__/vars-changes.spec.ts | 43 + .../dev-vars-loading.spec.ts | 44 + .../with-specified-env/vars-changes.spec.ts | 41 + .../playground/dev-vars/package.json | 19 + .../playground/dev-vars/src/index.ts | 16 + .../playground/dev-vars/tsconfig.json | 7 + .../playground/dev-vars/tsconfig.node.json | 4 + .../playground/dev-vars/tsconfig.worker.json | 4 + .../playground/dev-vars/vite.config.ts | 6 + .../vite.config.with-specified-env.ts | 7 + .../playground/dev-vars/wrangler.jsonc | 8 + .../playground/dot-env/.env | 4 + .../playground/dot-env/.env.staging | 3 + .../dot-env/.env.with-specified-env | 1 + .../playground/dot-env/.gitignore | 1 + .../dot-env/__tests__/dot-env-loading.spec.ts | 43 + .../dot-env/__tests__/vars-changes.spec.ts | 41 + .../dot-env-loading.spec.ts | 40 + .../with-specified-env/vars-changes.spec.ts | 38 + .../playground/dot-env/package.json | 19 + .../playground/dot-env/src/index.ts | 16 + .../playground/dot-env/tsconfig.json | 7 + .../playground/dot-env/tsconfig.node.json | 4 + .../playground/dot-env/tsconfig.worker.json | 4 + .../playground/dot-env/vite.config.ts | 6 + .../dot-env/vite.config.with-specified-env.ts | 7 + .../playground/dot-env/wrangler.jsonc | 8 + .../__tests__/durable-objects.spec.ts | 40 + .../playground/durable-objects/package.json | 19 + .../playground/durable-objects/src/index.ts | 110 + .../playground/durable-objects/tsconfig.json | 7 + .../durable-objects/tsconfig.node.json | 4 + .../durable-objects/tsconfig.worker.json | 4 + .../playground/durable-objects/vite.config.ts | 6 + .../playground/durable-objects/wrangler.jsonc | 23 + .../__tests__/dynamic-import-paths.spec.ts | 13 + .../dynamic-import-paths/package.json | 19 + .../dynamic-import-paths/src/another.js | 1 + .../dynamic-import-paths/src/index.ts | 12 + .../dynamic-import-paths/tsconfig.json | 7 + .../dynamic-import-paths/tsconfig.node.json | 4 + .../dynamic-import-paths/tsconfig.worker.json | 7 + .../dynamic-import-paths/vite.config.ts | 17 + .../dynamic-import-paths/wrangler.jsonc | 5 + .../__tests__/email-triggers.spec.ts | 44 + .../playground/email-worker/package.json | 23 + .../playground/email-worker/src/index.ts | 43 + .../playground/email-worker/tsconfig.json | 7 + .../email-worker/tsconfig.node.json | 4 + .../email-worker/tsconfig.worker.json | 4 + .../playground/email-worker/vite.config.ts | 6 + .../playground/email-worker/wrangler.jsonc | 12 + .../errors/__tests__/errors.spec.ts | 102 + .../playground/errors/package.json | 19 + .../playground/errors/src/worker-a/index.ts | 51 + .../errors/src/worker-a/wrangler.jsonc | 17 + .../playground/errors/src/worker-b/index.ts | 35 + .../errors/src/worker-b/wrangler.jsonc | 13 + .../playground/errors/tsconfig.json | 7 + .../playground/errors/tsconfig.node.json | 4 + .../playground/errors/tsconfig.worker.json | 4 + .../playground/errors/vite.config.ts | 13 + .../__tests__/build-output.spec.ts | 119 + .../cloudflare.config.ts | 14 + .../experimental-build-output/package.json | 19 + .../public/static.txt | 1 + .../experimental-build-output/src/index.ts | 11 + .../experimental-build-output/tsconfig.json | 7 + .../tsconfig.node.json | 4 + .../tsconfig.worker.json | 7 + .../experimental-build-output/vite.config.ts | 21 + .../worker-configuration.d.ts | 17 + .../__tests__/worker.spec.ts | 11 + .../experimental-config/cloudflare.config.ts | 14 + .../experimental-config/package.json | 19 + .../experimental-config/src/index.ts | 7 + .../experimental-config/tsconfig.json | 7 + .../experimental-config/tsconfig.node.json | 4 + .../experimental-config/tsconfig.worker.json | 7 + .../experimental-config/vite.config.ts | 12 + .../worker-configuration.d.ts | 17 + .../__tests__/external-durable-object.spec.ts | 21 + .../external-durable-objects/package.json | 19 + .../external-durable-objects/tsconfig.json | 7 + .../tsconfig.node.json | 4 + .../tsconfig.worker.json | 4 + .../external-durable-objects/vite.config.ts | 13 + .../worker-a/index.ts | 35 + .../worker-a/wrangler.jsonc | 20 + .../worker-b/index.ts | 17 + .../worker-b/wrangler.jsonc | 5 + .../playground/external-workers/package.json | 19 + .../playground/external-workers/src/index.ts | 104 + .../playground/external-workers/tsconfig.json | 7 + .../external-workers/tsconfig.node.json | 4 + .../external-workers/tsconfig.worker.json | 4 + .../external-workers/vite.config.ts | 6 + .../external-workers/wrangler.jsonc | 17 + .../__tests__/workflows.spec.ts | 19 + .../external-workflows/package.json | 19 + .../external-workflows/tsconfig.json | 7 + .../external-workflows/tsconfig.node.json | 4 + .../external-workflows/tsconfig.worker.json | 4 + .../external-workflows/vite.config.ts | 13 + .../external-workflows/worker-a/index.ts | 37 + .../worker-a/wrangler.jsonc | 13 + .../external-workflows/worker-b/index.ts | 22 + .../worker-b/wrangler.jsonc | 5 + .../hot-channel/__tests__/hot-channel.spec.ts | 29 + .../playground/hot-channel/package.json | 18 + .../playground/hot-channel/src/index.ts | 18 + .../playground/hot-channel/tsconfig.json | 7 + .../playground/hot-channel/tsconfig.node.json | 4 + .../hot-channel/tsconfig.worker.json | 4 + .../playground/hot-channel/vite.config.ts | 26 + .../playground/hot-channel/wrangler.jsonc | 5 + .../playground/importable-env/.dev.vars | 1 + .../playground/importable-env/.gitignore | 1 + .../__tests__/importable-env.spec.ts | 47 + .../playground/importable-env/package.json | 19 + .../playground/importable-env/src/index.ts | 27 + .../playground/importable-env/tsconfig.json | 7 + .../importable-env/tsconfig.node.json | 4 + .../importable-env/tsconfig.worker.json | 4 + .../playground/importable-env/vite.config.ts | 6 + .../playground/importable-env/wrangler.jsonc | 8 + .../__tests__/main-resolution.spec.ts | 11 + .../nested-config/main-resolution.spec.ts | 11 + .../main-resolution.spec.ts | 6 + .../main-resolution.spec.ts | 6 + .../nested-config/another/another.ts | 1 + .../main-resolution/nested-config/index.ts | 7 + .../nested-config/wrangler.jsonc | 5 + .../playground/main-resolution/package.json | 26 + .../main-resolution/package/entry.js | 5 + .../main-resolution/package/index.js | 1 + .../main-resolution/package/package.json | 10 + .../main-resolution/src/another/another.ts | 1 + .../playground/main-resolution/src/index.ts | 7 + .../playground/main-resolution/tsconfig.json | 7 + .../main-resolution/tsconfig.node.json | 4 + .../main-resolution/tsconfig.worker.json | 4 + .../vite.config.nested-config.ts | 12 + .../vite.config.package-export-main.ts | 19 + .../playground/main-resolution/vite.config.ts | 18 + .../vite.config.virtual-module-main.ts | 31 + .../playground/main-resolution/wrangler.jsonc | 6 + .../wrangler.package-export-main.jsonc | 7 + .../wrangler.virtual-module-main.jsonc | 5 + .../__tests__/middleware-mode.spec.ts | 7 + .../middleware-mode/__tests__/serve.ts | 26 + .../playground/middleware-mode/package.json | 19 + .../playground/middleware-mode/server.js | 30 + .../playground/middleware-mode/src/index.ts | 5 + .../playground/middleware-mode/tsconfig.json | 7 + .../middleware-mode/tsconfig.node.json | 4 + .../middleware-mode/tsconfig.worker.json | 4 + .../playground/middleware-mode/wrangler.jsonc | 5 + .../module-resolution/__tests__/base-tests.ts | 136 + .../__tests__/module-resolution.spec.ts | 1 + .../nodejs-compat/module-resolution.spec.ts | 1 + .../playground/module-resolution/package.json | 30 + .../packages/excludes/index.js | 3 + .../packages/excludes/package.json | 10 + .../packages/excludes/plugin.js | 17 + .../packages/imports/cloudflare-builtins.d.ts | 1 + .../packages/imports/cloudflare-builtins.js | 3 + .../packages/imports/package.json | 12 + .../packages/requires/ext.d.ts | 4 + .../packages/requires/ext.js | 13 + .../packages/requires/hello.js | 1 + .../packages/requires/index.d.ts | 1 + .../packages/requires/index.js | 1 + .../packages/requires/json.d.ts | 3 + .../packages/requires/json.js | 3 + .../packages/requires/no-ext.d.ts | 1 + .../packages/requires/no-ext.js | 1 + .../packages/requires/package.json | 19 + .../packages/requires/world.cjs | 1 + .../module-resolution/src/aliasing.ts | 3 + .../src/cloudflare-imports.ts | 8 + .../src/external-cloudflare-imports.ts | 5 + .../playground/module-resolution/src/index.ts | 57 + .../module-resolution/src/require-ext.ts | 11 + .../module-resolution/src/require-json.ts | 9 + .../module-resolution/src/require-no-ext.ts | 5 + .../src/third-party/discord-api-types.ts | 15 + .../src/third-party/react.ts | 7 + .../src/third-party/remix.ts | 7 + .../src/third-party/slash-create.ts | 22 + .../module-resolution/tsconfig.json | 7 + .../module-resolution/tsconfig.node.json | 4 + .../module-resolution/tsconfig.worker.json | 9 + .../vite.config.nodejs-compat.ts | 24 + .../module-resolution/vite.config.ts | 24 + .../module-resolution/wrangler.jsonc | 5 + .../wrangler.nodejs-compat.jsonc | 6 + .../playground/multi-worker/.gitignore | 5 + .../custom-build-app/multi-worker.spec.ts | 16 + .../multi-worker.spec.ts | 24 + .../customize-config/multi-worker.spec.ts | 33 + .../dev-only-auxiliary/multi-worker.spec.ts | 29 + .../__tests__/multi-worker.spec.ts | 64 + .../no-config-file/multi-worker.spec.ts | 19 + .../multi-worker.spec.ts | 26 + .../playground/multi-worker/package.json | 31 + .../playground/multi-worker/tsconfig.json | 7 + .../multi-worker/tsconfig.node.json | 4 + .../multi-worker/tsconfig.worker.json | 4 + .../vite.config.custom-build-app.ts | 26 + .../vite.config.custom-output-directories.ts | 23 + .../vite.config.customize-config.ts | 30 + .../vite.config.dev-only-auxiliary.ts | 18 + .../vite.config.no-config-file.ts | 28 + .../playground/multi-worker/vite.config.ts | 13 + ...vite.config.with-worker-configs-warning.ts | 15 + .../playground/multi-worker/worker-a/index.ts | 65 + .../multi-worker/worker-a/wrangler.jsonc | 17 + .../worker-a/wrangler.with-warning.jsonc | 18 + .../playground/multi-worker/worker-b/index.ts | 46 + .../multi-worker/worker-b/wrangler.jsonc | 5 + .../worker-b/wrangler.with-warning.jsonc | 8 + .../playground/multi-worker/worker-c/index.ts | 25 + .../playground/multi-worker/worker-d/index.ts | 13 + .../cloudflare-node/cloudflare-node.spec.ts | 8 + .../__tests__/worker-als/als.spec.ts | 15 + .../__tests__/worker-basic/basic.spec.ts | 9 + .../worker-cross-env/cross-env.spec.ts | 11 + .../__tests__/worker-crypto/crypto.spec.ts | 25 + .../__tests__/worker-debug/debug.spec.ts | 12 + .../__tests__/worker-https/https.spec.ts | 6 + .../worker-postgres/postgres.spec.ts | 24 + .../__tests__/worker-postgres/serve.ts | 25 + .../process.spec.ts | 9 + .../__tests__/worker-process/process.spec.ts | 8 + .../__tests__/worker-random/random.spec.ts | 17 + .../resolve-externals.spec.ts | 17 + .../node-compat/cloudflare-node/index.ts | 9 + .../cloudflare-node/wrangler.jsonc | 6 + .../playground/node-compat/package.json | 67 + .../playground/node-compat/tsconfig.json | 7 + .../playground/node-compat/tsconfig.node.json | 4 + .../node-compat/tsconfig.worker.json | 16 + .../vite.config.cloudflare-node.ts | 15 + .../node-compat/vite.config.worker-als.ts | 15 + .../node-compat/vite.config.worker-basic.ts | 15 + .../vite.config.worker-cross-env.ts | 15 + .../node-compat/vite.config.worker-crypto.ts | 15 + .../node-compat/vite.config.worker-debug.ts | 15 + .../node-compat/vite.config.worker-https.ts | 15 + .../vite.config.worker-postgres.ts | 29 + ...ite.config.worker-process-populated-env.ts | 15 + .../node-compat/vite.config.worker-process.ts | 15 + .../node-compat/vite.config.worker-random.ts | 15 + .../vite.config.worker-resolve-externals.ts | 30 + .../node-compat/worker-als/index.ts | 8 + .../node-compat/worker-als/wrangler.jsonc | 6 + .../node-compat/worker-basic/global.d.ts | 2 + .../node-compat/worker-basic/index.ts | 46 + .../node-compat/worker-basic/wrangler.jsonc | 27 + .../node-compat/worker-cross-env/index.ts | 31 + .../worker-cross-env/wrangler.jsonc | 6 + .../node-compat/worker-crypto/index.ts | 30 + .../node-compat/worker-crypto/wrangler.jsonc | 6 + .../node-compat/worker-debug/index.ts | 38 + .../node-compat/worker-debug/wrangler.jsonc | 9 + .../node-compat/worker-https/index.ts | 11 + .../node-compat/worker-https/wrangler.jsonc | 7 + .../node-compat/worker-postgres/index.ts | 39 + .../worker-postgres/wrangler.jsonc | 16 + .../worker-process-populated-env/.dev.vars | 1 + .../worker-process-populated-env/.gitignore | 1 + .../worker-process-populated-env/index.ts | 28 + .../wrangler.jsonc | 12 + .../node-compat/worker-process/.dev.vars | 1 + .../node-compat/worker-process/.gitignore | 1 + .../worker-process/early-process-access.ts | 1 + .../node-compat/worker-process/index.ts | 92 + .../node-compat/worker-process/wrangler.jsonc | 12 + .../node-compat/worker-random/global.d.ts | 2 + .../node-compat/worker-random/index.ts | 21 + .../node-compat/worker-random/wrangler.jsonc | 6 + .../worker-resolve-externals/index.ts | 5 + .../worker-resolve-externals/wrangler.jsonc | 6 + .../node-env/__tests__/base-tests.ts | 24 + .../node-env/__tests__/node-env.spec.ts | 1 + .../__tests__/nodejs-compat/node-env.spec.ts | 1 + .../playground/node-env/package.json | 27 + .../playground/node-env/src/index.ts | 33 + .../playground/node-env/tsconfig.json | 7 + .../playground/node-env/tsconfig.node.json | 4 + .../playground/node-env/tsconfig.worker.json | 4 + .../node-env/vite.config.nodejs-compat.ts | 12 + .../playground/node-env/vite.config.ts | 6 + .../playground/node-env/wrangler.jsonc | 5 + .../node-env/wrangler.nodejs-compat.jsonc | 6 + .../playground/package.json | 25 + .../partyserver/__tests__/partyserver.spec.ts | 15 + .../playground/partyserver/index.html | 13 + .../playground/partyserver/package.json | 30 + .../playground/partyserver/src/App.tsx | 29 + .../playground/partyserver/src/main.tsx | 12 + .../playground/partyserver/src/tailwind.css | 1 + .../partyserver/tsconfig.client.json | 4 + .../playground/partyserver/tsconfig.json | 8 + .../playground/partyserver/tsconfig.node.json | 4 + .../partyserver/tsconfig.worker.json | 4 + .../playground/partyserver/vite.config.ts | 12 + .../playground/partyserver/worker/index.ts | 21 + .../playground/partyserver/wrangler.jsonc | 23 + .../assets-only/prerendering.spec.ts | 17 + .../prerendering/__tests__/base-tests.ts | 26 + .../__tests__/prerendering.spec.ts | 17 + .../playground/prerendering/package.json | 20 + .../playground/prerendering/src/index.ts | 5 + .../playground/prerendering/src/prerender.ts | 27 + .../playground/prerendering/tsconfig.json | 7 + .../prerendering/tsconfig.node.json | 4 + .../prerendering/tsconfig.worker.json | 4 + .../prerendering/vite.config.assets-only.ts | 3 + .../playground/prerendering/vite.config.ts | 79 + .../playground/prerendering/wrangler.jsonc | 3 + .../playground/prisma/.gitignore | 1 + .../prisma/__tests__/prisma.spec.ts | 24 + .../playground/prisma/__tests__/serve.ts | 27 + .../migrations/0001_create_user_table.sql | 9 + .../playground/prisma/package.json | 27 + .../playground/prisma/prisma/schema.prisma | 18 + .../playground/prisma/src/index.ts | 16 + .../playground/prisma/tsconfig.json | 7 + .../playground/prisma/tsconfig.node.json | 4 + .../playground/prisma/tsconfig.worker.json | 4 + .../playground/prisma/vite.config.ts | 6 + .../playground/prisma/wrangler.jsonc | 12 + .../react-spa.spec.ts | 129 + .../react-spa/__tests__/react-spa.spec.ts | 79 + .../server-headers/react-spa.spec.ts | 27 + .../playground/react-spa/index.html | 13 + .../playground/react-spa/package.json | 27 + .../playground/react-spa/public/_headers | 2 + .../playground/react-spa/public/_redirects | 3 + .../playground/react-spa/public/file.txt | 1 + .../playground/react-spa/public/redirect.svg | 27 + .../playground/react-spa/public/rewrite.svg | 27 + .../playground/react-spa/public/target.svg | 8 + .../playground/react-spa/public/vite.svg | 1 + .../playground/react-spa/src/App.css | 42 + .../playground/react-spa/src/App.tsx | 38 + .../playground/react-spa/src/assets/react.svg | 1 + .../playground/react-spa/src/index.css | 68 + .../playground/react-spa/src/main.tsx | 13 + .../playground/react-spa/tsconfig.client.json | 4 + .../playground/react-spa/tsconfig.json | 7 + .../playground/react-spa/tsconfig.node.json | 4 + ...nfig.experimental-headers-and-redirects.ts | 14 + .../react-spa/vite.config.server-headers.ts | 14 + .../playground/react-spa/vite.config.ts | 7 + .../playground/react-spa/wrangler.jsonc | 7 + .../same-worker-service-bindings.spec.ts | 15 + .../same-worker-service-bindings/package.json | 19 + .../same-worker-service-bindings/src/index.ts | 32 + .../tsconfig.json | 7 + .../tsconfig.node.json | 4 + .../tsconfig.worker.json | 4 + .../vite.config.ts | 6 + .../wrangler.jsonc | 17 + .../playground/sensitive-files/.dev.vars | 1 + .../sensitive-files/.dev.vars.staging | 1 + .../playground/sensitive-files/.env | 1 + .../playground/sensitive-files/.env.staging | 1 + .../playground/sensitive-files/.gitignore | 2 + .../playground/sensitive-files/.npmrc | 1 + .../playground/sensitive-files/.yarnrc | 1 + .../playground/sensitive-files/.yarnrc.yml | 1 + .../__tests__/sensitive-files.spec.ts | 186 + .../playground/sensitive-files/client.jks | 1 + .../playground/sensitive-files/client.key | 1 + .../playground/sensitive-files/client.p12 | 1 + .../playground/sensitive-files/client.p8 | 1 + .../playground/sensitive-files/client.pfx | 1 + .../sensitive-files/custom-sensitive-file | 1 + .../playground/sensitive-files/package.json | 19 + .../playground/sensitive-files/tsconfig.json | 7 + .../sensitive-files/tsconfig.node.json | 4 + .../sensitive-files/tsconfig.worker.json | 4 + .../playground/sensitive-files/vite.config.ts | 17 + .../sensitive-files/worker-a/index.ts | 15 + .../sensitive-files/worker-b/.dev.vars | 1 + .../worker-b/.dev.vars.staging | 1 + .../playground/sensitive-files/worker-b/.env | 1 + .../sensitive-files/worker-b/.env.staging | 1 + .../sensitive-files/worker-b/index.ts | 5 + .../sensitive-files/worker-b/wrangler.jsonc | 5 + .../playground/sensitive-files/wrangler.jsonc | 11 + .../playground/spa-with-api/.gitignore | 2 + .../spa-with-api/__tests__/base-tests.ts | 18 + .../child-compiler/spa-with-api.spec.ts | 1 + .../spa-with-api.spec.ts | 20 + .../__tests__/https/spa-with-api.spec.ts | 1 + .../run-worker-first/spa-with-api.spec.ts | 64 + .../__tests__/spa-with-api.spec.ts | 66 + .../static-routing/spa-with-api.spec.ts | 66 + .../playground/spa-with-api/api/asset.txt | 1 + .../playground/spa-with-api/api/index.ts | 32 + .../playground/spa-with-api/api/some-file.txt | 1 + .../playground/spa-with-api/index.html | 13 + .../playground/spa-with-api/package.json | 34 + .../playground/spa-with-api/public/vite.svg | 1 + .../playground/spa-with-api/src/App.css | 42 + .../playground/spa-with-api/src/App.tsx | 54 + .../spa-with-api/src/assets/react.svg | 1 + .../playground/spa-with-api/src/index.css | 68 + .../playground/spa-with-api/src/main.tsx | 13 + .../spa-with-api/tsconfig.client.json | 4 + .../playground/spa-with-api/tsconfig.json | 8 + .../spa-with-api/tsconfig.node.json | 4 + .../spa-with-api/tsconfig.worker.json | 4 + .../vite.config.child-compiler.ts | 48 + .../vite.config.custom-output-directories.ts | 17 + .../spa-with-api/vite.config.https.ts | 12 + .../vite.config.run-worker-first.ts | 14 + .../vite.config.static-routing.ts | 14 + .../playground/spa-with-api/vite.config.ts | 7 + .../playground/spa-with-api/wrangler.jsonc | 9 + .../wrangler.run-worker-first.jsonc | 10 + .../wrangler.static-routing.jsonc | 10 + .../playground/static-mpa/404.html | 12 + .../static-mpa/__tests__/base-tests.ts | 103 + .../cf-build-output/static-mpa.spec.ts | 1 + .../static-mpa/__tests__/static-mpa.spec.ts | 14 + .../static-mpa.spec.ts | 19 + .../playground/static-mpa/about/404.html | 12 + .../playground/static-mpa/about/index.html | 12 + .../static-mpa/cloudflare.config.ts | 10 + .../playground/static-mpa/contact.html | 12 + .../playground/static-mpa/index.html | 12 + .../playground/static-mpa/package.json | 25 + .../playground/static-mpa/public-html.html | 12 + .../playground/static-mpa/public/_headers | 5 + .../playground/static-mpa/public/_redirects | 3 + .../static-mpa/public/public-html.html | 12 + .../static-mpa/public/text-file.txt | 1 + .../playground/static-mpa/public/vite.svg | 1 + .../playground/static-mpa/tsconfig.json | 4 + .../playground/static-mpa/tsconfig.node.json | 4 + .../static-mpa/vite.config.cf-build-output.ts | 33 + .../playground/static-mpa/vite.config.ts | 22 + ...vite.config.with-worker-configs-warning.ts | 28 + .../playground/static-mpa/wrangler.jsonc | 7 + .../static-mpa/wrangler.with-warning.jsonc | 9 + .../static/__tests__/static.spec.ts | 7 + .../static/__tests__/with-api/static.spec.ts | 12 + .../playground/static/api/index.ts | 13 + .../playground/static/index.html | 13 + .../playground/static/package.json | 22 + .../playground/static/public/vite.svg | 1 + .../playground/static/src/counter.ts | 9 + .../playground/static/src/main.ts | 30 + .../playground/static/src/style.css | 96 + .../playground/static/src/typescript.svg | 1 + .../playground/static/tsconfig.client.json | 4 + .../playground/static/tsconfig.json | 8 + .../playground/static/tsconfig.node.json | 4 + .../playground/static/tsconfig.worker.json | 4 + .../playground/static/vite.config.ts | 6 + .../playground/static/vite.config.with-api.ts | 12 + .../playground/static/wrangler.with-api.jsonc | 5 + .../stream-binding/__tests__/serve.ts | 53 + .../stream-binding/__tests__/worker.spec.ts | 30 + .../playground/stream-binding/package.json | 19 + .../playground/stream-binding/src/index.ts | 23 + .../playground/stream-binding/tsconfig.json | 7 + .../stream-binding/tsconfig.node.json | 4 + .../stream-binding/tsconfig.worker.json | 4 + .../playground/stream-binding/vite.config.ts | 6 + .../stream-binding/worker-configuration.d.ts | 12 + .../playground/stream-binding/wrangler.jsonc | 9 + .../streaming/__tests__/base-tests.ts | 12 + .../__tests__/https/streaming.spec.ts | 1 + .../streaming/__tests__/streaming.spec.ts | 1 + .../playground/streaming/package.json | 23 + .../playground/streaming/src/index.ts | 50 + .../playground/streaming/tsconfig.json | 7 + .../playground/streaming/tsconfig.node.json | 4 + .../playground/streaming/tsconfig.worker.json | 4 + .../playground/streaming/vite.config.https.ts | 10 + .../playground/streaming/vite.config.ts | 6 + .../playground/streaming/wrangler.jsonc | 5 + .../playground/tsconfig.json | 9 + .../playground/turbo.json | 36 + .../__tests__/basic-functionality.spec.ts | 6 + .../playground/virtual-modules/package.json | 19 + .../playground/virtual-modules/src/index.ts | 7 + .../playground/virtual-modules/tsconfig.json | 7 + .../virtual-modules/tsconfig.node.json | 4 + .../virtual-modules/tsconfig.worker.json | 4 + .../playground/virtual-modules/vite.config.ts | 21 + .../playground/virtual-modules/wrangler.jsonc | 5 + .../playground/vitest-global-setup.ts | 45 + .../playground/vitest-setup.ts | 441 + .../playground/vitest.config.e2e.ts | 19 + .../websockets/__tests__/websockets.spec.ts | 62 + .../playground/websockets/package.json | 19 + .../playground/websockets/src/index.html | 76 + .../playground/websockets/src/index.ts | 43 + .../playground/websockets/tsconfig.json | 7 + .../playground/websockets/tsconfig.node.json | 4 + .../websockets/tsconfig.worker.json | 4 + .../playground/websockets/vite.config.ts | 6 + .../playground/websockets/wrangler.jsonc | 19 + .../__tests__/base-config/worker.spec.ts | 37 + .../explicit-upload-source-maps/worker.spec.ts | 17 + .../infer-upload-source-maps/worker.spec.ts | 17 + .../__tests__/log-level-error/worker.spec.ts | 12 + .../__tests__/log-level-silent/worker.spec.ts | 8 + .../__tests__/log-level-warn/worker.spec.ts | 8 + .../worker-♫/__tests__/worker.spec.ts | 120 + .../playground/worker-♫/package.json | 19 + .../playground/worker-♫/src/a.ts | 1 + .../playground/worker-♫/src/b.ts | 5 + .../playground/worker-♫/src/index.ts | 44 + .../playground/worker-♫/tsconfig.json | 7 + .../playground/worker-♫/tsconfig.node.json | 4 + .../playground/worker-♫/tsconfig.worker.json | 4 + .../worker-♫/vite.config.base-config.ts | 7 + .../vite.config.explicit-upload-source-maps.ts | 19 + .../vite.config.infer-upload-source-maps.ts | 13 + .../worker-♫/vite.config.log-level-error.ts | 7 + .../worker-♫/vite.config.log-level-silent.ts | 7 + .../worker-♫/vite.config.log-level-warn.ts | 7 + .../playground/worker-♫/vite.config.ts | 6 + .../wrangler.explicit-upload-source-maps.jsonc | 6 + .../playground/worker-♫/wrangler.jsonc | 12 + .../workflows/__tests__/workflows.spec.ts | 19 + .../playground/workflows/package.json | 19 + .../playground/workflows/src/index.ts | 60 + .../playground/workflows/tsconfig.json | 7 + .../playground/workflows/tsconfig.node.json | 4 + .../playground/workflows/tsconfig.worker.json | 4 + .../playground/workflows/vite.config.ts | 6 + .../playground/workflows/wrangler.jsonc | 12 + .../vite-plugin-cloudflare/scripts/deps.ts | 15 + .../src/__tests__/build-output.spec.ts | 32 + .../__tests__/customize-worker-config.spec.ts | 124 + .../src/__tests__/dev-vars.spec.ts | 135 + .../__tests__/experimental-new-config.spec.ts | 470 + .../fixtures/empty-dir.jsonc/.gitkeep | 0 .../src/__tests__/fixtures/empty-dir/.gitkeep | 0 .../src/__tests__/fixtures/index.ts | 5 + .../fixtures/non-existing-main-wrangler.jsonc | 5 + .../__tests__/fixtures/simple-wrangler.jsonc | 5 + .../wrangler-with-fields-to-ignore.jsonc | 37 + .../src/__tests__/fixtures/wrangler.jsonc | 5 + .../get-modules-from-manifest.spec.ts | 102 + ...get-validated-wrangler-config-path.spec.ts | 118 + ...rning-for-workers-resolved-configs.spec.ts | 222 + .../src/__tests__/get-worker-config.spec.ts | 184 + .../src/__tests__/hmr-events.spec.ts | 59 + .../src/__tests__/output-config.spec.ts | 151 + .../src/__tests__/preview-server.spec.ts | 41 + .../__tests__/resolve-plugin-config.spec.ts | 1121 + .../src/__tests__/shortcuts.spec.ts | 319 + .../src/__tests__/tunnel.spec.ts | 724 + .../src/__tests__/utils.spec.ts | 191 + ...alidate-worker-environment-options.spec.ts | 106 + .../src/__tests__/websockets.spec.ts | 333 + .../src/assert-wrangler-version.ts | 36 + .../src/asset-config.ts | 144 + .../src/build-constants.ts | 9 + .../src/build-output-env.ts | 10 + .../src/build-output-preview.ts | 105 + packages/vite-plugin-cloudflare/src/build.ts | 209 + .../vite-plugin-cloudflare/src/cf-vite.ts | 250 + .../src/cloudflare-environment.ts | 409 + .../vite-plugin-cloudflare/src/constants.ts | 16 + .../vite-plugin-cloudflare/src/containers.ts | 63 + .../vite-plugin-cloudflare/src/context.ts | 268 + packages/vite-plugin-cloudflare/src/debug.ts | 119 + .../src/deploy-config.ts | 107 + .../vite-plugin-cloudflare/src/dev-vars.ts | 87 + .../src/experimental-config.ts | 5 + .../src/export-types.ts | 236 + packages/vite-plugin-cloudflare/src/index.ts | 125 + .../src/miniflare-options.ts | 854 + .../src/nodejs-compat.ts | 290 + .../src/plugin-config.ts | 854 + .../src/plugins/additional-modules.ts | 153 + .../src/plugins/build-output.ts | 109 + .../src/plugins/config.ts | 407 + .../src/plugins/debug.ts | 89 + .../vite-plugin-cloudflare/src/plugins/dev.ts | 368 + .../src/plugins/nodejs-compat.ts | 224 + .../src/plugins/output-config.ts | 262 + .../src/plugins/preview.ts | 129 + .../vite-plugin-cloudflare/src/plugins/rsc.ts | 14 + .../src/plugins/shortcuts.ts | 162 + .../src/plugins/trigger-handlers.ts | 28 + .../src/plugins/tunnel.ts | 690 + .../src/plugins/virtual-modules.ts | 178 + .../src/plugins/wasm.ts | 32 + packages/vite-plugin-cloudflare/src/shared.ts | 17 + .../src/update-check.ts | 32 + packages/vite-plugin-cloudflare/src/utils.ts | 180 + .../vite-plugin-cloudflare/src/vite-config.ts | 91 + .../vite-plugin-cloudflare/src/websockets.ts | 161 + .../src/workers-configs.ts | 559 + .../src/workers/asset-worker/index.ts | 56 + .../src/workers/router-worker/index.ts | 4 + .../runner-worker/__tests__/env.spec.ts | 37 + .../src/workers/runner-worker/env.ts | 35 + .../src/workers/runner-worker/errors.ts | 45 + .../src/workers/runner-worker/index.ts | 491 + .../src/workers/runner-worker/keys.ts | 56 + .../workers/runner-worker/module-runner.ts | 308 + .../src/workers/vite-proxy-worker/index.ts | 64 + packages/vite-plugin-cloudflare/tsconfig.json | 7 + .../tsconfig.plugin.json | 9 + .../tsconfig.worker.json | 4 + .../vite-plugin-cloudflare/tsdown.config.ts | 90 + packages/vite-plugin-cloudflare/turbo.json | 15 + .../vite-plugin-cloudflare/vitest.config.ts | 14 + packages/vitest-pool-workers/AGENTS.md | 61 + packages/vitest-pool-workers/CHANGELOG.md | 3277 ++ packages/vitest-pool-workers/CONTRIBUTING.md | 7 + packages/vitest-pool-workers/README.md | 12 + packages/vitest-pool-workers/package.json | 97 + packages/vitest-pool-workers/scripts/deps.ts | 16 + .../scripts/rtti/query.mjs | 48 + .../scripts/rtti/rtti.capnp | 335 + .../vitest-pool-workers/scripts/rtti/rtti.js | 1764 + .../src/codemods/vitest-v3-to-v4.ts | 271 + .../src/pool/cloudflare-pool-worker.ts | 312 + .../src/pool/compatibility-flag-assertions.ts | 178 + .../vitest-pool-workers/src/pool/config.ts | 382 + packages/vitest-pool-workers/src/pool/d1.ts | 36 + .../src/pool/guess-exports.ts | 76 + .../vitest-pool-workers/src/pool/helpers.ts | 26 + .../vitest-pool-workers/src/pool/index.ts | 853 + .../vitest-pool-workers/src/pool/loopback.ts | 85 + .../src/pool/module-fallback.ts | 590 + .../vitest-pool-workers/src/pool/pages.ts | 59 + .../vitest-pool-workers/src/pool/plugin.ts | 146 + packages/vitest-pool-workers/src/pool/pool.ts | 18 + .../src/shared/builtin-modules.ts | 5 + packages/vitest-pool-workers/src/shared/d1.ts | 6 + .../src/shared/types-global.d.ts | 6 + packages/vitest-pool-workers/src/worker/d1.ts | 86 + .../src/worker/durable-objects.ts | 281 + .../src/worker/entrypoints.ts | 635 + .../vitest-pool-workers/src/worker/env.ts | 53 + .../vitest-pool-workers/src/worker/events.ts | 483 + .../src/worker/fetch-mock.ts | 6 + .../vitest-pool-workers/src/worker/index.ts | 276 + .../src/worker/lib/README.md | 13 + .../worker/lib/cloudflare/empty-internal.cts | 1 + .../src/worker/lib/cloudflare/snapshot.ts | 43 + .../worker/lib/cloudflare/test-internal.ts | 13 + .../src/worker/lib/cloudflare/test.ts | 27 + .../src/worker/node/console.ts | 139 + .../vitest-pool-workers/src/worker/node/vm.ts | 14 + .../src/worker/patch-ctx.ts | 70 + .../vitest-pool-workers/src/worker/reset.ts | 21 + .../src/worker/secrets-store.ts | 31 + .../src/worker/tsconfig.json | 12 + .../src/worker/types-ambient.d.ts | 88 + .../src/worker/types-globals.d.ts | 19 + .../src/worker/wait-until.ts | 100 + .../src/worker/workflows.ts | 43 + packages/vitest-pool-workers/test/README.md | 11 + .../vitest-pool-workers/test/bindings.test.ts | 113 + .../vitest-pool-workers/test/chunking.test.ts | 35 + .../vitest-pool-workers/test/codemods.test.ts | 206 + .../compatibility-flag-assertions.test.ts | 276 + .../vitest-pool-workers/test/console.test.ts | 110 + .../vitest-pool-workers/test/coverage.test.ts | 120 + .../test/do-exports.test.ts | 62 + packages/vitest-pool-workers/test/env.d.ts | 7 + .../test/file-url-import.test.ts | 36 + .../test/filtering.test.ts | 34 + .../vitest-pool-workers/test/global-setup.ts | 85 + packages/vitest-pool-workers/test/helpers.ts | 201 + .../test/inspector.test.ts | 245 + .../test/isolation.test.ts | 188 + .../test/remote-proxy-cleanup.test.ts | 69 + .../test/snapshots.test.ts | 189 + .../test/structured-logs.test.ts | 35 + .../test/unhandled-rejection.test.ts | 61 + .../test/validation.test.ts | 183 + .../test/wait-until-timeout.test.ts | 31 + .../vitest-pool-workers/test/watch.test.ts | 191 + packages/vitest-pool-workers/tsconfig.json | 19 + packages/vitest-pool-workers/tsdown.config.ts | 106 + packages/vitest-pool-workers/turbo.json | 13 + .../types/cloudflare-test.d.ts | 861 + .../vitest-pool-workers/types/tsconfig.json | 7 + .../vitest-pool-workers.svg | 86 + .../vitest-pool-workers/vitest.config.mts | 12 + packages/workers-auth/AGENTS.md | 140 + packages/workers-auth/CHANGELOG.md | 125 + packages/workers-auth/LICENSE-APACHE | 176 + packages/workers-auth/LICENSE-MIT | 25 + packages/workers-auth/README.md | 31 + packages/workers-auth/package.json | 72 + packages/workers-auth/scripts/deps.ts | 15 + packages/workers-auth/src/access.ts | 201 + packages/workers-auth/src/callback-server.ts | 296 + packages/workers-auth/src/config-file/auth.ts | 15 + .../workers-auth/src/config-file/index.ts | 33 + .../workers-auth/src/config-file/temporary.ts | 19 + packages/workers-auth/src/context.ts | 150 + .../src/credential-store/crypto.ts | 169 + .../credential-store/encrypted-file-store.ts | 273 + .../src/credential-store/file-store.ts | 135 + .../src/credential-store/index.ts | 82 + .../src/credential-store/interface.ts | 39 + .../credential-store/key-providers/factory.ts | 125 + .../key-providers/interface.ts | 36 + .../key-providers/lazy-installer.ts | 286 + .../key-providers/linux-secret-tool.ts | 166 + .../key-providers/mac-security.ts | 129 + .../key-providers/napi-keyring.ts | 77 + .../credential-store/key-providers/shared.ts | 82 + .../src/credential-store/resolver.ts | 412 + .../src/credential-store/state.ts | 48 + packages/workers-auth/src/credentials.ts | 137 + packages/workers-auth/src/env-vars.ts | 110 + packages/workers-auth/src/errors.ts | 242 + packages/workers-auth/src/flow.ts | 567 + .../workers-auth/src/generate-auth-url.ts | 36 + .../workers-auth/src/generate-random-state.ts | 17 + packages/workers-auth/src/index.ts | 106 + packages/workers-auth/src/pkce.ts | 78 + packages/workers-auth/src/pow.ts | 56 + packages/workers-auth/src/profiles.ts | 201 + packages/workers-auth/src/state.ts | 118 + packages/workers-auth/src/temporary.ts | 276 + .../workers-auth/src/test-helpers/index.ts | 2 + .../src/test-helpers/msw-handlers/access.ts | 20 + .../src/test-helpers/msw-handlers/oauth.ts | 39 + packages/workers-auth/src/token-exchange.ts | 378 + packages/workers-auth/tests/access.test.ts | 224 + .../tests/credential-store/crypto.test.ts | 177 + .../encrypted-file-store.test.ts | 347 + .../tests/credential-store/file-store.test.ts | 95 + .../key-providers/integration.test.ts | 178 + .../key-providers/lazy-installer.test.ts | 216 + .../key-providers/linux-secret-tool.test.ts | 210 + .../key-providers/mac-security.test.ts | 187 + .../tests/credential-store/resolver.test.ts | 399 + .../workers-auth/tests/credentials.test.ts | 102 + .../tests/generate-auth-url.test.ts | 24 + packages/workers-auth/tests/pow.test.ts | 57 + packages/workers-auth/tests/state.test.ts | 53 + packages/workers-auth/tests/tsconfig.json | 9 + packages/workers-auth/tests/vitest.global.ts | 8 + packages/workers-auth/tests/vitest.setup.ts | 34 + packages/workers-auth/tsconfig.json | 10 + packages/workers-auth/tsup.config.ts | 20 + packages/workers-auth/turbo.json | 16 + packages/workers-auth/vitest.config.mts | 15 + packages/workers-editor-shared/.gitignore | 25 + packages/workers-editor-shared/CHANGELOG.md | 21 + packages/workers-editor-shared/lib/Frame.tsx | 15 + .../workers-editor-shared/lib/SplitPane.tsx | 94 + .../workers-editor-shared/lib/constants.ts | 6 + packages/workers-editor-shared/lib/index.ts | 27 + packages/workers-editor-shared/lib/ipc.ts | 89 + packages/workers-editor-shared/lib/types.ts | 4 + .../lib/useInjectSources.tsx | 111 + .../lib/useRefreshableIframe.tsx | 109 + packages/workers-editor-shared/package.json | 56 + .../workers-editor-shared/scripts/deps.ts | 11 + packages/workers-editor-shared/tsconfig.json | 19 + .../workers-editor-shared/tsconfig.node.json | 12 + packages/workers-editor-shared/turbo.json | 10 + packages/workers-editor-shared/vite.config.ts | 44 + packages/workers-playground/.env | 1 + packages/workers-playground/.gitignore | 25 + packages/workers-playground/CHANGELOG.md | 174 + packages/workers-playground/README.md | 30 + .../generate-default-hashes.ts | 126 + packages/workers-playground/index.html | 14 + packages/workers-playground/package.json | 62 + .../workers-playground/public/favicon.ico | Bin 0 -> 15086 bytes packages/workers-playground/public/flares.png | Bin 0 -> 96549 bytes .../public/welcome-style.css | 113 + .../public/workers-logo-dark.svg | 133 + .../public/workers-logo.svg | 132 + packages/workers-playground/src/App.tsx | 7 + .../src/QuickEditor/DevtoolsIframe.tsx | 485 + .../src/QuickEditor/EditorPane.tsx | 31 + .../src/QuickEditor/ExpandableLogMessage.tsx | 284 + .../src/QuickEditor/FrameErrorBoundary.tsx | 37 + .../src/QuickEditor/HTTPTab/HTTPTab.tsx | 234 + .../QuickEditor/HTTPTab/RequestHeaders.tsx | 69 + .../src/QuickEditor/HTTPTab/ResponseView.tsx | 89 + .../src/QuickEditor/HTTPTab/fetchWorker.ts | 23 + .../src/QuickEditor/InputField.tsx | 14 + .../src/QuickEditor/InvocationIcon.tsx | 25 + .../MigrateToWrangler/DocsLink.tsx | 42 + .../MigrateToWrangler/MigrateToWrangler.tsx | 82 + .../MigrateToWrangler/StepNumber.tsx | 28 + .../src/QuickEditor/PreviewTab/PreviewTab.tsx | 103 + .../src/QuickEditor/PreviewTab/UrlBar.tsx | 59 + .../src/QuickEditor/QuickEditor.tsx | 127 + .../src/QuickEditor/TabBar.tsx | 152 + .../src/QuickEditor/ToolsPane.tsx | 106 + .../src/QuickEditor/TopBar.tsx | 148 + .../src/QuickEditor/VSCodeEditor.tsx | 185 + .../src/QuickEditor/WorkersLogo.tsx | 159 + .../src/QuickEditor/defaultHashes.js | 14 + .../src/QuickEditor/felaRenderer.ts | 15 + .../src/QuickEditor/getPlaygroundWorker.ts | 74 + .../src/QuickEditor/module-collection.ts | 214 + .../src/QuickEditor/useDraftWorker.ts | 205 + packages/workers-playground/src/global.ts | 2 + packages/workers-playground/src/index.css | 24 + packages/workers-playground/src/main.tsx | 34 + packages/workers-playground/src/vite-env.d.ts | 1 + packages/workers-playground/src/worker.ts | 79 + packages/workers-playground/tsconfig.json | 22 + .../workers-playground/tsconfig.node.json | 12 + packages/workers-playground/turbo.json | 13 + packages/workers-playground/vite.config.ts | 38 + packages/workers-playground/welcome/data.js | 4 + packages/workers-playground/welcome/index.js | 29 + packages/workers-playground/welcome/index.py | 7 + .../workers-playground/welcome/welcome.html | 41 + .../workers-playground/wrangler.dev.jsonc | 16 + packages/workers-playground/wrangler.jsonc | 14 + packages/workers-shared/CHANGELOG.md | 492 + packages/workers-shared/README.md | 32 + .../workers-shared/asset-worker/README.md | 3 + .../asset-worker/fixtures/AssetManifest.bin | Bin 0 -> 140 bytes packages/workers-shared/asset-worker/index.ts | 2 + .../asset-worker/src/analytics.ts | 123 + .../asset-worker/src/assets-manifest.ts | 125 + .../asset-worker/src/compatibility-flags.ts | 55 + .../asset-worker/src/configuration.ts | 28 + .../asset-worker/src/constants.ts | 2 + .../asset-worker/src/experiment-analytics.ts | 54 + .../asset-worker/src/handler.ts | 1070 + .../workers-shared/asset-worker/src/types.ts | 13 + .../src/utils/final-operations.ts | 41 + .../asset-worker/src/utils/headers.ts | 113 + .../asset-worker/src/utils/kv.ts | 59 + .../asset-worker/src/utils/rules-engine.ts | 176 + .../workers-shared/asset-worker/src/worker.ts | 578 + .../asset-worker/tests/analytics.test.ts | 90 + .../tests/assets-manifest.test.ts | 246 + .../asset-worker/tests/cohort.test.ts | 85 + .../tests/compatibility-flags.test.ts | 72 + .../asset-worker/tests/handler.test.ts | 1559 + .../asset-worker/tests/kv.test.ts | 114 + .../asset-worker/tests/rules-engine.test.ts | 385 + .../asset-worker/tests/tsconfig.json | 10 + .../asset-worker/tests/types.d.ts | 4 + .../workers-shared/asset-worker/tsconfig.json | 17 + .../asset-worker/vitest.config.mts | 17 + .../asset-worker/worker-configuration.d.ts | 32 + .../asset-worker/wrangler.jsonc | 167 + packages/workers-shared/index.ts | 6 + packages/workers-shared/package.json | 67 + .../workers-shared/router-worker/README.md | 3 + .../workers-shared/router-worker/index.ts | 2 + .../router-worker/src/analytics.ts | 119 + .../router-worker/src/configuration.ts | 27 + .../router-worker/src/limited-response.ts | 1480 + .../workers-shared/router-worker/src/types.ts | 20 + .../router-worker/src/worker.ts | 387 + .../router-worker/tests/index.test.ts | 1061 + .../router-worker/tests/tsconfig.json | 10 + .../router-worker/tsconfig.json | 17 + .../router-worker/vitest.config.mts | 16 + .../router-worker/worker-configuration.d.ts | 9 + .../router-worker/wrangler.jsonc | 131 + .../scripts/copy-config-file.ts | 16 + .../workers-shared/scripts/get-version-tag.ts | 10 + .../scripts/upload-sourcemaps.ts | 70 + packages/workers-shared/tsconfig.json | 11 + packages/workers-shared/turbo.json | 5 + .../utils/configuration/constants.ts | 18 + .../configuration/constructConfiguration.ts | 164 + .../utils/configuration/parseHeaders.ts | 221 + .../utils/configuration/parseRedirects.ts | 182 + .../utils/configuration/parseStaticRouting.ts | 90 + .../utils/configuration/types.ts | 44 + .../utils/configuration/validateURL.ts | 76 + packages/workers-shared/utils/constants.ts | 36 + packages/workers-shared/utils/helpers.ts | 93 + packages/workers-shared/utils/performance.ts | 16 + packages/workers-shared/utils/responses.ts | 144 + packages/workers-shared/utils/sentry.ts | 68 + .../utils/tests/helpers.test.ts | 63 + .../utils/tests/parseHeaders.invalid.test.ts | 304 + .../utils/tests/parseHeaders.valid.test.ts | 178 + .../tests/parseRedirects.invalid.test.ts | 370 + .../utils/tests/parseRedirects.valid.test.ts | 440 + .../utils/tests/parseStaticRouting.test.ts | 97 + packages/workers-shared/utils/tracing.ts | 32 + packages/workers-shared/utils/tsconfig.json | 17 + packages/workers-shared/utils/types.ts | 140 + packages/workers-tsconfig/base.json | 19 + packages/workers-tsconfig/client.json | 8 + packages/workers-tsconfig/package.json | 9 + packages/workers-tsconfig/react.json | 8 + packages/workers-tsconfig/tsconfig.json | 20 + .../workers-tsconfig/worker-node-compat.json | 11 + packages/workers-tsconfig/worker.json | 7 + packages/workers-utils/AGENTS.md | 35 + packages/workers-utils/CHANGELOG.md | 687 + packages/workers-utils/README.md | 5 + packages/workers-utils/package.json | 91 + packages/workers-utils/scripts/deps.ts | 15 + packages/workers-utils/src/assert-never.ts | 1 + packages/workers-utils/src/browser.ts | 1 + packages/workers-utils/src/cfetch/errors.ts | 35 + packages/workers-utils/src/cfetch/index.ts | 544 + packages/workers-utils/src/cloudflared.ts | 736 + .../workers-utils/src/compatibility-date.ts | 39 + packages/workers-utils/src/config/README.md | 107 + .../src/config/binding-local-support.ts | 83 + .../src/config/config-helpers.ts | 183 + packages/workers-utils/src/config/config.ts | 431 + .../workers-utils/src/config/diagnostics.ts | 91 + .../src/config/durable-object-exports.ts | 18 + .../workers-utils/src/config/environment.ts | 1818 + packages/workers-utils/src/config/exports.ts | 31 + packages/workers-utils/src/config/index.ts | 152 + .../workers-utils/src/config/patch-config.ts | 97 + .../src/config/validation-helpers.ts | 685 + .../src/config/validation-pages.ts | 199 + .../workers-utils/src/config/validation.ts | 6591 ++++ packages/workers-utils/src/constants.ts | 14 + .../src/construct-wrangler-config.ts | 117 + .../src/environment-variables/factory.ts | 293 + .../environment-variables/misc-variables.ts | 430 + packages/workers-utils/src/errors.ts | 89 + packages/workers-utils/src/format-time.ts | 3 + packages/workers-utils/src/fs-helpers.ts | 79 + .../src/global-wrangler-config-path.ts | 61 + packages/workers-utils/src/index.ts | 166 + packages/workers-utils/src/logger.ts | 26 + .../src/map-worker-metadata-bindings.ts | 438 + packages/workers-utils/src/package-manager.ts | 54 + .../workers-utils/src/package-resolution.ts | 146 + packages/workers-utils/src/parse.ts | 458 + .../workers-utils/src/prometheus-metrics.ts | 84 + packages/workers-utils/src/retry.ts | 45 + packages/workers-utils/src/route-utils.ts | 84 + .../workers-utils/src/test-helpers/index.ts | 17 + .../workers-utils/src/test-helpers/mock.ts | 108 + .../src/test-helpers/normalize.ts | 113 + .../src/test-helpers/run-in-tmp.ts | 34 + .../workers-utils/src/test-helpers/seed.ts | 13 + .../src/test-helpers/wrangler-config.ts | 75 + packages/workers-utils/src/tunnel.ts | 339 + packages/workers-utils/src/types.ts | 713 + packages/workers-utils/src/update-check.ts | 64 + packages/workers-utils/src/worker-name.ts | 143 + packages/workers-utils/src/worker.ts | 553 + .../workers-utils/src/wrangler-tmp-dir.ts | 111 + packages/workers-utils/src/xdg-app-paths.ts | 209 + .../workers-utils/tests/cfetch-utils.test.ts | 59 + .../workers-utils/tests/cloudflared.test.ts | 143 + .../tests/compatibility-date.test.ts | 27 + .../tests/config/config-format.test.ts | 47 + .../tests/config/configSchema.test.ts | 18 + .../tests/config/exports.test.ts | 35 + .../tests/config/findWranglerConfig.test.ts | 261 + .../tests/config/patch-config.test.ts | 894 + .../config/validation/isDockerFile.test.ts | 76 + ...ormalize-and-validate-config.pages.test.ts | 1050 + .../normalize-and-validate-config.test.ts | 10782 ++++++ .../tests/construct-wrangler-config.test.ts | 376 + .../map-worker-metadata-bindings.test.ts | 726 + packages/workers-utils/tests/parse.test.ts | 406 + .../tests/prometheus-metrics.test.ts | 426 + packages/workers-utils/tests/tsconfig.json | 9 + packages/workers-utils/tests/tunnel.test.ts | 490 + packages/workers-utils/tests/vitest.global.ts | 8 + .../workers-utils/tests/worker-name.test.ts | 84 + .../tests/wrangler-tmp-dir.test.ts | 72 + .../workers-utils/tests/xdg-app-paths.test.ts | 321 + packages/workers-utils/tsconfig.json | 10 + packages/workers-utils/tsup.config.ts | 30 + packages/workers-utils/turbo.json | 16 + packages/workers-utils/vitest.config.mts | 14 + packages/workflows-shared/CHANGELOG.md | 296 + packages/workflows-shared/README.md | 3 + packages/workflows-shared/package.json | 59 + packages/workflows-shared/src/binding.ts | 464 + packages/workflows-shared/src/context.ts | 1640 + packages/workflows-shared/src/engine.ts | 1358 + packages/workflows-shared/src/index.ts | 5 + packages/workflows-shared/src/instance.ts | 224 + .../workflows-shared/src/introspection.ts | 292 + packages/workflows-shared/src/lib/cache.ts | 10 + packages/workflows-shared/src/lib/delay.ts | 79 + packages/workflows-shared/src/lib/errors.ts | 127 + .../src/lib/gracePeriodSemaphore.ts | 190 + packages/workflows-shared/src/lib/restart.ts | 171 + packages/workflows-shared/src/lib/retries.ts | 46 + packages/workflows-shared/src/lib/rollback.ts | 211 + packages/workflows-shared/src/lib/streams.ts | 698 + .../src/lib/timePriorityQueue.ts | 296 + .../workflows-shared/src/lib/validators.ts | 87 + .../src/local-binding-worker.ts | 2 + packages/workflows-shared/src/modifier.ts | 251 + packages/workflows-shared/src/types.ts | 89 + .../workflows-shared/tests/binding.test.ts | 695 + .../workflows-shared/tests/context.test.ts | 1453 + .../workflows-shared/tests/engine.test.ts | 2307 ++ packages/workflows-shared/tests/env.d.ts | 15 + .../workflows-shared/tests/lib/delay.test.ts | 164 + packages/workflows-shared/tests/test-entry.ts | 51 + packages/workflows-shared/tests/tsconfig.json | 11 + packages/workflows-shared/tests/utils.ts | 74 + .../workflows-shared/tests/validators.test.ts | 123 + packages/workflows-shared/tsconfig.json | 18 + packages/workflows-shared/turbo.json | 12 + packages/workflows-shared/vitest.config.mts | 19 + packages/workflows-shared/wrangler.jsonc | 17 + packages/wrangler/AGENTS.md | 57 + packages/wrangler/CHANGELOG.md | 17674 ++++++++++ packages/wrangler/CONTRIBUTING.md | 208 + packages/wrangler/README.md | 63 + packages/wrangler/bin/cf-wrangler.js | 131 + packages/wrangler/bin/wrangler.js | 91 + packages/wrangler/e2e/README.md | 206 + .../e2e/__snapshots__/dev.test.ts.snap | 37 + packages/wrangler/e2e/agent-memory.test.ts | 87 + .../wrangler/e2e/assets-multiworker.test.ts | 641 + packages/wrangler/e2e/auth-scopes.test.ts | 32 + .../fixtures/hono-cf-workers-app/package.json | 15 + .../fixtures/hono-cf-workers-app/src/index.ts | 9 + .../hono-cf-workers-app/tsconfig.json | 12 + .../hono-cf-workers-app/wrangler.jsonc | 6 + .../fixtures/hono-nodejs-app/package.json | 18 + .../fixtures/hono-nodejs-app/src/index.ts | 18 + .../fixtures/hono-nodejs-app/tsconfig.json | 14 + .../wrangler/e2e/autoconfig/setup.test.ts | 35 + packages/wrangler/e2e/c3-integration.test.ts | 57 + packages/wrangler/e2e/cert.test.ts | 84 + packages/wrangler/e2e/containers.dev.test.ts | 603 + .../wrangler/e2e/createTestHarness.test.ts | 1973 ++ packages/wrangler/e2e/deploy.test.ts | 138 + packages/wrangler/e2e/deployments.test.ts | 1050 + packages/wrangler/e2e/dev-env.test.ts | 123 + packages/wrangler/e2e/dev-registry.test.ts | 481 + packages/wrangler/e2e/dev.test.ts | 2942 ++ .../e2e/durable-objects-exports.test.ts | 533 + .../wrangler/e2e/get-platform-proxy.test.ts | 700 + packages/wrangler/e2e/helpers/account-id.ts | 12 + packages/wrangler/e2e/helpers/cert.ts | 117 + packages/wrangler/e2e/helpers/command.ts | 199 + .../wrangler/e2e/helpers/e2e-wrangler-test.ts | 494 + packages/wrangler/e2e/helpers/fetch-json.ts | 29 + packages/wrangler/e2e/helpers/fetch-text.ts | 15 + .../wrangler/e2e/helpers/fetch-with-etag.ts | 12 + .../e2e/helpers/generate-resource-name.ts | 5 + .../e2e/helpers/mysql-echo-handler.ts | 62 + packages/wrangler/e2e/helpers/normalize.ts | 260 + .../e2e/helpers/postgres-echo-handler.ts | 21 + packages/wrangler/e2e/helpers/read-until.ts | 42 + packages/wrangler/e2e/helpers/retry.ts | 24 + packages/wrangler/e2e/helpers/setup.ts | 34 + packages/wrangler/e2e/helpers/wait-for.ts | 34 + .../wrangler/e2e/helpers/workerd-processes.ts | 30 + packages/wrangler/e2e/helpers/wrangler.ts | 95 + packages/wrangler/e2e/multiworker-dev.test.ts | 652 + packages/wrangler/e2e/pages-deploy.test.ts | 52 + packages/wrangler/e2e/pages-dev.test.ts | 903 + packages/wrangler/e2e/provision.test.ts | 261 + packages/wrangler/e2e/r2.test.ts | 127 + .../dev-remote-bindings.test.ts | 333 + .../miniflare-remote-resources.test.ts | 891 + .../remote-bindings-api.test.ts | 196 + .../start-worker-remote-bindings.test.ts | 158 + .../remote-binding/workers/agent-memory.js | 7 + .../workers/ai-search-instance.js | 6 + .../workers/ai-search-namespace.js | 20 + .../wrangler/e2e/remote-binding/workers/ai.js | 18 + .../workers/alt-remote-worker.js | 5 + .../e2e/remote-binding/workers/browser.js | 7 + .../wrangler/e2e/remote-binding/workers/d1.js | 8 + .../workers/dispatch-namespace.js | 9 + .../e2e/remote-binding/workers/email.js | 23 + .../e2e/remote-binding/workers/images.js | 16 + .../e2e/remote-binding/workers/index.js | 7 + .../wrangler/e2e/remote-binding/workers/kv.js | 7 + .../local-and-remote-service-bindings.js | 13 + .../local-service-binding-and-remote-ai.js | 27 + .../e2e/remote-binding/workers/media.js | 16 + .../e2e/remote-binding/workers/mixed-kvs.js | 8 + .../e2e/remote-binding/workers/mtls.js | 5 + .../e2e/remote-binding/workers/pipelines.js | 11 + .../wrangler/e2e/remote-binding/workers/r2.js | 7 + .../remote-binding/workers/remote-worker.js | 5 + .../remote-binding/workers/service-binding.js | 11 + .../workers/simple-service-binding.js | 8 + .../e2e/remote-binding/workers/vectorize.js | 59 + .../e2e/remote-binding/workers/vpc-network.js | 15 + .../e2e/remote-binding/workers/vpc-service.js | 11 + packages/wrangler/e2e/secrets-store.test.ts | 186 + .../e2e/start-worker-auth-opts.test.ts | 318 + packages/wrangler/e2e/startWorker.test.ts | 959 + packages/wrangler/e2e/tsconfig.json | 10 + packages/wrangler/e2e/types.test.ts | 451 + .../wrangler/e2e/unenv-preset/preset.test.ts | 1142 + .../wrangler/e2e/unenv-preset/worker/index.ts | 1145 + .../e2e/unenv-preset/worker/wrangler.json | 6 + packages/wrangler/e2e/validate-environment.ts | 50 + packages/wrangler/e2e/versions.test.ts | 702 + packages/wrangler/e2e/vitest.config.mts | 23 + packages/wrangler/e2e/vitest.setup.ts | 38 + packages/wrangler/import_meta_url.js | 3 + packages/wrangler/kv-asset-handler.js | 1 + packages/wrangler/package.json | 207 + .../wrangler/scripts/assert-git-version.ts | 19 + packages/wrangler/scripts/deps.ts | 75 + .../wrangler/scripts/generate-json-schema.ts | 25 + .../scripts/node-options-for-tests.js | 23 + .../src/__tests__/agent-memory.test.ts | 428 + .../__tests__/agents-skills-install.test.ts | 1704 + .../wrangler/src/__tests__/ai-search.test.ts | 2626 ++ .../wrangler/src/__tests__/ai.local.test.ts | 148 + packages/wrangler/src/__tests__/ai.test.ts | 1104 + packages/wrangler/src/__tests__/api.test.ts | 122 + .../startDevWorker/BundleController.test.ts | 754 + .../BundlerController.emfile.test.ts | 138 + .../startDevWorker/ConfigController.test.ts | 320 + .../api/startDevWorker/DevEnv.test.ts | 99 + .../LocalRuntimeController.test.ts | 1617 + .../MultiworkerRuntimeController.test.ts | 286 + .../RemoteRuntimeController.test.ts | 531 + .../api/startDevWorker/utils.test.ts | 234 + .../wrangler/src/__tests__/artifacts.test.ts | 463 + .../wrangler/src/__tests__/assets.test.ts | 172 + .../src/__tests__/auth-credentials.test.ts | 90 + .../src/__tests__/autoconfig/index.test.ts | 268 + .../src/__tests__/autoconfig/run.test.ts | 940 + .../wrangler/src/__tests__/banner.test.ts | 44 + .../src/__tests__/browser-run.test.ts | 1011 + .../src/__tests__/build-output.test.ts | 334 + packages/wrangler/src/__tests__/cert.test.ts | 801 + .../src/__tests__/cf-wrangler/args.test.ts | 205 + .../src/__tests__/cf-wrangler/build.test.ts | 62 + .../src/__tests__/cfetch-internal.test.ts | 255 + .../src/__tests__/cfetch-utils.test.ts | 179 + .../src/__tests__/check-fetch.test.ts | 29 + .../src/__tests__/cli-hotkeys.test.ts | 344 + packages/wrangler/src/__tests__/cli.test.ts | 26 + .../src/__tests__/cloudchamber/apply.test.ts | 2084 ++ .../src/__tests__/cloudchamber/build.test.ts | 592 + .../src/__tests__/cloudchamber/common.test.ts | 63 + .../src/__tests__/cloudchamber/create.test.ts | 432 + .../src/__tests__/cloudchamber/curl.test.ts | 419 + .../src/__tests__/cloudchamber/delete.test.ts | 107 + .../src/__tests__/cloudchamber/images.test.ts | 527 + .../cloudchamber/instance-type.test.ts | 71 + .../src/__tests__/cloudchamber/limits.test.ts | 261 + .../src/__tests__/cloudchamber/list.test.ts | 181 + .../src/__tests__/cloudchamber/modify.test.ts | 168 + .../src/__tests__/cloudchamber/utils.ts | 72 + .../wrangler/src/__tests__/complete.test.ts | 204 + .../config-cache-wrangler-fallback.test.ts | 53 + .../src/__tests__/config-cache.test.ts | 115 + .../__tests__/config-validation-pages.test.ts | 351 + .../__tests__/config/configuration.test.ts | 309 + .../src/__tests__/config/loadDotEnv.test.ts | 119 + .../src/__tests__/containers/config.test.ts | 917 + .../src/__tests__/containers/delete.test.ts | 148 + .../src/__tests__/containers/deploy.test.ts | 3498 ++ .../src/__tests__/containers/images.test.ts | 365 + .../src/__tests__/containers/info.test.ts | 126 + .../__tests__/containers/instances.test.ts | 432 + .../src/__tests__/containers/list.test.ts | 499 + .../src/__tests__/containers/push.test.ts | 155 + .../__tests__/containers/registries.test.ts | 1624 + .../src/__tests__/containers/schema.test.ts | 47 + .../src/__tests__/containers/ssh.test.ts | 297 + .../core/command-registration.test.ts | 278 + .../src/__tests__/core/handle-errors.test.ts | 817 + .../core/main-handleerror-fallback.test.ts | 27 + .../bindings.test.ts | 622 + .../commonjs.test.ts | 115 + .../create-worker-upload-form/helpers.ts | 88 + .../metadata.test.ts | 328 + .../mime-types.test.ts | 42 + .../src/__tests__/custom-build.test.ts | 148 + .../d1/convert-timestamp-to-iso.test.ts | 99 + .../wrangler/src/__tests__/d1/create.test.ts | 184 + packages/wrangler/src/__tests__/d1/d1.test.ts | 137 + .../wrangler/src/__tests__/d1/delete.test.ts | 128 + .../wrangler/src/__tests__/d1/execute.test.ts | 378 + .../wrangler/src/__tests__/d1/export.test.ts | 392 + .../src/__tests__/d1/fixtures/db.sqlite3 | Bin 0 -> 4096 bytes .../wrangler/src/__tests__/d1/info.test.ts | 173 + .../src/__tests__/d1/insights.test.ts | 171 + .../wrangler/src/__tests__/d1/list.test.ts | 83 + .../wrangler/src/__tests__/d1/migrate.test.ts | 936 + .../__tests__/d1/migrations/helpers.test.ts | 988 + .../src/__tests__/d1/splitter.test.ts | 420 + .../src/__tests__/d1/timeTravel.test.ts | 254 + .../wrangler/src/__tests__/d1/trimmer.test.ts | 250 + .../wrangler/src/__tests__/d1/utils.test.ts | 334 + .../wrangler/src/__tests__/delete.test.ts | 706 + .../src/__tests__/deploy/assets.test.ts | 1667 + .../deploy/binding-depends-on-export.test.ts | 28 + .../src/__tests__/deploy/bindings.test.ts | 2578 ++ .../src/__tests__/deploy/build.test.ts | 1487 + .../check-remote-secrets-override.test.ts | 222 + .../deploy/check-workflow-conflicts.test.ts | 283 + .../deploy/config-args-merging.test.ts | 1365 + .../__tests__/deploy/config-remote.test.ts | 718 + .../src/__tests__/deploy/core.test.ts | 1983 ++ .../deploy/deploy-interactive-prompts.test.ts | 1254 + .../__tests__/deploy/durable-objects.test.ts | 1312 + .../src/__tests__/deploy/entry-points.test.ts | 1278 + .../src/__tests__/deploy/environments.test.ts | 1174 + .../deploy/exports-reconciliation.test.ts | 231 + .../src/__tests__/deploy/formats.test.ts | 1288 + .../__tests__/deploy/get-config-patch.test.ts | 312 + .../deploy/get-remote-config-diff.test.ts | 839 + .../wrangler/src/__tests__/deploy/helpers.ts | 883 + .../deploy/inconsistent-exports.test.ts | 46 + .../__tests__/deploy/legacy-assets.test.ts | 1681 + .../deploy/name-collision-guard.test.ts | 162 + .../src/__tests__/deploy/open-next.test.ts | 466 + .../src/__tests__/deploy/queues.test.ts | 707 + .../src/__tests__/deploy/routes.test.ts | 1486 + .../src/__tests__/deploy/secrets.test.ts | 491 + .../src/__tests__/deploy/workers-dev.test.ts | 1496 + .../src/__tests__/deploy/workflows.test.ts | 1344 + .../deployment-bundle/source-maps.test.ts | 178 + .../src/__tests__/deployments.test.ts | 92 + packages/wrangler/src/__tests__/dev.test.ts | 3188 ++ .../src/__tests__/dev/dev-vars.test.ts | 155 + .../dev/get-local-persistence-path.test.ts | 64 + .../dev/remote-bindings-errors.test.ts | 101 + .../src/__tests__/dev/remote-bindings.test.ts | 845 + .../wrangler/src/__tests__/dev/remote.test.ts | 81 + .../src/__tests__/dev/start-dev.test.ts | 79 + packages/wrangler/src/__tests__/docs.test.ts | 125 + .../src/__tests__/email-routing.test.ts | 1643 + .../wrangler/src/__tests__/errors.test.ts | 144 + .../experimental-commands-api.test.ts | 242 + .../experimental-config/convert.test.ts | 340 + .../experimental-config/load.test.ts | 412 + .../__tests__/fetch-graphql-result.test.ts | 79 + .../__tests__/find-additional-modules.test.ts | 494 + .../wrangler/src/__tests__/flagship.test.ts | 1757 + .../friendly-validator-errors.test.ts | 148 + .../wrangler/src/__tests__/get-entry.test.ts | 143 + .../src/__tests__/get-host-from-url.test.ts | 17 + .../src/__tests__/guess-worker-format.test.ts | 132 + .../src/__tests__/hello-world.local.test.ts | 57 + .../src/__tests__/helpers/assert-request.ts | 47 + .../helpers/capture-requests-from.ts | 43 + .../src/__tests__/helpers/cmd-shim.d.ts | 11 + .../__tests__/helpers/collect-cli-output.ts | 22 + .../src/__tests__/helpers/end-event-loop.ts | 6 + .../src/__tests__/helpers/fake-bus.ts | 58 + .../src/__tests__/helpers/mock-account-id.ts | 49 + .../src/__tests__/helpers/mock-auth-domain.ts | 22 + .../src/__tests__/helpers/mock-bin.ts | 37 + .../src/__tests__/helpers/mock-cli-output.ts | 43 + .../__tests__/helpers/mock-cloudchamber.ts | 329 + .../src/__tests__/helpers/mock-console.ts | 23 + .../src/__tests__/helpers/mock-dialogs.ts | 145 + .../helpers/mock-get-pages-upload-token.ts | 27 + .../helpers/mock-get-zone-from-host.ts | 57 + .../src/__tests__/helpers/mock-http-server.ts | 84 + .../src/__tests__/helpers/mock-istty.ts | 76 + .../wrangler/src/__tests__/helpers/mock-kv.ts | 89 + .../__tests__/helpers/mock-legacy-script.ts | 24 + .../src/__tests__/helpers/mock-oauth-flow.ts | 271 + .../src/__tests__/helpers/mock-process.ts | 37 + .../__tests__/helpers/mock-secrets-store.ts | 99 + .../src/__tests__/helpers/mock-set-timeout.ts | 19 + .../src/__tests__/helpers/mock-stdin.ts | 110 + .../__tests__/helpers/mock-upload-worker.ts | 343 + .../src/__tests__/helpers/mock-web-socket.ts | 53 + .../__tests__/helpers/mock-worker-settings.ts | 38 + .../helpers/mock-workers-subdomain.ts | 135 + .../src/__tests__/helpers/mock-zone-routes.ts | 41 + .../helpers/msw/handlers/deployments.ts | 181 + .../helpers/msw/handlers/namespaces.ts | 78 + .../src/__tests__/helpers/msw/handlers/r2.ts | 136 + .../__tests__/helpers/msw/handlers/script.ts | 50 + .../__tests__/helpers/msw/handlers/user.ts | 76 + .../helpers/msw/handlers/versions.ts | 341 + .../__tests__/helpers/msw/handlers/zones.ts | 20 + .../src/__tests__/helpers/msw/index.ts | 76 + .../__tests__/helpers/normalize-progress.ts | 18 + .../src/__tests__/helpers/run-wrangler.ts | 25 + .../helpers/serialize-form-data-entry.ts | 30 + .../helpers/string-dynamic-values-matcher.ts | 36 + .../src/__tests__/helpers/teardown.ts | 31 + .../src/__tests__/helpers/unusable.ts | 30 + .../__tests__/helpers/write-worker-source.ts | 65 + .../helpers/write-wrangler-config.ts | 78 + .../src/__tests__/https-options.test.ts | 91 + .../wrangler/src/__tests__/hyperdrive.test.ts | 2232 ++ packages/wrangler/src/__tests__/index.test.ts | 610 + packages/wrangler/src/__tests__/init.test.ts | 1439 + .../wrangler/src/__tests__/is-local.test.ts | 26 + .../kv/__snapshots__/namespace.test.ts.snap | 260 + .../wrangler/src/__tests__/kv/bulk.test.ts | 693 + .../wrangler/src/__tests__/kv/constant.ts | 21 + .../wrangler/src/__tests__/kv/help.test.ts | 111 + .../wrangler/src/__tests__/kv/key.test.ts | 1250 + .../wrangler/src/__tests__/kv/local.test.ts | 424 + .../src/__tests__/kv/namespace.test.ts | 797 + .../wrangler/src/__tests__/logger.test.ts | 376 + .../wrangler/src/__tests__/logout.test.ts | 299 + .../wrangler/src/__tests__/match-tag.test.ts | 283 + .../wrangler/src/__tests__/metrics.test.ts | 1020 + .../__tests__/metrics/sanitization.test.ts | 171 + .../__tests__/middleware.scheduled.test.ts | 270 + .../wrangler/src/__tests__/middleware.test.ts | 1151 + .../src/__tests__/mtls-certificates.test.ts | 665 + .../__tests__/navigator-user-agent.test.ts | 251 + .../src/__tests__/open-in-browser.test.ts | 80 + .../wrangler/src/__tests__/output.test.ts | 348 + .../src/__tests__/package-manager.test.ts | 199 + .../pages/__snapshots__/deploy.test.ts.snap | 65 + .../pages/delegate-to-workers.test.ts | 381 + .../src/__tests__/pages/deploy.test.ts | 6652 ++++ .../__tests__/pages/deployment-delete.test.ts | 160 + .../__tests__/pages/deployment-list.test.ts | 286 + .../wrangler/src/__tests__/pages/dev.test.ts | 53 + .../__tests__/pages/filepath-routing.test.ts | 290 + .../__tests__/pages/functions-build.test.ts | 1262 + .../__tests__/pages/pages-build-env.test.ts | 481 + .../pages/pages-deployment-tail.test.ts | 1239 + .../pages/pages-download-config.test.ts | 1265 + .../src/__tests__/pages/pages.test.ts | 322 + .../__tests__/pages/project-create.test.ts | 200 + .../__tests__/pages/project-delete.test.ts | 163 + .../src/__tests__/pages/project-list.test.ts | 192 + .../__tests__/pages/project-upload.test.ts | 735 + .../__tests__/pages/project-validate.test.ts | 143 + .../pages/routes-consolidation.test.ts | 276 + .../src/__tests__/pages/routes-module.test.ts | 56 + .../pages/routes-transformation.test.ts | 289 + .../__tests__/pages/routes-validation.test.ts | 416 + .../pages/run-workers-deploy.test.ts | 104 + .../src/__tests__/pages/secret.test.ts | 881 + .../__tests__/pages/utf8-truncation.test.ts | 141 + packages/wrangler/src/__tests__/paths.test.ts | 43 + .../src/__tests__/pipelines-setup.test.ts | 1474 + .../wrangler/src/__tests__/pipelines.test.ts | 2823 ++ .../src/__tests__/preview.secret.test.ts | 378 + .../src/__tests__/preview.settings.test.ts | 790 + .../wrangler/src/__tests__/preview.test.ts | 3170 ++ .../src/__tests__/print-bindings.test.ts | 244 + .../__tests__/process-env-populated.test.ts | 84 + .../wrangler/src/__tests__/profiles.test.ts | 806 + .../wrangler/src/__tests__/provision.test.ts | 1557 + .../src/__tests__/proxy-output.test.ts | 31 + .../queues/__snapshots__/queues.test.ts.snap | 105 + .../src/__tests__/queues/mock-utils.ts | 210 + .../queues/queues-subscription.test.ts | 855 + .../src/__tests__/queues/queues.test.ts | 3021 ++ .../r2/__snapshots__/bucket.test.ts.snap | 129 + .../wrangler/src/__tests__/r2/bucket.test.ts | 3820 +++ .../wrangler/src/__tests__/r2/bulk.test.ts | 294 + .../src/__tests__/r2/catalog-force.test.ts | 717 + .../wrangler/src/__tests__/r2/errors.test.ts | 21 + .../wrangler/src/__tests__/r2/help.test.ts | 75 + packages/wrangler/src/__tests__/r2/helper.ts | 29 + .../src/__tests__/r2/local-uploads.test.ts | 353 + .../wrangler/src/__tests__/r2/local.test.ts | 305 + .../src/__tests__/r2/notification.test.ts | 97 + .../wrangler/src/__tests__/r2/object.test.ts | 259 + .../wrangler/src/__tests__/r2/pipe.test.ts | 56 + .../wrangler/src/__tests__/r2/sql.test.ts | 348 + .../register-yargs-command-skills.test.ts | 125 + .../wrangler/src/__tests__/rollback.test.ts | 242 + .../wrangler/src/__tests__/secret.test.ts | 1670 + .../src/__tests__/secrets-store.test.ts | 1031 + .../wrangler/src/__tests__/sentry.test.ts | 455 + packages/wrangler/src/__tests__/setup.test.ts | 275 + .../src/__tests__/startup-profiling.test.ts | 162 + packages/wrangler/src/__tests__/tail.test.ts | 1821 + .../src/__tests__/test-old-node-version.js | 48 + .../wrangler/src/__tests__/tsconfig-sanity.ts | 9 + packages/wrangler/src/__tests__/tsconfig.json | 10 + .../__tests__/tunnel/tunnel-resolve.test.ts | 221 + .../src/__tests__/tunnel/tunnel.test.ts | 441 + .../wrangler/src/__tests__/turnstile.test.ts | 454 + .../type-generation-pipeline-schema.test.ts | 328 + .../src/__tests__/type-generation.test.ts | 4539 +++ ...table-get-miniflare-worker-options.test.ts | 234 + .../src/__tests__/update-config-file.test.ts | 468 + packages/wrangler/src/__tests__/user.test.ts | 2143 ++ .../__tests__/utils-collectKeyValues.test.ts | 58 + .../__tests__/utils-memoizeGetPort.test.ts | 85 + .../wrangler/src/__tests__/utils/box.test.ts | 149 + .../__tests__/utils/create-batches.test.ts | 31 + .../src/__tests__/utils/detect-agent.test.ts | 74 + .../src/__tests__/utils/did-you-mean.test.ts | 102 + .../__tests__/utils/format-message.test.ts | 79 + .../utils/getValidBindingName.test.ts | 44 + .../src/__tests__/utils/levenshtein.test.ts | 57 + .../src/__tests__/utils/log-file.test.ts | 207 + .../src/__tests__/utils/retry.test.ts | 233 + .../src/__tests__/utils/shell-quote.test.ts | 84 + .../src/__tests__/vectorize/vectorize.test.ts | 1778 + .../vectorize/vectorize.upsert.test.ts | 340 + .../wrangler/src/__tests__/version.test.ts | 42 + .../deployments/deployments.list.test.ts | 357 + .../deployments/deployments.status.test.ts | 155 + .../deployments/deployments.view.test.ts | 26 + .../src/__tests__/versions/secrets.test.ts | 395 + .../__tests__/versions/secrets/bulk.test.ts | 551 + .../__tests__/versions/secrets/delete.test.ts | 328 + .../__tests__/versions/secrets/list.test.ts | 287 + .../__tests__/versions/secrets/put.test.ts | 696 + .../src/__tests__/versions/secrets/utils.ts | 180 + .../versions/versions.deploy.test.ts | 1833 ++ .../__tests__/versions/versions.help.test.ts | 71 + .../__tests__/versions/versions.list.test.ts | 353 + .../versions/versions.upload.test.ts | 2816 ++ .../__tests__/versions/versions.view.test.ts | 700 + .../wrangler/src/__tests__/vitest.global.ts | 8 + .../wrangler/src/__tests__/vitest.setup.ts | 336 + packages/wrangler/src/__tests__/vpc.test.ts | 1213 + .../wrangler/src/__tests__/whoami.test.ts | 608 + .../src/__tests__/worker-namespace.test.ts | 362 + .../wrangler/src/__tests__/workflows.test.ts | 1998 ++ packages/wrangler/src/__tests__/zones.test.ts | 313 + packages/wrangler/src/abort.d.ts | 3 + packages/wrangler/src/agent-memory/client.ts | 111 + packages/wrangler/src/agent-memory/create.ts | 72 + packages/wrangler/src/agent-memory/delete.ts | 51 + packages/wrangler/src/agent-memory/get.ts | 57 + packages/wrangler/src/agent-memory/index.ts | 18 + packages/wrangler/src/agent-memory/list.ts | 48 + .../wrangler/src/agent-memory/provisioning.ts | 48 + .../wrangler/src/agents-skills-install.ts | 824 + packages/wrangler/src/ai-search.ts | 57 + packages/wrangler/src/ai-search/client.ts | 316 + packages/wrangler/src/ai-search/create.ts | 782 + packages/wrangler/src/ai-search/delete.ts | 47 + packages/wrangler/src/ai-search/get.ts | 55 + packages/wrangler/src/ai-search/index.ts | 10 + .../wrangler/src/ai-search/jobs/cancel.ts | 52 + .../wrangler/src/ai-search/jobs/create.ts | 62 + packages/wrangler/src/ai-search/jobs/get.ts | 58 + packages/wrangler/src/ai-search/jobs/index.ts | 9 + packages/wrangler/src/ai-search/jobs/list.ts | 86 + packages/wrangler/src/ai-search/jobs/logs.ts | 92 + packages/wrangler/src/ai-search/list.ts | 81 + .../src/ai-search/namespace/create.ts | 57 + .../src/ai-search/namespace/delete.ts | 41 + .../wrangler/src/ai-search/namespace/get.ts | 43 + .../wrangler/src/ai-search/namespace/index.ts | 9 + .../wrangler/src/ai-search/namespace/list.ts | 82 + .../src/ai-search/namespace/update.ts | 54 + packages/wrangler/src/ai-search/search.ts | 113 + packages/wrangler/src/ai-search/stats.ts | 52 + packages/wrangler/src/ai-search/types.ts | 188 + packages/wrangler/src/ai-search/update.ts | 139 + packages/wrangler/src/ai-search/utils.ts | 24 + packages/wrangler/src/ai/createFinetune.ts | 117 + packages/wrangler/src/ai/fetcher.ts | 61 + packages/wrangler/src/ai/index.ts | 18 + packages/wrangler/src/ai/listCatalog.ts | 118 + packages/wrangler/src/ai/listFinetune.ts | 46 + packages/wrangler/src/ai/modelSchema.ts | 31 + packages/wrangler/src/ai/types.ts | 23 + packages/wrangler/src/ai/utils.ts | 116 + .../src/api/deploy-helpers-context.ts | 24 + packages/wrangler/src/api/dev.ts | 281 + packages/wrangler/src/api/generate-types.ts | 52 + packages/wrangler/src/api/index.ts | 102 + .../wrangler/src/api/integrations/index.ts | 1 + .../src/api/integrations/platform/caches.ts | 84 + .../integrations/platform/executionContext.ts | 15 + .../src/api/integrations/platform/index.ts | 520 + packages/wrangler/src/api/mtls-certificate.ts | 204 + .../pages/create-worker-bundle-contents.ts | 82 + packages/wrangler/src/api/pages/deploy.ts | 510 + packages/wrangler/src/api/pages/index.ts | 5 + .../wrangler/src/api/remoteBindings/index.ts | 205 + .../start-remote-proxy-session.ts | 247 + .../src/api/startDevWorker/BaseController.ts | 81 + .../api/startDevWorker/BundlerController.ts | 479 + .../api/startDevWorker/ConfigController.ts | 774 + .../wrangler/src/api/startDevWorker/DevEnv.ts | 281 + .../startDevWorker/LocalRuntimeController.ts | 591 + .../MultiworkerRuntimeController.ts | 391 + .../api/startDevWorker/NoOpProxyController.ts | 14 + .../api/startDevWorker/NotImplementedError.ts | 19 + .../src/api/startDevWorker/ProxyController.ts | 684 + .../startDevWorker/RemoteRuntimeController.ts | 517 + .../src/api/startDevWorker/binding-utils.ts | 54 + .../startDevWorker/bundle-allowed-paths.ts | 115 + .../src/api/startDevWorker/devtools.ts | 13 + .../wrangler/src/api/startDevWorker/events.ts | 160 + .../wrangler/src/api/startDevWorker/index.ts | 16 + .../wrangler/src/api/startDevWorker/types.ts | 106 + .../wrangler/src/api/startDevWorker/utils.ts | 351 + packages/wrangler/src/api/test-harness.ts | 1229 + packages/wrangler/src/artifacts/client.ts | 131 + packages/wrangler/src/artifacts/index.ts | 24 + packages/wrangler/src/artifacts/namespaces.ts | 110 + packages/wrangler/src/artifacts/repos.ts | 320 + packages/wrangler/src/artifacts/types.ts | 58 + packages/wrangler/src/assets.ts | 202 + packages/wrangler/src/autoconfig-context.ts | 25 + packages/wrangler/src/autoconfig/index.ts | 145 + .../src/autoconfig/telemetry-utils.ts | 110 + .../wrangler/src/browser-rendering/close.ts | 47 + .../wrangler/src/browser-rendering/create.ts | 111 + .../wrangler/src/browser-rendering/index.ts | 15 + .../wrangler/src/browser-rendering/list.ts | 51 + .../wrangler/src/browser-rendering/types.ts | 45 + .../wrangler/src/browser-rendering/utils.ts | 82 + .../wrangler/src/browser-rendering/view.ts | 250 + packages/wrangler/src/build/index.ts | 50 + .../wrangler/src/build/run-build-output.ts | 41 + packages/wrangler/src/cert/cert.ts | 215 + packages/wrangler/src/cf-wrangler/args.ts | 96 + packages/wrangler/src/cf-wrangler/build.ts | 13 + packages/wrangler/src/cf-wrangler/dev.ts | 42 + packages/wrangler/src/cfetch/index.ts | 176 + packages/wrangler/src/cfetch/internal.ts | 359 + packages/wrangler/src/check/commands.ts | 272 + packages/wrangler/src/cli-hotkeys.ts | 132 + packages/wrangler/src/cli.ts | 154 + packages/wrangler/src/cloudchamber/apply.ts | 738 + packages/wrangler/src/cloudchamber/build.ts | 500 + .../src/cloudchamber/cli/deployments.ts | 191 + .../wrangler/src/cloudchamber/cli/index.ts | 406 + .../src/cloudchamber/cli/locations.ts | 123 + .../wrangler/src/cloudchamber/cli/util.ts | 27 + packages/wrangler/src/cloudchamber/common.ts | 598 + packages/wrangler/src/cloudchamber/create.ts | 498 + packages/wrangler/src/cloudchamber/curl.ts | 228 + packages/wrangler/src/cloudchamber/delete.ts | 102 + packages/wrangler/src/cloudchamber/enums.ts | 14 + .../wrangler/src/cloudchamber/helpers/wrap.ts | 7 + .../src/cloudchamber/images/images.ts | 335 + .../src/cloudchamber/images/registries.ts | 350 + packages/wrangler/src/cloudchamber/index.ts | 45 + .../instance-type/instance-type.ts | 200 + packages/wrangler/src/cloudchamber/limits.ts | 118 + packages/wrangler/src/cloudchamber/list.ts | 252 + .../wrangler/src/cloudchamber/locations.ts | 39 + packages/wrangler/src/cloudchamber/modify.ts | 393 + .../src/cloudchamber/network/network.ts | 18 + packages/wrangler/src/cloudchamber/ssh/ssh.ts | 428 + .../wrangler/src/cloudchamber/ssh/validate.ts | 71 + packages/wrangler/src/complete.ts | 166 + packages/wrangler/src/config-cache.ts | 109 + .../src/config/case-insensitive-env.ts | 40 + packages/wrangler/src/config/dot-env.ts | 85 + packages/wrangler/src/config/index.ts | 260 + packages/wrangler/src/containers/build.ts | 116 + packages/wrangler/src/containers/config.ts | 224 + .../wrangler/src/containers/containers.ts | 172 + packages/wrangler/src/containers/deploy.ts | 770 + packages/wrangler/src/containers/images.ts | 295 + packages/wrangler/src/containers/index.ts | 37 + packages/wrangler/src/containers/instances.ts | 355 + packages/wrangler/src/containers/list.ts | 240 + .../wrangler/src/containers/registries.ts | 840 + packages/wrangler/src/containers/ssh.ts | 410 + .../wrangler/src/core/CommandHandledError.ts | 10 + packages/wrangler/src/core/CommandRegistry.ts | 684 + packages/wrangler/src/core/create-command.ts | 32 + packages/wrangler/src/core/handle-errors.ts | 658 + packages/wrangler/src/core/helpers.ts | 129 + packages/wrangler/src/core/index.ts | 1 + .../src/core/register-yargs-command.ts | 435 + packages/wrangler/src/core/teams.d.ts | 32 + .../wrangler/src/core/temporary-commands.ts | 7 + packages/wrangler/src/core/types.ts | 330 + packages/wrangler/src/d1/constants.ts | 5 + packages/wrangler/src/d1/create.ts | 152 + packages/wrangler/src/d1/delete.ts | 65 + packages/wrangler/src/d1/execute.ts | 687 + packages/wrangler/src/d1/export.ts | 313 + packages/wrangler/src/d1/formatTimeAgo.ts | 17 + packages/wrangler/src/d1/index.ts | 10 + packages/wrangler/src/d1/info.ts | 161 + packages/wrangler/src/d1/insights.ts | 212 + packages/wrangler/src/d1/list.ts | 74 + packages/wrangler/src/d1/migrations/apply.ts | 228 + packages/wrangler/src/d1/migrations/create.ts | 154 + .../wrangler/src/d1/migrations/helpers.ts | 553 + packages/wrangler/src/d1/migrations/index.ts | 9 + packages/wrangler/src/d1/migrations/list.ts | 123 + .../wrangler/src/d1/migrations/options.ts | 28 + packages/wrangler/src/d1/options.ts | 17 + packages/wrangler/src/d1/splitter.ts | 170 + packages/wrangler/src/d1/timeTravel/index.ts | 10 + packages/wrangler/src/d1/timeTravel/info.ts | 69 + .../wrangler/src/d1/timeTravel/restore.ts | 141 + packages/wrangler/src/d1/timeTravel/types.ts | 9 + packages/wrangler/src/d1/timeTravel/utils.ts | 116 + packages/wrangler/src/d1/trimmer.ts | 37 + packages/wrangler/src/d1/types.ts | 194 + packages/wrangler/src/d1/utils.ts | 164 + packages/wrangler/src/delete.ts | 297 + packages/wrangler/src/deploy/autoconfig.ts | 358 + .../deploy/check-remote-secrets-override.ts | 11 + .../src/deploy/check-workflow-conflicts.ts | 13 + packages/wrangler/src/deploy/config-diffs.ts | 4 + packages/wrangler/src/deploy/index.ts | 213 + packages/wrangler/src/deploy/open-next.ts | 91 + .../src/deployment-bundle/apply-middleware.ts | 136 + .../auto-provisioned-name.ts | 9 + .../src/deployment-bundle/build-failures.ts | 125 + .../src/deployment-bundle/build-output.ts | 158 + .../src/deployment-bundle/bundle-reporter.ts | 1 + .../src/deployment-bundle/bundle-type.ts | 14 + .../wrangler/src/deployment-bundle/bundle.ts | 590 + .../wrangler/src/deployment-bundle/capnp.ts | 1 + .../create-worker-upload-form.ts | 5 + .../src/deployment-bundle/dedupe-modules.ts | 18 + .../src/deployment-bundle/deploy-args.ts | 273 + .../entry-point-from-metafile.ts | 36 + .../wrangler/src/deployment-bundle/entry.ts | 214 + .../esbuild-plugins/alias-external.ts | 32 + .../esbuild-plugins/als-external.ts | 13 + .../esbuild-plugins/cloudflare-internal.ts | 42 + .../esbuild-plugins/config-provider.ts | 48 + .../esbuild-plugins/hybrid-nodejs-compat.ts | 279 + .../esbuild-plugins/log-build-output.ts | 54 + .../esbuild-plugins/nodejs-compat.ts | 111 + .../esbuild-plugins/nodejs-plugins.ts | 29 + .../esbuild-plugins/standard-url.ts | 40 + .../find-additional-modules.ts | 370 + .../deployment-bundle/guess-worker-format.ts | 59 + .../deployment-bundle/maybe-build-worker.ts | 217 + .../deployment-bundle/merge-config-args.ts | 241 + .../deployment-bundle/module-collection.ts | 410 + .../src/deployment-bundle/no-bundle-worker.ts | 29 + .../src/deployment-bundle/node-compat.ts | 1 + .../deployment-bundle/resolve-config-args.ts | 53 + .../src/deployment-bundle/resolve-entry.ts | 56 + .../wrangler/src/deployment-bundle/rules.ts | 79 + .../src/deployment-bundle/run-custom-build.ts | 281 + .../deployment-bundle/secrets-validation.ts | 4 + .../src/deployment-bundle/source-maps.ts | 202 + .../src/deployment-bundle/source-url.ts | 38 + packages/wrangler/src/deployments.ts | 390 + packages/wrangler/src/dev.ts | 583 + .../wrangler/src/dev/class-names-sqlite.ts | 92 + .../wrangler/src/dev/create-worker-preview.ts | 337 + packages/wrangler/src/dev/dev-vars.ts | 190 + .../src/dev/get-local-persistence-path.ts | 46 + packages/wrangler/src/dev/hotkeys.ts | 187 + packages/wrangler/src/dev/inspect.ts | 300 + packages/wrangler/src/dev/miniflare/index.ts | 1263 + packages/wrangler/src/dev/remote.ts | 345 + packages/wrangler/src/dev/start-dev.ts | 370 + packages/wrangler/src/dev/use-esbuild.ts | 275 + packages/wrangler/src/dialogs.ts | 190 + packages/wrangler/src/dispatch-namespace.ts | 242 + packages/wrangler/src/docs/helpers.ts | 50 + packages/wrangler/src/docs/index.ts | 56 + .../src/email-routing/addresses/create.ts | 28 + .../src/email-routing/addresses/delete.ts | 42 + .../src/email-routing/addresses/get.ts | 28 + .../src/email-routing/addresses/list.ts | 29 + packages/wrangler/src/email-routing/client.ts | 384 + .../wrangler/src/email-routing/disable.ts | 44 + .../wrangler/src/email-routing/dns-get.ts | 37 + .../wrangler/src/email-routing/dns-unlock.ts | 44 + packages/wrangler/src/email-routing/enable.ts | 25 + packages/wrangler/src/email-routing/index.ts | 151 + packages/wrangler/src/email-routing/list.ts | 28 + .../src/email-routing/rules/create.ts | 89 + .../src/email-routing/rules/delete.ts | 47 + .../wrangler/src/email-routing/rules/get.ts | 94 + .../wrangler/src/email-routing/rules/list.ts | 64 + .../src/email-routing/rules/update.ts | 178 + .../src/email-routing/sending/disable.ts | 55 + .../src/email-routing/sending/dns-get.ts | 57 + .../src/email-routing/sending/enable.ts | 37 + .../src/email-routing/sending/list.ts | 71 + .../src/email-routing/sending/send-raw.ts | 73 + .../src/email-routing/sending/send.ts | 198 + .../src/email-routing/sending/settings.ts | 39 + .../src/email-routing/sending/utils.ts | 21 + .../wrangler/src/email-routing/settings.ts | 28 + packages/wrangler/src/email-routing/utils.ts | 128 + packages/wrangler/src/environments/index.ts | 6 + .../wrangler/src/experimental-commands-api.ts | 16 + packages/wrangler/src/experimental-config.ts | 21 + .../src/experimental-config/cli-flag.ts | 28 + .../src/experimental-config/convert.ts | 106 + .../wrangler/src/experimental-config/load.ts | 228 + .../src/experimental-config/public.ts | 3 + .../src/experimental-config/schema.ts | 125 + .../wrangler/src/experimental-config/types.ts | 75 + .../wrangler-definition.ts | 24 + packages/wrangler/src/experimental-flags.ts | 27 + packages/wrangler/src/flagship/apps/create.ts | 55 + packages/wrangler/src/flagship/apps/delete.ts | 58 + packages/wrangler/src/flagship/apps/get.ts | 36 + packages/wrangler/src/flagship/apps/list.ts | 43 + packages/wrangler/src/flagship/apps/update.ts | 42 + packages/wrangler/src/flagship/bulk.ts | 41 + packages/wrangler/src/flagship/client.ts | 350 + .../wrangler/src/flagship/flags/changelog.ts | 87 + .../wrangler/src/flagship/flags/create.ts | 120 + .../wrangler/src/flagship/flags/delete.ts | 64 + .../wrangler/src/flagship/flags/enable.ts | 61 + .../wrangler/src/flagship/flags/evaluate.ts | 64 + packages/wrangler/src/flagship/flags/get.ts | 41 + packages/wrangler/src/flagship/flags/list.ts | 85 + .../wrangler/src/flagship/flags/rollout.ts | 129 + .../src/flagship/flags/rules/delete.ts | 60 + .../wrangler/src/flagship/flags/rules/list.ts | 53 + .../src/flagship/flags/rules/reorder.ts | 103 + .../src/flagship/flags/rules/shared.ts | 53 + .../src/flagship/flags/rules/update.ts | 141 + packages/wrangler/src/flagship/flags/set.ts | 66 + packages/wrangler/src/flagship/flags/split.ts | 122 + .../wrangler/src/flagship/flags/update.ts | 204 + packages/wrangler/src/flagship/index.ts | 62 + packages/wrangler/src/flagship/render.ts | 143 + packages/wrangler/src/flagship/shared.ts | 820 + packages/wrangler/src/hello-world/index.ts | 134 + packages/wrangler/src/https-options.ts | 52 + packages/wrangler/src/hyperdrive/client.ts | 180 + packages/wrangler/src/hyperdrive/create.ts | 69 + packages/wrangler/src/hyperdrive/delete.ts | 25 + packages/wrangler/src/hyperdrive/get.ts | 24 + packages/wrangler/src/hyperdrive/index.ts | 397 + packages/wrangler/src/hyperdrive/list.ts | 33 + packages/wrangler/src/hyperdrive/shared.ts | 35 + packages/wrangler/src/hyperdrive/update.ts | 48 + packages/wrangler/src/index.ts | 2783 ++ packages/wrangler/src/init.ts | 200 + packages/wrangler/src/intl-polyfill.d.ts | 139 + packages/wrangler/src/is-interactive.ts | 58 + packages/wrangler/src/kv/helpers.ts | 600 + packages/wrangler/src/kv/index.ts | 1186 + packages/wrangler/src/logger.ts | 297 + packages/wrangler/src/match-tag.ts | 16 + packages/wrangler/src/metrics/commands.ts | 93 + packages/wrangler/src/metrics/helpers.ts | 35 + packages/wrangler/src/metrics/index.ts | 7 + .../wrangler/src/metrics/metrics-config.ts | 190 + .../src/metrics/metrics-dispatcher.ts | 285 + .../src/metrics/metrics-usage-headers.ts | 24 + packages/wrangler/src/metrics/sanitization.ts | 179 + packages/wrangler/src/metrics/send-event.ts | 120 + packages/wrangler/src/metrics/types.ts | 143 + packages/wrangler/src/miniflare-cli/assets.ts | 309 + .../wrangler/src/miniflare-cli/tsconfig.json | 9 + packages/wrangler/src/miniflare-cli/types.ts | 11 + packages/wrangler/src/mtls-certificate/cli.ts | 149 + packages/wrangler/src/navigator-user-agent.ts | 26 + packages/wrangler/src/open-in-browser.ts | 38 + packages/wrangler/src/output.ts | 200 + packages/wrangler/src/package-manager.ts | 136 + packages/wrangler/src/pages/README.md | 7 + packages/wrangler/src/pages/build-env.ts | 128 + packages/wrangler/src/pages/build.ts | 560 + packages/wrangler/src/pages/buildFunctions.ts | 159 + packages/wrangler/src/pages/constants.ts | 24 + .../wrangler/src/pages/delegate-to-workers.ts | 392 + packages/wrangler/src/pages/deploy.ts | 702 + .../wrangler/src/pages/deployment-tails.ts | 319 + packages/wrangler/src/pages/deployments.ts | 205 + packages/wrangler/src/pages/dev.ts | 1444 + .../wrangler/src/pages/download-config.ts | 359 + packages/wrangler/src/pages/errors.ts | 88 + packages/wrangler/src/pages/functions.ts | 87 + .../src/pages/functions/buildPlugin.ts | 128 + .../src/pages/functions/buildWorker.ts | 460 + .../src/pages/functions/filepath-routing.ts | 244 + .../src/pages/functions/identifiers.ts | 72 + .../pages/functions/routes-consolidation.ts | 73 + .../pages/functions/routes-transformation.ts | 119 + .../src/pages/functions/routes-validation.ts | 204 + .../wrangler/src/pages/functions/routes.ts | 165 + .../src/pages/functions/tsconfig.json | 8 + packages/wrangler/src/pages/hash.ts | 1 + packages/wrangler/src/pages/index.ts | 47 + packages/wrangler/src/pages/projects.ts | 325 + .../src/pages/prompt-select-project.ts | 17 + .../wrangler/src/pages/run-workers-deploy.ts | 131 + packages/wrangler/src/pages/secret/index.ts | 407 + packages/wrangler/src/pages/types.ts | 54 + packages/wrangler/src/pages/upload.ts | 442 + packages/wrangler/src/pages/utils.ts | 92 + packages/wrangler/src/pages/validate.ts | 159 + packages/wrangler/src/paths.ts | 71 + packages/wrangler/src/pipelines/cli/create.ts | 148 + packages/wrangler/src/pipelines/cli/delete.ts | 70 + packages/wrangler/src/pipelines/cli/get.ts | 111 + .../src/pipelines/cli/legacy-helpers.ts | 280 + packages/wrangler/src/pipelines/cli/list.ts | 118 + .../wrangler/src/pipelines/cli/resolve.ts | 198 + packages/wrangler/src/pipelines/cli/setup.ts | 1288 + .../src/pipelines/cli/sinks/create.ts | 297 + .../src/pipelines/cli/sinks/delete.ts | 50 + .../wrangler/src/pipelines/cli/sinks/get.ts | 52 + .../wrangler/src/pipelines/cli/sinks/index.ts | 9 + .../wrangler/src/pipelines/cli/sinks/list.ts | 70 + .../wrangler/src/pipelines/cli/sinks/utils.ts | 105 + .../src/pipelines/cli/streams/create.ts | 135 + .../src/pipelines/cli/streams/delete.ts | 50 + .../wrangler/src/pipelines/cli/streams/get.ts | 45 + .../src/pipelines/cli/streams/index.ts | 9 + .../src/pipelines/cli/streams/list.ts | 68 + .../src/pipelines/cli/streams/utils.ts | 256 + packages/wrangler/src/pipelines/cli/update.ts | 148 + packages/wrangler/src/pipelines/client.ts | 412 + packages/wrangler/src/pipelines/defaults.ts | 73 + packages/wrangler/src/pipelines/index.ts | 178 + .../wrangler/src/pipelines/legacy-client.ts | 260 + packages/wrangler/src/pipelines/types.ts | 252 + packages/wrangler/src/pipelines/validate.ts | 78 + packages/wrangler/src/preview/api.ts | 298 + packages/wrangler/src/preview/index.ts | 277 + packages/wrangler/src/preview/preview.ts | 950 + packages/wrangler/src/preview/secret.ts | 205 + packages/wrangler/src/preview/settings.ts | 228 + packages/wrangler/src/preview/shared.ts | 394 + packages/wrangler/src/process-env.ts | 35 + .../cli/commands/consumer/http-pull/add.ts | 71 + .../cli/commands/consumer/http-pull/index.ts | 9 + .../cli/commands/consumer/http-pull/list.ts | 57 + .../cli/commands/consumer/http-pull/remove.ts | 26 + .../src/queues/cli/commands/consumer/index.ts | 9 + .../src/queues/cli/commands/consumer/list.ts | 56 + .../cli/commands/consumer/worker/add.ts | 82 + .../cli/commands/consumer/worker/index.ts | 9 + .../cli/commands/consumer/worker/list.ts | 58 + .../cli/commands/consumer/worker/remove.ts | 35 + .../src/queues/cli/commands/create.ts | 124 + .../src/queues/cli/commands/delete.ts | 25 + .../wrangler/src/queues/cli/commands/index.ts | 10 + .../wrangler/src/queues/cli/commands/info.ts | 58 + .../wrangler/src/queues/cli/commands/list.ts | 31 + .../src/queues/cli/commands/pause-resume.ts | 72 + .../wrangler/src/queues/cli/commands/purge.ts | 51 + .../cli/commands/subscription/create.ts | 174 + .../cli/commands/subscription/delete.ts | 58 + .../queues/cli/commands/subscription/get.ts | 62 + .../queues/cli/commands/subscription/index.ts | 9 + .../queues/cli/commands/subscription/list.ts | 68 + .../cli/commands/subscription/update.ts | 108 + .../queues/cli/commands/subscription/utils.ts | 19 + .../src/queues/cli/commands/update.ts | 111 + packages/wrangler/src/queues/client.ts | 382 + packages/wrangler/src/queues/constants.ts | 8 + .../wrangler/src/queues/subscription-types.ts | 92 + packages/wrangler/src/queues/utils.ts | 21 + packages/wrangler/src/r2/bucket.ts | 317 + packages/wrangler/src/r2/catalog.ts | 586 + packages/wrangler/src/r2/constants.ts | 5 + packages/wrangler/src/r2/cors.ts | 217 + packages/wrangler/src/r2/domain.ts | 308 + packages/wrangler/src/r2/helpers/bucket.ts | 577 + packages/wrangler/src/r2/helpers/bulk.ts | 120 + packages/wrangler/src/r2/helpers/catalog.ts | 353 + packages/wrangler/src/r2/helpers/domain.ts | 226 + .../wrangler/src/r2/helpers/local-uploads.ts | 47 + packages/wrangler/src/r2/helpers/misc.ts | 50 + .../wrangler/src/r2/helpers/notification.ts | 258 + packages/wrangler/src/r2/helpers/object.ts | 232 + packages/wrangler/src/r2/helpers/sippy.ts | 100 + packages/wrangler/src/r2/index.ts | 10 + packages/wrangler/src/r2/lifecycle.ts | 545 + packages/wrangler/src/r2/local-uploads.ts | 142 + packages/wrangler/src/r2/lock.ts | 408 + packages/wrangler/src/r2/notification.ts | 200 + packages/wrangler/src/r2/object.ts | 757 + packages/wrangler/src/r2/public-dev-url.ts | 167 + packages/wrangler/src/r2/sippy.ts | 436 + packages/wrangler/src/r2/sql.ts | 200 + packages/wrangler/src/secret/index.ts | 527 + packages/wrangler/src/secrets-store/client.ts | 209 + .../wrangler/src/secrets-store/commands.ts | 791 + packages/wrangler/src/secrets-store/index.ts | 26 + packages/wrangler/src/sentry/index.ts | 186 + packages/wrangler/src/setup.ts | 140 + packages/wrangler/src/sites.ts | 479 + packages/wrangler/src/sourcemap.ts | 6 + packages/wrangler/src/streams.d.ts | 18 + packages/wrangler/src/tail/createTail.ts | 479 + packages/wrangler/src/tail/filters.ts | 306 + packages/wrangler/src/tail/index.ts | 550 + packages/wrangler/src/tail/printing.ts | 206 + packages/wrangler/src/triggers/index.ts | 84 + packages/wrangler/src/tsconfig-sanity.ts | 12 + packages/wrangler/src/tunnel/client.ts | 417 + packages/wrangler/src/tunnel/create.ts | 34 + packages/wrangler/src/tunnel/delete.ts | 44 + packages/wrangler/src/tunnel/dev.ts | 182 + packages/wrangler/src/tunnel/index.ts | 10 + packages/wrangler/src/tunnel/info.ts | 41 + packages/wrangler/src/tunnel/list.ts | 37 + packages/wrangler/src/tunnel/quick-start.ts | 181 + packages/wrangler/src/tunnel/run.ts | 220 + packages/wrangler/src/turnstile/client.ts | 127 + packages/wrangler/src/turnstile/create.ts | 70 + packages/wrangler/src/turnstile/delete.ts | 65 + packages/wrangler/src/turnstile/get.ts | 56 + packages/wrangler/src/turnstile/index.ts | 73 + packages/wrangler/src/turnstile/list.ts | 53 + packages/wrangler/src/turnstile/update.ts | 80 + .../wrangler/src/type-generation/helpers.ts | 478 + .../wrangler/src/type-generation/index.ts | 4170 +++ .../src/type-generation/new-config.ts | 64 + .../src/type-generation/pipeline-schema.ts | 280 + .../src/type-generation/runtime/index.ts | 46 + .../runtime/log-runtime-types-message.ts | 67 + packages/wrangler/src/update-check.ts | 28 + packages/wrangler/src/user/access.ts | 27 + .../wrangler/src/user/auth-config-file.ts | 144 + packages/wrangler/src/user/auth-variables.ts | 49 + packages/wrangler/src/user/commands.ts | 390 + packages/wrangler/src/user/fetch-accounts.ts | 118 + .../wrangler/src/user/generate-auth-url.ts | 10 + .../src/user/generate-random-state.ts | 7 + packages/wrangler/src/user/index.ts | 2 + .../wrangler/src/user/keyring-preference.ts | 194 + packages/wrangler/src/user/membership.ts | 28 + packages/wrangler/src/user/preferences.ts | 62 + packages/wrangler/src/user/profile-store.ts | 121 + packages/wrangler/src/user/profiles.ts | 316 + packages/wrangler/src/user/shared.ts | 4 + .../src/user/temporary-account-path.ts | 22 + packages/wrangler/src/user/temporary-terms.ts | 20 + packages/wrangler/src/user/user.ts | 631 + packages/wrangler/src/user/whoami.ts | 408 + .../src/utils/add-created-resource-config.ts | 170 + packages/wrangler/src/utils/assert-never.ts | 1 + packages/wrangler/src/utils/box.ts | 151 + .../wrangler/src/utils/collectKeyValues.ts | 39 + packages/wrangler/src/utils/constants.ts | 13 + packages/wrangler/src/utils/create-batches.ts | 8 + packages/wrangler/src/utils/debounce.ts | 18 + packages/wrangler/src/utils/dedent.ts | 43 + packages/wrangler/src/utils/detect-agent.ts | 46 + packages/wrangler/src/utils/did-you-mean.ts | 76 + packages/wrangler/src/utils/diff-json.ts | 6 + packages/wrangler/src/utils/diff.ts | 513 + .../src/utils/download-worker-config.ts | 27 + packages/wrangler/src/utils/error-codes.ts | 1 + packages/wrangler/src/utils/filesystem.ts | 15 + packages/wrangler/src/utils/format-message.ts | 23 + .../src/utils/friendly-validator-errors.ts | 5 + .../wrangler/src/utils/getLegacyScriptName.ts | 14 + packages/wrangler/src/utils/getRules.ts | 5 + packages/wrangler/src/utils/getScriptName.ts | 8 + .../wrangler/src/utils/getValidBindingName.ts | 27 + packages/wrangler/src/utils/is-local.ts | 19 + packages/wrangler/src/utils/isAbortError.ts | 22 + packages/wrangler/src/utils/levenshtein.ts | 40 + packages/wrangler/src/utils/log-file.ts | 142 + .../src/utils/logPossibleBugMessage.ts | 25 + packages/wrangler/src/utils/memoizeGetPort.ts | 63 + packages/wrangler/src/utils/merge-deep.ts | 32 + .../wrangler/src/utils/mergeWithOverride.ts | 16 + packages/wrangler/src/utils/onKeyPress.ts | 57 + packages/wrangler/src/utils/placement.ts | 1 + packages/wrangler/src/utils/print-bindings.ts | 1 + .../src/utils/render-labelled-values.ts | 83 + packages/wrangler/src/utils/retry.ts | 47 + packages/wrangler/src/utils/shell-quote.ts | 47 + .../wrangler/src/utils/sortObjectRecursive.ts | 62 + packages/wrangler/src/utils/std.ts | 40 + packages/wrangler/src/utils/strings.ts | 11 + packages/wrangler/src/utils/types.ts | 7 + .../src/utils/worker-not-found-error.ts | 6 + packages/wrangler/src/vectorize/client.ts | 324 + packages/wrangler/src/vectorize/common.ts | 54 + packages/wrangler/src/vectorize/create.ts | 137 + .../src/vectorize/createMetadataIndex.ts | 49 + packages/wrangler/src/vectorize/delete.ts | 51 + .../wrangler/src/vectorize/deleteByIds.ts | 48 + .../src/vectorize/deleteMetadataIndex.ts | 37 + packages/wrangler/src/vectorize/get.ts | 53 + packages/wrangler/src/vectorize/getByIds.ts | 52 + packages/wrangler/src/vectorize/index.ts | 10 + packages/wrangler/src/vectorize/info.ts | 47 + packages/wrangler/src/vectorize/insert.ts | 138 + packages/wrangler/src/vectorize/list.ts | 59 + .../src/vectorize/listMetadataIndex.ts | 56 + .../wrangler/src/vectorize/listVectors.ts | 92 + packages/wrangler/src/vectorize/query.ts | 238 + packages/wrangler/src/vectorize/types.ts | 215 + packages/wrangler/src/vectorize/upsert.ts | 108 + packages/wrangler/src/versions/api.ts | 132 + packages/wrangler/src/versions/deploy.ts | 937 + .../src/versions/deployments/index.ts | 11 + .../wrangler/src/versions/deployments/list.ts | 122 + .../src/versions/deployments/status.ts | 115 + .../wrangler/src/versions/deployments/view.ts | 48 + packages/wrangler/src/versions/index.ts | 11 + packages/wrangler/src/versions/list.ts | 126 + .../wrangler/src/versions/rollback/index.ts | 187 + .../wrangler/src/versions/secrets/bulk.ts | 123 + .../wrangler/src/versions/secrets/delete.ts | 125 + .../wrangler/src/versions/secrets/index.ts | 333 + .../wrangler/src/versions/secrets/list.ts | 106 + packages/wrangler/src/versions/secrets/put.ts | 121 + packages/wrangler/src/versions/types.ts | 55 + packages/wrangler/src/versions/upload.ts | 105 + packages/wrangler/src/versions/view.ts | 126 + packages/wrangler/src/vitest.d.ts | 4 + packages/wrangler/src/vpc/client.ts | 84 + packages/wrangler/src/vpc/create.ts | 31 + packages/wrangler/src/vpc/delete.ts | 26 + packages/wrangler/src/vpc/get.ts | 31 + packages/wrangler/src/vpc/index.ts | 173 + packages/wrangler/src/vpc/list.ts | 25 + packages/wrangler/src/vpc/shared.ts | 109 + packages/wrangler/src/vpc/update.ts | 36 + packages/wrangler/src/vpc/validation.ts | 272 + packages/wrangler/src/websearch/client.ts | 22 + packages/wrangler/src/websearch/index.ts | 10 + packages/wrangler/src/websearch/search.ts | 65 + packages/wrangler/src/websearch/types.ts | 47 + packages/wrangler/src/worker.d.ts | 3 + packages/wrangler/src/workerd.d.ts | 6 + .../wrangler/src/workflows/commands/delete.ts | 49 + .../src/workflows/commands/describe.ts | 96 + .../workflows/commands/instances/describe.ts | 308 + .../src/workflows/commands/instances/list.ts | 163 + .../src/workflows/commands/instances/pause.ts | 49 + .../workflows/commands/instances/restart.ts | 118 + .../workflows/commands/instances/resume.ts | 49 + .../commands/instances/send-event.ts | 94 + .../commands/instances/terminate-all.ts | 76 + .../workflows/commands/instances/terminate.ts | 69 + .../wrangler/src/workflows/commands/list.ts | 99 + .../src/workflows/commands/trigger.ts | 93 + packages/wrangler/src/workflows/index.ts | 18 + packages/wrangler/src/workflows/local.ts | 189 + packages/wrangler/src/workflows/types.ts | 129 + packages/wrangler/src/workflows/utils.ts | 145 + packages/wrangler/src/wrangler-banner.ts | 94 + packages/wrangler/src/xxhash-wasm.d.ts | 7 + packages/wrangler/src/yargs-types.ts | 44 + packages/wrangler/src/zones.ts | 129 + packages/wrangler/telemetry.md | 88 + .../__tests__/pages-dev-util.test.ts | 124 + .../templates/__tests__/tsconfig-sanity.ts | 12 + .../templates/__tests__/tsconfig.json | 8 + packages/wrangler/templates/checked-fetch.js | 28 + packages/wrangler/templates/facade.d.ts | 19 + .../wrangler/templates/middleware/common.ts | 67 + .../templates/middleware/loader-modules.ts | 134 + .../templates/middleware/loader-sw.ts | 229 + .../middleware-ensure-req-body-drained.ts | 18 + .../middleware-miniflare3-json-error.ts | 32 + .../middleware-patch-console-prefix.d.ts | 3 + .../middleware-patch-console-prefix.ts | 21 + .../middleware/middleware-pretty-error.ts | 40 + .../middleware/middleware-scheduled.ts | 29 + .../wrangler/templates/modules-watch-stub.js | 4 + .../templates/new-worker-scheduled.js | 17 + .../templates/new-worker-scheduled.ts | 32 + packages/wrangler/templates/new-worker.js | 15 + packages/wrangler/templates/new-worker.ts | 33 + packages/wrangler/templates/no-op-worker.js | 10 + .../wrangler/templates/pages-dev-pipeline.ts | 31 + packages/wrangler/templates/pages-dev-util.ts | 55 + packages/wrangler/templates/pages-shim.ts | 9 + .../templates/pages-template-plugin.ts | 190 + .../templates/pages-template-worker.ts | 198 + .../remoteBindings/ProxyServerWorker.ts | 143 + .../templates/remoteBindings/wrangler.jsonc | 4 + .../startDevWorker/InspectorProxyWorker.ts | 713 + .../templates/startDevWorker/ProxyWorker.ts | 368 + .../wrangler/templates/tsconfig-sanity.ts | 11 + .../wrangler/templates/tsconfig.init.json | 22 + packages/wrangler/templates/tsconfig.json | 14 + packages/wrangler/tsconfig.emit.json | 17 + .../tsconfig.experimental-config.json | 8 + packages/wrangler/tsconfig.json | 21 + packages/wrangler/tsdown.config.mts | 14 + packages/wrangler/tsup.config.ts | 137 + packages/wrangler/turbo.json | 82 + packages/wrangler/vitest.config.mts | 74 + ...cloudflare__component-listbox@1.10.6.patch | 85 + patches/@netlify__build-info.patch | 33 + .../buffer-equal-constant-time@1.0.1.patch | 32 + patches/postal-mime.patch | 10 + patches/toucan-js@4.0.0.patch | 18 + patches/youch@4.1.0-beta.10.patch | 13 + pnpm-lock.yaml | 27454 ++++++++++++++++ pnpm-workspace.yaml | 137 + tools/README.md | 26 + tools/clean/__tests__/clean.test.ts | 79 + tools/clean/clean.ts | 17 + .../generate-dependabot-pr-changesets.test.ts | 399 + .../generate-dependabot-pr-changesets.ts | 214 + .../__tests__/changeset-version.test.ts | 33 + .../__tests__/deploy-non-npm-packages.test.ts | 176 + .../__tests__/validate-changesets.test.ts | 237 + .../validate-package-dependencies.test.ts | 737 + .../validate-pinned-dependencies.test.ts | 198 + .../__tests__/validate-pr-description.test.ts | 151 + tools/deployments/alert-on-error.ts | 52 + tools/deployments/check-npm-version.ts | 20 + tools/deployments/deploy-non-npm-packages.ts | 146 + tools/deployments/validate-catalog-usage.ts | 131 + tools/deployments/validate-changesets.ts | 110 + tools/deployments/validate-fixtures.ts | 78 + .../validate-package-dependencies.ts | 555 + .../validate-pinned-dependencies.ts | 174 + tools/deployments/validate-pr-description.ts | 85 + .../deployments/validate-private-packages.ts | 75 + tools/e2e/__tests__/common.test.ts | 437 + tools/e2e/common.ts | 500 + tools/e2e/e2eCleanup.ts | 216 + tools/e2e/runIndividualE2EFiles.ts | 181 + .../__tests__/validate-action-pinning.test.ts | 246 + .../validate-action-pinning.ts | 106 + tools/package.json | 23 + tools/test/run-test-file.ts | 27 + tools/tsconfig.json | 13 + tools/turbo.json | 13 + tools/vitest.config.mts | 12 + turbo.json | 72 + vitest.shared.ts | 23 + vitest.workspace.ts | 5 + 5213 files changed, 731895 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/bright-doors-evict.md create mode 100644 .changeset/config.json create mode 100644 .changeset/fix-schema-editor-dirty-state.md create mode 100644 .changeset/fix-tanstack-no-install.md create mode 100644 .changeset/gentle-lions-list-do-ids.md create mode 100644 .changeset/green-badgers-evict.md create mode 100644 .changeset/lazy-register-hooks.md create mode 100644 .changeset/pages-delegation-force-notice.md create mode 100644 .changeset/remove-service-environments.md create mode 100644 .changeset/suggest-similar-commands-on-typo.md create mode 100644 .codeowners create mode 100644 .editorconfig create mode 100644 .git-blame-ignore-revs create mode 100644 .gitattributes create mode 100644 .gitguardian.yml create mode 100644 .github/ISSUE_TEMPLATE/bug-template.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/actions/check-remote-tests/action.yml create mode 100644 .github/actions/expose-actions-variables/action.yml create mode 100644 .github/actions/expose-actions-variables/index.mjs create mode 100644 .github/actions/install-dependencies/action.yml create mode 100644 .github/actions/install-python-uv/action.yml create mode 100644 .github/bonk_reviewer.md create mode 100644 .github/changeset-version.js create mode 100644 .github/dependabot.yml create mode 100644 .github/extract-pr-and-workflow-id.js create mode 100644 .github/get-c3-dependabot-bumped-framework.cjs create mode 100644 .github/holopin.yml create mode 100644 .github/opencode.json create mode 100644 .github/prereleases/upload.mjs create mode 100644 .github/pull_request_template.md create mode 100644 .github/skills/issue-review.md create mode 100644 .github/version-script.js create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/bonk-pr-review.yml create mode 100644 .github/workflows/bonk.yml create mode 100644 .github/workflows/c3-dependabot-versioning-prs.yml create mode 100644 .github/workflows/c3-e2e.yml create mode 100644 .github/workflows/changeset-review.yml create mode 100644 .github/workflows/changesets.yml create mode 100644 .github/workflows/codeowners.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/deploy-previews.yml create mode 100644 .github/workflows/e2e-local-explorer-ui.yml create mode 100644 .github/workflows/e2e-project-cleanup.yml create mode 100644 .github/workflows/e2e-vite.yml create mode 100644 .github/workflows/e2e-wrangler.yml create mode 100644 .github/workflows/hotfix-release.yml create mode 100644 .github/workflows/issues.yml create mode 100644 .github/workflows/miniflare-dependabot-versioning-prs.yml create mode 100644 .github/workflows/opencode-issue-solver.yml create mode 100644 .github/workflows/prerelease.yml create mode 100644 .github/workflows/rerun-codeowners-privileged.yml create mode 100644 .github/workflows/rerun-codeowners.yml create mode 100644 .github/workflows/rerun-remote-tests.yml create mode 100644 .github/workflows/run-ci-for-external-forks.yml create mode 100644 .github/workflows/semgrep.yml create mode 100644 .github/workflows/test-and-check-other-node.yml create mode 100644 .github/workflows/test-and-check.yml create mode 100644 .github/workflows/triage-issue.yml create mode 100644 .github/workflows/validate-pr-description.yml create mode 100644 .github/workflows/vite-plugin-playgrounds.yml create mode 100644 .github/workflows/worker-playground-preview-testing-env-deploy-and-test.yml create mode 100644 .github/workflows/workers-shared-deploy-production.yml create mode 100644 .github/workflows/workers-shared-deploy-staging.yml create mode 100644 .gitignore create mode 100644 .markdownlint.json create mode 100644 .opencode/agents/bonk.md create mode 100644 .opencode/skills/local-explorer/SKILL.md create mode 100644 .opencode/skills/local-explorer/assets/local-explorer-diagram.md create mode 100644 .oxfmtrc.jsonc create mode 100644 .oxlintrc.jsonc create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 CODEOWNERS create mode 100644 CODEOWNERS.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 REVIEW.md create mode 100644 SECURITY.md create mode 100644 STYLEGUIDE.md create mode 100644 cloudflare-workers-outline.png create mode 100644 codeowners.toml create mode 100644 fixtures/additional-modules/package.json create mode 100644 fixtures/additional-modules/src/common.cjs create mode 100644 fixtures/additional-modules/src/dep.ts create mode 100644 fixtures/additional-modules/src/dynamic.js create mode 100644 fixtures/additional-modules/src/example.sql create mode 100644 fixtures/additional-modules/src/index.ts create mode 100644 fixtures/additional-modules/src/lang/en.js create mode 100644 fixtures/additional-modules/src/lang/fr.js create mode 100644 fixtures/additional-modules/src/text.d.ts create mode 100644 fixtures/additional-modules/src/text.txt create mode 100644 fixtures/additional-modules/test/index.test.ts create mode 100644 fixtures/additional-modules/test/tsconfig.json create mode 100644 fixtures/additional-modules/tsconfig.json create mode 100644 fixtures/additional-modules/turbo.json create mode 100644 fixtures/additional-modules/vitest.config.mts create mode 100644 fixtures/additional-modules/wrangler.jsonc create mode 100644 fixtures/browser-run/package.json create mode 100644 fixtures/browser-run/src/index.ts create mode 100644 fixtures/browser-run/src/playwright.ts create mode 100644 fixtures/browser-run/src/puppeteer.ts create mode 100644 fixtures/browser-run/test/index.spec.ts create mode 100644 fixtures/browser-run/test/tsconfig.json create mode 100644 fixtures/browser-run/tsconfig.json create mode 100644 fixtures/browser-run/vitest.config.mts create mode 100644 fixtures/browser-run/worker-configuration.d.ts create mode 100644 fixtures/browser-run/wrangler.jsonc create mode 100644 fixtures/container-app/.gitignore create mode 100644 fixtures/container-app/Dockerfile create mode 100644 fixtures/container-app/README.md create mode 100644 fixtures/container-app/container/simple-node-app.js create mode 100644 fixtures/container-app/package.json create mode 100644 fixtures/container-app/src/index.ts create mode 100644 fixtures/container-app/tsconfig.json create mode 100644 fixtures/container-app/wrangler.jsonc create mode 100644 fixtures/container-app/wrangler.registry.jsonc create mode 100644 fixtures/create-test-harness-example/.gitignore create mode 100644 fixtures/create-test-harness-example/README.md create mode 100644 fixtures/create-test-harness-example/package.json create mode 100644 fixtures/create-test-harness-example/playwright.config.ts create mode 100644 fixtures/create-test-harness-example/tests/playwright.test.ts create mode 100644 fixtures/create-test-harness-example/tests/tsconfig.json create mode 100644 fixtures/create-test-harness-example/tests/vitest.test.ts create mode 100644 fixtures/create-test-harness-example/tsconfig.json create mode 100644 fixtures/create-test-harness-example/turbo.json create mode 100644 fixtures/create-test-harness-example/vite.config.ts create mode 100644 fixtures/create-test-harness-example/vitest.config.mts create mode 100644 fixtures/create-test-harness-example/workers/api/index.ts create mode 100644 fixtures/create-test-harness-example/workers/api/migrations/0001_reports.sql create mode 100644 fixtures/create-test-harness-example/workers/api/worker-configuration.d.ts create mode 100644 fixtures/create-test-harness-example/workers/api/wrangler.jsonc create mode 100644 fixtures/create-test-harness-example/workers/mock-browser/index.ts create mode 100644 fixtures/create-test-harness-example/workers/mock-browser/wrangler.jsonc create mode 100644 fixtures/create-test-harness-example/workers/web/index.ts create mode 100644 fixtures/create-test-harness-example/workers/web/worker-configuration.d.ts create mode 100644 fixtures/create-test-harness-example/workers/web/wrangler.jsonc create mode 100644 fixtures/d1-read-replication-app/.gitignore create mode 100644 fixtures/d1-read-replication-app/README.md create mode 100644 fixtures/d1-read-replication-app/package.json create mode 100644 fixtures/d1-read-replication-app/src/index.ts create mode 100644 fixtures/d1-read-replication-app/tests/index.test.ts create mode 100644 fixtures/d1-read-replication-app/tests/tsconfig.json create mode 100644 fixtures/d1-read-replication-app/tsconfig.json create mode 100644 fixtures/d1-read-replication-app/vitest.config.mts create mode 100644 fixtures/d1-read-replication-app/wrangler.jsonc create mode 100644 fixtures/d1-worker-app/.gitignore create mode 100644 fixtures/d1-worker-app/README.md create mode 100644 fixtures/d1-worker-app/package.json create mode 100644 fixtures/d1-worker-app/schema.sql create mode 100644 fixtures/d1-worker-app/src/index.ts create mode 100644 fixtures/d1-worker-app/tests/index.test.ts create mode 100644 fixtures/d1-worker-app/tests/tsconfig.json create mode 100644 fixtures/d1-worker-app/tsconfig.json create mode 100644 fixtures/d1-worker-app/vitest.config.mts create mode 100644 fixtures/d1-worker-app/wrangler.jsonc create mode 100644 fixtures/dev-registry/assets/example.txt create mode 100644 fixtures/dev-registry/package.json create mode 100644 fixtures/dev-registry/tests/dev-registry.test.ts create mode 100644 fixtures/dev-registry/tests/tsconfig.json create mode 100644 fixtures/dev-registry/tsconfig.json create mode 100644 fixtures/dev-registry/tsconfig.node.json create mode 100644 fixtures/dev-registry/tsconfig.worker.json create mode 100644 fixtures/dev-registry/vite.exported-handler-with-assets.config.ts create mode 100644 fixtures/dev-registry/vite.exported-handler.config.ts create mode 100644 fixtures/dev-registry/vite.worker-entrypoint-with-assets.config.ts create mode 100644 fixtures/dev-registry/vite.worker-entrypoint.config.ts create mode 100644 fixtures/dev-registry/vitest.config.mts create mode 100644 fixtures/dev-registry/workers/durable-object.ts create mode 100644 fixtures/dev-registry/workers/exported-handler.ts create mode 100644 fixtures/dev-registry/workers/service-worker.ts create mode 100644 fixtures/dev-registry/workers/worker-entrypoint.ts create mode 100644 fixtures/dev-registry/wrangler.exported-handler-with-assets.jsonc create mode 100644 fixtures/dev-registry/wrangler.exported-handler.jsonc create mode 100644 fixtures/dev-registry/wrangler.external-durable-object.jsonc create mode 100644 fixtures/dev-registry/wrangler.internal-durable-object.jsonc create mode 100644 fixtures/dev-registry/wrangler.nonexistent-durable-object.jsonc create mode 100644 fixtures/dev-registry/wrangler.nonexistent-entrypoint.jsonc create mode 100644 fixtures/dev-registry/wrangler.service-worker.jsonc create mode 100644 fixtures/dev-registry/wrangler.worker-entrypoint-with-assets.jsonc create mode 100644 fixtures/dev-registry/wrangler.worker-entrypoint.jsonc create mode 100644 fixtures/durable-objects-app/package.json create mode 100644 fixtures/durable-objects-app/src/index.js create mode 100644 fixtures/durable-objects-app/tests/index.test.ts create mode 100644 fixtures/durable-objects-app/tests/tsconfig.json create mode 100644 fixtures/durable-objects-app/tsconfig.json create mode 100644 fixtures/durable-objects-app/vitest.config.mts create mode 100644 fixtures/durable-objects-app/wrangler.jsonc create mode 100644 fixtures/durable-objects-exports-app/package.json create mode 100644 fixtures/durable-objects-exports-app/src/index.ts create mode 100644 fixtures/durable-objects-exports-app/tests/index.test.ts create mode 100644 fixtures/durable-objects-exports-app/tests/tsconfig.json create mode 100644 fixtures/durable-objects-exports-app/tsconfig.json create mode 100644 fixtures/durable-objects-exports-app/vitest.config.mts create mode 100644 fixtures/durable-objects-exports-app/worker-configuration.d.ts create mode 100644 fixtures/durable-objects-exports-app/wrangler.jsonc create mode 100644 fixtures/dynamic-worker-loading/package.json create mode 100644 fixtures/dynamic-worker-loading/src/index.ts create mode 100644 fixtures/dynamic-worker-loading/tests/index.test.ts create mode 100644 fixtures/dynamic-worker-loading/tests/tsconfig.json create mode 100644 fixtures/dynamic-worker-loading/tsconfig.json create mode 100644 fixtures/dynamic-worker-loading/vitest.config.mts create mode 100644 fixtures/dynamic-worker-loading/wrangler.jsonc create mode 100644 fixtures/email-worker/README.md create mode 100644 fixtures/email-worker/package.json create mode 100644 fixtures/email-worker/src/index.ts create mode 100644 fixtures/email-worker/tsconfig.json create mode 100644 fixtures/email-worker/worker-configuration.d.ts create mode 100644 fixtures/email-worker/wrangler.jsonc create mode 100644 fixtures/entrypoints-rpc-tests/package.json create mode 100644 fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts create mode 100644 fixtures/entrypoints-rpc-tests/tsconfig.json create mode 100644 fixtures/entrypoints-rpc-tests/vitest.config.mts create mode 100644 fixtures/entrypoints-rpc-tests/wrangler.jsonc create mode 100644 fixtures/experimental-new-config/cloudflare.config.ts create mode 100644 fixtures/experimental-new-config/package.json create mode 100644 fixtures/experimental-new-config/src/index.ts create mode 100644 fixtures/experimental-new-config/test/index.test.ts create mode 100644 fixtures/experimental-new-config/tsconfig.json create mode 100644 fixtures/experimental-new-config/tsconfig.node.json create mode 100644 fixtures/experimental-new-config/tsconfig.worker.json create mode 100644 fixtures/experimental-new-config/turbo.json create mode 100644 fixtures/experimental-new-config/vitest.config.mts create mode 100644 fixtures/experimental-new-config/worker-configuration.d.ts create mode 100644 fixtures/experimental-new-config/wrangler.config.ts create mode 100644 fixtures/get-platform-proxy-remote-bindings/package.json create mode 100644 fixtures/get-platform-proxy-remote-bindings/remote-worker.js create mode 100644 fixtures/get-platform-proxy-remote-bindings/remote-worker.staging.js create mode 100644 fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts create mode 100644 fixtures/get-platform-proxy-remote-bindings/tests/remote-bindings-false.test.ts create mode 100644 fixtures/get-platform-proxy-remote-bindings/tests/tsconfig.json create mode 100644 fixtures/get-platform-proxy-remote-bindings/turbo.json create mode 100644 fixtures/get-platform-proxy-remote-bindings/wrangler.remote-bindings-false.jsonc create mode 100644 fixtures/get-platform-proxy/custom-toml/path/test.toml create mode 100644 fixtures/get-platform-proxy/package.json create mode 100644 fixtures/get-platform-proxy/public/test.txt create mode 100644 fixtures/get-platform-proxy/tests/__image_snapshots__/get-platform-proxy-env-test-ts-get-platform-proxy-env-correctly-obtains-functioning-image-bindings-2-snap.png create mode 100644 fixtures/get-platform-proxy/tests/get-platform-proxy.caches.test.ts create mode 100644 fixtures/get-platform-proxy/tests/get-platform-proxy.cf.test.ts create mode 100644 fixtures/get-platform-proxy/tests/get-platform-proxy.ctx.test.ts create mode 100644 fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts create mode 100644 fixtures/get-platform-proxy/tests/shared.ts create mode 100644 fixtures/get-platform-proxy/tests/tsconfig.json create mode 100644 fixtures/get-platform-proxy/vitest.config.mts create mode 100644 fixtures/get-platform-proxy/wrangler.json create mode 100644 fixtures/get-platform-proxy/wrangler.jsonc create mode 100644 fixtures/get-platform-proxy/wrangler_external_do.jsonc create mode 100644 fixtures/get-platform-proxy/wrangler_internal_do.jsonc create mode 100644 fixtures/get-platform-proxy/wrangler_workflow.jsonc create mode 100644 fixtures/import-npm/.gitignore create mode 100644 fixtures/import-npm/README.md create mode 100644 fixtures/import-npm/package.json create mode 100644 fixtures/import-npm/packages/import-example/package.json create mode 100644 fixtures/import-npm/packages/import-example/src/index.js create mode 100644 fixtures/import-npm/packages/import-example/tests/index.test.ts create mode 100644 fixtures/import-npm/packages/import-example/tests/tsconfig.json create mode 100644 fixtures/import-npm/packages/import-example/tsconfig.json create mode 100644 fixtures/import-npm/packages/import-example/vitest.config.mts create mode 100644 fixtures/import-npm/packages/import-example/wrangler.jsonc create mode 100644 fixtures/import-npm/turbo.json create mode 100644 fixtures/import-wasm-example/.gitignore create mode 100644 fixtures/import-wasm-example/README.md create mode 100644 fixtures/import-wasm-example/package.json create mode 100644 fixtures/import-wasm-example/src/index.js create mode 100644 fixtures/import-wasm-example/tests/index.test.ts create mode 100644 fixtures/import-wasm-example/tests/tsconfig.json create mode 100644 fixtures/import-wasm-example/tsconfig.json create mode 100644 fixtures/import-wasm-example/vitest.config.mts create mode 100644 fixtures/import-wasm-example/wrangler.jsonc create mode 100644 fixtures/import-wasm-static/README.md create mode 100644 fixtures/import-wasm-static/package.json create mode 100644 fixtures/import-wasm-static/wasm/multiply.wasm create mode 100644 fixtures/import-wasm-static/wasm/multiply.wat create mode 100644 fixtures/import-wasm-static/wasm/not-exported.wasm create mode 100644 fixtures/import-wasm-static/wasm/not-exported.wat create mode 100644 fixtures/interactive-dev-tests/README.md create mode 100644 fixtures/interactive-dev-tests/container-app/Dockerfile create mode 100644 fixtures/interactive-dev-tests/container-app/DockerfileWithLongSleep create mode 100644 fixtures/interactive-dev-tests/container-app/container/simple-node-app.js create mode 100644 fixtures/interactive-dev-tests/container-app/src/index.ts create mode 100644 fixtures/interactive-dev-tests/container-app/src/tsconfig.json create mode 100644 fixtures/interactive-dev-tests/container-app/src/worker-configuration.d.ts create mode 100644 fixtures/interactive-dev-tests/container-app/wrangler.jsonc create mode 100644 fixtures/interactive-dev-tests/index.js create mode 100644 fixtures/interactive-dev-tests/multi-containers-app/DockerfileA create mode 100644 fixtures/interactive-dev-tests/multi-containers-app/DockerfileB create mode 100644 fixtures/interactive-dev-tests/multi-containers-app/src/index.ts create mode 100644 fixtures/interactive-dev-tests/multi-containers-app/src/tsconfig.json create mode 100644 fixtures/interactive-dev-tests/multi-containers-app/src/worker-configuration.d.ts create mode 100644 fixtures/interactive-dev-tests/multi-containers-app/wrangler.jsonc create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/tsconfig.json create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/Dockerfile create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/index.ts create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/worker-configuration.d.ts create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/wrangler.jsonc create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/Dockerfile create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/index.ts create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/worker-configuration.d.ts create mode 100644 fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/wrangler.jsonc create mode 100644 fixtures/interactive-dev-tests/package.json create mode 100644 fixtures/interactive-dev-tests/public/index.html create mode 100644 fixtures/interactive-dev-tests/src/index.mjs create mode 100644 fixtures/interactive-dev-tests/src/startup-error.ts create mode 100644 fixtures/interactive-dev-tests/src/worker-a.mjs create mode 100644 fixtures/interactive-dev-tests/src/worker-b.mjs create mode 100644 fixtures/interactive-dev-tests/tests/index.test.ts create mode 100644 fixtures/interactive-dev-tests/tsconfig.json create mode 100644 fixtures/interactive-dev-tests/vitest.config.mts create mode 100644 fixtures/interactive-dev-tests/wrangler.a.jsonc create mode 100644 fixtures/interactive-dev-tests/wrangler.b.jsonc create mode 100644 fixtures/interactive-dev-tests/wrangler.jsonc create mode 100644 fixtures/isomorphic-random-example/README.md create mode 100644 fixtures/isomorphic-random-example/package.json create mode 100644 fixtures/isomorphic-random-example/src/default.js create mode 100644 fixtures/isomorphic-random-example/src/node.js create mode 100644 fixtures/isomorphic-random-example/src/other.js create mode 100644 fixtures/isomorphic-random-example/src/workerd.mjs create mode 100644 fixtures/legacy-site-app/package.json create mode 100644 fixtures/legacy-site-app/public/404.html create mode 100644 fixtures/legacy-site-app/public/favicon.ico create mode 100644 fixtures/legacy-site-app/public/img/200-wrangler-ferris.gif create mode 100644 fixtures/legacy-site-app/public/img/404-wrangler-ferris.gif create mode 100644 fixtures/legacy-site-app/public/index.html create mode 100644 fixtures/legacy-site-app/workers-site/.gitignore create mode 100644 fixtures/legacy-site-app/workers-site/index.js create mode 100644 fixtures/legacy-site-app/workers-site/package.json create mode 100644 fixtures/legacy-site-app/wrangler.jsonc create mode 100644 fixtures/miniflare-node-test/package.json create mode 100644 fixtures/miniflare-node-test/src/index-build.test.js create mode 100644 fixtures/miniflare-node-test/src/index-with-bindings.test.js create mode 100644 fixtures/miniflare-node-test/src/index-with-imports.js create mode 100644 fixtures/miniflare-node-test/src/index-with-imports.test.js create mode 100644 fixtures/miniflare-node-test/src/index.js create mode 100644 fixtures/miniflare-node-test/src/index.test.js create mode 100644 fixtures/miniflare-node-test/src/index.ts create mode 100644 fixtures/miniflare-node-test/src/say-hello.js create mode 100644 fixtures/miniflare-node-test/tsconfig.json create mode 100644 fixtures/miniflare-node-test/wrangler-build.json create mode 100644 fixtures/miniflare-node-test/wrangler.jsonc create mode 100644 fixtures/multi-worker/package.json create mode 100644 fixtures/multi-worker/tests/multi-worker.test.ts create mode 100644 fixtures/multi-worker/vitest.config.mts create mode 100644 fixtures/multi-worker/workers/default/src/index.ts create mode 100644 fixtures/multi-worker/workers/default/tsconfig.json create mode 100644 fixtures/multi-worker/workers/default/worker-configuration.d.ts create mode 100644 fixtures/multi-worker/workers/default/wrangler.jsonc create mode 100644 fixtures/multi-worker/workers/sentry/src/index.ts create mode 100644 fixtures/multi-worker/workers/sentry/tsconfig.json create mode 100644 fixtures/multi-worker/workers/sentry/worker-configuration.d.ts create mode 100644 fixtures/multi-worker/workers/sentry/wrangler.jsonc create mode 100644 fixtures/no-bundle-import/CHANGELOG.md create mode 100644 fixtures/no-bundle-import/README.md create mode 100644 fixtures/no-bundle-import/package.json create mode 100644 fixtures/no-bundle-import/src/data.bin create mode 100644 fixtures/no-bundle-import/src/data.txt create mode 100644 fixtures/no-bundle-import/src/dynamic.cjs create mode 100644 fixtures/no-bundle-import/src/dynamic.js create mode 100644 fixtures/no-bundle-import/src/index.js create mode 100644 fixtures/no-bundle-import/src/index.test.ts create mode 100644 fixtures/no-bundle-import/src/lang/en.json create mode 100644 fixtures/no-bundle-import/src/lang/fr.json create mode 100644 fixtures/no-bundle-import/src/nested/index.js create mode 100644 fixtures/no-bundle-import/src/nested/say-hello.js create mode 100644 fixtures/no-bundle-import/src/nested/simple.wasm create mode 100644 fixtures/no-bundle-import/src/say-hello.cjs create mode 100644 fixtures/no-bundle-import/src/say-hello.js create mode 100644 fixtures/no-bundle-import/src/simple.wasm create mode 100644 fixtures/no-bundle-import/vitest.config.mts create mode 100644 fixtures/no-bundle-import/wrangler.jsonc create mode 100644 fixtures/node-app-pages/.gitignore create mode 100644 fixtures/node-app-pages/functions/stripe.ts create mode 100644 fixtures/node-app-pages/package.json create mode 100644 fixtures/node-app-pages/public/index.html create mode 100644 fixtures/node-app-pages/tests/index.test.ts create mode 100644 fixtures/node-app-pages/tests/tsconfig.json create mode 100644 fixtures/node-app-pages/tsconfig.json create mode 100644 fixtures/node-app-pages/vitest.config.mts create mode 100644 fixtures/node-env/package.json create mode 100644 fixtures/node-env/src/index.ts create mode 100644 fixtures/node-env/tests/node-env.test.ts create mode 100644 fixtures/node-env/tsconfig.json create mode 100644 fixtures/node-env/vitest.config.mts create mode 100644 fixtures/node-env/wrangler.jsonc create mode 100644 fixtures/nodejs-als-app/package.json create mode 100644 fixtures/nodejs-als-app/src/index.ts create mode 100644 fixtures/nodejs-als-app/tests/index.test.ts create mode 100644 fixtures/nodejs-als-app/tests/tsconfig.json create mode 100644 fixtures/nodejs-als-app/tsconfig.json create mode 100644 fixtures/nodejs-als-app/turbo.json create mode 100644 fixtures/nodejs-als-app/vitest.config.mts create mode 100644 fixtures/nodejs-als-app/wrangler.jsonc create mode 100644 fixtures/nodejs-hybrid-app/.env create mode 100644 fixtures/nodejs-hybrid-app/.gitignore create mode 100644 fixtures/nodejs-hybrid-app/package.json create mode 100644 fixtures/nodejs-hybrid-app/src/dep.cjs create mode 100644 fixtures/nodejs-hybrid-app/src/index.ts create mode 100644 fixtures/nodejs-hybrid-app/tests/index.test.ts create mode 100644 fixtures/nodejs-hybrid-app/tests/tsconfig.json create mode 100644 fixtures/nodejs-hybrid-app/tsconfig.json create mode 100644 fixtures/nodejs-hybrid-app/turbo.json create mode 100644 fixtures/nodejs-hybrid-app/vitest.config.mts create mode 100644 fixtures/nodejs-hybrid-app/worker-configuration.d.ts create mode 100644 fixtures/nodejs-hybrid-app/wrangler.jsonc create mode 100644 fixtures/pages-dev-proxy-with-script/_worker.js create mode 100644 fixtures/pages-dev-proxy-with-script/custom/script/path/index.js create mode 100644 fixtures/pages-dev-proxy-with-script/package.json create mode 100644 fixtures/pages-dev-proxy-with-script/tests/index.test.ts create mode 100644 fixtures/pages-dev-proxy-with-script/tests/tsconfig.json create mode 100644 fixtures/pages-dev-proxy-with-script/tsconfig.json create mode 100644 fixtures/pages-dev-proxy-with-script/vitest.config.mts create mode 100644 fixtures/pages-functions-app/.gitignore create mode 100644 fixtures/pages-functions-app/functions/_middleware.ts create mode 100644 fixtures/pages-functions-app/functions/blog/[slug].ts create mode 100644 fixtures/pages-functions-app/functions/date.ts create mode 100644 fixtures/pages-functions-app/functions/import-html.ts create mode 100644 fixtures/pages-functions-app/functions/intercept.ts create mode 100644 fixtures/pages-functions-app/functions/middleware-data/additional-data.ts create mode 100644 fixtures/pages-functions-app/functions/middleware-data/bad-data.ts create mode 100644 fixtures/pages-functions-app/functions/middleware-data/merge-data.ts create mode 100644 fixtures/pages-functions-app/functions/middleware-data/mutate-data.ts create mode 100644 fixtures/pages-functions-app/functions/mounted-plugin/_middleware.ts create mode 100644 fixtures/pages-functions-app/functions/mounted-with-param/[p]/plugin/_middleware.ts create mode 100644 fixtures/pages-functions-app/functions/next.ts create mode 100644 fixtures/pages-functions-app/functions/passThroughOnException/_middleware.ts create mode 100644 fixtures/pages-functions-app/functions/passThroughOnException/nested.ts create mode 100644 fixtures/pages-functions-app/functions/passThroughOnExceptionClosed.ts create mode 100644 fixtures/pages-functions-app/functions/passThroughOnExceptionOpen.ts create mode 100644 fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/_middleware.ts create mode 100644 fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/nested.ts create mode 100644 fixtures/pages-functions-app/functions/r2/create.ts create mode 100644 fixtures/pages-functions-app/functions/r2/get.ts create mode 100644 fixtures/pages-functions-app/functions/regex_chars/my-file.ts create mode 100644 fixtures/pages-functions-app/functions/static/_middleware.ts create mode 100644 fixtures/pages-functions-app/functions/variables.ts create mode 100644 fixtures/pages-functions-app/index.d.ts create mode 100644 fixtures/pages-functions-app/package.json create mode 100644 fixtures/pages-functions-app/public/[id].js create mode 100644 fixtures/pages-functions-app/public/_headers create mode 100644 fixtures/pages-functions-app/public/_redirects create mode 100644 fixtures/pages-functions-app/public/a.b.html create mode 100644 fixtures/pages-functions-app/public/here.html create mode 100644 fixtures/pages-functions-app/public/index.html create mode 100644 fixtures/pages-functions-app/public/some-asset.html create mode 100644 fixtures/pages-functions-app/public/users/[id].html create mode 100644 fixtures/pages-functions-app/static-assets/index.html create mode 100644 fixtures/pages-functions-app/tests/index.test.ts create mode 100644 fixtures/pages-functions-app/tests/tsconfig.json create mode 100644 fixtures/pages-functions-app/tsconfig.json create mode 100644 fixtures/pages-functions-app/vitest.config.mts create mode 100644 fixtures/pages-functions-unenv-alias/functions/[[path]].ts create mode 100644 fixtures/pages-functions-unenv-alias/package.json create mode 100644 fixtures/pages-functions-unenv-alias/tests/index.test.ts create mode 100644 fixtures/pages-functions-unenv-alias/tests/tsconfig.json create mode 100644 fixtures/pages-functions-unenv-alias/tsconfig.json create mode 100644 fixtures/pages-functions-unenv-alias/vitest.config.mts create mode 100644 fixtures/pages-functions-unenv-alias/wrangler.jsonc create mode 100644 fixtures/pages-functions-wasm-app/README.md create mode 100644 fixtures/pages-functions-wasm-app/external-modules/add.wasm create mode 100644 fixtures/pages-functions-wasm-app/external-modules/add.wat create mode 100644 fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.html create mode 100644 fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.txt create mode 100644 fixtures/pages-functions-wasm-app/functions/meaning-of-life-html.js create mode 100644 fixtures/pages-functions-wasm-app/functions/meaning-of-life-text.js create mode 100644 fixtures/pages-functions-wasm-app/functions/meaning-of-life-wasm.js create mode 100644 fixtures/pages-functions-wasm-app/package.json create mode 100644 fixtures/pages-functions-wasm-app/public/index.html create mode 100644 fixtures/pages-functions-wasm-app/tests/index.test.ts create mode 100644 fixtures/pages-functions-wasm-app/tests/tsconfig.json create mode 100644 fixtures/pages-functions-wasm-app/tsconfig.json create mode 100644 fixtures/pages-functions-wasm-app/vitest.config.mts create mode 100644 fixtures/pages-functions-with-config-file-app/README.md create mode 100644 fixtures/pages-functions-with-config-file-app/functions/celebrate.ts create mode 100644 fixtures/pages-functions-with-config-file-app/functions/holiday.ts create mode 100644 fixtures/pages-functions-with-config-file-app/package.json create mode 100644 fixtures/pages-functions-with-config-file-app/public/_routes.json create mode 100644 fixtures/pages-functions-with-config-file-app/public/index.html create mode 100644 fixtures/pages-functions-with-config-file-app/tests/index.test.ts create mode 100644 fixtures/pages-functions-with-config-file-app/tests/tsconfig.json create mode 100644 fixtures/pages-functions-with-config-file-app/tsconfig.json create mode 100644 fixtures/pages-functions-with-config-file-app/vitest.config.mts create mode 100644 fixtures/pages-functions-with-config-file-app/wrangler.jsonc create mode 100644 fixtures/pages-functions-with-routes-app/CHANGELOG.md create mode 100644 fixtures/pages-functions-with-routes-app/README.md create mode 100644 fixtures/pages-functions-with-routes-app/functions/bye.ts create mode 100644 fixtures/pages-functions-with-routes-app/functions/date.ts create mode 100644 fixtures/pages-functions-with-routes-app/functions/greeting/bye.ts create mode 100644 fixtures/pages-functions-with-routes-app/functions/greeting/hello.ts create mode 100644 fixtures/pages-functions-with-routes-app/functions/greeting/index.ts create mode 100644 fixtures/pages-functions-with-routes-app/functions/greetings.ts create mode 100644 fixtures/pages-functions-with-routes-app/functions/index.ts create mode 100644 fixtures/pages-functions-with-routes-app/package.json create mode 100644 fixtures/pages-functions-with-routes-app/public/_routes.json create mode 100644 fixtures/pages-functions-with-routes-app/public/greeting/test.json create mode 100644 fixtures/pages-functions-with-routes-app/public/index.html create mode 100644 fixtures/pages-functions-with-routes-app/tests/index.test.ts create mode 100644 fixtures/pages-functions-with-routes-app/tests/tsconfig.json create mode 100644 fixtures/pages-functions-with-routes-app/tsconfig.json create mode 100644 fixtures/pages-functions-with-routes-app/vitest.config.mts create mode 100644 fixtures/pages-nodejs-v2-compat/apps/functions/README.md create mode 100644 fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/README.md create mode 100644 fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/_worker.js/index.js create mode 100644 fixtures/pages-nodejs-v2-compat/apps/workerjs-file/README.md create mode 100644 fixtures/pages-nodejs-v2-compat/apps/workerjs-file/_worker.js create mode 100644 fixtures/pages-nodejs-v2-compat/functions/[[path]].ts create mode 100644 fixtures/pages-nodejs-v2-compat/package.json create mode 100644 fixtures/pages-nodejs-v2-compat/tests/index.test.ts create mode 100644 fixtures/pages-nodejs-v2-compat/tests/tsconfig.json create mode 100644 fixtures/pages-nodejs-v2-compat/tsconfig.json create mode 100644 fixtures/pages-nodejs-v2-compat/vitest.config.mts create mode 100644 fixtures/pages-nodejs-v2-compat/wrangler.jsonc create mode 100644 fixtures/pages-plugin-example/.gitignore create mode 100644 fixtures/pages-plugin-example/functions/_middleware.ts create mode 100644 fixtures/pages-plugin-example/functions/api/v1/instance.ts create mode 100644 fixtures/pages-plugin-example/functions/ext.ts create mode 100644 fixtures/pages-plugin-example/functions/fixed.ts create mode 100644 fixtures/pages-plugin-example/functions/proxy-me-somewhere-else.ts create mode 100644 fixtures/pages-plugin-example/functions/static/_middleware.ts create mode 100644 fixtures/pages-plugin-example/functions/status.ts create mode 100644 fixtures/pages-plugin-example/index.d.ts create mode 100644 fixtures/pages-plugin-example/package.json create mode 100644 fixtures/pages-plugin-example/public/dir/bar.html create mode 100644 fixtures/pages-plugin-example/public/foo.html create mode 100644 fixtures/pages-plugin-example/public/index.html create mode 100644 fixtures/pages-plugin-example/tsconfig.json create mode 100644 fixtures/pages-plugin-example/turbo.json create mode 100644 fixtures/pages-plugin-mounted-on-root-app/.gitignore create mode 100644 fixtures/pages-plugin-mounted-on-root-app/functions/_middleware.ts create mode 100644 fixtures/pages-plugin-mounted-on-root-app/index.d.ts create mode 100644 fixtures/pages-plugin-mounted-on-root-app/package.json create mode 100644 fixtures/pages-plugin-mounted-on-root-app/public/index.html create mode 100644 fixtures/pages-plugin-mounted-on-root-app/tests/index.test.ts create mode 100644 fixtures/pages-plugin-mounted-on-root-app/tests/tsconfig.json create mode 100644 fixtures/pages-plugin-mounted-on-root-app/tsconfig.json create mode 100644 fixtures/pages-plugin-mounted-on-root-app/vitest.config.mts create mode 100644 fixtures/pages-proxy-app/package.json create mode 100644 fixtures/pages-proxy-app/server/index.ts create mode 100644 fixtures/pages-proxy-app/tests/index.test.ts create mode 100644 fixtures/pages-proxy-app/tests/tsconfig.json create mode 100644 fixtures/pages-proxy-app/tsconfig.json create mode 100644 fixtures/pages-proxy-app/turbo.json create mode 100644 fixtures/pages-proxy-app/vitest.config.mts create mode 100644 fixtures/pages-redirected-config/.gitignore create mode 100644 fixtures/pages-redirected-config/package.json create mode 100644 fixtures/pages-redirected-config/src/index.js create mode 100644 fixtures/pages-redirected-config/tests/index.test.ts create mode 100644 fixtures/pages-redirected-config/tools/build.ts create mode 100644 fixtures/pages-redirected-config/tsconfig.json create mode 100644 fixtures/pages-redirected-config/turbo.json create mode 100644 fixtures/pages-redirected-config/vitest.config.mts create mode 100644 fixtures/pages-simple-assets/index.d.ts create mode 100644 fixtures/pages-simple-assets/package.json create mode 100644 fixtures/pages-simple-assets/public/index.html create mode 100644 fixtures/pages-simple-assets/tests/index.test.ts create mode 100644 fixtures/pages-simple-assets/tests/tsconfig.json create mode 100644 fixtures/pages-simple-assets/tsconfig.json create mode 100644 fixtures/pages-simple-assets/vitest.config.mts create mode 100644 fixtures/pages-workerjs-and-functions-app/CHANGELOG.md create mode 100644 fixtures/pages-workerjs-and-functions-app/README.md create mode 100644 fixtures/pages-workerjs-and-functions-app/functions/date.ts create mode 100644 fixtures/pages-workerjs-and-functions-app/functions/greeting/hello.ts create mode 100644 fixtures/pages-workerjs-and-functions-app/package.json create mode 100644 fixtures/pages-workerjs-and-functions-app/public/_worker.js create mode 100644 fixtures/pages-workerjs-and-functions-app/public/index.html create mode 100644 fixtures/pages-workerjs-and-functions-app/tests/index.test.ts create mode 100644 fixtures/pages-workerjs-and-functions-app/tests/tsconfig.json create mode 100644 fixtures/pages-workerjs-and-functions-app/tsconfig.json create mode 100644 fixtures/pages-workerjs-and-functions-app/vitest.config.mts create mode 100644 fixtures/pages-workerjs-app/.env create mode 100644 fixtures/pages-workerjs-app/.gitignore create mode 100644 fixtures/pages-workerjs-app/package.json create mode 100644 fixtures/pages-workerjs-app/tests/index.test.ts create mode 100644 fixtures/pages-workerjs-app/tests/tsconfig.json create mode 100644 fixtures/pages-workerjs-app/tsconfig.json create mode 100644 fixtures/pages-workerjs-app/vitest.config.mts create mode 100644 fixtures/pages-workerjs-app/workerjs-test/_routes.json create mode 100644 fixtures/pages-workerjs-app/workerjs-test/_worker.js create mode 100644 fixtures/pages-workerjs-app/workerjs-test/other-script.js create mode 100644 fixtures/pages-workerjs-directory/CHANGELOG.md create mode 100644 fixtures/pages-workerjs-directory/package.json create mode 100644 fixtures/pages-workerjs-directory/public/_worker.js/add.wasm create mode 100644 fixtures/pages-workerjs-directory/public/_worker.js/index.js create mode 100644 fixtures/pages-workerjs-directory/public/_worker.js/other-other-script.mjs create mode 100644 fixtures/pages-workerjs-directory/public/_worker.js/other-script.js create mode 100644 fixtures/pages-workerjs-directory/public/_worker.js/static.js create mode 100644 fixtures/pages-workerjs-directory/public/_worker.js/static.mjs create mode 100644 fixtures/pages-workerjs-directory/public/index.html create mode 100644 fixtures/pages-workerjs-directory/tests/index.test.ts create mode 100644 fixtures/pages-workerjs-directory/tests/tsconfig.json create mode 100644 fixtures/pages-workerjs-directory/tsconfig.json create mode 100644 fixtures/pages-workerjs-directory/vitest.config.mts create mode 100644 fixtures/pages-workerjs-wasm-app/README.md create mode 100644 fixtures/pages-workerjs-wasm-app/external-modules/meaning-of-life.html create mode 100644 fixtures/pages-workerjs-wasm-app/package.json create mode 100644 fixtures/pages-workerjs-wasm-app/public/_worker.js create mode 100644 fixtures/pages-workerjs-wasm-app/public/index.html create mode 100644 fixtures/pages-workerjs-wasm-app/tests/index.test.ts create mode 100644 fixtures/pages-workerjs-wasm-app/tests/tsconfig.json create mode 100644 fixtures/pages-workerjs-wasm-app/tsconfig.json create mode 100644 fixtures/pages-workerjs-wasm-app/vitest.config.mts create mode 100644 fixtures/pages-workerjs-wasm-app/wasm/multiply.wasm create mode 100644 fixtures/pages-workerjs-wasm-app/wasm/multiply.wat create mode 100644 fixtures/pages-workerjs-with-config-file-app/README.md create mode 100644 fixtures/pages-workerjs-with-config-file-app/package.json create mode 100644 fixtures/pages-workerjs-with-config-file-app/public/_routes.json create mode 100644 fixtures/pages-workerjs-with-config-file-app/public/_worker.js create mode 100644 fixtures/pages-workerjs-with-config-file-app/public/index.html create mode 100644 fixtures/pages-workerjs-with-config-file-app/tests/index.test.ts create mode 100644 fixtures/pages-workerjs-with-config-file-app/tests/tsconfig.json create mode 100644 fixtures/pages-workerjs-with-config-file-app/tsconfig.json create mode 100644 fixtures/pages-workerjs-with-config-file-app/vitest.config.mts create mode 100644 fixtures/pages-workerjs-with-config-file-app/wrangler.jsonc create mode 100644 fixtures/pages-workerjs-with-routes-app/CHANGELOG.md create mode 100644 fixtures/pages-workerjs-with-routes-app/README.md create mode 100644 fixtures/pages-workerjs-with-routes-app/package.json create mode 100644 fixtures/pages-workerjs-with-routes-app/public/_routes.json create mode 100644 fixtures/pages-workerjs-with-routes-app/public/_worker.js create mode 100644 fixtures/pages-workerjs-with-routes-app/public/index.html create mode 100644 fixtures/pages-workerjs-with-routes-app/tests/index.test.ts create mode 100644 fixtures/pages-workerjs-with-routes-app/tests/tsconfig.json create mode 100644 fixtures/pages-workerjs-with-routes-app/tsconfig.json create mode 100644 fixtures/pages-workerjs-with-routes-app/vitest.config.mts create mode 100644 fixtures/python-worker/requirements.txt create mode 100644 fixtures/python-worker/src/arith.py create mode 100644 fixtures/python-worker/src/index.py create mode 100644 fixtures/python-worker/src/other.py create mode 100644 fixtures/python-worker/wrangler.jsonc create mode 100644 fixtures/ratelimit-app/.gitignore create mode 100644 fixtures/ratelimit-app/package.json create mode 100644 fixtures/ratelimit-app/src/index.js create mode 100644 fixtures/ratelimit-app/tests/index.test.ts create mode 100644 fixtures/ratelimit-app/tests/tsconfig.json create mode 100644 fixtures/ratelimit-app/tsconfig.json create mode 100644 fixtures/ratelimit-app/vitest.config.mts create mode 100644 fixtures/ratelimit-app/wrangler.jsonc create mode 100644 fixtures/redirected-config-worker-with-environments/.gitignore create mode 100644 fixtures/redirected-config-worker-with-environments/package.json create mode 100644 fixtures/redirected-config-worker-with-environments/src/index.js create mode 100644 fixtures/redirected-config-worker-with-environments/tests/index.test.ts create mode 100644 fixtures/redirected-config-worker-with-environments/tools/build.ts create mode 100644 fixtures/redirected-config-worker-with-environments/tsconfig.json create mode 100644 fixtures/redirected-config-worker-with-environments/turbo.json create mode 100644 fixtures/redirected-config-worker-with-environments/vitest.config.mts create mode 100644 fixtures/redirected-config-worker-with-environments/wrangler.jsonc create mode 100644 fixtures/redirected-config-worker/.gitignore create mode 100644 fixtures/redirected-config-worker/package.json create mode 100644 fixtures/redirected-config-worker/src/index.js create mode 100644 fixtures/redirected-config-worker/tests/index.test.ts create mode 100644 fixtures/redirected-config-worker/tools/build.ts create mode 100644 fixtures/redirected-config-worker/tsconfig.json create mode 100644 fixtures/redirected-config-worker/turbo.json create mode 100644 fixtures/redirected-config-worker/vitest.config.mts create mode 100644 fixtures/redirected-config-worker/wrangler.jsonc create mode 100644 fixtures/routing-app/functions/[default].js create mode 100644 fixtures/routing-app/functions/_middleware.js create mode 100644 fixtures/routing-app/functions/bar/[stuff].js create mode 100644 fixtures/routing-app/functions/bar/baz/[[all]].js create mode 100644 fixtures/routing-app/functions/bar/foo.js create mode 100644 fixtures/routing-app/functions/blah.js create mode 100644 fixtures/routing-app/functions/foo.js create mode 100644 fixtures/routing-app/functions/index.ts create mode 100644 fixtures/routing-app/package.json create mode 100644 fixtures/routing-app/public/_routes.json create mode 100644 fixtures/routing-app/public/index.html create mode 100644 fixtures/routing-app/tsconfig.json create mode 100644 fixtures/rules-app/md.d.ts create mode 100644 fixtures/rules-app/package.json create mode 100644 fixtures/rules-app/src/content.md create mode 100644 fixtures/rules-app/src/index.ts create mode 100644 fixtures/rules-app/tsconfig.json create mode 100644 fixtures/rules-app/wrangler.jsonc create mode 100644 fixtures/secrets-store/package.json create mode 100644 fixtures/secrets-store/src/index.ts create mode 100644 fixtures/secrets-store/tsconfig.json create mode 100644 fixtures/secrets-store/wrangler.jsonc create mode 100644 fixtures/shared/package.json create mode 100644 fixtures/shared/src/fs-helpers.ts create mode 100644 fixtures/shared/src/mock-console.ts create mode 100644 fixtures/shared/src/mock-postgres-server.ts create mode 100644 fixtures/shared/src/run-wrangler-long-lived.ts create mode 100644 fixtures/sites-app/package.json create mode 100644 fixtures/sites-app/public/404.html create mode 100644 fixtures/sites-app/public/favicon.ico create mode 100644 fixtures/sites-app/public/img/200-wrangler-ferris.gif create mode 100644 fixtures/sites-app/public/img/404-wrangler-ferris.gif create mode 100644 fixtures/sites-app/public/index.html create mode 100644 fixtures/sites-app/src/modules.js create mode 100644 fixtures/sites-app/src/service-worker.js create mode 100644 fixtures/sites-app/wrangler.jsonc create mode 100644 fixtures/start-worker-node-test/package.json create mode 100644 fixtures/start-worker-node-test/src/config-errors.test.js create mode 100644 fixtures/start-worker-node-test/src/index.test.js create mode 100644 fixtures/start-worker-node-test/src/index.ts create mode 100644 fixtures/start-worker-node-test/src/say-hello.ts create mode 100644 fixtures/start-worker-node-test/tsconfig.json create mode 100644 fixtures/start-worker-node-test/wrangler.json create mode 100644 fixtures/unbound-durable-object/package.json create mode 100644 fixtures/unbound-durable-object/src/index.ts create mode 100644 fixtures/unbound-durable-object/tests/index.test.ts create mode 100644 fixtures/unbound-durable-object/tests/tsconfig.json create mode 100644 fixtures/unbound-durable-object/tsconfig.json create mode 100644 fixtures/unbound-durable-object/vitest.config.mts create mode 100644 fixtures/unbound-durable-object/wrangler.jsonc create mode 100644 fixtures/unsafe-external-plugin/README.md create mode 100644 fixtures/unsafe-external-plugin/package.json create mode 100644 fixtures/unsafe-external-plugin/src/index.ts create mode 100644 fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts create mode 100644 fixtures/unsafe-external-plugin/src/plugins/worker-shim.ts create mode 100644 fixtures/unsafe-external-plugin/src/workers/binding.worker.ts create mode 100644 fixtures/unsafe-external-plugin/src/workers/object.worker.ts create mode 100644 fixtures/unsafe-external-plugin/tools/build.ts create mode 100644 fixtures/unsafe-external-plugin/tsconfig.json create mode 100644 fixtures/unsafe-external-plugin/turbo.json create mode 100644 fixtures/vitest-pool-workers-examples/README.md create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/README.md create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/test/index.spec.ts create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/ai-vectorize/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/README.md create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/fetch-integration-auxiliary.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/scheduled-integration-auxiliary.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/README.md create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/greet.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/basics-unit-integration-self/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/container-app/Dockerfile create mode 100644 fixtures/vitest-pool-workers-examples/container-app/README.md create mode 100644 fixtures/vitest-pool-workers-examples/container-app/container/simple-node-app.js create mode 100644 fixtures/vitest-pool-workers-examples/container-app/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/container-app/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/container-app/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/container-app/test/container.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/container-app/test/container.unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/container-app/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/container-app/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/container-app/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/container-app/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/README.md create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/worker-configuration.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/src/virtual.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/src/worker-configuration.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/src/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/test/auxiliary.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/test/durable-objects.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/test/integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/test/unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/context-exports/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/README.md create mode 100644 fixtures/vitest-pool-workers-examples/d1/migrations/0000_initial.sql create mode 100644 fixtures/vitest-pool-workers-examples/d1/migrations/0001_admin.sql create mode 100644 fixtures/vitest-pool-workers-examples/d1/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/d1/src/utils.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/test/apply-migrations.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/test/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/test/queries.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/test/routes.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/d1/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/d1/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/d1/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/README.md create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/test/exports.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects-exports/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/README.md create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/alarm.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/direct-access.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/sqlite-in-do.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/durable-objects/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/src/greeting.ts create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/src/worker-configuration.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/test/dynamic-import.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/dynamic-import/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/README.md create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/test/echo.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/hyperdrive/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/images/README.md create mode 100644 fixtures/vitest-pool-workers-examples/images/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/images/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/images/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/images/test/images.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/images/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/images/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/images/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/images/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/README.md create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/src/helpers.ts create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/test/kv.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/test/r2.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/kv-r2-caches/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/misc/README.md create mode 100644 fixtures/vitest-pool-workers-examples/misc/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/public/test.txt create mode 100644 fixtures/vitest-pool-workers-examples/misc/src/other-worker.mjs create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/bare-specifiers.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/defines.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/durable-objects.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/fake-timers.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/global.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/large-provide.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/module-mocking.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/msw.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/nodejs.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/pages-functions.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/server.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/misc/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/misc/vitest.assets.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/vitest.nodejs.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/misc/wrangler.assets.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/misc/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/misc/wrangler.nodejs.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/README.md create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/src/test.sql create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/test/index.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/test/index.spec.ts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/add.wasm create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.js create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/package.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.d.mts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.mjs create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/package.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/value.mjs create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/index.js create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/package.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/subfolder/index.js create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/add.wasm create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.d.mts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.mjs create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/package.json create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/module-resolution/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/README.md create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/api-service/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/database-service/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/test/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/test/integration.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/multiple-workers/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/package.json create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/.gitignore create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/README.md create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/_middleware.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/kv/[key].ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/ping.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/global-setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/404.html create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_headers create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_redirects create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/index.html create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/secure.html create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-with-config/README.md create mode 100644 fixtures/vitest-pool-workers-examples/pages-with-config/pages-config.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-with-config/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pages-with-config/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/pages-with-config/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/README.md create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/pipelines/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/queues/README.md create mode 100644 fixtures/vitest-pool-workers-examples/queues/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/test/queue-producer-integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/test/queue-producer-unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/queues/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/queues/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/queues/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/README.md create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/test/declarative.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/test/imperative.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/test/server.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/test/setup.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/request-mocking/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/reset/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/reset/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/reset/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/reset/test/reset.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/reset/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/reset/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/reset/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/reset/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/rpc/README.md create mode 100644 fixtures/vitest-pool-workers-examples/rpc/src/counter.ts create mode 100644 fixtures/vitest-pool-workers-examples/rpc/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/rpc/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/rpc/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/rpc/test/integration-self.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/rpc/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/rpc/test/unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/rpc/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/rpc/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/rpc/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/tsc-all.mjs create mode 100644 fixtures/vitest-pool-workers-examples/tsconfig.node.json create mode 100644 fixtures/vitest-pool-workers-examples/tsconfig.workerd-test.json create mode 100644 fixtures/vitest-pool-workers-examples/tsconfig.workerd.json create mode 100644 fixtures/vitest-pool-workers-examples/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/vitest.global.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/README.md create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/src/add.wasm create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/src/wasm-shim.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/test/add.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/web-assembly/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/assets-no-dir.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets-no-dir/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/README.md create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/public/binding.html create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/public/index.html create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/test/assets.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/workers-assets/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-examples/workflows/README.md create mode 100644 fixtures/vitest-pool-workers-examples/workflows/src/env.d.ts create mode 100644 fixtures/vitest-pool-workers-examples/workflows/src/index.ts create mode 100644 fixtures/vitest-pool-workers-examples/workflows/src/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workflows/test/integration.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/workflows/test/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workflows/test/unit.test.ts create mode 100644 fixtures/vitest-pool-workers-examples/workflows/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-examples/workflows/vitest.config.ts create mode 100644 fixtures/vitest-pool-workers-examples/workflows/wrangler.jsonc create mode 100644 fixtures/vitest-pool-workers-remote-bindings/env.d.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/package.json create mode 100644 fixtures/vitest-pool-workers-remote-bindings/remote-worker.js create mode 100644 fixtures/vitest-pool-workers-remote-bindings/remote-worker.staging.js create mode 100644 fixtures/vitest-pool-workers-remote-bindings/run-tests.mjs create mode 100644 fixtures/vitest-pool-workers-remote-bindings/src/index.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/test-staging/index.spec.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/test/index.spec.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/tsconfig.json create mode 100644 fixtures/vitest-pool-workers-remote-bindings/turbo.json create mode 100644 fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.staging.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/worker-configuration.d.ts create mode 100644 fixtures/vitest-pool-workers-remote-bindings/wrangler.json create mode 100644 fixtures/wasm-app/README.md create mode 100644 fixtures/wasm-app/package.json create mode 100644 fixtures/wasm-app/worker/index_bg.wasm create mode 100644 fixtures/wasm-app/worker/module/export_wasm.js create mode 100644 fixtures/wasm-app/worker/module/index.js create mode 100644 fixtures/wasm-app/worker/module/index_bg.js create mode 100644 fixtures/wasm-app/worker/service-worker-module/export_wasm.js create mode 100644 fixtures/wasm-app/worker/service-worker-module/index.js create mode 100644 fixtures/wasm-app/worker/service-worker-module/index_bg.js create mode 100644 fixtures/wasm-app/worker/service-worker/export_wasm.js create mode 100644 fixtures/wasm-app/worker/service-worker/index.js create mode 100644 fixtures/wasm-app/worker/service-worker/index_bg.js create mode 100644 fixtures/wasm-app/worker/service-worker/wrangler.jsonc create mode 100644 fixtures/wildcard-modules/package.json create mode 100644 fixtures/wildcard-modules/src/common.cjs create mode 100644 fixtures/wildcard-modules/src/dep.ts create mode 100644 fixtures/wildcard-modules/src/dynamic.js create mode 100644 fixtures/wildcard-modules/src/index.ts create mode 100644 fixtures/wildcard-modules/src/lang/en.js create mode 100644 fixtures/wildcard-modules/src/lang/fr.js create mode 100644 fixtures/wildcard-modules/src/text.d.ts create mode 100644 fixtures/wildcard-modules/src/text.txt create mode 100644 fixtures/wildcard-modules/test/index.test.ts create mode 100644 fixtures/wildcard-modules/test/tsconfig.json create mode 100644 fixtures/wildcard-modules/tsconfig.json create mode 100644 fixtures/wildcard-modules/turbo.json create mode 100644 fixtures/wildcard-modules/vitest.config.mts create mode 100644 fixtures/wildcard-modules/wrangler.jsonc create mode 100644 fixtures/worker-app/.env create mode 100644 fixtures/worker-app/.gitignore create mode 100644 fixtures/worker-app/package.json create mode 100644 fixtures/worker-app/public/404.html create mode 100644 fixtures/worker-app/public/favicon.ico create mode 100644 fixtures/worker-app/public/img/200-wrangler-ferris.gif create mode 100644 fixtures/worker-app/public/img/404-wrangler-ferris.gif create mode 100644 fixtures/worker-app/public/index.html create mode 100644 fixtures/worker-app/src/dep.js create mode 100644 fixtures/worker-app/src/explicit-resource-management.js create mode 100644 fixtures/worker-app/src/index.js create mode 100644 fixtures/worker-app/src/log.ts create mode 100644 fixtures/worker-app/tests/build-conditions.test.ts create mode 100644 fixtures/worker-app/tests/https.test.ts create mode 100644 fixtures/worker-app/tests/index.test.ts create mode 100644 fixtures/worker-app/tests/tsconfig.json create mode 100644 fixtures/worker-app/tests/undrained-body.test.ts create mode 100644 fixtures/worker-app/tsconfig.json create mode 100644 fixtures/worker-app/vitest.config.mts create mode 100644 fixtures/worker-app/wrangler.jsonc create mode 100644 fixtures/worker-logs/package.json create mode 100644 fixtures/worker-logs/src/module.js create mode 100644 fixtures/worker-logs/src/service.js create mode 100644 fixtures/worker-logs/tests/index.test.ts create mode 100644 fixtures/worker-logs/tsconfig.json create mode 100644 fixtures/worker-logs/turbo.json create mode 100644 fixtures/worker-logs/vitest.config.mts create mode 100644 fixtures/worker-logs/wrangler.module.jsonc create mode 100644 fixtures/worker-logs/wrangler.service.jsonc create mode 100644 fixtures/worker-ts/CHANGELOG.md create mode 100644 fixtures/worker-ts/package.json create mode 100644 fixtures/worker-ts/src/index.ts create mode 100644 fixtures/worker-ts/tsconfig.json create mode 100644 fixtures/worker-ts/wrangler.jsonc create mode 100644 fixtures/worker-with-resources/package.json create mode 100644 fixtures/worker-with-resources/src/index.ts create mode 100644 fixtures/worker-with-resources/tests/index.test.ts create mode 100644 fixtures/worker-with-resources/tsconfig.json create mode 100644 fixtures/worker-with-resources/vitest.config.mts create mode 100644 fixtures/worker-with-resources/worker-b/index.ts create mode 100644 fixtures/worker-with-resources/worker-b/tsconfig.json create mode 100644 fixtures/worker-with-resources/worker-b/worker-configuration.d.ts create mode 100644 fixtures/worker-with-resources/worker-b/wrangler.jsonc create mode 100644 fixtures/worker-with-resources/worker-configuration.d.ts create mode 100644 fixtures/worker-with-resources/wrangler.jsonc create mode 100644 fixtures/worker-with-unsafe-external-plugin/README.md create mode 100644 fixtures/worker-with-unsafe-external-plugin/package.json create mode 100644 fixtures/worker-with-unsafe-external-plugin/src/index.ts create mode 100644 fixtures/worker-with-unsafe-external-plugin/tests/env.d.ts create mode 100644 fixtures/worker-with-unsafe-external-plugin/tests/index.spec.ts create mode 100644 fixtures/worker-with-unsafe-external-plugin/tests/tsconfig.json create mode 100644 fixtures/worker-with-unsafe-external-plugin/tsconfig.json create mode 100644 fixtures/worker-with-unsafe-external-plugin/vitest.config.ts create mode 100644 fixtures/worker-with-unsafe-external-plugin/wrangler.jsonc create mode 100644 fixtures/workers-shared-asset-config/README.md create mode 100644 fixtures/workers-shared-asset-config/env.d.ts create mode 100644 fixtures/workers-shared-asset-config/html-handling.test.ts create mode 100644 fixtures/workers-shared-asset-config/loopback.test.ts create mode 100644 fixtures/workers-shared-asset-config/package.json create mode 100644 fixtures/workers-shared-asset-config/redirects.test.ts create mode 100644 fixtures/workers-shared-asset-config/test-cases/encoding-test-cases.ts create mode 100644 fixtures/workers-shared-asset-config/test-cases/html-handling-test-cases.ts create mode 100644 fixtures/workers-shared-asset-config/tsconfig.json create mode 100644 fixtures/workers-shared-asset-config/url-normalization.test.ts create mode 100644 fixtures/workers-shared-asset-config/vitest.config.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/README.md create mode 100644 fixtures/workers-with-assets-and-service-bindings/package.json create mode 100644 fixtures/workers-with-assets-and-service-bindings/tests/index.test.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/tests/tsconfig.json create mode 100644 fixtures/workers-with-assets-and-service-bindings/tsconfig.json create mode 100644 fixtures/workers-with-assets-and-service-bindings/vitest.config.mts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerA/src/index.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerA/src/workerB.util.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerA/src/workerC.util.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerA/src/workerD.util.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerA/src/workerWS.util.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerA/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerB-with-default-export/public/busy-bee.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerB-with-default-export/public/index.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerB-with-default-export/src/index.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerB-with-default-export/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerC-with-default-entrypoint/public/busy-bee.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerC-with-default-entrypoint/public/index.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerC-with-default-entrypoint/src/index.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerC-with-default-entrypoint/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerD-with-named-entrypoint/public/busy-bee.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerD-with-named-entrypoint/public/index.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerD-with-named-entrypoint/src/index.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerD-with-named-entrypoint/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerWS/public/busy-bee.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerWS/public/index.html create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerWS/src/index.ts create mode 100644 fixtures/workers-with-assets-and-service-bindings/workerWS/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-only/README.md create mode 100644 fixtures/workers-with-assets-only/package.json create mode 100644 fixtures/workers-with-assets-only/public/404.html create mode 100644 fixtures/workers-with-assets-only/public/README.md create mode 100644 fixtures/workers-with-assets-only/public/about/%5Bboop%5D.html create mode 100644 fixtures/workers-with-assets-only/public/about/404.html create mode 100644 fixtures/workers-with-assets-only/public/about/[boop].html create mode 100644 fixtures/workers-with-assets-only/public/about/[fünky].txt create mode 100644 fixtures/workers-with-assets-only/public/about/index.html create mode 100644 fixtures/workers-with-assets-only/public/bin create mode 100644 fixtures/workers-with-assets-only/public/bin.html create mode 100644 fixtures/workers-with-assets-only/public/both.html create mode 100644 fixtures/workers-with-assets-only/public/both/index.html create mode 100644 fixtures/workers-with-assets-only/public/file-bin create mode 100644 fixtures/workers-with-assets-only/public/file-bin.html create mode 100644 fixtures/workers-with-assets-only/public/file.html create mode 100644 fixtures/workers-with-assets-only/public/folder-bin create mode 100644 fixtures/workers-with-assets-only/public/folder/index.html create mode 100644 fixtures/workers-with-assets-only/public/index-bin/index create mode 100644 fixtures/workers-with-assets-only/public/index-bin/index.html create mode 100644 fixtures/workers-with-assets-only/public/index.html create mode 100644 fixtures/workers-with-assets-only/public/lava-lamps.jpg create mode 100644 fixtures/workers-with-assets-only/public/yay.txt create mode 100644 fixtures/workers-with-assets-only/tests/index.test.ts create mode 100644 fixtures/workers-with-assets-only/tests/tsconfig.json create mode 100644 fixtures/workers-with-assets-only/tsconfig.json create mode 100644 fixtures/workers-with-assets-only/vitest.config.mts create mode 100644 fixtures/workers-with-assets-only/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-run-worker-first/README.md create mode 100644 fixtures/workers-with-assets-run-worker-first/package.json create mode 100644 fixtures/workers-with-assets-run-worker-first/public/index.html create mode 100644 fixtures/workers-with-assets-run-worker-first/src/index.ts create mode 100644 fixtures/workers-with-assets-run-worker-first/tests/index.test.ts create mode 100644 fixtures/workers-with-assets-run-worker-first/tests/tsconfig.json create mode 100644 fixtures/workers-with-assets-run-worker-first/tsconfig.json create mode 100644 fixtures/workers-with-assets-run-worker-first/vitest.config.mts create mode 100644 fixtures/workers-with-assets-run-worker-first/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-spa/README.md create mode 100644 fixtures/workers-with-assets-spa/package.json create mode 100644 fixtures/workers-with-assets-spa/public/index.html create mode 100644 fixtures/workers-with-assets-spa/public/shadowed-by-asset.txt create mode 100644 fixtures/workers-with-assets-spa/src/index.ts create mode 100644 fixtures/workers-with-assets-spa/tests/__image_snapshots__/index-test-ts-workers-assets-spa-renders-the-homepage-in-a-browser-correctly-1-snap.png create mode 100644 fixtures/workers-with-assets-spa/tests/index.test.ts create mode 100644 fixtures/workers-with-assets-spa/tests/tsconfig.json create mode 100644 fixtures/workers-with-assets-spa/tsconfig.json create mode 100644 fixtures/workers-with-assets-spa/vitest.config.mts create mode 100644 fixtures/workers-with-assets-spa/wrangler.jsonc create mode 100644 fixtures/workers-with-assets-static-routing/package.json create mode 100644 fixtures/workers-with-assets-static-routing/public/static/page.html create mode 100644 fixtures/workers-with-assets-static-routing/public/worker/asset.html create mode 100644 fixtures/workers-with-assets-static-routing/public/worker/worker-runs.html create mode 100644 fixtures/workers-with-assets-static-routing/spa-assets/index.html create mode 100644 fixtures/workers-with-assets-static-routing/spa.wrangler.jsonc create mode 100644 fixtures/workers-with-assets-static-routing/src/index.ts create mode 100644 fixtures/workers-with-assets-static-routing/test/index.test.ts create mode 100644 fixtures/workers-with-assets-static-routing/test/tsconfig.json create mode 100644 fixtures/workers-with-assets-static-routing/tsconfig.json create mode 100644 fixtures/workers-with-assets-static-routing/vitest.config.mts create mode 100644 fixtures/workers-with-assets-static-routing/wrangler.jsonc create mode 100644 fixtures/workers-with-assets/.gitignore create mode 100644 fixtures/workers-with-assets/README.md create mode 100644 fixtures/workers-with-assets/package.json create mode 100644 fixtures/workers-with-assets/public/.assetsignore create mode 100644 fixtures/workers-with-assets/public/.dot/index.html create mode 100644 fixtures/workers-with-assets/public/.dotfile.html create mode 100644 fixtures/workers-with-assets/public/README.md create mode 100644 fixtures/workers-with-assets/public/_headers create mode 100644 fixtures/workers-with-assets/public/_redirects create mode 100644 fixtures/workers-with-assets/public/_worker.js create mode 100644 fixtures/workers-with-assets/public/about/%5Bboop%5D.html create mode 100644 fixtures/workers-with-assets/public/about/%5Bwomp%5D.html create mode 100644 fixtures/workers-with-assets/public/about/[boop].html create mode 100644 fixtures/workers-with-assets/public/about/[fünky].txt create mode 100644 fixtures/workers-with-assets/public/about/index.html create mode 100644 fixtures/workers-with-assets/public/bar.html create mode 100644 fixtures/workers-with-assets/public/binding.html create mode 100644 fixtures/workers-with-assets/public/foo.html create mode 100644 fixtures/workers-with-assets/public/ignore-me.txt create mode 100644 fixtures/workers-with-assets/public/index.html create mode 100644 fixtures/workers-with-assets/public/lava-lamps.jpg create mode 100644 fixtures/workers-with-assets/public/totallyinvalidextension.greg create mode 100644 fixtures/workers-with-assets/public/yay.txt create mode 100644 fixtures/workers-with-assets/src/index.ts create mode 100644 fixtures/workers-with-assets/tests/index.test.ts create mode 100644 fixtures/workers-with-assets/tests/tsconfig.json create mode 100644 fixtures/workers-with-assets/tsconfig.json create mode 100644 fixtures/workers-with-assets/vitest.config.mts create mode 100644 fixtures/workers-with-assets/wrangler.jsonc create mode 100644 fixtures/workflow-multiple/package.json create mode 100644 fixtures/workflow-multiple/src/index.ts create mode 100644 fixtures/workflow-multiple/tests/index.test.ts create mode 100644 fixtures/workflow-multiple/tests/tsconfig.json create mode 100644 fixtures/workflow-multiple/tsconfig.json create mode 100644 fixtures/workflow-multiple/vitest.config.mts create mode 100644 fixtures/workflow-multiple/wrangler.jsonc create mode 100644 fixtures/workflow/package.json create mode 100644 fixtures/workflow/src/index.ts create mode 100644 fixtures/workflow/tests/index.test.ts create mode 100644 fixtures/workflow/tests/tsconfig.json create mode 100644 fixtures/workflow/tsconfig.json create mode 100644 fixtures/workflow/vitest.config.mts create mode 100644 fixtures/workflow/wrangler.jsonc create mode 100644 lint-turbo.mjs create mode 100644 package.json create mode 100644 packages/autoconfig/CHANGELOG.md create mode 100644 packages/autoconfig/package.json create mode 100644 packages/autoconfig/scripts/deps.ts create mode 100644 packages/autoconfig/src/context.ts create mode 100644 packages/autoconfig/src/details/framework-detection.ts create mode 100644 packages/autoconfig/src/details/index.ts create mode 100644 packages/autoconfig/src/errors.ts create mode 100644 packages/autoconfig/src/frameworks/all-frameworks.ts create mode 100644 packages/autoconfig/src/frameworks/analog.ts create mode 100644 packages/autoconfig/src/frameworks/angular.ts create mode 100644 packages/autoconfig/src/frameworks/astro.ts create mode 100644 packages/autoconfig/src/frameworks/framework-class.ts create mode 100644 packages/autoconfig/src/frameworks/index.ts create mode 100644 packages/autoconfig/src/frameworks/next.ts create mode 100644 packages/autoconfig/src/frameworks/no-op.ts create mode 100644 packages/autoconfig/src/frameworks/nuxt.ts create mode 100644 packages/autoconfig/src/frameworks/qwik.ts create mode 100644 packages/autoconfig/src/frameworks/react-router.ts create mode 100644 packages/autoconfig/src/frameworks/solid-start.ts create mode 100644 packages/autoconfig/src/frameworks/static.ts create mode 100644 packages/autoconfig/src/frameworks/sveltekit.ts create mode 100644 packages/autoconfig/src/frameworks/tanstack.ts create mode 100644 packages/autoconfig/src/frameworks/utils/packages.ts create mode 100644 packages/autoconfig/src/frameworks/utils/vite-config.ts create mode 100644 packages/autoconfig/src/frameworks/utils/vite-plugin.ts create mode 100644 packages/autoconfig/src/frameworks/vike.ts create mode 100644 packages/autoconfig/src/frameworks/vite.ts create mode 100644 packages/autoconfig/src/frameworks/waku.ts create mode 100644 packages/autoconfig/src/index.ts create mode 100644 packages/autoconfig/src/run.ts create mode 100644 packages/autoconfig/src/types.ts create mode 100644 packages/autoconfig/src/uses-typescript.ts create mode 100644 packages/autoconfig/tests/details/confirm-auto-config-details.test.ts create mode 100644 packages/autoconfig/tests/details/display-auto-config-details.test.ts create mode 100644 packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts create mode 100644 packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts create mode 100644 packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts create mode 100644 packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts create mode 100644 packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts create mode 100644 packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts create mode 100644 packages/autoconfig/tests/details/get-details-for-auto-config.test.ts create mode 100644 packages/autoconfig/tests/frameworks/angular.test.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/config-future-no-middleware.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-and-split.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-false.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-true.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/config-no-future.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/config-plain-object-middleware.ts create mode 100644 packages/autoconfig/tests/frameworks/fixtures/react-router/vite-config-basic.ts create mode 100644 packages/autoconfig/tests/frameworks/get-framework-class.test.ts create mode 100644 packages/autoconfig/tests/frameworks/is-framework-supported.test.ts create mode 100644 packages/autoconfig/tests/frameworks/is-known-framework.test.ts create mode 100644 packages/autoconfig/tests/frameworks/react-router.test.ts create mode 100644 packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts create mode 100644 packages/autoconfig/tests/frameworks/validate-framework-version.test.ts create mode 100644 packages/autoconfig/tests/frameworks/vike.test.ts create mode 100644 packages/autoconfig/tests/frameworks/vite.test.ts create mode 100644 packages/autoconfig/tests/get-installed-package-version.test.ts create mode 100644 packages/autoconfig/tests/helpers/mock-context.ts create mode 100644 packages/autoconfig/tests/run-summary.test.ts create mode 100644 packages/autoconfig/tests/tsconfig.json create mode 100644 packages/autoconfig/tests/vite-config.test.ts create mode 100644 packages/autoconfig/tsconfig.json create mode 100644 packages/autoconfig/tsup.config.ts create mode 100644 packages/autoconfig/turbo.json create mode 100644 packages/autoconfig/vitest.config.mts create mode 100644 packages/chrome-devtools-patches/CHANGELOG.md create mode 100644 packages/chrome-devtools-patches/Makefile create mode 100644 packages/chrome-devtools-patches/README.md create mode 100644 packages/chrome-devtools-patches/package.json create mode 100644 packages/chrome-devtools-patches/patches/0001-Expand-Browser-support-make-it-work-in-Firefox-Safar.patch create mode 100644 packages/chrome-devtools-patches/patches/0002-Setup-Cloudflare-devtools-target-type.patch create mode 100644 packages/chrome-devtools-patches/patches/0003-Add-ping-to-improve-connection-stability.-Without-th.patch create mode 100644 packages/chrome-devtools-patches/patches/0004-Support-viewing-source-files-over-the-network.-This-.patch create mode 100644 packages/chrome-devtools-patches/patches/0005-Support-forcing-the-devtools-theme-via-a-query-param.patch create mode 100644 packages/chrome-devtools-patches/patches/0006-All-about-the-network-tab.patch create mode 100644 packages/chrome-devtools-patches/patches/0007-Limit-heap-profiling-modes-available.patch create mode 100644 packages/chrome-devtools-patches/patches/0008-Use-the-worker-name-as-the-title-for-the-Javascript-.patch create mode 100644 packages/chrome-devtools-patches/wrangler.jsonc create mode 100644 packages/cli/CHANGELOG.md create mode 100644 packages/cli/README.md create mode 100644 packages/cli/__tests__/check-macos-version.test.ts create mode 100644 packages/cli/__tests__/cli.test.ts create mode 100644 packages/cli/__tests__/command.test.ts create mode 100644 packages/cli/__tests__/gitignore.test.ts create mode 100644 packages/cli/__tests__/packages.test.ts create mode 100644 packages/cli/args.ts create mode 100644 packages/cli/check-macos-version.ts create mode 100644 packages/cli/colors.ts create mode 100644 packages/cli/command.ts create mode 100644 packages/cli/cursor.ts create mode 100644 packages/cli/error.ts create mode 100644 packages/cli/gitignore.ts create mode 100644 packages/cli/index.ts create mode 100644 packages/cli/interactive.ts create mode 100644 packages/cli/package.json create mode 100644 packages/cli/packages.ts create mode 100644 packages/cli/scripts/deps.ts create mode 100644 packages/cli/select-list.ts create mode 100644 packages/cli/streams.ts create mode 100644 packages/cli/test-util.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/tsdown.config.ts create mode 100644 packages/cli/turbo.json create mode 100644 packages/cli/vite.setup.ts create mode 100644 packages/cli/vitest.config.mts create mode 100644 packages/codemod/.gitignore create mode 100644 packages/codemod/CHANGELOG.md create mode 100644 packages/codemod/README.md create mode 100644 packages/codemod/dev-snippets/test.ts create mode 100644 packages/codemod/package.json create mode 100644 packages/codemod/src/dev.ts create mode 100644 packages/codemod/src/index.ts create mode 100644 packages/codemod/tests/index.test.ts create mode 100644 packages/codemod/tests/tsconfig.json create mode 100644 packages/codemod/tsconfig.json create mode 100644 packages/codemod/tsup.config.ts create mode 100644 packages/codemod/turbo.json create mode 100644 packages/codemod/vitest.config.mts create mode 100644 packages/config/package.json create mode 100644 packages/config/src/__tests__/convert.test.ts create mode 100644 packages/config/src/__tests__/generate.test.ts create mode 100644 packages/config/src/__tests__/load.test.ts create mode 100644 packages/config/src/__tests__/schema.test.ts create mode 100644 packages/config/src/bindings.ts create mode 100644 packages/config/src/build-output.ts create mode 100644 packages/config/src/convert.ts create mode 100644 packages/config/src/exports.ts create mode 100644 packages/config/src/generate.ts create mode 100644 packages/config/src/index.ts create mode 100644 packages/config/src/inference.ts create mode 100644 packages/config/src/load.ts create mode 100644 packages/config/src/public.ts create mode 100644 packages/config/src/schema.ts create mode 100644 packages/config/src/triggers.ts create mode 100644 packages/config/src/types.ts create mode 100644 packages/config/src/utils.ts create mode 100644 packages/config/src/worker-definition.ts create mode 100644 packages/config/tsconfig.json create mode 100644 packages/config/tsdown.config.ts create mode 100644 packages/config/turbo.json create mode 100644 packages/config/vitest.config.ts create mode 100644 packages/containers-shared/CHANGELOG.md create mode 100644 packages/containers-shared/README.md create mode 100644 packages/containers-shared/index.ts create mode 100644 packages/containers-shared/package.json create mode 100644 packages/containers-shared/src/build.ts create mode 100644 packages/containers-shared/src/client/core/ApiError.ts create mode 100644 packages/containers-shared/src/client/core/ApiRequestOptions.ts create mode 100644 packages/containers-shared/src/client/core/ApiResult.ts create mode 100644 packages/containers-shared/src/client/core/CancelablePromise.ts create mode 100644 packages/containers-shared/src/client/core/OpenAPI.ts create mode 100644 packages/containers-shared/src/client/core/PaginatedResult.ts create mode 100644 packages/containers-shared/src/client/core/request.ts create mode 100644 packages/containers-shared/src/client/index.ts create mode 100644 packages/containers-shared/src/client/models/AccountDefaults.ts create mode 100644 packages/containers-shared/src/client/models/AccountID.ts create mode 100644 packages/containers-shared/src/client/models/AccountLimit.ts create mode 100644 packages/containers-shared/src/client/models/AccountLocation.ts create mode 100644 packages/containers-shared/src/client/models/AccountLocationLimits.ts create mode 100644 packages/containers-shared/src/client/models/AccountLocationLimitsAsProperty.ts create mode 100644 packages/containers-shared/src/client/models/AccountRegistryToken.ts create mode 100644 packages/containers-shared/src/client/models/AddressAssignment.ts create mode 100644 packages/containers-shared/src/client/models/Application.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationAffinities.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationAffinityColocation.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationAffinityHardwareGeneration.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationConstraintPop.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationConstraints.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationHealth.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationHealthInstances.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationID.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationJob.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationJobsConfig.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationMutationError.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationName.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationNotFoundError.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationPriorities.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationPriority.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationRollout.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationRolloutActiveGracePeriod.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationRolloutProgress.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationSchedulingHint.ts create mode 100644 packages/containers-shared/src/client/models/ApplicationStatus.ts create mode 100644 packages/containers-shared/src/client/models/AssignIPv4.ts create mode 100644 packages/containers-shared/src/client/models/AssignIPv6.ts create mode 100644 packages/containers-shared/src/client/models/BadRequestError.ts create mode 100644 packages/containers-shared/src/client/models/BadRequestWithCodeError.ts create mode 100644 packages/containers-shared/src/client/models/City.ts create mode 100644 packages/containers-shared/src/client/models/Command.ts create mode 100644 packages/containers-shared/src/client/models/CompleteAccountCustomer.ts create mode 100644 packages/containers-shared/src/client/models/CompleteAccountLocationCustomer.ts create mode 100644 packages/containers-shared/src/client/models/ContainerNetworkMode.ts create mode 100644 packages/containers-shared/src/client/models/CreateApplicationBadRequest.ts create mode 100644 packages/containers-shared/src/client/models/CreateApplicationJobBadRequest.ts create mode 100644 packages/containers-shared/src/client/models/CreateApplicationJobRequest.ts create mode 100644 packages/containers-shared/src/client/models/CreateApplicationRequest.ts create mode 100644 packages/containers-shared/src/client/models/CreateApplicationRolloutRequest.ts create mode 100644 packages/containers-shared/src/client/models/CreateDeploymentBadRequest.ts create mode 100644 packages/containers-shared/src/client/models/CreateDeploymentV2RequestBody.ts create mode 100644 packages/containers-shared/src/client/models/CreateImageRegistryRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/CreateSSHPublicKeyError.ts create mode 100644 packages/containers-shared/src/client/models/CreateSSHPublicKeyRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/CustomerImageRegistry.ts create mode 100644 packages/containers-shared/src/client/models/DNSConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/DashApplication.ts create mode 100644 packages/containers-shared/src/client/models/DashApplicationDurableObjectInstance.ts create mode 100644 packages/containers-shared/src/client/models/DashApplicationInstance.ts create mode 100644 packages/containers-shared/src/client/models/DashApplicationInstances.ts create mode 100644 packages/containers-shared/src/client/models/DefaultImageRegistryKind.ts create mode 100644 packages/containers-shared/src/client/models/DeleteDeploymentError.ts create mode 100644 packages/containers-shared/src/client/models/DeleteImageRegistryResponse.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentAlreadyExists.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCheck.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCheckHTTP.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCheckHTTPRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCheckKind.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCheckRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCheckType.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentCreationError.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentID.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentListError.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentLocation.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentModificationError.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentMutationError.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentNotFoundError.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentPlacementState.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentQueuedDetails.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentQueuedReason.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentReplacementError.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentSchedulingState.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentSecretMap.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentState.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentType.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentV2.ts create mode 100644 packages/containers-shared/src/client/models/DeploymentVersion.ts create mode 100644 packages/containers-shared/src/client/models/Disk.ts create mode 100644 packages/containers-shared/src/client/models/DiskMB.ts create mode 100644 packages/containers-shared/src/client/models/DiskSizeWithUnit.ts create mode 100644 packages/containers-shared/src/client/models/Domain.ts create mode 100644 packages/containers-shared/src/client/models/DurableObjectStatusHealth.ts create mode 100644 packages/containers-shared/src/client/models/DurableObjectsConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/Duration.ts create mode 100644 packages/containers-shared/src/client/models/EmptyResponse.ts create mode 100644 packages/containers-shared/src/client/models/Entrypoint.ts create mode 100644 packages/containers-shared/src/client/models/EnvironmentVariable.ts create mode 100644 packages/containers-shared/src/client/models/EnvironmentVariableName.ts create mode 100644 packages/containers-shared/src/client/models/EnvironmentVariableValue.ts create mode 100644 packages/containers-shared/src/client/models/EventName.ts create mode 100644 packages/containers-shared/src/client/models/EventType.ts create mode 100644 packages/containers-shared/src/client/models/ExecFormParam.ts create mode 100644 packages/containers-shared/src/client/models/ExternalRegistryKind.ts create mode 100644 packages/containers-shared/src/client/models/GenericErrorDetails.ts create mode 100644 packages/containers-shared/src/client/models/GenericErrorResponseWithRequestID.ts create mode 100644 packages/containers-shared/src/client/models/GenericMessageResponse.ts create mode 100644 packages/containers-shared/src/client/models/GetDeploymentError.ts create mode 100644 packages/containers-shared/src/client/models/GetPlacementError.ts create mode 100644 packages/containers-shared/src/client/models/HTTPMethod.ts create mode 100644 packages/containers-shared/src/client/models/IP.ts create mode 100644 packages/containers-shared/src/client/models/IPAllocation.ts create mode 100644 packages/containers-shared/src/client/models/IPAllocationConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/IPAllocationsWithFilter.ts create mode 100644 packages/containers-shared/src/client/models/IPType.ts create mode 100644 packages/containers-shared/src/client/models/IPV4.ts create mode 100644 packages/containers-shared/src/client/models/ISO8601Timestamp.ts create mode 100644 packages/containers-shared/src/client/models/Identity.ts create mode 100644 packages/containers-shared/src/client/models/Image.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryAlreadyExistsError.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryAuth.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryCredentialsConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryIsPublic.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryNotAllowedError.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryNotFoundError.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryPermissions.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryProtoDomain.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryProtocol.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryProtocolAlreadyExists.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryProtocolIsReferencedError.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryProtocolNotFound.ts create mode 100644 packages/containers-shared/src/client/models/ImageRegistryProtocols.ts create mode 100644 packages/containers-shared/src/client/models/InstanceType.ts create mode 100644 packages/containers-shared/src/client/models/InternalError.ts create mode 100644 packages/containers-shared/src/client/models/JobEvents.ts create mode 100644 packages/containers-shared/src/client/models/JobID.ts create mode 100644 packages/containers-shared/src/client/models/JobNotFoundError.ts create mode 100644 packages/containers-shared/src/client/models/JobSecretMap.ts create mode 100644 packages/containers-shared/src/client/models/JobStatus.ts create mode 100644 packages/containers-shared/src/client/models/JobStatusHealth.ts create mode 100644 packages/containers-shared/src/client/models/JobTimeoutSeconds.ts create mode 100644 packages/containers-shared/src/client/models/Label.ts create mode 100644 packages/containers-shared/src/client/models/LabelName.ts create mode 100644 packages/containers-shared/src/client/models/LabelValue.ts create mode 100644 packages/containers-shared/src/client/models/ListApplications.ts create mode 100644 packages/containers-shared/src/client/models/ListDeploymentsV2.ts create mode 100644 packages/containers-shared/src/client/models/ListIPsIsAllocated.ts create mode 100644 packages/containers-shared/src/client/models/ListPlacements.ts create mode 100644 packages/containers-shared/src/client/models/ListPlacementsError.ts create mode 100644 packages/containers-shared/src/client/models/ListSSHPublicKeys.ts create mode 100644 packages/containers-shared/src/client/models/ListSSHPublicKeysError.ts create mode 100644 packages/containers-shared/src/client/models/ListSecretsMetadata.ts create mode 100644 packages/containers-shared/src/client/models/Location.ts create mode 100644 packages/containers-shared/src/client/models/LocationID.ts create mode 100644 packages/containers-shared/src/client/models/MemorySizeWithUnit.ts create mode 100644 packages/containers-shared/src/client/models/ModifyApplicationBadRequest.ts create mode 100644 packages/containers-shared/src/client/models/ModifyApplicationJobBadRequest.ts create mode 100644 packages/containers-shared/src/client/models/ModifyApplicationJobRequest.ts create mode 100644 packages/containers-shared/src/client/models/ModifyApplicationRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/ModifyDeploymentBadRequest.ts create mode 100644 packages/containers-shared/src/client/models/ModifyDeploymentV2RequestBody.ts create mode 100644 packages/containers-shared/src/client/models/ModifyMeRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/ModifySecretRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/ModifyUserDeploymentConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/Network.ts create mode 100644 packages/containers-shared/src/client/models/NetworkMode.ts create mode 100644 packages/containers-shared/src/client/models/NetworkParameters.ts create mode 100644 packages/containers-shared/src/client/models/NodeGroup.ts create mode 100644 packages/containers-shared/src/client/models/Observability.ts create mode 100644 packages/containers-shared/src/client/models/ObservabilityLogs.ts create mode 100644 packages/containers-shared/src/client/models/Placement.ts create mode 100644 packages/containers-shared/src/client/models/PlacementEvent.ts create mode 100644 packages/containers-shared/src/client/models/PlacementEvents.ts create mode 100644 packages/containers-shared/src/client/models/PlacementID.ts create mode 100644 packages/containers-shared/src/client/models/PlacementNotFoundError.ts create mode 100644 packages/containers-shared/src/client/models/PlacementStatus.ts create mode 100644 packages/containers-shared/src/client/models/PlacementStatusHealth.ts create mode 100644 packages/containers-shared/src/client/models/PlacementWithEvents.ts create mode 100644 packages/containers-shared/src/client/models/PlainTextSecretValue.ts create mode 100644 packages/containers-shared/src/client/models/Port.ts create mode 100644 packages/containers-shared/src/client/models/PortRange.ts create mode 100644 packages/containers-shared/src/client/models/PortRangeAllocation.ts create mode 100644 packages/containers-shared/src/client/models/ProvisionerConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/Ref.ts create mode 100644 packages/containers-shared/src/client/models/Region.ts create mode 100644 packages/containers-shared/src/client/models/ReplaceDeploymentRequestBody.ts create mode 100644 packages/containers-shared/src/client/models/RolloutID.ts create mode 100644 packages/containers-shared/src/client/models/RolloutStep.ts create mode 100644 packages/containers-shared/src/client/models/RolloutStepRequest.ts create mode 100644 packages/containers-shared/src/client/models/SSHPublicKey.ts create mode 100644 packages/containers-shared/src/client/models/SSHPublicKeyID.ts create mode 100644 packages/containers-shared/src/client/models/SSHPublicKeyItem.ts create mode 100644 packages/containers-shared/src/client/models/SSHPublicKeyNotFoundError.ts create mode 100644 packages/containers-shared/src/client/models/SchedulerDeploymentConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/SchedulingPolicy.ts create mode 100644 packages/containers-shared/src/client/models/Secret.ts create mode 100644 packages/containers-shared/src/client/models/SecretAccessType.ts create mode 100644 packages/containers-shared/src/client/models/SecretMap.ts create mode 100644 packages/containers-shared/src/client/models/SecretMetadata.ts create mode 100644 packages/containers-shared/src/client/models/SecretName.ts create mode 100644 packages/containers-shared/src/client/models/SecretNameAlreadyExists.ts create mode 100644 packages/containers-shared/src/client/models/SecretNotFound.ts create mode 100644 packages/containers-shared/src/client/models/SecretsStoreRef.ts create mode 100644 packages/containers-shared/src/client/models/UnAuthorizedError.ts create mode 100644 packages/containers-shared/src/client/models/UnixTimestamp.ts create mode 100644 packages/containers-shared/src/client/models/UnknownAccount.ts create mode 100644 packages/containers-shared/src/client/models/UpdateApplicationRolloutRequest.ts create mode 100644 packages/containers-shared/src/client/models/UpdateRolloutResponse.ts create mode 100644 packages/containers-shared/src/client/models/UserDeploymentConfiguration.ts create mode 100644 packages/containers-shared/src/client/models/UserSSHPublicKey.ts create mode 100644 packages/containers-shared/src/client/models/WranglerSSHConfig.ts create mode 100644 packages/containers-shared/src/client/models/WranglerSSHResponse.ts create mode 100644 packages/containers-shared/src/client/services/AccountService.ts create mode 100644 packages/containers-shared/src/client/services/ApplicationsService.ts create mode 100644 packages/containers-shared/src/client/services/DeploymentsService.ts create mode 100644 packages/containers-shared/src/client/services/IPsService.ts create mode 100644 packages/containers-shared/src/client/services/ImageRegistriesService.ts create mode 100644 packages/containers-shared/src/client/services/JobsService.ts create mode 100644 packages/containers-shared/src/client/services/PlacementsService.ts create mode 100644 packages/containers-shared/src/client/services/RolloutsService.ts create mode 100644 packages/containers-shared/src/client/services/SecretsService.ts create mode 100644 packages/containers-shared/src/client/services/SshPublicKeysService.ts create mode 100644 packages/containers-shared/src/images.ts create mode 100644 packages/containers-shared/src/inspect.ts create mode 100644 packages/containers-shared/src/knobs.ts create mode 100644 packages/containers-shared/src/login.ts create mode 100644 packages/containers-shared/src/registry.ts create mode 100644 packages/containers-shared/src/types.ts create mode 100644 packages/containers-shared/src/utils.ts create mode 100644 packages/containers-shared/tests/build.test.ts create mode 100644 packages/containers-shared/tests/docker-context.test.ts create mode 100644 packages/containers-shared/tests/images.test.ts create mode 100644 packages/containers-shared/tests/knobs.test.ts create mode 100644 packages/containers-shared/tests/login.test.ts create mode 100644 packages/containers-shared/tests/request.test.ts create mode 100644 packages/containers-shared/tests/tsconfig.json create mode 100644 packages/containers-shared/tests/utils.test.ts create mode 100644 packages/containers-shared/tsconfig.json create mode 100644 packages/containers-shared/turbo.json create mode 100644 packages/containers-shared/vitest.config.mts create mode 100644 packages/create-cloudflare/.editorconfig create mode 100644 packages/create-cloudflare/.env.example create mode 100644 packages/create-cloudflare/.gitignore create mode 100644 packages/create-cloudflare/AGENTS.md create mode 100644 packages/create-cloudflare/CHANGELOG.md create mode 100644 packages/create-cloudflare/README.md create mode 100644 packages/create-cloudflare/bin/c3.js create mode 100644 packages/create-cloudflare/dns2.d.ts create mode 100644 packages/create-cloudflare/e2e/README.md create mode 100644 packages/create-cloudflare/e2e/helpers/constants.ts create mode 100644 packages/create-cloudflare/e2e/helpers/debuglog.ts create mode 100644 packages/create-cloudflare/e2e/helpers/framework-helpers.ts create mode 100644 packages/create-cloudflare/e2e/helpers/global-setup.ts create mode 100644 packages/create-cloudflare/e2e/helpers/index.ts create mode 100644 packages/create-cloudflare/e2e/helpers/log-stream.ts create mode 100644 packages/create-cloudflare/e2e/helpers/run-c3.ts create mode 100644 packages/create-cloudflare/e2e/helpers/spawn.ts create mode 100644 packages/create-cloudflare/e2e/helpers/to-exist.ts create mode 100644 packages/create-cloudflare/e2e/helpers/workers-helpers.ts create mode 100644 packages/create-cloudflare/e2e/tests/cli/cli.test.ts create mode 100644 packages/create-cloudflare/e2e/tests/cli/fixtures/existing-script-test-do-not-delete/index.js create mode 100644 packages/create-cloudflare/e2e/tests/cli/fixtures/existing-script-test-do-not-delete/wrangler.jsonc create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/fixtures/analog/src/server/routes/v1/test.ts create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/fixtures/astro/src/pages/test.astro create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/fixtures/nuxt/server/routes/test.ts create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/fixtures/qwik/src/routes/test/index.ts create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/fixtures/svelte/src/routes/test/+server.ts create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/frameworks.test.ts create mode 100644 packages/create-cloudflare/e2e/tests/frameworks/test-config.ts create mode 100644 packages/create-cloudflare/e2e/tests/workers/test-config.ts create mode 100644 packages/create-cloudflare/e2e/tests/workers/workers.test.ts create mode 100644 packages/create-cloudflare/package.json create mode 100644 packages/create-cloudflare/scripts/build.ts create mode 100644 packages/create-cloudflare/scripts/common.ts create mode 100644 packages/create-cloudflare/scripts/deps.ts create mode 100644 packages/create-cloudflare/scripts/e2e/run-tests.ts create mode 100644 packages/create-cloudflare/src/__tests__/check-node.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/deploy.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/dialog.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/git.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/helpers.ts create mode 100644 packages/create-cloudflare/src/__tests__/metrics.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/pre-existing.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/templates.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/validators.test.ts create mode 100644 packages/create-cloudflare/src/__tests__/workers.test.ts create mode 100644 packages/create-cloudflare/src/agents-md.ts create mode 100644 packages/create-cloudflare/src/cli.ts create mode 100644 packages/create-cloudflare/src/deploy.ts create mode 100644 packages/create-cloudflare/src/dialog.ts create mode 100644 packages/create-cloudflare/src/event.ts create mode 100644 packages/create-cloudflare/src/frameworks/__tests__/index.test.ts create mode 100644 packages/create-cloudflare/src/frameworks/index.ts create mode 100644 packages/create-cloudflare/src/frameworks/package.json create mode 100644 packages/create-cloudflare/src/frameworks/tsconfig.json create mode 100644 packages/create-cloudflare/src/git.ts create mode 100644 packages/create-cloudflare/src/help.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/args.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/cli.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/command.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/compatDate.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/json.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/mocks.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/packageManagers.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/packages.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/pnpmBuildApprovals.test.ts create mode 100644 packages/create-cloudflare/src/helpers/__tests__/retry.test.ts create mode 100644 packages/create-cloudflare/src/helpers/args.ts create mode 100644 packages/create-cloudflare/src/helpers/cli.ts create mode 100644 packages/create-cloudflare/src/helpers/codemod.ts create mode 100644 packages/create-cloudflare/src/helpers/command.ts create mode 100644 packages/create-cloudflare/src/helpers/compatDate.ts create mode 100644 packages/create-cloudflare/src/helpers/files.ts create mode 100644 packages/create-cloudflare/src/helpers/json.ts create mode 100644 packages/create-cloudflare/src/helpers/metrics-config.ts create mode 100644 packages/create-cloudflare/src/helpers/packageManagers.ts create mode 100644 packages/create-cloudflare/src/helpers/packages.ts create mode 100644 packages/create-cloudflare/src/helpers/pnpmBuildApprovals.ts create mode 100644 packages/create-cloudflare/src/helpers/poll.ts create mode 100644 packages/create-cloudflare/src/helpers/retry.ts create mode 100644 packages/create-cloudflare/src/helpers/sparrow.ts create mode 100644 packages/create-cloudflare/src/metrics.ts create mode 100644 packages/create-cloudflare/src/pages.ts create mode 100644 packages/create-cloudflare/src/templates.ts create mode 100644 packages/create-cloudflare/src/types.ts create mode 100644 packages/create-cloudflare/src/validators.ts create mode 100644 packages/create-cloudflare/src/workers.ts create mode 100644 packages/create-cloudflare/src/wrangler/__tests__/accounts.test.ts create mode 100644 packages/create-cloudflare/src/wrangler/__tests__/config.test.ts create mode 100644 packages/create-cloudflare/src/wrangler/accounts.ts create mode 100644 packages/create-cloudflare/src/wrangler/config.ts create mode 100644 packages/create-cloudflare/telemetry.md create mode 100644 packages/create-cloudflare/templates/analog/c3.ts create mode 100644 packages/create-cloudflare/templates/analog/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/angular/c3.ts create mode 100644 packages/create-cloudflare/templates/angular/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/angular/pages/templates/src/_routes.json create mode 100644 packages/create-cloudflare/templates/angular/pages/templates/src/server.ts create mode 100644 packages/create-cloudflare/templates/angular/pages/templates/tools/copy-files.mjs create mode 100644 packages/create-cloudflare/templates/angular/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/angular/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/angular/workers/templates/src/server.ts create mode 100644 packages/create-cloudflare/templates/angular/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/astro/c3.ts create mode 100644 packages/create-cloudflare/templates/astro/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/astro/pages/templates/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/astro/pages/templates/ts/src/env.d.ts create mode 100644 packages/create-cloudflare/templates/astro/pages/templates/ts/worker-configuration.d.ts create mode 100644 packages/create-cloudflare/templates/astro/pages/templates/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/astro/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/astro/workers/templates/js/public/.assetsignore create mode 100644 packages/create-cloudflare/templates/astro/workers/templates/ts/public/.assetsignore create mode 100644 packages/create-cloudflare/templates/astro/workers/templates/ts/src/env.d.ts create mode 100644 packages/create-cloudflare/templates/common/c3.ts create mode 100644 packages/create-cloudflare/templates/common/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/common/js/.gitignore create mode 100644 packages/create-cloudflare/templates/common/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/common/js/package.json create mode 100644 packages/create-cloudflare/templates/common/js/src/ab-test.js create mode 100644 packages/create-cloudflare/templates/common/js/src/index.js create mode 100644 packages/create-cloudflare/templates/common/js/src/proxy.js create mode 100644 packages/create-cloudflare/templates/common/js/src/redirect.js create mode 100644 packages/create-cloudflare/templates/common/js/src/router.js create mode 100644 packages/create-cloudflare/templates/common/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/common/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/common/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/common/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/common/ts/package.json create mode 100644 packages/create-cloudflare/templates/common/ts/src/ab-test.ts create mode 100644 packages/create-cloudflare/templates/common/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/common/ts/src/proxy.ts create mode 100644 packages/create-cloudflare/templates/common/ts/src/redirect.ts create mode 100644 packages/create-cloudflare/templates/common/ts/src/router.ts create mode 100644 packages/create-cloudflare/templates/common/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/common/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/docusaurus/c3.ts create mode 100644 packages/create-cloudflare/templates/docusaurus/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/docusaurus/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/docusaurus/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/gatsby/c3.ts create mode 100644 packages/create-cloudflare/templates/gatsby/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/gatsby/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/gatsby/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-assets-only/c3.ts create mode 100644 packages/create-cloudflare/templates/hello-world-assets-only/templates/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-assets-only/templates/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-assets-only/templates/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-assets-only/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/c3.ts create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/src/index.js create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/.python-version create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/README.md create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/pyproject.toml create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/src/entry.py create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/py/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object-with-assets/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/c3.ts create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/js/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/js/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/js/src/index.js create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/.python-version create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/README.md create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/pyproject.toml create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/src/entry.py create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/py/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world-durable-object/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/c3.ts create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/src/index.js create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/test/index.spec.js create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/vitest.config.js create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/.python-version create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/README.md create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/pyproject.toml create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/src/entry.py create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/py/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/public/index.html create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/test/env.d.ts create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/test/index.spec.ts create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/test/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/vitest.config.mts create mode 100644 packages/create-cloudflare/templates/hello-world-with-assets/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/c3.ts create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/js/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/js/src/index.js create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/ts/package.json create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world-workflows/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world/c3.ts create mode 100644 packages/create-cloudflare/templates/hello-world/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world/js/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world/js/package.json create mode 100644 packages/create-cloudflare/templates/hello-world/js/src/index.js create mode 100644 packages/create-cloudflare/templates/hello-world/js/test/index.spec.js create mode 100644 packages/create-cloudflare/templates/hello-world/js/vitest.config.js create mode 100644 packages/create-cloudflare/templates/hello-world/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world/py/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world/py/.python-version create mode 100644 packages/create-cloudflare/templates/hello-world/py/README.md create mode 100644 packages/create-cloudflare/templates/hello-world/py/package.json create mode 100644 packages/create-cloudflare/templates/hello-world/py/pyproject.toml create mode 100644 packages/create-cloudflare/templates/hello-world/py/src/entry.py create mode 100644 packages/create-cloudflare/templates/hello-world/py/src/submodule.py create mode 100644 packages/create-cloudflare/templates/hello-world/py/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hello-world/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/hello-world/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/hello-world/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/hello-world/ts/package.json create mode 100644 packages/create-cloudflare/templates/hello-world/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/hello-world/ts/test/env.d.ts create mode 100644 packages/create-cloudflare/templates/hello-world/ts/test/index.spec.ts create mode 100644 packages/create-cloudflare/templates/hello-world/ts/test/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/hello-world/ts/vitest.config.mts create mode 100644 packages/create-cloudflare/templates/hello-world/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hono/c3.ts create mode 100644 packages/create-cloudflare/templates/hono/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/hono/pages/templates/src/index.tsx create mode 100644 packages/create-cloudflare/templates/hono/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/hono/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/hono/workers/templates/public/index.html create mode 100644 packages/create-cloudflare/templates/hono/workers/templates/src/index.ts create mode 100644 packages/create-cloudflare/templates/hono/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/next/c3.ts create mode 100644 packages/create-cloudflare/templates/next/experimental_c3.ts create mode 100644 packages/create-cloudflare/templates/nuxt/c3.ts create mode 100644 packages/create-cloudflare/templates/nuxt/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/nuxt/pages/templates/_cloudflare/env.d.ts create mode 100644 packages/create-cloudflare/templates/nuxt/pages/templates/app/env.d.ts create mode 100644 packages/create-cloudflare/templates/nuxt/pages/templates/server/env.d.ts create mode 100644 packages/create-cloudflare/templates/nuxt/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/nuxt/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/nuxt/workers/templates/_cloudflare/env.d.ts create mode 100644 packages/create-cloudflare/templates/nuxt/workers/templates/app/env.d.ts create mode 100644 packages/create-cloudflare/templates/nuxt/workers/templates/server/env.d.ts create mode 100644 packages/create-cloudflare/templates/nuxt/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/openapi/c3.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/openapi/ts/README.md create mode 100644 packages/create-cloudflare/templates/openapi/ts/package.json create mode 100644 packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskCreate.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskDelete.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskFetch.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskList.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/src/types.ts create mode 100644 packages/create-cloudflare/templates/openapi/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/openapi/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/pre-existing/c3.ts create mode 100644 packages/create-cloudflare/templates/pre-existing/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/pre-existing/js/.gitignore create mode 100644 packages/create-cloudflare/templates/pre-existing/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/pre-existing/js/package.json create mode 100644 packages/create-cloudflare/templates/queues/c3.ts create mode 100644 packages/create-cloudflare/templates/queues/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/queues/js/.gitignore create mode 100644 packages/create-cloudflare/templates/queues/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/queues/js/package.json create mode 100644 packages/create-cloudflare/templates/queues/js/src/index.js create mode 100644 packages/create-cloudflare/templates/queues/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/queues/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/queues/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/queues/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/queues/ts/package.json create mode 100644 packages/create-cloudflare/templates/queues/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/queues/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/queues/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/qwik/c3.ts create mode 100644 packages/create-cloudflare/templates/qwik/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/qwik/pages/snippets/getPlatformProxy.ts create mode 100644 packages/create-cloudflare/templates/qwik/pages/templates/worker-configuration.d.ts create mode 100644 packages/create-cloudflare/templates/qwik/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/qwik/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/qwik/workers/snippets/getPlatformProxy.ts create mode 100644 packages/create-cloudflare/templates/qwik/workers/templates/public/.assetsignore create mode 100644 packages/create-cloudflare/templates/qwik/workers/templates/worker-configuration.d.ts create mode 100644 packages/create-cloudflare/templates/qwik/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/react-router/c3.ts create mode 100644 packages/create-cloudflare/templates/react-router/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/react-router/ts/README.md create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/app.css create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/entry.server.tsx create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/routes/home.tsx create mode 100644 packages/create-cloudflare/templates/react-router/ts/app/welcome/welcome.tsx create mode 100644 packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json create mode 100644 packages/create-cloudflare/templates/react-router/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json create mode 100644 packages/create-cloudflare/templates/react-router/ts/vite.config.ts create mode 100644 packages/create-cloudflare/templates/react-router/ts/workers/app.ts create mode 100644 packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/react/c3.ts create mode 100644 packages/create-cloudflare/templates/react/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/react/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/react/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/react/workers/js/src/App.jsx create mode 100644 packages/create-cloudflare/templates/react/workers/js/src/assets/cloudflare.svg create mode 100644 packages/create-cloudflare/templates/react/workers/js/worker/index.js create mode 100644 packages/create-cloudflare/templates/react/workers/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/react/workers/ts/src/App.tsx create mode 100644 packages/create-cloudflare/templates/react/workers/ts/src/assets/cloudflare.svg create mode 100644 packages/create-cloudflare/templates/react/workers/ts/tsconfig.worker.json create mode 100644 packages/create-cloudflare/templates/react/workers/ts/worker/index.ts create mode 100644 packages/create-cloudflare/templates/react/workers/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/redwood/c3.ts create mode 100644 packages/create-cloudflare/templates/scheduled/c3.ts create mode 100644 packages/create-cloudflare/templates/scheduled/js/.editorconfig create mode 100644 packages/create-cloudflare/templates/scheduled/js/.gitignore create mode 100644 packages/create-cloudflare/templates/scheduled/js/.prettierrc create mode 100644 packages/create-cloudflare/templates/scheduled/js/package.json create mode 100644 packages/create-cloudflare/templates/scheduled/js/src/index.js create mode 100644 packages/create-cloudflare/templates/scheduled/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/scheduled/ts/.editorconfig create mode 100644 packages/create-cloudflare/templates/scheduled/ts/.gitignore create mode 100644 packages/create-cloudflare/templates/scheduled/ts/.prettierrc create mode 100644 packages/create-cloudflare/templates/scheduled/ts/package.json create mode 100644 packages/create-cloudflare/templates/scheduled/ts/src/index.ts create mode 100644 packages/create-cloudflare/templates/scheduled/ts/tsconfig.json create mode 100644 packages/create-cloudflare/templates/scheduled/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/solid/c3.ts create mode 100644 packages/create-cloudflare/templates/solid/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/svelte/c3.ts create mode 100644 packages/create-cloudflare/templates/svelte/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/svelte/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/svelte/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/svelte/workers/templates/static/.assetsignore create mode 100644 packages/create-cloudflare/templates/svelte/workers/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/tanstack-start/c3.ts create mode 100644 packages/create-cloudflare/templates/vike/c3.ts create mode 100644 packages/create-cloudflare/templates/vike/experimental-c3.ts create mode 100644 packages/create-cloudflare/templates/vue/c3.ts create mode 100644 packages/create-cloudflare/templates/vue/pages/c3.ts create mode 100644 packages/create-cloudflare/templates/vue/pages/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/vue/workers/c3.ts create mode 100644 packages/create-cloudflare/templates/vue/workers/js/server/index.js create mode 100644 packages/create-cloudflare/templates/vue/workers/js/src/App.vue create mode 100644 packages/create-cloudflare/templates/vue/workers/js/src/components/HelloWorld.vue create mode 100644 packages/create-cloudflare/templates/vue/workers/js/vite.config.js create mode 100644 packages/create-cloudflare/templates/vue/workers/js/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/vue/workers/ts/server/index.ts create mode 100644 packages/create-cloudflare/templates/vue/workers/ts/src/App.vue create mode 100644 packages/create-cloudflare/templates/vue/workers/ts/src/components/HelloWorld.vue create mode 100644 packages/create-cloudflare/templates/vue/workers/ts/tsconfig.worker.json create mode 100644 packages/create-cloudflare/templates/vue/workers/ts/vite.config.ts create mode 100644 packages/create-cloudflare/templates/vue/workers/ts/wrangler.jsonc create mode 100644 packages/create-cloudflare/templates/waku/c3.ts create mode 100644 packages/create-cloudflare/templates/waku/experimental-c3.ts create mode 100644 packages/create-cloudflare/templates/waku/templates/public/404.html create mode 100644 packages/create-cloudflare/templates/waku/templates/public/_headers create mode 100644 packages/create-cloudflare/templates/waku/templates/src/waku.server.tsx create mode 100644 packages/create-cloudflare/templates/waku/templates/waku.config.ts create mode 100644 packages/create-cloudflare/templates/waku/templates/wrangler.jsonc create mode 100644 packages/create-cloudflare/tsconfig.json create mode 100644 packages/create-cloudflare/turbo.json create mode 100644 packages/create-cloudflare/vitest-e2e.config.mts create mode 100644 packages/create-cloudflare/vitest.config.mts create mode 100644 packages/create-cloudflare/vitest.setup.ts create mode 100644 packages/deploy-helpers/CHANGELOG.md create mode 100644 packages/deploy-helpers/package.json create mode 100644 packages/deploy-helpers/scripts/deps.ts create mode 100644 packages/deploy-helpers/src/config.ts create mode 100644 packages/deploy-helpers/src/deploy/deploy.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/assets.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/binding-depends-on-export.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/binding-utils.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/bundle-reporter.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/capnp.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/check-remote-secrets-override.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/check-workflow-conflicts.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/config-diffs.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/confirm-latest-deployment-overwrite.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/deploy-confirm.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/deploy-wfp.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/diff-json.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/download-worker-config.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/durable.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/environments.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/error-codes.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/exports-reconciliation.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/exports.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/friendly-validator-errors.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/hash.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/inconsistent-exports.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/jwt.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/match-tag.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/node-compat.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/parse-bulk-input.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/placement.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/preview-alias.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/print-bindings.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/secrets-validation.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/sourcemap.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/validate-routes.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/versions-api.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/versions-types.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/worker-not-found-error.ts create mode 100644 packages/deploy-helpers/src/deploy/helpers/workers-sites-bindings.ts create mode 100644 packages/deploy-helpers/src/deploy/versions-upload.ts create mode 100644 packages/deploy-helpers/src/index.ts create mode 100644 packages/deploy-helpers/src/shared/context.ts create mode 100644 packages/deploy-helpers/src/shared/types.ts create mode 100644 packages/deploy-helpers/src/triggers/deploy.ts create mode 100644 packages/deploy-helpers/src/triggers/publish-routes.ts create mode 100644 packages/deploy-helpers/src/triggers/queue-consumers.ts create mode 100644 packages/deploy-helpers/src/triggers/subdomain.ts create mode 100644 packages/deploy-helpers/src/triggers/zones.ts create mode 100644 packages/deploy-helpers/tests/index.test.ts create mode 100644 packages/deploy-helpers/tests/package-dependencies.test.ts create mode 100644 packages/deploy-helpers/tests/sourcemap.test.ts create mode 100644 packages/deploy-helpers/tests/tsconfig.json create mode 100644 packages/deploy-helpers/tsconfig.json create mode 100644 packages/deploy-helpers/tsup.config.ts create mode 100644 packages/deploy-helpers/turbo.json create mode 100644 packages/deploy-helpers/vitest.config.mts create mode 100644 packages/devprod-status-bot/CHANGELOG.md create mode 100644 packages/devprod-status-bot/package.json create mode 100644 packages/devprod-status-bot/src/index.ts create mode 100644 packages/devprod-status-bot/src/status-bot.png create mode 100644 packages/devprod-status-bot/tsconfig.json create mode 100644 packages/devprod-status-bot/worker-configuration.d.ts create mode 100644 packages/devprod-status-bot/wrangler.jsonc create mode 100644 packages/edge-preview-authenticated-proxy/CHANGELOG.md create mode 100644 packages/edge-preview-authenticated-proxy/README.md create mode 100644 packages/edge-preview-authenticated-proxy/package.json create mode 100644 packages/edge-preview-authenticated-proxy/src/index.ts create mode 100644 packages/edge-preview-authenticated-proxy/tests/index.test.ts create mode 100644 packages/edge-preview-authenticated-proxy/tests/tsconfig.json create mode 100644 packages/edge-preview-authenticated-proxy/tsconfig.json create mode 100644 packages/edge-preview-authenticated-proxy/vitest.config.mts create mode 100644 packages/edge-preview-authenticated-proxy/worker-configuration.d.ts create mode 100644 packages/edge-preview-authenticated-proxy/wrangler.jsonc create mode 100644 packages/format-errors/CHANGELOG.md create mode 100644 packages/format-errors/README.md create mode 100644 packages/format-errors/images/modules-worker.png create mode 100644 packages/format-errors/images/service-worker.png create mode 100644 packages/format-errors/package.json create mode 100644 packages/format-errors/src/Stacktracey.js create mode 100644 packages/format-errors/src/Youch.js create mode 100644 packages/format-errors/src/__tests__/index.test.ts create mode 100644 packages/format-errors/src/error.compiled.mustache create mode 100644 packages/format-errors/src/impl/partition.js create mode 100644 packages/format-errors/src/index.ts create mode 100644 packages/format-errors/tsconfig.json create mode 100644 packages/format-errors/turbo.json create mode 100644 packages/format-errors/vitest.config.mts create mode 100644 packages/format-errors/wrangler.jsonc create mode 100644 packages/kv-asset-handler/CHANGELOG.md create mode 100644 packages/kv-asset-handler/README.md create mode 100644 packages/kv-asset-handler/package.json create mode 100644 packages/kv-asset-handler/src/index.ts create mode 100644 packages/kv-asset-handler/src/types.ts create mode 100644 packages/kv-asset-handler/test/getAssetFromKV-optional.test.ts create mode 100644 packages/kv-asset-handler/test/getAssetFromKV.test.ts create mode 100644 packages/kv-asset-handler/test/mapRequestToAsset.test.ts create mode 100644 packages/kv-asset-handler/test/mocks.ts create mode 100644 packages/kv-asset-handler/test/serveSinglePageApp.test.ts create mode 100644 packages/kv-asset-handler/tsconfig.json create mode 100644 packages/kv-asset-handler/tsup.config.ts create mode 100644 packages/kv-asset-handler/turbo.json create mode 100644 packages/kv-asset-handler/vitest.config.mts create mode 100644 packages/lint-config-shared/CHANGELOG.md create mode 100644 packages/lint-config-shared/oxlint-plugin.mjs create mode 100644 packages/lint-config-shared/package.json create mode 100644 packages/lint-config-shared/rules/__tests__/no-direct-recursive-rm.test.mjs create mode 100644 packages/lint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs create mode 100644 packages/lint-config-shared/rules/no-direct-recursive-rm.mjs create mode 100644 packages/lint-config-shared/rules/no-unsafe-command-execution.mjs create mode 100644 packages/lint-config-shared/rules/no-wrangler-named-imports.mjs create mode 100644 packages/lint-config-shared/rules/require-description-when-disabling.mjs create mode 100644 packages/local-explorer-ui/.gitignore create mode 100644 packages/local-explorer-ui/CHANGELOG.md create mode 100644 packages/local-explorer-ui/README.md create mode 100644 packages/local-explorer-ui/index.html create mode 100644 packages/local-explorer-ui/openapi-ts.config.ts create mode 100644 packages/local-explorer-ui/package.json create mode 100644 packages/local-explorer-ui/public/favicon.svg create mode 100644 packages/local-explorer-ui/src/__e2e__/d1/d1-database.spec.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/do/durable-object.spec.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/global-setup.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/kv/kv-namespace.spec.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/setup.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/utils.ts create mode 100644 packages/local-explorer-ui/src/__e2e__/workflows/workflow.spec.ts create mode 100644 packages/local-explorer-ui/src/__tests__/drivers/common.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/drivers/sqlite/driver.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/drivers/sqlite/generate.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/drivers/sqlite/parsers.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/utils/format.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/utils/kv-validation.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/utils/studio.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/utils/theme-state.test.ts create mode 100644 packages/local-explorer-ui/src/__tests__/workflows/step-helpers.test.ts create mode 100644 packages/local-explorer-ui/src/api/client-config.ts create mode 100644 packages/local-explorer-ui/src/api/index.ts create mode 100644 packages/local-explorer-ui/src/assets/icons/d1.svg create mode 100644 packages/local-explorer-ui/src/assets/icons/durable-objects.svg create mode 100644 packages/local-explorer-ui/src/assets/icons/kv.svg create mode 100644 packages/local-explorer-ui/src/assets/icons/r2.svg create mode 100644 packages/local-explorer-ui/src/assets/icons/workflows.svg create mode 100644 packages/local-explorer-ui/src/components/AddKVForm.tsx create mode 100644 packages/local-explorer-ui/src/components/AnimatedCloudflareLogo.tsx create mode 100644 packages/local-explorer-ui/src/components/Breadcrumbs.tsx create mode 100644 packages/local-explorer-ui/src/components/CopyButton.tsx create mode 100644 packages/local-explorer-ui/src/components/KVTable.tsx create mode 100644 packages/local-explorer-ui/src/components/NotFound.tsx create mode 100644 packages/local-explorer-ui/src/components/R2ObjectTable.tsx create mode 100644 packages/local-explorer-ui/src/components/R2UploadDialog.tsx create mode 100644 packages/local-explorer-ui/src/components/ResourceError.tsx create mode 100644 packages/local-explorer-ui/src/components/SearchForm.tsx create mode 100644 packages/local-explorer-ui/src/components/Sidebar.tsx create mode 100644 packages/local-explorer-ui/src/components/SidebarGroupPopup.tsx create mode 100644 packages/local-explorer-ui/src/components/TableSelect.tsx create mode 100644 packages/local-explorer-ui/src/components/WorkerSelector.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Code/Block.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Code/Mirror.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Context.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/ContextMenu.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Explain/SQLiteExplainTab.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/HighlightText.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Modal.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Modal/CommitConfirmation.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Modal/DeleteConfirmation.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Modal/DropTableConfirmation.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Modal/Export/OptionsEditor.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Modal/Export/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/QueryResult/Stats.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/QueryResult/Summary.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/QueryResult/Tab.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/SQLEditor/SQLThemePlugin.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/SQLEditor/SQLiteDialect.ts create mode 100644 packages/local-explorer-ui/src/components/studio/SQLEditor/SQLiteTableNameHighlightPlugin.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/SQLEditor/StatementHighlightExtension.ts create mode 100644 packages/local-explorer-ui/src/components/studio/SQLEditor/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/SQLWhereEditor.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/SkeletonBlock.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/TabRegister.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/ActionsDropdown.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/BaseTable.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/Cell.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/FakePadding.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/HeaderList.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/HeaderResizer.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/Result/DisplayCell.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/Result/EditableCell.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/Result/ResultTableContextMenu.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/Result/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/SchemaEditor/Column.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/SchemaEditor/ConstraintListEditor.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/SchemaEditor/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/State/Helpers.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/State/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Table/useVisibilityCalculation.ts create mode 100644 packages/local-explorer-ui/src/components/studio/Tabs/CreateUpdateTable.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Tabs/Query.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/Tabs/TableExplorer.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WhereFilterInput.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/ContentWrapper.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/Context.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/ItemRenderer.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/MenuProps.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/Pane.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/index.tsx create mode 100644 packages/local-explorer-ui/src/components/studio/WindowTab/types.ts create mode 100644 packages/local-explorer-ui/src/components/studio/index.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/CopyButton.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/CreateInstanceDialog.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/ScrollableCodeBlock.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/StatusBadge.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/StatusIcon.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/StepRow.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/Timestamp.tsx create mode 100644 packages/local-explorer-ui/src/components/workflows/helpers.ts create mode 100644 packages/local-explorer-ui/src/components/workflows/types.ts create mode 100644 packages/local-explorer-ui/src/drivers/common.ts create mode 100644 packages/local-explorer-ui/src/drivers/d1.ts create mode 100644 packages/local-explorer-ui/src/drivers/do.ts create mode 100644 packages/local-explorer-ui/src/drivers/sqlite/generate.ts create mode 100644 packages/local-explorer-ui/src/drivers/sqlite/index.ts create mode 100644 packages/local-explorer-ui/src/drivers/sqlite/parsers.ts create mode 100644 packages/local-explorer-ui/src/hooks/leave-guard.ts create mode 100644 packages/local-explorer-ui/src/main.tsx create mode 100644 packages/local-explorer-ui/src/routeTree.gen.ts create mode 100644 packages/local-explorer-ui/src/routes/__root.tsx create mode 100644 packages/local-explorer-ui/src/routes/d1/$databaseId.tsx create mode 100644 packages/local-explorer-ui/src/routes/do/$className.tsx create mode 100644 packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx create mode 100644 packages/local-explorer-ui/src/routes/do/$className/index.tsx create mode 100644 packages/local-explorer-ui/src/routes/index.tsx create mode 100644 packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx create mode 100644 packages/local-explorer-ui/src/routes/r2/$bucketName.tsx create mode 100644 packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx create mode 100644 packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx create mode 100644 packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx create mode 100644 packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx create mode 100644 packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx create mode 100644 packages/local-explorer-ui/src/styles/tailwind.css create mode 100644 packages/local-explorer-ui/src/types/studio.ts create mode 100644 packages/local-explorer-ui/src/utils/agent-prompt.ts create mode 100644 packages/local-explorer-ui/src/utils/async.ts create mode 100644 packages/local-explorer-ui/src/utils/format.ts create mode 100644 packages/local-explorer-ui/src/utils/is-equal.ts create mode 100644 packages/local-explorer-ui/src/utils/kv-validation.ts create mode 100644 packages/local-explorer-ui/src/utils/sidebar-state.ts create mode 100644 packages/local-explorer-ui/src/utils/studio/commit.ts create mode 100644 packages/local-explorer-ui/src/utils/studio/export.ts create mode 100644 packages/local-explorer-ui/src/utils/studio/formatter.ts create mode 100644 packages/local-explorer-ui/src/utils/studio/index.ts create mode 100644 packages/local-explorer-ui/src/utils/studio/sql.ts create mode 100644 packages/local-explorer-ui/src/utils/studio/where-parser.ts create mode 100644 packages/local-explorer-ui/src/utils/theme-state.ts create mode 100644 packages/local-explorer-ui/src/vite-env.d.ts create mode 100644 packages/local-explorer-ui/tsconfig.client.json create mode 100644 packages/local-explorer-ui/tsconfig.e2e.json create mode 100644 packages/local-explorer-ui/tsconfig.json create mode 100644 packages/local-explorer-ui/tsconfig.node.json create mode 100644 packages/local-explorer-ui/turbo.json create mode 100644 packages/local-explorer-ui/vite.config.ts create mode 100644 packages/local-explorer-ui/vitest.config.e2e.ts create mode 100644 packages/local-explorer-ui/vitest.config.mts create mode 100644 packages/miniflare/AGENTS.md create mode 100644 packages/miniflare/CHANGELOG.md create mode 100644 packages/miniflare/CONTRIBUTING.md create mode 100644 packages/miniflare/README.md create mode 100755 packages/miniflare/bootstrap.js create mode 100644 packages/miniflare/miniflare.drawio.svg create mode 100644 packages/miniflare/openapi-ts.config.ts create mode 100644 packages/miniflare/package.json create mode 100644 packages/miniflare/scripts/build.mjs create mode 100644 packages/miniflare/scripts/check-generate-api.ts create mode 100644 packages/miniflare/scripts/common.mjs create mode 100644 packages/miniflare/scripts/deps.ts create mode 100644 packages/miniflare/scripts/filter-openapi.ts create mode 100644 packages/miniflare/scripts/openapi-filter-config.ts create mode 100644 packages/miniflare/scripts/types.mjs create mode 100644 packages/miniflare/src/cf.ts create mode 100644 packages/miniflare/src/exit-hook.ts create mode 100644 packages/miniflare/src/http/cert.ts create mode 100644 packages/miniflare/src/http/fetch.ts create mode 100644 packages/miniflare/src/http/helpers.ts create mode 100644 packages/miniflare/src/http/index.ts create mode 100644 packages/miniflare/src/http/request.ts create mode 100644 packages/miniflare/src/http/response.ts create mode 100644 packages/miniflare/src/http/server.ts create mode 100644 packages/miniflare/src/http/websocket.ts create mode 100644 packages/miniflare/src/index.ts create mode 100644 packages/miniflare/src/merge.ts create mode 100644 packages/miniflare/src/plugins/agent-memory/index.ts create mode 100644 packages/miniflare/src/plugins/ai-search/index.ts create mode 100644 packages/miniflare/src/plugins/ai/index.ts create mode 100644 packages/miniflare/src/plugins/analytics-engine/index.ts create mode 100644 packages/miniflare/src/plugins/artifacts/index.ts create mode 100644 packages/miniflare/src/plugins/assets/constants.ts create mode 100644 packages/miniflare/src/plugins/assets/index.ts create mode 100644 packages/miniflare/src/plugins/assets/schema.ts create mode 100644 packages/miniflare/src/plugins/browser-rendering/browser-version.ts create mode 100644 packages/miniflare/src/plugins/browser-rendering/constants.ts create mode 100644 packages/miniflare/src/plugins/browser-rendering/index.ts create mode 100644 packages/miniflare/src/plugins/cache/index.ts create mode 100644 packages/miniflare/src/plugins/core/constants.ts create mode 100644 packages/miniflare/src/plugins/core/errors/callsite.ts create mode 100644 packages/miniflare/src/plugins/core/errors/index.ts create mode 100644 packages/miniflare/src/plugins/core/errors/sourcemap.ts create mode 100644 packages/miniflare/src/plugins/core/explorer.ts create mode 100644 packages/miniflare/src/plugins/core/index.ts create mode 100644 packages/miniflare/src/plugins/core/inspector-proxy/devtools.ts create mode 100644 packages/miniflare/src/plugins/core/inspector-proxy/index.ts create mode 100644 packages/miniflare/src/plugins/core/inspector-proxy/inspector-proxy-controller.ts create mode 100644 packages/miniflare/src/plugins/core/inspector-proxy/inspector-proxy.ts create mode 100644 packages/miniflare/src/plugins/core/module-fallback.ts create mode 100644 packages/miniflare/src/plugins/core/modules.ts create mode 100644 packages/miniflare/src/plugins/core/node-compat.ts create mode 100644 packages/miniflare/src/plugins/core/proxy/README.md create mode 100644 packages/miniflare/src/plugins/core/proxy/client.ts create mode 100644 packages/miniflare/src/plugins/core/proxy/fetch-sync.ts create mode 100644 packages/miniflare/src/plugins/core/proxy/index.ts create mode 100644 packages/miniflare/src/plugins/core/proxy/types.ts create mode 100644 packages/miniflare/src/plugins/core/services.ts create mode 100644 packages/miniflare/src/plugins/core/types.ts create mode 100644 packages/miniflare/src/plugins/d1/index.ts create mode 100644 packages/miniflare/src/plugins/dispatch-namespace/index.ts create mode 100644 packages/miniflare/src/plugins/do/index.ts create mode 100644 packages/miniflare/src/plugins/email/index.ts create mode 100644 packages/miniflare/src/plugins/flagship/index.ts create mode 100644 packages/miniflare/src/plugins/hello-world/index.ts create mode 100644 packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts create mode 100644 packages/miniflare/src/plugins/hyperdrive/index.ts create mode 100644 packages/miniflare/src/plugins/images/fetcher.ts create mode 100644 packages/miniflare/src/plugins/images/index.ts create mode 100644 packages/miniflare/src/plugins/index.ts create mode 100644 packages/miniflare/src/plugins/kv/constants.ts create mode 100644 packages/miniflare/src/plugins/kv/index.ts create mode 100644 packages/miniflare/src/plugins/kv/sites.ts create mode 100644 packages/miniflare/src/plugins/media/index.ts create mode 100644 packages/miniflare/src/plugins/mtls/index.ts create mode 100644 packages/miniflare/src/plugins/pipelines/index.ts create mode 100644 packages/miniflare/src/plugins/queues/errors.ts create mode 100644 packages/miniflare/src/plugins/queues/index.ts create mode 100644 packages/miniflare/src/plugins/r2/index.ts create mode 100644 packages/miniflare/src/plugins/ratelimit/index.ts create mode 100644 packages/miniflare/src/plugins/secret-store/index.ts create mode 100644 packages/miniflare/src/plugins/shared/constants.ts create mode 100644 packages/miniflare/src/plugins/shared/index.ts create mode 100644 packages/miniflare/src/plugins/shared/routing.ts create mode 100644 packages/miniflare/src/plugins/stream/index.ts create mode 100644 packages/miniflare/src/plugins/vectorize/index.ts create mode 100644 packages/miniflare/src/plugins/version-metadata/index.ts create mode 100644 packages/miniflare/src/plugins/vpc-networks/index.ts create mode 100644 packages/miniflare/src/plugins/vpc-services/index.ts create mode 100644 packages/miniflare/src/plugins/websearch/index.ts create mode 100644 packages/miniflare/src/plugins/worker-loader/index.ts create mode 100644 packages/miniflare/src/plugins/workflows/index.ts create mode 100644 packages/miniflare/src/runtime/config/generated/workerd.ts create mode 100644 packages/miniflare/src/runtime/config/index.ts create mode 100644 packages/miniflare/src/runtime/config/workerd.ts create mode 100644 packages/miniflare/src/runtime/index.ts create mode 100644 packages/miniflare/src/runtime/structured-logs.ts create mode 100644 packages/miniflare/src/shared/DEV_REGISTRY.md create mode 100644 packages/miniflare/src/shared/colour.ts create mode 100644 packages/miniflare/src/shared/dev-control.ts create mode 100644 packages/miniflare/src/shared/dev-registry-types.ts create mode 100644 packages/miniflare/src/shared/dev-registry.ts create mode 100644 packages/miniflare/src/shared/error.ts create mode 100644 packages/miniflare/src/shared/event.ts create mode 100644 packages/miniflare/src/shared/external-service.ts create mode 100644 packages/miniflare/src/shared/index.ts create mode 100644 packages/miniflare/src/shared/log.ts create mode 100644 packages/miniflare/src/shared/matcher.ts create mode 100644 packages/miniflare/src/shared/mime-types.ts create mode 100644 packages/miniflare/src/shared/streams.ts create mode 100644 packages/miniflare/src/shared/types.ts create mode 100644 packages/miniflare/src/workers/README.md create mode 100644 packages/miniflare/src/workers/analytics-engine/analytics-engine.worker.ts create mode 100644 packages/miniflare/src/workers/assets/assets-kv.worker.ts create mode 100644 packages/miniflare/src/workers/assets/assets.worker.ts create mode 100644 packages/miniflare/src/workers/assets/router.worker.ts create mode 100644 packages/miniflare/src/workers/assets/rpc-proxy.worker.ts create mode 100644 packages/miniflare/src/workers/browser-rendering/binding.worker.ts create mode 100644 packages/miniflare/src/workers/cache/README.md create mode 100644 packages/miniflare/src/workers/cache/cache-entry-noop.worker.ts create mode 100644 packages/miniflare/src/workers/cache/cache-entry.worker.ts create mode 100644 packages/miniflare/src/workers/cache/cache.worker.ts create mode 100644 packages/miniflare/src/workers/cache/constants.ts create mode 100644 packages/miniflare/src/workers/cache/errors.worker.ts create mode 100644 packages/miniflare/src/workers/cache/index.ts create mode 100644 packages/miniflare/src/workers/core/constants.ts create mode 100644 packages/miniflare/src/workers/core/dev-control.worker.ts create mode 100644 packages/miniflare/src/workers/core/dev-registry-proxy-shared.worker.ts create mode 100644 packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts create mode 100644 packages/miniflare/src/workers/core/devalue.ts create mode 100644 packages/miniflare/src/workers/core/do-wrapper.worker.ts create mode 100644 packages/miniflare/src/workers/core/email.ts create mode 100644 packages/miniflare/src/workers/core/entry.worker.ts create mode 100644 packages/miniflare/src/workers/core/http.ts create mode 100644 packages/miniflare/src/workers/core/index.ts create mode 100644 packages/miniflare/src/workers/core/outbound.worker.ts create mode 100644 packages/miniflare/src/workers/core/proxy.worker.ts create mode 100644 packages/miniflare/src/workers/core/routing.ts create mode 100644 packages/miniflare/src/workers/core/scheduled.ts create mode 100644 packages/miniflare/src/workers/d1/database.worker.ts create mode 100644 packages/miniflare/src/workers/d1/dumpSql.ts create mode 100644 packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace-proxy.worker.ts create mode 100644 packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace.worker.ts create mode 100644 packages/miniflare/src/workers/email/constants.ts create mode 100644 packages/miniflare/src/workers/email/email.worker.ts create mode 100644 packages/miniflare/src/workers/email/send_email.worker.ts create mode 100644 packages/miniflare/src/workers/email/types.ts create mode 100644 packages/miniflare/src/workers/email/validate.ts create mode 100644 packages/miniflare/src/workers/hello-world/binding.worker.ts create mode 100644 packages/miniflare/src/workers/hello-world/object.worker.ts create mode 100644 packages/miniflare/src/workers/images/images.worker.ts create mode 100644 packages/miniflare/src/workers/index.ts create mode 100644 packages/miniflare/src/workers/kv/constants.ts create mode 100644 packages/miniflare/src/workers/kv/index.ts create mode 100644 packages/miniflare/src/workers/kv/namespace.worker.ts create mode 100644 packages/miniflare/src/workers/kv/sites.worker.ts create mode 100644 packages/miniflare/src/workers/kv/validator.worker.ts create mode 100644 packages/miniflare/src/workers/local-explorer/aggregation.ts create mode 100644 packages/miniflare/src/workers/local-explorer/common.ts create mode 100644 packages/miniflare/src/workers/local-explorer/explorer.worker.ts create mode 100644 packages/miniflare/src/workers/local-explorer/generated/index.ts create mode 100644 packages/miniflare/src/workers/local-explorer/generated/types.gen.ts create mode 100644 packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts create mode 100644 packages/miniflare/src/workers/local-explorer/openapi.local.json create mode 100644 packages/miniflare/src/workers/local-explorer/resources/d1.ts create mode 100644 packages/miniflare/src/workers/local-explorer/resources/do.ts create mode 100644 packages/miniflare/src/workers/local-explorer/resources/kv.ts create mode 100644 packages/miniflare/src/workers/local-explorer/resources/r2.ts create mode 100644 packages/miniflare/src/workers/local-explorer/resources/workflows.ts create mode 100644 packages/miniflare/src/workers/local-explorer/route-names.ts create mode 100644 packages/miniflare/src/workers/local-explorer/telemetry.ts create mode 100644 packages/miniflare/src/workers/node.d.ts create mode 100644 packages/miniflare/src/workers/pipelines/pipeline.worker.ts create mode 100644 packages/miniflare/src/workers/queues/broker.worker.ts create mode 100644 packages/miniflare/src/workers/queues/constants.ts create mode 100644 packages/miniflare/src/workers/queues/index.ts create mode 100644 packages/miniflare/src/workers/queues/schemas.ts create mode 100644 packages/miniflare/src/workers/r2/bucket.worker.ts create mode 100644 packages/miniflare/src/workers/r2/constants.ts create mode 100644 packages/miniflare/src/workers/r2/errors.worker.ts create mode 100644 packages/miniflare/src/workers/r2/public.worker.ts create mode 100644 packages/miniflare/src/workers/r2/r2Object.worker.ts create mode 100644 packages/miniflare/src/workers/r2/schemas.worker.ts create mode 100644 packages/miniflare/src/workers/r2/validator.worker.ts create mode 100644 packages/miniflare/src/workers/ratelimit/ratelimit-object.worker.ts create mode 100644 packages/miniflare/src/workers/ratelimit/ratelimit.worker.ts create mode 100644 packages/miniflare/src/workers/secrets-store/constants.ts create mode 100644 packages/miniflare/src/workers/secrets-store/secret.worker.ts create mode 100644 packages/miniflare/src/workers/shared/blob.worker.ts create mode 100644 packages/miniflare/src/workers/shared/constants.ts create mode 100644 packages/miniflare/src/workers/shared/data.ts create mode 100644 packages/miniflare/src/workers/shared/index.ts create mode 100644 packages/miniflare/src/workers/shared/index.worker.ts create mode 100644 packages/miniflare/src/workers/shared/keyvalue.worker.ts create mode 100644 packages/miniflare/src/workers/shared/matcher.ts create mode 100644 packages/miniflare/src/workers/shared/object-entry.worker.ts create mode 100644 packages/miniflare/src/workers/shared/object.worker.ts create mode 100644 packages/miniflare/src/workers/shared/public-url.ts create mode 100644 packages/miniflare/src/workers/shared/range.ts create mode 100644 packages/miniflare/src/workers/shared/remote-bindings-utils.ts create mode 100644 packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts create mode 100644 packages/miniflare/src/workers/shared/router.worker.ts create mode 100644 packages/miniflare/src/workers/shared/sql.worker.ts create mode 100644 packages/miniflare/src/workers/shared/sync.ts create mode 100644 packages/miniflare/src/workers/shared/timers.worker.ts create mode 100644 packages/miniflare/src/workers/shared/types.ts create mode 100644 packages/miniflare/src/workers/shared/zod.worker.ts create mode 100644 packages/miniflare/src/workers/stream/binding.worker.ts create mode 100644 packages/miniflare/src/workers/stream/errors.ts create mode 100644 packages/miniflare/src/workers/stream/object.worker.ts create mode 100644 packages/miniflare/src/workers/stream/schemas.ts create mode 100644 packages/miniflare/src/workers/tsconfig.json create mode 100644 packages/miniflare/src/workers/workerd-unsafe.d.ts create mode 100644 packages/miniflare/src/workers/workflows/binding.worker.ts create mode 100644 packages/miniflare/src/workers/workflows/wrapped-binding.worker.ts create mode 100644 packages/miniflare/src/zod-format.ts create mode 100644 packages/miniflare/test/cf.spec.ts create mode 100644 packages/miniflare/test/dev-registry.spec.ts create mode 100644 packages/miniflare/test/fixtures/cache/parse-http.ts create mode 100755 packages/miniflare/test/fixtures/crashing-workerd.mjs create mode 100644 packages/miniflare/test/fixtures/d1/README.md create mode 100644 packages/miniflare/test/fixtures/d1/worker.dist.mjs create mode 100644 packages/miniflare/test/fixtures/d1/worker.mjs create mode 100644 packages/miniflare/test/fixtures/d1/wrangler.jsonc create mode 100755 packages/miniflare/test/fixtures/little-workerd.mjs create mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/README.md create mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/d1/DATABASE/db.sqlite create mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/blobs/c708857cd996a02e1c61a4d5af45519baf21ce5c1a39c5baea36f40e912e81e6000637434f5e7544 create mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/kv/NAMESPACE/db.sqlite create mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/blobs/776759739fb970e463921f00822d78ae8455152966feb09ac09ec3cc4b27dcff000637435043236c create mode 100644 packages/miniflare/test/fixtures/migrations/3.20230821.0/r2/BUCKET/db.sqlite create mode 100644 packages/miniflare/test/fixtures/modules/add.wasm create mode 100644 packages/miniflare/test/fixtures/modules/blobs-indirect.mjs create mode 100644 packages/miniflare/test/fixtures/modules/blobs.mjs create mode 100644 packages/miniflare/test/fixtures/modules/blobs/data.bin create mode 100644 packages/miniflare/test/fixtures/modules/blobs/text.txt create mode 100644 packages/miniflare/test/fixtures/modules/cyclic/cyclic1.mjs create mode 100644 packages/miniflare/test/fixtures/modules/cyclic/cyclic2.mjs create mode 100644 packages/miniflare/test/fixtures/modules/cyclic/index.mjs create mode 100644 packages/miniflare/test/fixtures/modules/dynamic-require.cjs create mode 100644 packages/miniflare/test/fixtures/modules/index-dynamic.mjs create mode 100644 packages/miniflare/test/fixtures/modules/index.cjs create mode 100644 packages/miniflare/test/fixtures/modules/index.mjs create mode 100644 packages/miniflare/test/fixtures/modules/index.node.cjs create mode 100644 packages/miniflare/test/fixtures/r2/validator.ts create mode 100644 packages/miniflare/test/fixtures/shared/router.ts create mode 100644 packages/miniflare/test/fixtures/sites/manifest.d.ts create mode 100644 packages/miniflare/test/fixtures/sites/modules.ts create mode 100644 packages/miniflare/test/fixtures/sites/service-worker.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/inline-sourcemap-worker.js create mode 100644 packages/miniflare/test/fixtures/source-maps/modules.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/nested/dep.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/reduce.ts create mode 100644 packages/miniflare/test/fixtures/source-maps/service-worker.ts create mode 100644 packages/miniflare/test/fixtures/tsconfig.json create mode 100644 packages/miniflare/test/fixtures/unsafe-plugin-bad/no-export.cjs create mode 100644 packages/miniflare/test/fixtures/unsafe-plugin-bad/not-function.cjs create mode 100644 packages/miniflare/test/fixtures/unsafe-plugin/index.ts create mode 100644 packages/miniflare/test/fixtures/unsafe-plugin/tsconfig.json create mode 100644 packages/miniflare/test/fixtures/worker-test.ts create mode 100644 packages/miniflare/test/http/fetch.spec.ts create mode 100644 packages/miniflare/test/http/request.spec.ts create mode 100644 packages/miniflare/test/http/response.spec.ts create mode 100644 packages/miniflare/test/http/websocket.spec.ts create mode 100644 packages/miniflare/test/index.spec.ts create mode 100644 packages/miniflare/test/logs.spec.ts create mode 100644 packages/miniflare/test/merge.spec.ts create mode 100644 packages/miniflare/test/plugins/assets/index.spec.ts create mode 100644 packages/miniflare/test/plugins/browser/index.spec.ts create mode 100644 packages/miniflare/test/plugins/cache/index.spec.ts create mode 100644 packages/miniflare/test/plugins/cache/parse-http.spec.ts create mode 100644 packages/miniflare/test/plugins/core/cf-image.spec.ts create mode 100644 packages/miniflare/test/plugins/core/errors/index.spec.ts create mode 100644 packages/miniflare/test/plugins/core/index.spec.ts create mode 100644 packages/miniflare/test/plugins/core/inspector-proxy/index.spec.ts create mode 100644 packages/miniflare/test/plugins/core/modules.spec.ts create mode 100644 packages/miniflare/test/plugins/core/proxy/client.spec.ts create mode 100644 packages/miniflare/test/plugins/d1/index.spec.ts create mode 100644 packages/miniflare/test/plugins/d1/index.with-wrangler-shim.spec.ts create mode 100644 packages/miniflare/test/plugins/d1/suite.ts create mode 100644 packages/miniflare/test/plugins/d1/test.ts create mode 100644 packages/miniflare/test/plugins/do/index.spec.ts create mode 100644 packages/miniflare/test/plugins/email/index.spec.ts create mode 100644 packages/miniflare/test/plugins/flagship/index.spec.ts create mode 100644 packages/miniflare/test/plugins/hello-world/index.spec.ts create mode 100644 packages/miniflare/test/plugins/hyperdrive/index.spec.ts create mode 100644 packages/miniflare/test/plugins/hyperdrive/proxy.spec.ts create mode 100644 packages/miniflare/test/plugins/images/index.spec.ts create mode 100644 packages/miniflare/test/plugins/kv/index.spec.ts create mode 100644 packages/miniflare/test/plugins/kv/sites.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/aggregation.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/d1.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/do-wrapper.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/do.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/helpers.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/index.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/kv.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/r2.spec.ts create mode 100644 packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts create mode 100644 packages/miniflare/test/plugins/pipelines/index.spec.ts create mode 100644 packages/miniflare/test/plugins/queues/cross-process.spec.ts create mode 100644 packages/miniflare/test/plugins/queues/delay.spec.ts create mode 100644 packages/miniflare/test/plugins/queues/index.spec.ts create mode 100644 packages/miniflare/test/plugins/queues/retry.spec.ts create mode 100644 packages/miniflare/test/plugins/r2/index.spec.ts create mode 100644 packages/miniflare/test/plugins/r2/public.spec.ts create mode 100644 packages/miniflare/test/plugins/r2/validator.spec.ts create mode 100644 packages/miniflare/test/plugins/ratelimit/index.spec.ts create mode 100644 packages/miniflare/test/plugins/secret-store/index.spec.ts create mode 100644 packages/miniflare/test/plugins/shared/range.spec.ts create mode 100644 packages/miniflare/test/plugins/shared/remote-bindings-access-warning.spec.ts create mode 100644 packages/miniflare/test/plugins/shared/router.spec.ts create mode 100644 packages/miniflare/test/plugins/shared/routing.spec.ts create mode 100644 packages/miniflare/test/plugins/stream/index.spec.ts create mode 100644 packages/miniflare/test/plugins/unsafe/index.spec.ts create mode 100644 packages/miniflare/test/plugins/version-metadata/index.spec.ts create mode 100644 packages/miniflare/test/plugins/workflows/index.spec.ts create mode 100644 packages/miniflare/test/public-url.spec.ts create mode 100644 packages/miniflare/test/setup.mjs create mode 100644 packages/miniflare/test/shared/blob.spec.ts create mode 100644 packages/miniflare/test/shared/matcher.spec.ts create mode 100644 packages/miniflare/test/shared/sync.spec.ts create mode 100644 packages/miniflare/test/shared/types.spec.ts create mode 100644 packages/miniflare/test/snapshots/zod-format.spec.ts.md create mode 100644 packages/miniflare/test/snapshots/zod-format.spec.ts.snap create mode 100644 packages/miniflare/test/sourcemap.spec.ts create mode 100644 packages/miniflare/test/test-shared/http.ts create mode 100644 packages/miniflare/test/test-shared/index.ts create mode 100644 packages/miniflare/test/test-shared/log.ts create mode 100644 packages/miniflare/test/test-shared/miniflare.ts create mode 100644 packages/miniflare/test/test-shared/object.ts create mode 100644 packages/miniflare/test/test-shared/registry.ts create mode 100644 packages/miniflare/test/test-shared/storage.ts create mode 100644 packages/miniflare/test/test-shared/worker-test.ts create mode 100644 packages/miniflare/test/workers/core/serialize.spec.ts create mode 100644 packages/miniflare/test/zod-format.spec.ts create mode 100644 packages/miniflare/tsconfig.bundle.json create mode 100644 packages/miniflare/tsconfig.json create mode 100644 packages/miniflare/turbo.json create mode 100644 packages/miniflare/types/env.d.ts create mode 100644 packages/miniflare/types/events.d.ts create mode 100644 packages/miniflare/types/streams.d.ts create mode 100644 packages/miniflare/types/webassembly.d.ts create mode 100644 packages/miniflare/types/worker-shim.d.ts create mode 100644 packages/miniflare/types/workerd.d.ts create mode 100644 packages/miniflare/vitest.config.mts create mode 100644 packages/mock-npm-registry/README.md create mode 100644 packages/mock-npm-registry/package.json create mode 100644 packages/mock-npm-registry/src/index.ts create mode 100644 packages/mock-npm-registry/tsconfig.json create mode 100644 packages/mock-npm-registry/turbo.json create mode 100644 packages/pages-shared/CHANGELOG.md create mode 100644 packages/pages-shared/README.md create mode 100644 packages/pages-shared/__tests__/asset-server/handler.test.ts create mode 100644 packages/pages-shared/__tests__/asset-server/responses.test.ts create mode 100644 packages/pages-shared/__tests__/metadata-generator/createMetadataObject.test.ts create mode 100644 packages/pages-shared/__tests__/metadata-generator/fixtures/_redirects create mode 100644 packages/pages-shared/asset-server/handler.ts create mode 100644 packages/pages-shared/asset-server/metadata.ts create mode 100644 packages/pages-shared/asset-server/patchUrl.ts create mode 100644 packages/pages-shared/asset-server/responses.ts create mode 100644 packages/pages-shared/environment-polyfills/html-rewriter.ts create mode 100644 packages/pages-shared/environment-polyfills/index.ts create mode 100644 packages/pages-shared/environment-polyfills/miniflare.ts create mode 100644 packages/pages-shared/environment-polyfills/types.ts create mode 100644 packages/pages-shared/metadata-generator/constants.ts create mode 100644 packages/pages-shared/metadata-generator/createMetadataObject.ts create mode 100644 packages/pages-shared/metadata-generator/types.ts create mode 100644 packages/pages-shared/package.json create mode 100644 packages/pages-shared/scripts/build.ts create mode 100644 packages/pages-shared/tsconfig.build.json create mode 100644 packages/pages-shared/tsconfig.json create mode 100644 packages/pages-shared/turbo.json create mode 100644 packages/pages-shared/vitest.config.mts create mode 100644 packages/playground-preview-worker/.gitignore create mode 100644 packages/playground-preview-worker/CHANGELOG.md create mode 100644 packages/playground-preview-worker/package.json create mode 100644 packages/playground-preview-worker/src/errors.ts create mode 100644 packages/playground-preview-worker/src/index.ts create mode 100644 packages/playground-preview-worker/src/inject-middleware.ts create mode 100644 packages/playground-preview-worker/src/middleware/common.module.template create mode 100644 packages/playground-preview-worker/src/middleware/definitions/json.module.template create mode 100644 packages/playground-preview-worker/src/middleware/loader.module.template create mode 100644 packages/playground-preview-worker/src/realish.ts create mode 100644 packages/playground-preview-worker/src/sentry.ts create mode 100644 packages/playground-preview-worker/src/user.do.ts create mode 100644 packages/playground-preview-worker/tests/index.test.ts create mode 100644 packages/playground-preview-worker/tests/tsconfig.json create mode 100644 packages/playground-preview-worker/tsconfig.json create mode 100644 packages/playground-preview-worker/worker-configuration.d.ts create mode 100644 packages/playground-preview-worker/wrangler.json create mode 100644 packages/quick-edit-extension/.vscodeignore create mode 100644 packages/quick-edit-extension/README.md create mode 100644 packages/quick-edit-extension/package.json create mode 100644 packages/quick-edit-extension/scripts/bundle.ts create mode 100644 packages/quick-edit-extension/scripts/tsconfig.json create mode 100644 packages/quick-edit-extension/src/cfs.ts create mode 100644 packages/quick-edit-extension/src/extension.ts create mode 100644 packages/quick-edit-extension/src/ipc.ts create mode 100644 packages/quick-edit-extension/src/raw.d.ts create mode 100644 packages/quick-edit-extension/tsconfig.json create mode 100644 packages/quick-edit-extension/vscode.d.ts create mode 100644 packages/quick-edit-extension/vscode.proposed.fileSearchProvider.d.ts create mode 100644 packages/quick-edit-extension/vscode.proposed.ipc.d.ts create mode 100644 packages/quick-edit-extension/vscode.proposed.textSearchProvider.d.ts create mode 100644 packages/quick-edit/.node-version create mode 100644 packages/quick-edit/.nvmrc create mode 100644 packages/quick-edit/CHANGELOG.md create mode 100644 packages/quick-edit/README.md create mode 100755 packages/quick-edit/build.sh create mode 100644 packages/quick-edit/bundle-dts.ts create mode 100644 packages/quick-edit/editor-files/code.d.ts create mode 100644 packages/quick-edit/editor-files/tsconfig.json create mode 100644 packages/quick-edit/editor-files/workbench.html create mode 100644 packages/quick-edit/editor-files/workbench.ts create mode 100644 packages/quick-edit/package.json create mode 100644 packages/quick-edit/patches/hide-unwanted-ui.diff create mode 100644 packages/quick-edit/patches/typechecking-without-shared-array-buffer.diff create mode 100755 packages/quick-edit/setup.sh create mode 100644 packages/quick-edit/src/index.ts create mode 100644 packages/quick-edit/tsconfig.json create mode 100644 packages/quick-edit/worker-configuration.d.ts create mode 100644 packages/quick-edit/wrangler.jsonc create mode 100644 packages/runtime-types/CHANGELOG.md create mode 100644 packages/runtime-types/package.json create mode 100644 packages/runtime-types/src/__tests__/header.test.ts create mode 100644 packages/runtime-types/src/__tests__/runtime.test.ts create mode 100644 packages/runtime-types/src/header.ts create mode 100644 packages/runtime-types/src/index.ts create mode 100644 packages/runtime-types/src/runtime.ts create mode 100644 packages/runtime-types/src/workerd.d.ts create mode 100644 packages/runtime-types/tsconfig.json create mode 100644 packages/runtime-types/tsdown.config.ts create mode 100644 packages/runtime-types/turbo.json create mode 100644 packages/runtime-types/vitest.config.ts create mode 100644 packages/solarflare-theme/.vscode/launch.json create mode 100644 packages/solarflare-theme/.vscodeignore create mode 100644 packages/solarflare-theme/CHANGELOG.md create mode 100644 packages/solarflare-theme/README.md create mode 100644 packages/solarflare-theme/package.json create mode 100644 packages/solarflare-theme/solarflare-quickstart.md create mode 100644 packages/solarflare-theme/themes/cloudflare-dark-color-theme.json create mode 100644 packages/solarflare-theme/themes/cloudflare-light-color-theme.json create mode 100644 packages/turbo-r2-archive/.gitignore create mode 100644 packages/turbo-r2-archive/CHANGELOG.md create mode 100644 packages/turbo-r2-archive/README.md create mode 100644 packages/turbo-r2-archive/package.json create mode 100644 packages/turbo-r2-archive/src/autoCacheBust.ts create mode 100644 packages/turbo-r2-archive/src/index.ts create mode 100644 packages/turbo-r2-archive/src/routes.ts create mode 100644 packages/turbo-r2-archive/tsconfig.json create mode 100644 packages/turbo-r2-archive/turbo.json create mode 100644 packages/turbo-r2-archive/worker-configuration.d.ts create mode 100644 packages/turbo-r2-archive/wrangler.json create mode 100644 packages/unenv-preset/CHANGELOG.md create mode 100644 packages/unenv-preset/LICENSE-APACHE create mode 100644 packages/unenv-preset/LICENSE-MIT create mode 100644 packages/unenv-preset/README.md create mode 100644 packages/unenv-preset/build.config.ts create mode 100644 packages/unenv-preset/package.json create mode 100644 packages/unenv-preset/src/index.ts create mode 100644 packages/unenv-preset/src/preset.ts create mode 100644 packages/unenv-preset/src/runtime/node/console.ts create mode 100644 packages/unenv-preset/src/runtime/node/process.ts create mode 100644 packages/unenv-preset/src/runtime/polyfill/package.json create mode 100644 packages/unenv-preset/src/runtime/polyfill/performance.ts create mode 100644 packages/unenv-preset/tsconfig.json create mode 100644 packages/unenv-preset/turbo.json create mode 100644 packages/vite-plugin-cloudflare/AGENTS.md create mode 100644 packages/vite-plugin-cloudflare/CHANGELOG.md create mode 100644 packages/vite-plugin-cloudflare/README.md create mode 100755 packages/vite-plugin-cloudflare/bin/cf-vite create mode 100644 packages/vite-plugin-cloudflare/e2e/README.md create mode 100644 packages/vite-plugin-cloudflare/e2e/basic.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/dynamic.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/.gitignore create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/README.md create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/api/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/index.html create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/public/vite.svg create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/src/App.css create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/src/App.tsx create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/src/assets/react.svg create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/src/index.css create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/src/main.tsx create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/src/vite-env.d.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/tsconfig.app.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/basic/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/src/dynamic.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/dynamic/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-worker-environment-options/index.js create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-worker-environment-options/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-worker-environment-options/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-worker-environment-options/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/invalid-wrangler-version/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/no-wrangler-config-for-auxiliary-worker/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/no-wrangler-config-for-auxiliary-worker/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/no-wrangler-config-for-auxiliary-worker/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/no-wrangler-config-for-auxiliary-worker/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/dependency-import/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/dependency-import/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/dependency-import/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/dependency-import/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/direct-import/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/direct-import/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/direct-import/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/nodejs-compat-warnings/direct-import/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-config-account-id/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-incorrect-r2-config/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/auxiliary-worker/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/auxiliary-worker/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/entry-worker/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/entry-worker/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/remote-worker-alt/index.js create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/remote-worker-alt/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/remote-worker/index.js create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/remote-worker/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/unresolved-main/package.json create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/unresolved-main/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/fixtures/unresolved-main/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/e2e/generated-wrangler-config-path.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/global-setup.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/helpers.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/invalid-worker-environment-options.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/invalid-wrangler-version.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/nodejs-compat-warnings.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/remote-bindings.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/e2e/unresolved-main.test.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/vitest.config.ts create mode 100644 packages/vite-plugin-cloudflare/e2e/wrangler-configs-validation.test.ts create mode 100644 packages/vite-plugin-cloudflare/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/__test-utils__/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/__test-utils__/responses.ts create mode 100644 packages/vite-plugin-cloudflare/playground/__test-utils__/vite-version.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/README.md create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/__tests__/additional-modules.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/__tests__/cf-build-output/additional-modules.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/__tests__/new-module-registry/additional-modules.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/cloudflare.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/modules/bin-example.bin create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/modules/html-example.html create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/modules/sql-example.sql create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/modules/text-example.txt create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/modules/text__example__2.txt create mode 100755 packages/vite-plugin-cloudflare/playground/additional-modules/src/modules/wasm-example.wasm create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/src/types.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/vite.config.cf-build-output.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/vite.config.new-module-registry.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/additional-modules/wrangler.new-module-registry.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/assets/__tests__/assets.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/__tests__/cf-build-output/assets.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/__tests__/no-client-entry/assets.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/__tests__/public-dir-only/assets.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/cloudflare.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/html-page.html create mode 100644 packages/vite-plugin-cloudflare/playground/assets/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/assets/public/.assetsignore create mode 100644 packages/vite-plugin-cloudflare/playground/assets/public/public-image.svg create mode 100644 packages/vite-plugin-cloudflare/playground/assets/src/imported-image.svg create mode 100644 packages/vite-plugin-cloudflare/playground/assets/src/imported-text.txt create mode 100644 packages/vite-plugin-cloudflare/playground/assets/src/index.css create mode 100644 packages/vite-plugin-cloudflare/playground/assets/src/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/assets/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/src/inline-image.svg create mode 100644 packages/vite-plugin-cloudflare/playground/assets/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/assets/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/assets/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/assets/vite.config.cf-build-output.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/vite.config.no-client-entry.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/vite.config.public-dir-only.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/assets/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/assets/wrangler.public-dir-only.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/src/image.png create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/worker-configuration.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/bindings/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/__tests__/block-concurrency-while.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/block-concurrency-while/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/__tests__/child-environment.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/src/child-environment-module.ts create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/child-environment/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/__tests__/cloudflare-env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/__tests__/custom-mode/cloudflare-env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/vite.config.custom-mode.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cloudflare-env/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/__tests__/config-changes.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/vite.config.with-cloudflare-env.ts create mode 100644 packages/vite-plugin-cloudflare/playground/config-changes/wrangler.json create mode 100644 packages/vite-plugin-cloudflare/playground/containers/Dockerfile create mode 100644 packages/vite-plugin-cloudflare/playground/containers/__tests__/containers.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/containers/container/simple-node-app.js create mode 100644 packages/vite-plugin-cloudflare/playground/containers/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/containers/src/entry.ts create mode 100644 packages/vite-plugin-cloudflare/playground/containers/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/containers/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/containers/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/containers/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/containers/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/containers/wrangler.docker.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/containers/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/containers/wrangler.registry.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cf-worker-header.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/cron-triggers/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/__tests__/ctx-exports.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/worker-configuration.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/ctx-exports/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/__tests__/custom-build-app.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/custom-build-app/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/__tests__/assets-importing-from-dependencies.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/deps-assets-importing/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/.dev.vars.staging create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/dev-vars-loading.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/vars-changes.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/dev-vars-loading.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/vars-changes.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/vite.config.with-specified-env.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/.env create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/.env.staging create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/.env.with-specified-env create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/__tests__/dot-env-loading.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/__tests__/vars-changes.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/dot-env-loading.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/vars-changes.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/vite.config.with-specified-env.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/__tests__/durable-objects.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/durable-objects/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/__tests__/dynamic-import-paths.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/src/another.js create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/dynamic-import-paths/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/email-worker/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/errors/__tests__/errors.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/errors/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/errors/src/worker-a/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/errors/src/worker-a/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/errors/src/worker-b/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/errors/src/worker-b/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/errors/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/errors/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/errors/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/errors/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/__tests__/build-output.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/cloudflare.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/public/static.txt create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-build-output/worker-configuration.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/__tests__/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/cloudflare.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/experimental-config/worker-configuration.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/__tests__/external-durable-object.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/worker-a/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/worker-a/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/worker-b/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-durable-objects/worker-b/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-workers/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/__tests__/workflows.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/worker-a/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/worker-a/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/worker-b/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/external-workflows/worker-b/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/__tests__/hot-channel.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/hot-channel/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/__tests__/importable-env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/importable-env/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/__tests__/main-resolution.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/__tests__/nested-config/main-resolution.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/__tests__/package-export-main/main-resolution.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/__tests__/virtual-module-main/main-resolution.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/nested-config/another/another.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/nested-config/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/nested-config/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/package/entry.js create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/package/index.js create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/package/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/src/another/another.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/vite.config.nested-config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/vite.config.package-export-main.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/vite.config.virtual-module-main.ts create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/wrangler.package-export-main.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/main-resolution/wrangler.virtual-module-main.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/__tests__/middleware-mode.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/__tests__/serve.ts create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/server.js create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/middleware-mode/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/__tests__/module-resolution.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/__tests__/nodejs-compat/module-resolution.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes/index.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes/plugin.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/imports/cloudflare-builtins.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/imports/cloudflare-builtins.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/imports/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/ext.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/ext.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/hello.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/index.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/index.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/json.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/json.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/no-ext.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/no-ext.js create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires/world.cjs create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/aliasing.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/cloudflare-imports.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/external-cloudflare-imports.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/require-ext.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/require-json.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/require-no-ext.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/third-party/discord-api-types.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/third-party/react.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/third-party/remix.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/src/third-party/slash-create.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/vite.config.nodejs-compat.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/module-resolution/wrangler.nodejs-compat.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/custom-build-app/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/custom-output-directories/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/customize-config/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/dev-only-auxiliary/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/no-config-file/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/with-worker-configs-warning/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.custom-build-app.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.custom-output-directories.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.customize-config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.dev-only-auxiliary.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.no-config-file.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.with-worker-configs-warning.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-a/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-a/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-a/wrangler.with-warning.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-b/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-b/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-b/wrangler.with-warning.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-c/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/worker-d/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/cloudflare-node/cloudflare-node.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-als/als.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-basic/basic.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-cross-env/cross-env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-crypto/crypto.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-debug/debug.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-https/https.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-postgres/postgres.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-postgres/serve.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-process-populated-env/process.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-process/process.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-random/random.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-resolve-externals/resolve-externals.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/cloudflare-node/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/cloudflare-node/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.cloudflare-node.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-als.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-basic.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-cross-env.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-crypto.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-debug.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-https.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-postgres.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-process-populated-env.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-process.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-random.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-resolve-externals.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-als/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-als/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-basic/global.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-basic/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-basic/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-cross-env/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-cross-env/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-crypto/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-crypto/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-debug/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-debug/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-https/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-https/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-postgres/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-postgres/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process-populated-env/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process-populated-env/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process-populated-env/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process-populated-env/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process/early-process-access.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-process/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-random/global.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-random/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-random/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-resolve-externals/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-compat/worker-resolve-externals/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/__tests__/node-env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/__tests__/nodejs-compat/node-env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/vite.config.nodejs-compat.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/node-env/wrangler.nodejs-compat.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/__tests__/partyserver.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/src/App.tsx create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/src/main.tsx create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/src/tailwind.css create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/tsconfig.client.json create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/worker/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/partyserver/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/__tests__/assets-only/prerendering.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/__tests__/prerendering.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/src/prerender.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/vite.config.assets-only.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/__tests__/prisma.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/__tests__/serve.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/migrations/0001_create_user_table.sql create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/prisma/schema.prisma create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prisma/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/__tests__/experimental-headers-and-redirects/react-spa.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/__tests__/react-spa.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/__tests__/server-headers/react-spa.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/_headers create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/_redirects create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/file.txt create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/redirect.svg create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/rewrite.svg create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/target.svg create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/public/vite.svg create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/src/App.css create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/src/App.tsx create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/src/assets/react.svg create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/src/index.css create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/src/main.tsx create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/tsconfig.client.json create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/vite.config.experimental-headers-and-redirects.ts create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/vite.config.server-headers.ts create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/react-spa/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/__tests__/same-worker-service-bindings.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/same-worker-service-bindings/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.dev.vars.staging create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.env create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.env.staging create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.npmrc create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.yarnrc create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/.yarnrc.yml create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/__tests__/sensitive-files.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/client.jks create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/client.key create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/client.p12 create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/client.p8 create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/client.pfx create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/custom-sensitive-file create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-a/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-b/.dev.vars create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-b/.dev.vars.staging create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-b/.env create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-b/.env.staging create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-b/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/worker-b/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/sensitive-files/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/.gitignore create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/child-compiler/spa-with-api.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/custom-output-directories/spa-with-api.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/https/spa-with-api.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/run-worker-first/spa-with-api.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/spa-with-api.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/__tests__/static-routing/spa-with-api.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/api/asset.txt create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/api/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/api/some-file.txt create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/public/vite.svg create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/src/App.css create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/src/App.tsx create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/src/assets/react.svg create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/src/index.css create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/src/main.tsx create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/tsconfig.client.json create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/vite.config.child-compiler.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/vite.config.custom-output-directories.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/vite.config.https.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/vite.config.run-worker-first.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/vite.config.static-routing.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/wrangler.run-worker-first.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/spa-with-api/wrangler.static-routing.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/404.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/__tests__/cf-build-output/static-mpa.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/__tests__/static-mpa.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/__tests__/with-worker-configs-warning/static-mpa.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/about/404.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/about/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/cloudflare.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/contact.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/public-html.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/public/_headers create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/public/_redirects create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/public/public-html.html create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/public/text-file.txt create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/public/vite.svg create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/vite.config.cf-build-output.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/vite.config.with-worker-configs-warning.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/static-mpa/wrangler.with-warning.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/static/__tests__/static.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/__tests__/with-api/static.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/api/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/static/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/static/public/vite.svg create mode 100644 packages/vite-plugin-cloudflare/playground/static/src/counter.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/src/main.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/src/style.css create mode 100644 packages/vite-plugin-cloudflare/playground/static/src/typescript.svg create mode 100644 packages/vite-plugin-cloudflare/playground/static/tsconfig.client.json create mode 100644 packages/vite-plugin-cloudflare/playground/static/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/static/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/static/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/static/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/vite.config.with-api.ts create mode 100644 packages/vite-plugin-cloudflare/playground/static/wrangler.with-api.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/serve.ts create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/worker-configuration.d.ts create mode 100644 packages/vite-plugin-cloudflare/playground/stream-binding/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/__tests__/https/streaming.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/__tests__/streaming.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/vite.config.https.ts create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/streaming/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/turbo.json create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/__tests__/basic-functionality.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/virtual-modules/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/vitest-global-setup.ts create mode 100644 packages/vite-plugin-cloudflare/playground/vitest-setup.ts create mode 100644 packages/vite-plugin-cloudflare/playground/vitest.config.e2e.ts create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/__tests__/websockets.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/src/index.html create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/websockets/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/base-config/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/explicit-upload-source-maps/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/infer-upload-source-maps/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/log-level-error/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/log-level-silent/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/log-level-warn/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/__tests__/worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/src/a.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/src/b.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.base-config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.explicit-upload-source-maps.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.infer-upload-source-maps.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.log-level-error.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.log-level-silent.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.log-level-warn.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/wrangler.explicit-upload-source-maps.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/worker-♫/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/__tests__/workflows.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/package.json create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/tsconfig.node.json create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/vite.config.ts create mode 100644 packages/vite-plugin-cloudflare/playground/workflows/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/scripts/deps.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/build-output.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/customize-worker-config.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/dev-vars.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/experimental-new-config.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/empty-dir.jsonc/.gitkeep create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/empty-dir/.gitkeep create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/index.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/non-existing-main-wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/simple-wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/wrangler-with-fields-to-ignore.jsonc create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/fixtures/wrangler.jsonc create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/get-modules-from-manifest.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/get-validated-wrangler-config-path.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/get-warning-for-workers-resolved-configs.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/get-worker-config.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/hmr-events.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/output-config.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/preview-server.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/tunnel.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/utils.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/validate-worker-environment-options.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/websockets.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/assert-wrangler-version.ts create mode 100644 packages/vite-plugin-cloudflare/src/asset-config.ts create mode 100644 packages/vite-plugin-cloudflare/src/build-constants.ts create mode 100644 packages/vite-plugin-cloudflare/src/build-output-env.ts create mode 100644 packages/vite-plugin-cloudflare/src/build-output-preview.ts create mode 100644 packages/vite-plugin-cloudflare/src/build.ts create mode 100644 packages/vite-plugin-cloudflare/src/cf-vite.ts create mode 100644 packages/vite-plugin-cloudflare/src/cloudflare-environment.ts create mode 100644 packages/vite-plugin-cloudflare/src/constants.ts create mode 100644 packages/vite-plugin-cloudflare/src/containers.ts create mode 100644 packages/vite-plugin-cloudflare/src/context.ts create mode 100644 packages/vite-plugin-cloudflare/src/debug.ts create mode 100644 packages/vite-plugin-cloudflare/src/deploy-config.ts create mode 100644 packages/vite-plugin-cloudflare/src/dev-vars.ts create mode 100644 packages/vite-plugin-cloudflare/src/experimental-config.ts create mode 100644 packages/vite-plugin-cloudflare/src/export-types.ts create mode 100644 packages/vite-plugin-cloudflare/src/index.ts create mode 100644 packages/vite-plugin-cloudflare/src/miniflare-options.ts create mode 100644 packages/vite-plugin-cloudflare/src/nodejs-compat.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugin-config.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/additional-modules.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/build-output.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/config.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/debug.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/dev.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/nodejs-compat.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/output-config.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/preview.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/rsc.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/tunnel.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/virtual-modules.ts create mode 100644 packages/vite-plugin-cloudflare/src/plugins/wasm.ts create mode 100644 packages/vite-plugin-cloudflare/src/shared.ts create mode 100644 packages/vite-plugin-cloudflare/src/update-check.ts create mode 100644 packages/vite-plugin-cloudflare/src/utils.ts create mode 100644 packages/vite-plugin-cloudflare/src/vite-config.ts create mode 100644 packages/vite-plugin-cloudflare/src/websockets.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers-configs.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/asset-worker/index.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/router-worker/index.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/runner-worker/__tests__/env.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/runner-worker/env.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/runner-worker/errors.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/runner-worker/index.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/runner-worker/keys.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/runner-worker/module-runner.ts create mode 100644 packages/vite-plugin-cloudflare/src/workers/vite-proxy-worker/index.ts create mode 100644 packages/vite-plugin-cloudflare/tsconfig.json create mode 100644 packages/vite-plugin-cloudflare/tsconfig.plugin.json create mode 100644 packages/vite-plugin-cloudflare/tsconfig.worker.json create mode 100644 packages/vite-plugin-cloudflare/tsdown.config.ts create mode 100644 packages/vite-plugin-cloudflare/turbo.json create mode 100644 packages/vite-plugin-cloudflare/vitest.config.ts create mode 100644 packages/vitest-pool-workers/AGENTS.md create mode 100644 packages/vitest-pool-workers/CHANGELOG.md create mode 100644 packages/vitest-pool-workers/CONTRIBUTING.md create mode 100644 packages/vitest-pool-workers/README.md create mode 100644 packages/vitest-pool-workers/package.json create mode 100644 packages/vitest-pool-workers/scripts/deps.ts create mode 100644 packages/vitest-pool-workers/scripts/rtti/query.mjs create mode 100644 packages/vitest-pool-workers/scripts/rtti/rtti.capnp create mode 100644 packages/vitest-pool-workers/scripts/rtti/rtti.js create mode 100644 packages/vitest-pool-workers/src/codemods/vitest-v3-to-v4.ts create mode 100644 packages/vitest-pool-workers/src/pool/cloudflare-pool-worker.ts create mode 100644 packages/vitest-pool-workers/src/pool/compatibility-flag-assertions.ts create mode 100644 packages/vitest-pool-workers/src/pool/config.ts create mode 100644 packages/vitest-pool-workers/src/pool/d1.ts create mode 100644 packages/vitest-pool-workers/src/pool/guess-exports.ts create mode 100644 packages/vitest-pool-workers/src/pool/helpers.ts create mode 100644 packages/vitest-pool-workers/src/pool/index.ts create mode 100644 packages/vitest-pool-workers/src/pool/loopback.ts create mode 100644 packages/vitest-pool-workers/src/pool/module-fallback.ts create mode 100644 packages/vitest-pool-workers/src/pool/pages.ts create mode 100644 packages/vitest-pool-workers/src/pool/plugin.ts create mode 100644 packages/vitest-pool-workers/src/pool/pool.ts create mode 100644 packages/vitest-pool-workers/src/shared/builtin-modules.ts create mode 100644 packages/vitest-pool-workers/src/shared/d1.ts create mode 100644 packages/vitest-pool-workers/src/shared/types-global.d.ts create mode 100644 packages/vitest-pool-workers/src/worker/d1.ts create mode 100644 packages/vitest-pool-workers/src/worker/durable-objects.ts create mode 100644 packages/vitest-pool-workers/src/worker/entrypoints.ts create mode 100644 packages/vitest-pool-workers/src/worker/env.ts create mode 100644 packages/vitest-pool-workers/src/worker/events.ts create mode 100644 packages/vitest-pool-workers/src/worker/fetch-mock.ts create mode 100644 packages/vitest-pool-workers/src/worker/index.ts create mode 100644 packages/vitest-pool-workers/src/worker/lib/README.md create mode 100644 packages/vitest-pool-workers/src/worker/lib/cloudflare/empty-internal.cts create mode 100644 packages/vitest-pool-workers/src/worker/lib/cloudflare/snapshot.ts create mode 100644 packages/vitest-pool-workers/src/worker/lib/cloudflare/test-internal.ts create mode 100644 packages/vitest-pool-workers/src/worker/lib/cloudflare/test.ts create mode 100644 packages/vitest-pool-workers/src/worker/node/console.ts create mode 100644 packages/vitest-pool-workers/src/worker/node/vm.ts create mode 100644 packages/vitest-pool-workers/src/worker/patch-ctx.ts create mode 100644 packages/vitest-pool-workers/src/worker/reset.ts create mode 100644 packages/vitest-pool-workers/src/worker/secrets-store.ts create mode 100644 packages/vitest-pool-workers/src/worker/tsconfig.json create mode 100644 packages/vitest-pool-workers/src/worker/types-ambient.d.ts create mode 100644 packages/vitest-pool-workers/src/worker/types-globals.d.ts create mode 100644 packages/vitest-pool-workers/src/worker/wait-until.ts create mode 100644 packages/vitest-pool-workers/src/worker/workflows.ts create mode 100644 packages/vitest-pool-workers/test/README.md create mode 100644 packages/vitest-pool-workers/test/bindings.test.ts create mode 100644 packages/vitest-pool-workers/test/chunking.test.ts create mode 100644 packages/vitest-pool-workers/test/codemods.test.ts create mode 100644 packages/vitest-pool-workers/test/compatibility-flag-assertions.test.ts create mode 100644 packages/vitest-pool-workers/test/console.test.ts create mode 100644 packages/vitest-pool-workers/test/coverage.test.ts create mode 100644 packages/vitest-pool-workers/test/do-exports.test.ts create mode 100644 packages/vitest-pool-workers/test/env.d.ts create mode 100644 packages/vitest-pool-workers/test/file-url-import.test.ts create mode 100644 packages/vitest-pool-workers/test/filtering.test.ts create mode 100644 packages/vitest-pool-workers/test/global-setup.ts create mode 100644 packages/vitest-pool-workers/test/helpers.ts create mode 100644 packages/vitest-pool-workers/test/inspector.test.ts create mode 100644 packages/vitest-pool-workers/test/isolation.test.ts create mode 100644 packages/vitest-pool-workers/test/remote-proxy-cleanup.test.ts create mode 100644 packages/vitest-pool-workers/test/snapshots.test.ts create mode 100644 packages/vitest-pool-workers/test/structured-logs.test.ts create mode 100644 packages/vitest-pool-workers/test/unhandled-rejection.test.ts create mode 100644 packages/vitest-pool-workers/test/validation.test.ts create mode 100644 packages/vitest-pool-workers/test/wait-until-timeout.test.ts create mode 100644 packages/vitest-pool-workers/test/watch.test.ts create mode 100644 packages/vitest-pool-workers/tsconfig.json create mode 100644 packages/vitest-pool-workers/tsdown.config.ts create mode 100644 packages/vitest-pool-workers/turbo.json create mode 100644 packages/vitest-pool-workers/types/cloudflare-test.d.ts create mode 100644 packages/vitest-pool-workers/types/tsconfig.json create mode 100644 packages/vitest-pool-workers/vitest-pool-workers.svg create mode 100644 packages/vitest-pool-workers/vitest.config.mts create mode 100644 packages/workers-auth/AGENTS.md create mode 100644 packages/workers-auth/CHANGELOG.md create mode 100644 packages/workers-auth/LICENSE-APACHE create mode 100644 packages/workers-auth/LICENSE-MIT create mode 100644 packages/workers-auth/README.md create mode 100644 packages/workers-auth/package.json create mode 100644 packages/workers-auth/scripts/deps.ts create mode 100644 packages/workers-auth/src/access.ts create mode 100644 packages/workers-auth/src/callback-server.ts create mode 100644 packages/workers-auth/src/config-file/auth.ts create mode 100644 packages/workers-auth/src/config-file/index.ts create mode 100644 packages/workers-auth/src/config-file/temporary.ts create mode 100644 packages/workers-auth/src/context.ts create mode 100644 packages/workers-auth/src/credential-store/crypto.ts create mode 100644 packages/workers-auth/src/credential-store/encrypted-file-store.ts create mode 100644 packages/workers-auth/src/credential-store/file-store.ts create mode 100644 packages/workers-auth/src/credential-store/index.ts create mode 100644 packages/workers-auth/src/credential-store/interface.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/factory.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/interface.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/lazy-installer.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/linux-secret-tool.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/mac-security.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/napi-keyring.ts create mode 100644 packages/workers-auth/src/credential-store/key-providers/shared.ts create mode 100644 packages/workers-auth/src/credential-store/resolver.ts create mode 100644 packages/workers-auth/src/credential-store/state.ts create mode 100644 packages/workers-auth/src/credentials.ts create mode 100644 packages/workers-auth/src/env-vars.ts create mode 100644 packages/workers-auth/src/errors.ts create mode 100644 packages/workers-auth/src/flow.ts create mode 100644 packages/workers-auth/src/generate-auth-url.ts create mode 100644 packages/workers-auth/src/generate-random-state.ts create mode 100644 packages/workers-auth/src/index.ts create mode 100644 packages/workers-auth/src/pkce.ts create mode 100644 packages/workers-auth/src/pow.ts create mode 100644 packages/workers-auth/src/profiles.ts create mode 100644 packages/workers-auth/src/state.ts create mode 100644 packages/workers-auth/src/temporary.ts create mode 100644 packages/workers-auth/src/test-helpers/index.ts create mode 100644 packages/workers-auth/src/test-helpers/msw-handlers/access.ts create mode 100644 packages/workers-auth/src/test-helpers/msw-handlers/oauth.ts create mode 100644 packages/workers-auth/src/token-exchange.ts create mode 100644 packages/workers-auth/tests/access.test.ts create mode 100644 packages/workers-auth/tests/credential-store/crypto.test.ts create mode 100644 packages/workers-auth/tests/credential-store/encrypted-file-store.test.ts create mode 100644 packages/workers-auth/tests/credential-store/file-store.test.ts create mode 100644 packages/workers-auth/tests/credential-store/key-providers/integration.test.ts create mode 100644 packages/workers-auth/tests/credential-store/key-providers/lazy-installer.test.ts create mode 100644 packages/workers-auth/tests/credential-store/key-providers/linux-secret-tool.test.ts create mode 100644 packages/workers-auth/tests/credential-store/key-providers/mac-security.test.ts create mode 100644 packages/workers-auth/tests/credential-store/resolver.test.ts create mode 100644 packages/workers-auth/tests/credentials.test.ts create mode 100644 packages/workers-auth/tests/generate-auth-url.test.ts create mode 100644 packages/workers-auth/tests/pow.test.ts create mode 100644 packages/workers-auth/tests/state.test.ts create mode 100644 packages/workers-auth/tests/tsconfig.json create mode 100644 packages/workers-auth/tests/vitest.global.ts create mode 100644 packages/workers-auth/tests/vitest.setup.ts create mode 100644 packages/workers-auth/tsconfig.json create mode 100644 packages/workers-auth/tsup.config.ts create mode 100644 packages/workers-auth/turbo.json create mode 100644 packages/workers-auth/vitest.config.mts create mode 100644 packages/workers-editor-shared/.gitignore create mode 100644 packages/workers-editor-shared/CHANGELOG.md create mode 100644 packages/workers-editor-shared/lib/Frame.tsx create mode 100644 packages/workers-editor-shared/lib/SplitPane.tsx create mode 100644 packages/workers-editor-shared/lib/constants.ts create mode 100644 packages/workers-editor-shared/lib/index.ts create mode 100644 packages/workers-editor-shared/lib/ipc.ts create mode 100644 packages/workers-editor-shared/lib/types.ts create mode 100644 packages/workers-editor-shared/lib/useInjectSources.tsx create mode 100644 packages/workers-editor-shared/lib/useRefreshableIframe.tsx create mode 100644 packages/workers-editor-shared/package.json create mode 100644 packages/workers-editor-shared/scripts/deps.ts create mode 100644 packages/workers-editor-shared/tsconfig.json create mode 100644 packages/workers-editor-shared/tsconfig.node.json create mode 100644 packages/workers-editor-shared/turbo.json create mode 100644 packages/workers-editor-shared/vite.config.ts create mode 100644 packages/workers-playground/.env create mode 100644 packages/workers-playground/.gitignore create mode 100644 packages/workers-playground/CHANGELOG.md create mode 100644 packages/workers-playground/README.md create mode 100644 packages/workers-playground/generate-default-hashes.ts create mode 100644 packages/workers-playground/index.html create mode 100644 packages/workers-playground/package.json create mode 100644 packages/workers-playground/public/favicon.ico create mode 100644 packages/workers-playground/public/flares.png create mode 100644 packages/workers-playground/public/welcome-style.css create mode 100644 packages/workers-playground/public/workers-logo-dark.svg create mode 100644 packages/workers-playground/public/workers-logo.svg create mode 100644 packages/workers-playground/src/App.tsx create mode 100644 packages/workers-playground/src/QuickEditor/DevtoolsIframe.tsx create mode 100644 packages/workers-playground/src/QuickEditor/EditorPane.tsx create mode 100644 packages/workers-playground/src/QuickEditor/ExpandableLogMessage.tsx create mode 100644 packages/workers-playground/src/QuickEditor/FrameErrorBoundary.tsx create mode 100644 packages/workers-playground/src/QuickEditor/HTTPTab/HTTPTab.tsx create mode 100644 packages/workers-playground/src/QuickEditor/HTTPTab/RequestHeaders.tsx create mode 100644 packages/workers-playground/src/QuickEditor/HTTPTab/ResponseView.tsx create mode 100644 packages/workers-playground/src/QuickEditor/HTTPTab/fetchWorker.ts create mode 100644 packages/workers-playground/src/QuickEditor/InputField.tsx create mode 100644 packages/workers-playground/src/QuickEditor/InvocationIcon.tsx create mode 100644 packages/workers-playground/src/QuickEditor/MigrateToWrangler/DocsLink.tsx create mode 100644 packages/workers-playground/src/QuickEditor/MigrateToWrangler/MigrateToWrangler.tsx create mode 100644 packages/workers-playground/src/QuickEditor/MigrateToWrangler/StepNumber.tsx create mode 100644 packages/workers-playground/src/QuickEditor/PreviewTab/PreviewTab.tsx create mode 100644 packages/workers-playground/src/QuickEditor/PreviewTab/UrlBar.tsx create mode 100644 packages/workers-playground/src/QuickEditor/QuickEditor.tsx create mode 100644 packages/workers-playground/src/QuickEditor/TabBar.tsx create mode 100644 packages/workers-playground/src/QuickEditor/ToolsPane.tsx create mode 100644 packages/workers-playground/src/QuickEditor/TopBar.tsx create mode 100644 packages/workers-playground/src/QuickEditor/VSCodeEditor.tsx create mode 100644 packages/workers-playground/src/QuickEditor/WorkersLogo.tsx create mode 100644 packages/workers-playground/src/QuickEditor/defaultHashes.js create mode 100644 packages/workers-playground/src/QuickEditor/felaRenderer.ts create mode 100644 packages/workers-playground/src/QuickEditor/getPlaygroundWorker.ts create mode 100644 packages/workers-playground/src/QuickEditor/module-collection.ts create mode 100644 packages/workers-playground/src/QuickEditor/useDraftWorker.ts create mode 100644 packages/workers-playground/src/global.ts create mode 100644 packages/workers-playground/src/index.css create mode 100644 packages/workers-playground/src/main.tsx create mode 100644 packages/workers-playground/src/vite-env.d.ts create mode 100644 packages/workers-playground/src/worker.ts create mode 100644 packages/workers-playground/tsconfig.json create mode 100644 packages/workers-playground/tsconfig.node.json create mode 100644 packages/workers-playground/turbo.json create mode 100644 packages/workers-playground/vite.config.ts create mode 100644 packages/workers-playground/welcome/data.js create mode 100644 packages/workers-playground/welcome/index.js create mode 100644 packages/workers-playground/welcome/index.py create mode 100644 packages/workers-playground/welcome/welcome.html create mode 100644 packages/workers-playground/wrangler.dev.jsonc create mode 100644 packages/workers-playground/wrangler.jsonc create mode 100644 packages/workers-shared/CHANGELOG.md create mode 100644 packages/workers-shared/README.md create mode 100644 packages/workers-shared/asset-worker/README.md create mode 100644 packages/workers-shared/asset-worker/fixtures/AssetManifest.bin create mode 100644 packages/workers-shared/asset-worker/index.ts create mode 100644 packages/workers-shared/asset-worker/src/analytics.ts create mode 100644 packages/workers-shared/asset-worker/src/assets-manifest.ts create mode 100644 packages/workers-shared/asset-worker/src/compatibility-flags.ts create mode 100644 packages/workers-shared/asset-worker/src/configuration.ts create mode 100644 packages/workers-shared/asset-worker/src/constants.ts create mode 100644 packages/workers-shared/asset-worker/src/experiment-analytics.ts create mode 100644 packages/workers-shared/asset-worker/src/handler.ts create mode 100644 packages/workers-shared/asset-worker/src/types.ts create mode 100644 packages/workers-shared/asset-worker/src/utils/final-operations.ts create mode 100644 packages/workers-shared/asset-worker/src/utils/headers.ts create mode 100644 packages/workers-shared/asset-worker/src/utils/kv.ts create mode 100644 packages/workers-shared/asset-worker/src/utils/rules-engine.ts create mode 100644 packages/workers-shared/asset-worker/src/worker.ts create mode 100644 packages/workers-shared/asset-worker/tests/analytics.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/assets-manifest.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/cohort.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/compatibility-flags.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/handler.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/kv.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/rules-engine.test.ts create mode 100644 packages/workers-shared/asset-worker/tests/tsconfig.json create mode 100644 packages/workers-shared/asset-worker/tests/types.d.ts create mode 100644 packages/workers-shared/asset-worker/tsconfig.json create mode 100644 packages/workers-shared/asset-worker/vitest.config.mts create mode 100644 packages/workers-shared/asset-worker/worker-configuration.d.ts create mode 100644 packages/workers-shared/asset-worker/wrangler.jsonc create mode 100644 packages/workers-shared/index.ts create mode 100644 packages/workers-shared/package.json create mode 100644 packages/workers-shared/router-worker/README.md create mode 100644 packages/workers-shared/router-worker/index.ts create mode 100644 packages/workers-shared/router-worker/src/analytics.ts create mode 100644 packages/workers-shared/router-worker/src/configuration.ts create mode 100644 packages/workers-shared/router-worker/src/limited-response.ts create mode 100644 packages/workers-shared/router-worker/src/types.ts create mode 100644 packages/workers-shared/router-worker/src/worker.ts create mode 100644 packages/workers-shared/router-worker/tests/index.test.ts create mode 100644 packages/workers-shared/router-worker/tests/tsconfig.json create mode 100644 packages/workers-shared/router-worker/tsconfig.json create mode 100644 packages/workers-shared/router-worker/vitest.config.mts create mode 100644 packages/workers-shared/router-worker/worker-configuration.d.ts create mode 100644 packages/workers-shared/router-worker/wrangler.jsonc create mode 100644 packages/workers-shared/scripts/copy-config-file.ts create mode 100755 packages/workers-shared/scripts/get-version-tag.ts create mode 100644 packages/workers-shared/scripts/upload-sourcemaps.ts create mode 100644 packages/workers-shared/tsconfig.json create mode 100644 packages/workers-shared/turbo.json create mode 100644 packages/workers-shared/utils/configuration/constants.ts create mode 100644 packages/workers-shared/utils/configuration/constructConfiguration.ts create mode 100644 packages/workers-shared/utils/configuration/parseHeaders.ts create mode 100644 packages/workers-shared/utils/configuration/parseRedirects.ts create mode 100644 packages/workers-shared/utils/configuration/parseStaticRouting.ts create mode 100644 packages/workers-shared/utils/configuration/types.ts create mode 100644 packages/workers-shared/utils/configuration/validateURL.ts create mode 100644 packages/workers-shared/utils/constants.ts create mode 100644 packages/workers-shared/utils/helpers.ts create mode 100644 packages/workers-shared/utils/performance.ts create mode 100644 packages/workers-shared/utils/responses.ts create mode 100644 packages/workers-shared/utils/sentry.ts create mode 100644 packages/workers-shared/utils/tests/helpers.test.ts create mode 100644 packages/workers-shared/utils/tests/parseHeaders.invalid.test.ts create mode 100644 packages/workers-shared/utils/tests/parseHeaders.valid.test.ts create mode 100644 packages/workers-shared/utils/tests/parseRedirects.invalid.test.ts create mode 100644 packages/workers-shared/utils/tests/parseRedirects.valid.test.ts create mode 100644 packages/workers-shared/utils/tests/parseStaticRouting.test.ts create mode 100644 packages/workers-shared/utils/tracing.ts create mode 100644 packages/workers-shared/utils/tsconfig.json create mode 100644 packages/workers-shared/utils/types.ts create mode 100644 packages/workers-tsconfig/base.json create mode 100644 packages/workers-tsconfig/client.json create mode 100644 packages/workers-tsconfig/package.json create mode 100644 packages/workers-tsconfig/react.json create mode 100644 packages/workers-tsconfig/tsconfig.json create mode 100644 packages/workers-tsconfig/worker-node-compat.json create mode 100644 packages/workers-tsconfig/worker.json create mode 100644 packages/workers-utils/AGENTS.md create mode 100644 packages/workers-utils/CHANGELOG.md create mode 100644 packages/workers-utils/README.md create mode 100644 packages/workers-utils/package.json create mode 100644 packages/workers-utils/scripts/deps.ts create mode 100644 packages/workers-utils/src/assert-never.ts create mode 100644 packages/workers-utils/src/browser.ts create mode 100644 packages/workers-utils/src/cfetch/errors.ts create mode 100644 packages/workers-utils/src/cfetch/index.ts create mode 100644 packages/workers-utils/src/cloudflared.ts create mode 100644 packages/workers-utils/src/compatibility-date.ts create mode 100644 packages/workers-utils/src/config/README.md create mode 100644 packages/workers-utils/src/config/binding-local-support.ts create mode 100644 packages/workers-utils/src/config/config-helpers.ts create mode 100644 packages/workers-utils/src/config/config.ts create mode 100644 packages/workers-utils/src/config/diagnostics.ts create mode 100644 packages/workers-utils/src/config/durable-object-exports.ts create mode 100644 packages/workers-utils/src/config/environment.ts create mode 100644 packages/workers-utils/src/config/exports.ts create mode 100644 packages/workers-utils/src/config/index.ts create mode 100644 packages/workers-utils/src/config/patch-config.ts create mode 100644 packages/workers-utils/src/config/validation-helpers.ts create mode 100644 packages/workers-utils/src/config/validation-pages.ts create mode 100644 packages/workers-utils/src/config/validation.ts create mode 100644 packages/workers-utils/src/constants.ts create mode 100644 packages/workers-utils/src/construct-wrangler-config.ts create mode 100644 packages/workers-utils/src/environment-variables/factory.ts create mode 100644 packages/workers-utils/src/environment-variables/misc-variables.ts create mode 100644 packages/workers-utils/src/errors.ts create mode 100644 packages/workers-utils/src/format-time.ts create mode 100644 packages/workers-utils/src/fs-helpers.ts create mode 100644 packages/workers-utils/src/global-wrangler-config-path.ts create mode 100644 packages/workers-utils/src/index.ts create mode 100644 packages/workers-utils/src/logger.ts create mode 100644 packages/workers-utils/src/map-worker-metadata-bindings.ts create mode 100644 packages/workers-utils/src/package-manager.ts create mode 100644 packages/workers-utils/src/package-resolution.ts create mode 100644 packages/workers-utils/src/parse.ts create mode 100644 packages/workers-utils/src/prometheus-metrics.ts create mode 100644 packages/workers-utils/src/retry.ts create mode 100644 packages/workers-utils/src/route-utils.ts create mode 100644 packages/workers-utils/src/test-helpers/index.ts create mode 100644 packages/workers-utils/src/test-helpers/mock.ts create mode 100644 packages/workers-utils/src/test-helpers/normalize.ts create mode 100644 packages/workers-utils/src/test-helpers/run-in-tmp.ts create mode 100644 packages/workers-utils/src/test-helpers/seed.ts create mode 100644 packages/workers-utils/src/test-helpers/wrangler-config.ts create mode 100644 packages/workers-utils/src/tunnel.ts create mode 100644 packages/workers-utils/src/types.ts create mode 100644 packages/workers-utils/src/update-check.ts create mode 100644 packages/workers-utils/src/worker-name.ts create mode 100644 packages/workers-utils/src/worker.ts create mode 100644 packages/workers-utils/src/wrangler-tmp-dir.ts create mode 100644 packages/workers-utils/src/xdg-app-paths.ts create mode 100644 packages/workers-utils/tests/cfetch-utils.test.ts create mode 100644 packages/workers-utils/tests/cloudflared.test.ts create mode 100644 packages/workers-utils/tests/compatibility-date.test.ts create mode 100644 packages/workers-utils/tests/config/config-format.test.ts create mode 100644 packages/workers-utils/tests/config/configSchema.test.ts create mode 100644 packages/workers-utils/tests/config/exports.test.ts create mode 100644 packages/workers-utils/tests/config/findWranglerConfig.test.ts create mode 100644 packages/workers-utils/tests/config/patch-config.test.ts create mode 100644 packages/workers-utils/tests/config/validation/isDockerFile.test.ts create mode 100644 packages/workers-utils/tests/config/validation/normalize-and-validate-config.pages.test.ts create mode 100644 packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts create mode 100644 packages/workers-utils/tests/construct-wrangler-config.test.ts create mode 100644 packages/workers-utils/tests/map-worker-metadata-bindings.test.ts create mode 100644 packages/workers-utils/tests/parse.test.ts create mode 100644 packages/workers-utils/tests/prometheus-metrics.test.ts create mode 100644 packages/workers-utils/tests/tsconfig.json create mode 100644 packages/workers-utils/tests/tunnel.test.ts create mode 100644 packages/workers-utils/tests/vitest.global.ts create mode 100644 packages/workers-utils/tests/worker-name.test.ts create mode 100644 packages/workers-utils/tests/wrangler-tmp-dir.test.ts create mode 100644 packages/workers-utils/tests/xdg-app-paths.test.ts create mode 100644 packages/workers-utils/tsconfig.json create mode 100644 packages/workers-utils/tsup.config.ts create mode 100644 packages/workers-utils/turbo.json create mode 100644 packages/workers-utils/vitest.config.mts create mode 100644 packages/workflows-shared/CHANGELOG.md create mode 100644 packages/workflows-shared/README.md create mode 100644 packages/workflows-shared/package.json create mode 100644 packages/workflows-shared/src/binding.ts create mode 100644 packages/workflows-shared/src/context.ts create mode 100644 packages/workflows-shared/src/engine.ts create mode 100644 packages/workflows-shared/src/index.ts create mode 100644 packages/workflows-shared/src/instance.ts create mode 100644 packages/workflows-shared/src/introspection.ts create mode 100644 packages/workflows-shared/src/lib/cache.ts create mode 100644 packages/workflows-shared/src/lib/delay.ts create mode 100644 packages/workflows-shared/src/lib/errors.ts create mode 100644 packages/workflows-shared/src/lib/gracePeriodSemaphore.ts create mode 100644 packages/workflows-shared/src/lib/restart.ts create mode 100644 packages/workflows-shared/src/lib/retries.ts create mode 100644 packages/workflows-shared/src/lib/rollback.ts create mode 100644 packages/workflows-shared/src/lib/streams.ts create mode 100644 packages/workflows-shared/src/lib/timePriorityQueue.ts create mode 100644 packages/workflows-shared/src/lib/validators.ts create mode 100644 packages/workflows-shared/src/local-binding-worker.ts create mode 100644 packages/workflows-shared/src/modifier.ts create mode 100644 packages/workflows-shared/src/types.ts create mode 100644 packages/workflows-shared/tests/binding.test.ts create mode 100644 packages/workflows-shared/tests/context.test.ts create mode 100644 packages/workflows-shared/tests/engine.test.ts create mode 100644 packages/workflows-shared/tests/env.d.ts create mode 100644 packages/workflows-shared/tests/lib/delay.test.ts create mode 100644 packages/workflows-shared/tests/test-entry.ts create mode 100644 packages/workflows-shared/tests/tsconfig.json create mode 100644 packages/workflows-shared/tests/utils.ts create mode 100644 packages/workflows-shared/tests/validators.test.ts create mode 100644 packages/workflows-shared/tsconfig.json create mode 100644 packages/workflows-shared/turbo.json create mode 100644 packages/workflows-shared/vitest.config.mts create mode 100644 packages/workflows-shared/wrangler.jsonc create mode 100644 packages/wrangler/AGENTS.md create mode 100644 packages/wrangler/CHANGELOG.md create mode 100644 packages/wrangler/CONTRIBUTING.md create mode 100644 packages/wrangler/README.md create mode 100755 packages/wrangler/bin/cf-wrangler.js create mode 100755 packages/wrangler/bin/wrangler.js create mode 100644 packages/wrangler/e2e/README.md create mode 100644 packages/wrangler/e2e/__snapshots__/dev.test.ts.snap create mode 100644 packages/wrangler/e2e/agent-memory.test.ts create mode 100644 packages/wrangler/e2e/assets-multiworker.test.ts create mode 100644 packages/wrangler/e2e/auth-scopes.test.ts create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-cf-workers-app/package.json create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-cf-workers-app/src/index.ts create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-cf-workers-app/tsconfig.json create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-cf-workers-app/wrangler.jsonc create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-nodejs-app/package.json create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-nodejs-app/src/index.ts create mode 100644 packages/wrangler/e2e/autoconfig/fixtures/hono-nodejs-app/tsconfig.json create mode 100644 packages/wrangler/e2e/autoconfig/setup.test.ts create mode 100644 packages/wrangler/e2e/c3-integration.test.ts create mode 100644 packages/wrangler/e2e/cert.test.ts create mode 100644 packages/wrangler/e2e/containers.dev.test.ts create mode 100644 packages/wrangler/e2e/createTestHarness.test.ts create mode 100644 packages/wrangler/e2e/deploy.test.ts create mode 100644 packages/wrangler/e2e/deployments.test.ts create mode 100644 packages/wrangler/e2e/dev-env.test.ts create mode 100644 packages/wrangler/e2e/dev-registry.test.ts create mode 100644 packages/wrangler/e2e/dev.test.ts create mode 100644 packages/wrangler/e2e/durable-objects-exports.test.ts create mode 100644 packages/wrangler/e2e/get-platform-proxy.test.ts create mode 100644 packages/wrangler/e2e/helpers/account-id.ts create mode 100644 packages/wrangler/e2e/helpers/cert.ts create mode 100644 packages/wrangler/e2e/helpers/command.ts create mode 100644 packages/wrangler/e2e/helpers/e2e-wrangler-test.ts create mode 100644 packages/wrangler/e2e/helpers/fetch-json.ts create mode 100644 packages/wrangler/e2e/helpers/fetch-text.ts create mode 100644 packages/wrangler/e2e/helpers/fetch-with-etag.ts create mode 100644 packages/wrangler/e2e/helpers/generate-resource-name.ts create mode 100644 packages/wrangler/e2e/helpers/mysql-echo-handler.ts create mode 100644 packages/wrangler/e2e/helpers/normalize.ts create mode 100644 packages/wrangler/e2e/helpers/postgres-echo-handler.ts create mode 100644 packages/wrangler/e2e/helpers/read-until.ts create mode 100644 packages/wrangler/e2e/helpers/retry.ts create mode 100644 packages/wrangler/e2e/helpers/setup.ts create mode 100644 packages/wrangler/e2e/helpers/wait-for.ts create mode 100644 packages/wrangler/e2e/helpers/workerd-processes.ts create mode 100644 packages/wrangler/e2e/helpers/wrangler.ts create mode 100644 packages/wrangler/e2e/multiworker-dev.test.ts create mode 100644 packages/wrangler/e2e/pages-deploy.test.ts create mode 100644 packages/wrangler/e2e/pages-dev.test.ts create mode 100644 packages/wrangler/e2e/provision.test.ts create mode 100644 packages/wrangler/e2e/r2.test.ts create mode 100644 packages/wrangler/e2e/remote-binding/dev-remote-bindings.test.ts create mode 100644 packages/wrangler/e2e/remote-binding/miniflare-remote-resources.test.ts create mode 100644 packages/wrangler/e2e/remote-binding/remote-bindings-api.test.ts create mode 100644 packages/wrangler/e2e/remote-binding/start-worker-remote-bindings.test.ts create mode 100644 packages/wrangler/e2e/remote-binding/workers/agent-memory.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/ai-search-instance.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/ai-search-namespace.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/ai.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/alt-remote-worker.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/browser.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/d1.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/dispatch-namespace.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/email.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/images.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/index.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/kv.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/local-and-remote-service-bindings.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/local-service-binding-and-remote-ai.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/media.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/mixed-kvs.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/mtls.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/pipelines.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/r2.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/remote-worker.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/service-binding.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/simple-service-binding.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/vectorize.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/vpc-network.js create mode 100644 packages/wrangler/e2e/remote-binding/workers/vpc-service.js create mode 100644 packages/wrangler/e2e/secrets-store.test.ts create mode 100644 packages/wrangler/e2e/start-worker-auth-opts.test.ts create mode 100644 packages/wrangler/e2e/startWorker.test.ts create mode 100644 packages/wrangler/e2e/tsconfig.json create mode 100644 packages/wrangler/e2e/types.test.ts create mode 100644 packages/wrangler/e2e/unenv-preset/preset.test.ts create mode 100644 packages/wrangler/e2e/unenv-preset/worker/index.ts create mode 100644 packages/wrangler/e2e/unenv-preset/worker/wrangler.json create mode 100644 packages/wrangler/e2e/validate-environment.ts create mode 100644 packages/wrangler/e2e/versions.test.ts create mode 100644 packages/wrangler/e2e/vitest.config.mts create mode 100644 packages/wrangler/e2e/vitest.setup.ts create mode 100644 packages/wrangler/import_meta_url.js create mode 100644 packages/wrangler/kv-asset-handler.js create mode 100644 packages/wrangler/package.json create mode 100644 packages/wrangler/scripts/assert-git-version.ts create mode 100644 packages/wrangler/scripts/deps.ts create mode 100644 packages/wrangler/scripts/generate-json-schema.ts create mode 100644 packages/wrangler/scripts/node-options-for-tests.js create mode 100644 packages/wrangler/src/__tests__/agent-memory.test.ts create mode 100644 packages/wrangler/src/__tests__/agents-skills-install.test.ts create mode 100644 packages/wrangler/src/__tests__/ai-search.test.ts create mode 100644 packages/wrangler/src/__tests__/ai.local.test.ts create mode 100644 packages/wrangler/src/__tests__/ai.test.ts create mode 100644 packages/wrangler/src/__tests__/api.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/BundlerController.emfile.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/ConfigController.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/DevEnv.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/LocalRuntimeController.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/MultiworkerRuntimeController.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts create mode 100644 packages/wrangler/src/__tests__/api/startDevWorker/utils.test.ts create mode 100644 packages/wrangler/src/__tests__/artifacts.test.ts create mode 100644 packages/wrangler/src/__tests__/assets.test.ts create mode 100644 packages/wrangler/src/__tests__/auth-credentials.test.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/index.test.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/run.test.ts create mode 100644 packages/wrangler/src/__tests__/banner.test.ts create mode 100644 packages/wrangler/src/__tests__/browser-run.test.ts create mode 100644 packages/wrangler/src/__tests__/build-output.test.ts create mode 100644 packages/wrangler/src/__tests__/cert.test.ts create mode 100644 packages/wrangler/src/__tests__/cf-wrangler/args.test.ts create mode 100644 packages/wrangler/src/__tests__/cf-wrangler/build.test.ts create mode 100644 packages/wrangler/src/__tests__/cfetch-internal.test.ts create mode 100644 packages/wrangler/src/__tests__/cfetch-utils.test.ts create mode 100644 packages/wrangler/src/__tests__/check-fetch.test.ts create mode 100644 packages/wrangler/src/__tests__/cli-hotkeys.test.ts create mode 100644 packages/wrangler/src/__tests__/cli.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/apply.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/build.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/common.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/create.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/curl.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/delete.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/images.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/instance-type.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/limits.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/list.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/modify.test.ts create mode 100644 packages/wrangler/src/__tests__/cloudchamber/utils.ts create mode 100644 packages/wrangler/src/__tests__/complete.test.ts create mode 100644 packages/wrangler/src/__tests__/config-cache-wrangler-fallback.test.ts create mode 100644 packages/wrangler/src/__tests__/config-cache.test.ts create mode 100644 packages/wrangler/src/__tests__/config-validation-pages.test.ts create mode 100644 packages/wrangler/src/__tests__/config/configuration.test.ts create mode 100644 packages/wrangler/src/__tests__/config/loadDotEnv.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/config.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/delete.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/deploy.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/images.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/info.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/instances.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/list.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/push.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/registries.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/schema.test.ts create mode 100644 packages/wrangler/src/__tests__/containers/ssh.test.ts create mode 100644 packages/wrangler/src/__tests__/core/command-registration.test.ts create mode 100644 packages/wrangler/src/__tests__/core/handle-errors.test.ts create mode 100644 packages/wrangler/src/__tests__/core/main-handleerror-fallback.test.ts create mode 100644 packages/wrangler/src/__tests__/create-worker-upload-form/bindings.test.ts create mode 100644 packages/wrangler/src/__tests__/create-worker-upload-form/commonjs.test.ts create mode 100644 packages/wrangler/src/__tests__/create-worker-upload-form/helpers.ts create mode 100644 packages/wrangler/src/__tests__/create-worker-upload-form/metadata.test.ts create mode 100644 packages/wrangler/src/__tests__/create-worker-upload-form/mime-types.test.ts create mode 100644 packages/wrangler/src/__tests__/custom-build.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/convert-timestamp-to-iso.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/create.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/d1.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/delete.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/execute.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/export.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/fixtures/db.sqlite3 create mode 100644 packages/wrangler/src/__tests__/d1/info.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/insights.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/list.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/migrate.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/migrations/helpers.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/splitter.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/timeTravel.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/trimmer.test.ts create mode 100644 packages/wrangler/src/__tests__/d1/utils.test.ts create mode 100644 packages/wrangler/src/__tests__/delete.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/assets.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/binding-depends-on-export.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/bindings.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/build.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/check-remote-secrets-override.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/check-workflow-conflicts.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/config-remote.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/core.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/durable-objects.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/entry-points.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/environments.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/exports-reconciliation.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/formats.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/get-config-patch.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/helpers.ts create mode 100644 packages/wrangler/src/__tests__/deploy/inconsistent-exports.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/legacy-assets.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/name-collision-guard.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/open-next.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/queues.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/routes.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/secrets.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/workers-dev.test.ts create mode 100644 packages/wrangler/src/__tests__/deploy/workflows.test.ts create mode 100644 packages/wrangler/src/__tests__/deployment-bundle/source-maps.test.ts create mode 100644 packages/wrangler/src/__tests__/deployments.test.ts create mode 100644 packages/wrangler/src/__tests__/dev.test.ts create mode 100644 packages/wrangler/src/__tests__/dev/dev-vars.test.ts create mode 100644 packages/wrangler/src/__tests__/dev/get-local-persistence-path.test.ts create mode 100644 packages/wrangler/src/__tests__/dev/remote-bindings-errors.test.ts create mode 100644 packages/wrangler/src/__tests__/dev/remote-bindings.test.ts create mode 100644 packages/wrangler/src/__tests__/dev/remote.test.ts create mode 100644 packages/wrangler/src/__tests__/dev/start-dev.test.ts create mode 100644 packages/wrangler/src/__tests__/docs.test.ts create mode 100644 packages/wrangler/src/__tests__/email-routing.test.ts create mode 100644 packages/wrangler/src/__tests__/errors.test.ts create mode 100644 packages/wrangler/src/__tests__/experimental-commands-api.test.ts create mode 100644 packages/wrangler/src/__tests__/experimental-config/convert.test.ts create mode 100644 packages/wrangler/src/__tests__/experimental-config/load.test.ts create mode 100644 packages/wrangler/src/__tests__/fetch-graphql-result.test.ts create mode 100644 packages/wrangler/src/__tests__/find-additional-modules.test.ts create mode 100644 packages/wrangler/src/__tests__/flagship.test.ts create mode 100644 packages/wrangler/src/__tests__/friendly-validator-errors.test.ts create mode 100644 packages/wrangler/src/__tests__/get-entry.test.ts create mode 100644 packages/wrangler/src/__tests__/get-host-from-url.test.ts create mode 100644 packages/wrangler/src/__tests__/guess-worker-format.test.ts create mode 100644 packages/wrangler/src/__tests__/hello-world.local.test.ts create mode 100644 packages/wrangler/src/__tests__/helpers/assert-request.ts create mode 100644 packages/wrangler/src/__tests__/helpers/capture-requests-from.ts create mode 100644 packages/wrangler/src/__tests__/helpers/cmd-shim.d.ts create mode 100644 packages/wrangler/src/__tests__/helpers/collect-cli-output.ts create mode 100644 packages/wrangler/src/__tests__/helpers/end-event-loop.ts create mode 100644 packages/wrangler/src/__tests__/helpers/fake-bus.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-account-id.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-auth-domain.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-bin.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-cli-output.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-cloudchamber.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-console.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-dialogs.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-get-pages-upload-token.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-get-zone-from-host.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-http-server.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-istty.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-kv.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-legacy-script.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-oauth-flow.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-process.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-secrets-store.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-set-timeout.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-stdin.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-upload-worker.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-web-socket.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-worker-settings.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-workers-subdomain.ts create mode 100644 packages/wrangler/src/__tests__/helpers/mock-zone-routes.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/deployments.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/namespaces.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/r2.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/script.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/user.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/versions.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/handlers/zones.ts create mode 100644 packages/wrangler/src/__tests__/helpers/msw/index.ts create mode 100644 packages/wrangler/src/__tests__/helpers/normalize-progress.ts create mode 100644 packages/wrangler/src/__tests__/helpers/run-wrangler.ts create mode 100644 packages/wrangler/src/__tests__/helpers/serialize-form-data-entry.ts create mode 100644 packages/wrangler/src/__tests__/helpers/string-dynamic-values-matcher.ts create mode 100644 packages/wrangler/src/__tests__/helpers/teardown.ts create mode 100644 packages/wrangler/src/__tests__/helpers/unusable.ts create mode 100644 packages/wrangler/src/__tests__/helpers/write-worker-source.ts create mode 100644 packages/wrangler/src/__tests__/helpers/write-wrangler-config.ts create mode 100644 packages/wrangler/src/__tests__/https-options.test.ts create mode 100644 packages/wrangler/src/__tests__/hyperdrive.test.ts create mode 100644 packages/wrangler/src/__tests__/index.test.ts create mode 100644 packages/wrangler/src/__tests__/init.test.ts create mode 100644 packages/wrangler/src/__tests__/is-local.test.ts create mode 100644 packages/wrangler/src/__tests__/kv/__snapshots__/namespace.test.ts.snap create mode 100644 packages/wrangler/src/__tests__/kv/bulk.test.ts create mode 100644 packages/wrangler/src/__tests__/kv/constant.ts create mode 100644 packages/wrangler/src/__tests__/kv/help.test.ts create mode 100644 packages/wrangler/src/__tests__/kv/key.test.ts create mode 100644 packages/wrangler/src/__tests__/kv/local.test.ts create mode 100644 packages/wrangler/src/__tests__/kv/namespace.test.ts create mode 100644 packages/wrangler/src/__tests__/logger.test.ts create mode 100644 packages/wrangler/src/__tests__/logout.test.ts create mode 100644 packages/wrangler/src/__tests__/match-tag.test.ts create mode 100644 packages/wrangler/src/__tests__/metrics.test.ts create mode 100644 packages/wrangler/src/__tests__/metrics/sanitization.test.ts create mode 100644 packages/wrangler/src/__tests__/middleware.scheduled.test.ts create mode 100644 packages/wrangler/src/__tests__/middleware.test.ts create mode 100644 packages/wrangler/src/__tests__/mtls-certificates.test.ts create mode 100644 packages/wrangler/src/__tests__/navigator-user-agent.test.ts create mode 100644 packages/wrangler/src/__tests__/open-in-browser.test.ts create mode 100644 packages/wrangler/src/__tests__/output.test.ts create mode 100644 packages/wrangler/src/__tests__/package-manager.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/__snapshots__/deploy.test.ts.snap create mode 100644 packages/wrangler/src/__tests__/pages/delegate-to-workers.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/deploy.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/deployment-delete.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/deployment-list.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/dev.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/filepath-routing.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/functions-build.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/pages-build-env.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/pages-download-config.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/pages.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/project-create.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/project-delete.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/project-list.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/project-upload.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/project-validate.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/routes-consolidation.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/routes-module.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/routes-transformation.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/routes-validation.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/run-workers-deploy.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/secret.test.ts create mode 100644 packages/wrangler/src/__tests__/pages/utf8-truncation.test.ts create mode 100644 packages/wrangler/src/__tests__/paths.test.ts create mode 100644 packages/wrangler/src/__tests__/pipelines-setup.test.ts create mode 100644 packages/wrangler/src/__tests__/pipelines.test.ts create mode 100644 packages/wrangler/src/__tests__/preview.secret.test.ts create mode 100644 packages/wrangler/src/__tests__/preview.settings.test.ts create mode 100644 packages/wrangler/src/__tests__/preview.test.ts create mode 100644 packages/wrangler/src/__tests__/print-bindings.test.ts create mode 100644 packages/wrangler/src/__tests__/process-env-populated.test.ts create mode 100644 packages/wrangler/src/__tests__/profiles.test.ts create mode 100644 packages/wrangler/src/__tests__/provision.test.ts create mode 100644 packages/wrangler/src/__tests__/proxy-output.test.ts create mode 100644 packages/wrangler/src/__tests__/queues/__snapshots__/queues.test.ts.snap create mode 100644 packages/wrangler/src/__tests__/queues/mock-utils.ts create mode 100644 packages/wrangler/src/__tests__/queues/queues-subscription.test.ts create mode 100644 packages/wrangler/src/__tests__/queues/queues.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/__snapshots__/bucket.test.ts.snap create mode 100644 packages/wrangler/src/__tests__/r2/bucket.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/bulk.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/catalog-force.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/errors.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/help.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/helper.ts create mode 100644 packages/wrangler/src/__tests__/r2/local-uploads.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/local.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/notification.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/object.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/pipe.test.ts create mode 100644 packages/wrangler/src/__tests__/r2/sql.test.ts create mode 100644 packages/wrangler/src/__tests__/register-yargs-command-skills.test.ts create mode 100644 packages/wrangler/src/__tests__/rollback.test.ts create mode 100644 packages/wrangler/src/__tests__/secret.test.ts create mode 100644 packages/wrangler/src/__tests__/secrets-store.test.ts create mode 100644 packages/wrangler/src/__tests__/sentry.test.ts create mode 100644 packages/wrangler/src/__tests__/setup.test.ts create mode 100644 packages/wrangler/src/__tests__/startup-profiling.test.ts create mode 100644 packages/wrangler/src/__tests__/tail.test.ts create mode 100644 packages/wrangler/src/__tests__/test-old-node-version.js create mode 100644 packages/wrangler/src/__tests__/tsconfig-sanity.ts create mode 100644 packages/wrangler/src/__tests__/tsconfig.json create mode 100644 packages/wrangler/src/__tests__/tunnel/tunnel-resolve.test.ts create mode 100644 packages/wrangler/src/__tests__/tunnel/tunnel.test.ts create mode 100644 packages/wrangler/src/__tests__/turnstile.test.ts create mode 100644 packages/wrangler/src/__tests__/type-generation-pipeline-schema.test.ts create mode 100644 packages/wrangler/src/__tests__/type-generation.test.ts create mode 100644 packages/wrangler/src/__tests__/unstable-get-miniflare-worker-options.test.ts create mode 100644 packages/wrangler/src/__tests__/update-config-file.test.ts create mode 100644 packages/wrangler/src/__tests__/user.test.ts create mode 100644 packages/wrangler/src/__tests__/utils-collectKeyValues.test.ts create mode 100644 packages/wrangler/src/__tests__/utils-memoizeGetPort.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/box.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/create-batches.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/detect-agent.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/did-you-mean.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/format-message.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/getValidBindingName.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/levenshtein.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/log-file.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/retry.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/shell-quote.test.ts create mode 100644 packages/wrangler/src/__tests__/vectorize/vectorize.test.ts create mode 100644 packages/wrangler/src/__tests__/vectorize/vectorize.upsert.test.ts create mode 100644 packages/wrangler/src/__tests__/version.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/deployments/deployments.list.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/deployments/deployments.status.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/deployments/deployments.view.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/secrets.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/secrets/bulk.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/secrets/delete.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/secrets/list.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/secrets/put.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/secrets/utils.ts create mode 100644 packages/wrangler/src/__tests__/versions/versions.deploy.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/versions.help.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/versions.list.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/versions.upload.test.ts create mode 100644 packages/wrangler/src/__tests__/versions/versions.view.test.ts create mode 100644 packages/wrangler/src/__tests__/vitest.global.ts create mode 100644 packages/wrangler/src/__tests__/vitest.setup.ts create mode 100644 packages/wrangler/src/__tests__/vpc.test.ts create mode 100644 packages/wrangler/src/__tests__/whoami.test.ts create mode 100644 packages/wrangler/src/__tests__/worker-namespace.test.ts create mode 100644 packages/wrangler/src/__tests__/workflows.test.ts create mode 100644 packages/wrangler/src/__tests__/zones.test.ts create mode 100644 packages/wrangler/src/abort.d.ts create mode 100644 packages/wrangler/src/agent-memory/client.ts create mode 100644 packages/wrangler/src/agent-memory/create.ts create mode 100644 packages/wrangler/src/agent-memory/delete.ts create mode 100644 packages/wrangler/src/agent-memory/get.ts create mode 100644 packages/wrangler/src/agent-memory/index.ts create mode 100644 packages/wrangler/src/agent-memory/list.ts create mode 100644 packages/wrangler/src/agent-memory/provisioning.ts create mode 100644 packages/wrangler/src/agents-skills-install.ts create mode 100644 packages/wrangler/src/ai-search.ts create mode 100644 packages/wrangler/src/ai-search/client.ts create mode 100644 packages/wrangler/src/ai-search/create.ts create mode 100644 packages/wrangler/src/ai-search/delete.ts create mode 100644 packages/wrangler/src/ai-search/get.ts create mode 100644 packages/wrangler/src/ai-search/index.ts create mode 100644 packages/wrangler/src/ai-search/jobs/cancel.ts create mode 100644 packages/wrangler/src/ai-search/jobs/create.ts create mode 100644 packages/wrangler/src/ai-search/jobs/get.ts create mode 100644 packages/wrangler/src/ai-search/jobs/index.ts create mode 100644 packages/wrangler/src/ai-search/jobs/list.ts create mode 100644 packages/wrangler/src/ai-search/jobs/logs.ts create mode 100644 packages/wrangler/src/ai-search/list.ts create mode 100644 packages/wrangler/src/ai-search/namespace/create.ts create mode 100644 packages/wrangler/src/ai-search/namespace/delete.ts create mode 100644 packages/wrangler/src/ai-search/namespace/get.ts create mode 100644 packages/wrangler/src/ai-search/namespace/index.ts create mode 100644 packages/wrangler/src/ai-search/namespace/list.ts create mode 100644 packages/wrangler/src/ai-search/namespace/update.ts create mode 100644 packages/wrangler/src/ai-search/search.ts create mode 100644 packages/wrangler/src/ai-search/stats.ts create mode 100644 packages/wrangler/src/ai-search/types.ts create mode 100644 packages/wrangler/src/ai-search/update.ts create mode 100644 packages/wrangler/src/ai-search/utils.ts create mode 100644 packages/wrangler/src/ai/createFinetune.ts create mode 100644 packages/wrangler/src/ai/fetcher.ts create mode 100644 packages/wrangler/src/ai/index.ts create mode 100644 packages/wrangler/src/ai/listCatalog.ts create mode 100644 packages/wrangler/src/ai/listFinetune.ts create mode 100644 packages/wrangler/src/ai/modelSchema.ts create mode 100644 packages/wrangler/src/ai/types.ts create mode 100644 packages/wrangler/src/ai/utils.ts create mode 100644 packages/wrangler/src/api/deploy-helpers-context.ts create mode 100644 packages/wrangler/src/api/dev.ts create mode 100644 packages/wrangler/src/api/generate-types.ts create mode 100644 packages/wrangler/src/api/index.ts create mode 100644 packages/wrangler/src/api/integrations/index.ts create mode 100644 packages/wrangler/src/api/integrations/platform/caches.ts create mode 100644 packages/wrangler/src/api/integrations/platform/executionContext.ts create mode 100644 packages/wrangler/src/api/integrations/platform/index.ts create mode 100644 packages/wrangler/src/api/mtls-certificate.ts create mode 100644 packages/wrangler/src/api/pages/create-worker-bundle-contents.ts create mode 100644 packages/wrangler/src/api/pages/deploy.ts create mode 100644 packages/wrangler/src/api/pages/index.ts create mode 100644 packages/wrangler/src/api/remoteBindings/index.ts create mode 100644 packages/wrangler/src/api/remoteBindings/start-remote-proxy-session.ts create mode 100644 packages/wrangler/src/api/startDevWorker/BaseController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/BundlerController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/ConfigController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/DevEnv.ts create mode 100644 packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/MultiworkerRuntimeController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/NoOpProxyController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/NotImplementedError.ts create mode 100644 packages/wrangler/src/api/startDevWorker/ProxyController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts create mode 100644 packages/wrangler/src/api/startDevWorker/binding-utils.ts create mode 100644 packages/wrangler/src/api/startDevWorker/bundle-allowed-paths.ts create mode 100644 packages/wrangler/src/api/startDevWorker/devtools.ts create mode 100644 packages/wrangler/src/api/startDevWorker/events.ts create mode 100644 packages/wrangler/src/api/startDevWorker/index.ts create mode 100644 packages/wrangler/src/api/startDevWorker/types.ts create mode 100644 packages/wrangler/src/api/startDevWorker/utils.ts create mode 100644 packages/wrangler/src/api/test-harness.ts create mode 100644 packages/wrangler/src/artifacts/client.ts create mode 100644 packages/wrangler/src/artifacts/index.ts create mode 100644 packages/wrangler/src/artifacts/namespaces.ts create mode 100644 packages/wrangler/src/artifacts/repos.ts create mode 100644 packages/wrangler/src/artifacts/types.ts create mode 100644 packages/wrangler/src/assets.ts create mode 100644 packages/wrangler/src/autoconfig-context.ts create mode 100644 packages/wrangler/src/autoconfig/index.ts create mode 100644 packages/wrangler/src/autoconfig/telemetry-utils.ts create mode 100644 packages/wrangler/src/browser-rendering/close.ts create mode 100644 packages/wrangler/src/browser-rendering/create.ts create mode 100644 packages/wrangler/src/browser-rendering/index.ts create mode 100644 packages/wrangler/src/browser-rendering/list.ts create mode 100644 packages/wrangler/src/browser-rendering/types.ts create mode 100644 packages/wrangler/src/browser-rendering/utils.ts create mode 100644 packages/wrangler/src/browser-rendering/view.ts create mode 100644 packages/wrangler/src/build/index.ts create mode 100644 packages/wrangler/src/build/run-build-output.ts create mode 100644 packages/wrangler/src/cert/cert.ts create mode 100644 packages/wrangler/src/cf-wrangler/args.ts create mode 100644 packages/wrangler/src/cf-wrangler/build.ts create mode 100644 packages/wrangler/src/cf-wrangler/dev.ts create mode 100644 packages/wrangler/src/cfetch/index.ts create mode 100644 packages/wrangler/src/cfetch/internal.ts create mode 100644 packages/wrangler/src/check/commands.ts create mode 100644 packages/wrangler/src/cli-hotkeys.ts create mode 100644 packages/wrangler/src/cli.ts create mode 100644 packages/wrangler/src/cloudchamber/apply.ts create mode 100644 packages/wrangler/src/cloudchamber/build.ts create mode 100644 packages/wrangler/src/cloudchamber/cli/deployments.ts create mode 100644 packages/wrangler/src/cloudchamber/cli/index.ts create mode 100644 packages/wrangler/src/cloudchamber/cli/locations.ts create mode 100644 packages/wrangler/src/cloudchamber/cli/util.ts create mode 100644 packages/wrangler/src/cloudchamber/common.ts create mode 100644 packages/wrangler/src/cloudchamber/create.ts create mode 100644 packages/wrangler/src/cloudchamber/curl.ts create mode 100644 packages/wrangler/src/cloudchamber/delete.ts create mode 100644 packages/wrangler/src/cloudchamber/enums.ts create mode 100644 packages/wrangler/src/cloudchamber/helpers/wrap.ts create mode 100644 packages/wrangler/src/cloudchamber/images/images.ts create mode 100644 packages/wrangler/src/cloudchamber/images/registries.ts create mode 100644 packages/wrangler/src/cloudchamber/index.ts create mode 100644 packages/wrangler/src/cloudchamber/instance-type/instance-type.ts create mode 100644 packages/wrangler/src/cloudchamber/limits.ts create mode 100644 packages/wrangler/src/cloudchamber/list.ts create mode 100644 packages/wrangler/src/cloudchamber/locations.ts create mode 100644 packages/wrangler/src/cloudchamber/modify.ts create mode 100644 packages/wrangler/src/cloudchamber/network/network.ts create mode 100644 packages/wrangler/src/cloudchamber/ssh/ssh.ts create mode 100644 packages/wrangler/src/cloudchamber/ssh/validate.ts create mode 100644 packages/wrangler/src/complete.ts create mode 100644 packages/wrangler/src/config-cache.ts create mode 100644 packages/wrangler/src/config/case-insensitive-env.ts create mode 100644 packages/wrangler/src/config/dot-env.ts create mode 100644 packages/wrangler/src/config/index.ts create mode 100644 packages/wrangler/src/containers/build.ts create mode 100644 packages/wrangler/src/containers/config.ts create mode 100644 packages/wrangler/src/containers/containers.ts create mode 100644 packages/wrangler/src/containers/deploy.ts create mode 100644 packages/wrangler/src/containers/images.ts create mode 100644 packages/wrangler/src/containers/index.ts create mode 100644 packages/wrangler/src/containers/instances.ts create mode 100644 packages/wrangler/src/containers/list.ts create mode 100644 packages/wrangler/src/containers/registries.ts create mode 100644 packages/wrangler/src/containers/ssh.ts create mode 100644 packages/wrangler/src/core/CommandHandledError.ts create mode 100644 packages/wrangler/src/core/CommandRegistry.ts create mode 100644 packages/wrangler/src/core/create-command.ts create mode 100644 packages/wrangler/src/core/handle-errors.ts create mode 100644 packages/wrangler/src/core/helpers.ts create mode 100644 packages/wrangler/src/core/index.ts create mode 100644 packages/wrangler/src/core/register-yargs-command.ts create mode 100644 packages/wrangler/src/core/teams.d.ts create mode 100644 packages/wrangler/src/core/temporary-commands.ts create mode 100644 packages/wrangler/src/core/types.ts create mode 100644 packages/wrangler/src/d1/constants.ts create mode 100644 packages/wrangler/src/d1/create.ts create mode 100644 packages/wrangler/src/d1/delete.ts create mode 100644 packages/wrangler/src/d1/execute.ts create mode 100644 packages/wrangler/src/d1/export.ts create mode 100644 packages/wrangler/src/d1/formatTimeAgo.ts create mode 100644 packages/wrangler/src/d1/index.ts create mode 100644 packages/wrangler/src/d1/info.ts create mode 100644 packages/wrangler/src/d1/insights.ts create mode 100644 packages/wrangler/src/d1/list.ts create mode 100644 packages/wrangler/src/d1/migrations/apply.ts create mode 100644 packages/wrangler/src/d1/migrations/create.ts create mode 100644 packages/wrangler/src/d1/migrations/helpers.ts create mode 100644 packages/wrangler/src/d1/migrations/index.ts create mode 100644 packages/wrangler/src/d1/migrations/list.ts create mode 100644 packages/wrangler/src/d1/migrations/options.ts create mode 100644 packages/wrangler/src/d1/options.ts create mode 100644 packages/wrangler/src/d1/splitter.ts create mode 100644 packages/wrangler/src/d1/timeTravel/index.ts create mode 100644 packages/wrangler/src/d1/timeTravel/info.ts create mode 100644 packages/wrangler/src/d1/timeTravel/restore.ts create mode 100644 packages/wrangler/src/d1/timeTravel/types.ts create mode 100644 packages/wrangler/src/d1/timeTravel/utils.ts create mode 100644 packages/wrangler/src/d1/trimmer.ts create mode 100644 packages/wrangler/src/d1/types.ts create mode 100644 packages/wrangler/src/d1/utils.ts create mode 100644 packages/wrangler/src/delete.ts create mode 100644 packages/wrangler/src/deploy/autoconfig.ts create mode 100644 packages/wrangler/src/deploy/check-remote-secrets-override.ts create mode 100644 packages/wrangler/src/deploy/check-workflow-conflicts.ts create mode 100644 packages/wrangler/src/deploy/config-diffs.ts create mode 100644 packages/wrangler/src/deploy/index.ts create mode 100644 packages/wrangler/src/deploy/open-next.ts create mode 100644 packages/wrangler/src/deployment-bundle/apply-middleware.ts create mode 100644 packages/wrangler/src/deployment-bundle/auto-provisioned-name.ts create mode 100644 packages/wrangler/src/deployment-bundle/build-failures.ts create mode 100644 packages/wrangler/src/deployment-bundle/build-output.ts create mode 100644 packages/wrangler/src/deployment-bundle/bundle-reporter.ts create mode 100644 packages/wrangler/src/deployment-bundle/bundle-type.ts create mode 100644 packages/wrangler/src/deployment-bundle/bundle.ts create mode 100644 packages/wrangler/src/deployment-bundle/capnp.ts create mode 100644 packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts create mode 100644 packages/wrangler/src/deployment-bundle/dedupe-modules.ts create mode 100644 packages/wrangler/src/deployment-bundle/deploy-args.ts create mode 100644 packages/wrangler/src/deployment-bundle/entry-point-from-metafile.ts create mode 100644 packages/wrangler/src/deployment-bundle/entry.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/alias-external.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/als-external.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/config-provider.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/log-build-output.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-compat.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts create mode 100644 packages/wrangler/src/deployment-bundle/esbuild-plugins/standard-url.ts create mode 100644 packages/wrangler/src/deployment-bundle/find-additional-modules.ts create mode 100644 packages/wrangler/src/deployment-bundle/guess-worker-format.ts create mode 100644 packages/wrangler/src/deployment-bundle/maybe-build-worker.ts create mode 100644 packages/wrangler/src/deployment-bundle/merge-config-args.ts create mode 100644 packages/wrangler/src/deployment-bundle/module-collection.ts create mode 100644 packages/wrangler/src/deployment-bundle/no-bundle-worker.ts create mode 100644 packages/wrangler/src/deployment-bundle/node-compat.ts create mode 100644 packages/wrangler/src/deployment-bundle/resolve-config-args.ts create mode 100644 packages/wrangler/src/deployment-bundle/resolve-entry.ts create mode 100644 packages/wrangler/src/deployment-bundle/rules.ts create mode 100644 packages/wrangler/src/deployment-bundle/run-custom-build.ts create mode 100644 packages/wrangler/src/deployment-bundle/secrets-validation.ts create mode 100644 packages/wrangler/src/deployment-bundle/source-maps.ts create mode 100644 packages/wrangler/src/deployment-bundle/source-url.ts create mode 100644 packages/wrangler/src/deployments.ts create mode 100644 packages/wrangler/src/dev.ts create mode 100644 packages/wrangler/src/dev/class-names-sqlite.ts create mode 100644 packages/wrangler/src/dev/create-worker-preview.ts create mode 100644 packages/wrangler/src/dev/dev-vars.ts create mode 100644 packages/wrangler/src/dev/get-local-persistence-path.ts create mode 100644 packages/wrangler/src/dev/hotkeys.ts create mode 100644 packages/wrangler/src/dev/inspect.ts create mode 100644 packages/wrangler/src/dev/miniflare/index.ts create mode 100644 packages/wrangler/src/dev/remote.ts create mode 100644 packages/wrangler/src/dev/start-dev.ts create mode 100644 packages/wrangler/src/dev/use-esbuild.ts create mode 100644 packages/wrangler/src/dialogs.ts create mode 100644 packages/wrangler/src/dispatch-namespace.ts create mode 100644 packages/wrangler/src/docs/helpers.ts create mode 100644 packages/wrangler/src/docs/index.ts create mode 100644 packages/wrangler/src/email-routing/addresses/create.ts create mode 100644 packages/wrangler/src/email-routing/addresses/delete.ts create mode 100644 packages/wrangler/src/email-routing/addresses/get.ts create mode 100644 packages/wrangler/src/email-routing/addresses/list.ts create mode 100644 packages/wrangler/src/email-routing/client.ts create mode 100644 packages/wrangler/src/email-routing/disable.ts create mode 100644 packages/wrangler/src/email-routing/dns-get.ts create mode 100644 packages/wrangler/src/email-routing/dns-unlock.ts create mode 100644 packages/wrangler/src/email-routing/enable.ts create mode 100644 packages/wrangler/src/email-routing/index.ts create mode 100644 packages/wrangler/src/email-routing/list.ts create mode 100644 packages/wrangler/src/email-routing/rules/create.ts create mode 100644 packages/wrangler/src/email-routing/rules/delete.ts create mode 100644 packages/wrangler/src/email-routing/rules/get.ts create mode 100644 packages/wrangler/src/email-routing/rules/list.ts create mode 100644 packages/wrangler/src/email-routing/rules/update.ts create mode 100644 packages/wrangler/src/email-routing/sending/disable.ts create mode 100644 packages/wrangler/src/email-routing/sending/dns-get.ts create mode 100644 packages/wrangler/src/email-routing/sending/enable.ts create mode 100644 packages/wrangler/src/email-routing/sending/list.ts create mode 100644 packages/wrangler/src/email-routing/sending/send-raw.ts create mode 100644 packages/wrangler/src/email-routing/sending/send.ts create mode 100644 packages/wrangler/src/email-routing/sending/settings.ts create mode 100644 packages/wrangler/src/email-routing/sending/utils.ts create mode 100644 packages/wrangler/src/email-routing/settings.ts create mode 100644 packages/wrangler/src/email-routing/utils.ts create mode 100644 packages/wrangler/src/environments/index.ts create mode 100644 packages/wrangler/src/experimental-commands-api.ts create mode 100644 packages/wrangler/src/experimental-config.ts create mode 100644 packages/wrangler/src/experimental-config/cli-flag.ts create mode 100644 packages/wrangler/src/experimental-config/convert.ts create mode 100644 packages/wrangler/src/experimental-config/load.ts create mode 100644 packages/wrangler/src/experimental-config/public.ts create mode 100644 packages/wrangler/src/experimental-config/schema.ts create mode 100644 packages/wrangler/src/experimental-config/types.ts create mode 100644 packages/wrangler/src/experimental-config/wrangler-definition.ts create mode 100644 packages/wrangler/src/experimental-flags.ts create mode 100644 packages/wrangler/src/flagship/apps/create.ts create mode 100644 packages/wrangler/src/flagship/apps/delete.ts create mode 100644 packages/wrangler/src/flagship/apps/get.ts create mode 100644 packages/wrangler/src/flagship/apps/list.ts create mode 100644 packages/wrangler/src/flagship/apps/update.ts create mode 100644 packages/wrangler/src/flagship/bulk.ts create mode 100644 packages/wrangler/src/flagship/client.ts create mode 100644 packages/wrangler/src/flagship/flags/changelog.ts create mode 100644 packages/wrangler/src/flagship/flags/create.ts create mode 100644 packages/wrangler/src/flagship/flags/delete.ts create mode 100644 packages/wrangler/src/flagship/flags/enable.ts create mode 100644 packages/wrangler/src/flagship/flags/evaluate.ts create mode 100644 packages/wrangler/src/flagship/flags/get.ts create mode 100644 packages/wrangler/src/flagship/flags/list.ts create mode 100644 packages/wrangler/src/flagship/flags/rollout.ts create mode 100644 packages/wrangler/src/flagship/flags/rules/delete.ts create mode 100644 packages/wrangler/src/flagship/flags/rules/list.ts create mode 100644 packages/wrangler/src/flagship/flags/rules/reorder.ts create mode 100644 packages/wrangler/src/flagship/flags/rules/shared.ts create mode 100644 packages/wrangler/src/flagship/flags/rules/update.ts create mode 100644 packages/wrangler/src/flagship/flags/set.ts create mode 100644 packages/wrangler/src/flagship/flags/split.ts create mode 100644 packages/wrangler/src/flagship/flags/update.ts create mode 100644 packages/wrangler/src/flagship/index.ts create mode 100644 packages/wrangler/src/flagship/render.ts create mode 100644 packages/wrangler/src/flagship/shared.ts create mode 100644 packages/wrangler/src/hello-world/index.ts create mode 100644 packages/wrangler/src/https-options.ts create mode 100644 packages/wrangler/src/hyperdrive/client.ts create mode 100644 packages/wrangler/src/hyperdrive/create.ts create mode 100644 packages/wrangler/src/hyperdrive/delete.ts create mode 100644 packages/wrangler/src/hyperdrive/get.ts create mode 100644 packages/wrangler/src/hyperdrive/index.ts create mode 100644 packages/wrangler/src/hyperdrive/list.ts create mode 100644 packages/wrangler/src/hyperdrive/shared.ts create mode 100644 packages/wrangler/src/hyperdrive/update.ts create mode 100644 packages/wrangler/src/index.ts create mode 100644 packages/wrangler/src/init.ts create mode 100644 packages/wrangler/src/intl-polyfill.d.ts create mode 100644 packages/wrangler/src/is-interactive.ts create mode 100644 packages/wrangler/src/kv/helpers.ts create mode 100644 packages/wrangler/src/kv/index.ts create mode 100644 packages/wrangler/src/logger.ts create mode 100644 packages/wrangler/src/match-tag.ts create mode 100644 packages/wrangler/src/metrics/commands.ts create mode 100644 packages/wrangler/src/metrics/helpers.ts create mode 100644 packages/wrangler/src/metrics/index.ts create mode 100644 packages/wrangler/src/metrics/metrics-config.ts create mode 100644 packages/wrangler/src/metrics/metrics-dispatcher.ts create mode 100644 packages/wrangler/src/metrics/metrics-usage-headers.ts create mode 100644 packages/wrangler/src/metrics/sanitization.ts create mode 100644 packages/wrangler/src/metrics/send-event.ts create mode 100644 packages/wrangler/src/metrics/types.ts create mode 100644 packages/wrangler/src/miniflare-cli/assets.ts create mode 100644 packages/wrangler/src/miniflare-cli/tsconfig.json create mode 100644 packages/wrangler/src/miniflare-cli/types.ts create mode 100644 packages/wrangler/src/mtls-certificate/cli.ts create mode 100644 packages/wrangler/src/navigator-user-agent.ts create mode 100644 packages/wrangler/src/open-in-browser.ts create mode 100644 packages/wrangler/src/output.ts create mode 100644 packages/wrangler/src/package-manager.ts create mode 100644 packages/wrangler/src/pages/README.md create mode 100644 packages/wrangler/src/pages/build-env.ts create mode 100644 packages/wrangler/src/pages/build.ts create mode 100644 packages/wrangler/src/pages/buildFunctions.ts create mode 100644 packages/wrangler/src/pages/constants.ts create mode 100644 packages/wrangler/src/pages/delegate-to-workers.ts create mode 100644 packages/wrangler/src/pages/deploy.ts create mode 100644 packages/wrangler/src/pages/deployment-tails.ts create mode 100644 packages/wrangler/src/pages/deployments.ts create mode 100644 packages/wrangler/src/pages/dev.ts create mode 100644 packages/wrangler/src/pages/download-config.ts create mode 100644 packages/wrangler/src/pages/errors.ts create mode 100644 packages/wrangler/src/pages/functions.ts create mode 100644 packages/wrangler/src/pages/functions/buildPlugin.ts create mode 100644 packages/wrangler/src/pages/functions/buildWorker.ts create mode 100644 packages/wrangler/src/pages/functions/filepath-routing.ts create mode 100644 packages/wrangler/src/pages/functions/identifiers.ts create mode 100644 packages/wrangler/src/pages/functions/routes-consolidation.ts create mode 100644 packages/wrangler/src/pages/functions/routes-transformation.ts create mode 100644 packages/wrangler/src/pages/functions/routes-validation.ts create mode 100755 packages/wrangler/src/pages/functions/routes.ts create mode 100644 packages/wrangler/src/pages/functions/tsconfig.json create mode 100644 packages/wrangler/src/pages/hash.ts create mode 100644 packages/wrangler/src/pages/index.ts create mode 100644 packages/wrangler/src/pages/projects.ts create mode 100644 packages/wrangler/src/pages/prompt-select-project.ts create mode 100644 packages/wrangler/src/pages/run-workers-deploy.ts create mode 100644 packages/wrangler/src/pages/secret/index.ts create mode 100644 packages/wrangler/src/pages/types.ts create mode 100644 packages/wrangler/src/pages/upload.ts create mode 100644 packages/wrangler/src/pages/utils.ts create mode 100644 packages/wrangler/src/pages/validate.ts create mode 100644 packages/wrangler/src/paths.ts create mode 100644 packages/wrangler/src/pipelines/cli/create.ts create mode 100644 packages/wrangler/src/pipelines/cli/delete.ts create mode 100644 packages/wrangler/src/pipelines/cli/get.ts create mode 100644 packages/wrangler/src/pipelines/cli/legacy-helpers.ts create mode 100644 packages/wrangler/src/pipelines/cli/list.ts create mode 100644 packages/wrangler/src/pipelines/cli/resolve.ts create mode 100644 packages/wrangler/src/pipelines/cli/setup.ts create mode 100644 packages/wrangler/src/pipelines/cli/sinks/create.ts create mode 100644 packages/wrangler/src/pipelines/cli/sinks/delete.ts create mode 100644 packages/wrangler/src/pipelines/cli/sinks/get.ts create mode 100644 packages/wrangler/src/pipelines/cli/sinks/index.ts create mode 100644 packages/wrangler/src/pipelines/cli/sinks/list.ts create mode 100644 packages/wrangler/src/pipelines/cli/sinks/utils.ts create mode 100644 packages/wrangler/src/pipelines/cli/streams/create.ts create mode 100644 packages/wrangler/src/pipelines/cli/streams/delete.ts create mode 100644 packages/wrangler/src/pipelines/cli/streams/get.ts create mode 100644 packages/wrangler/src/pipelines/cli/streams/index.ts create mode 100644 packages/wrangler/src/pipelines/cli/streams/list.ts create mode 100644 packages/wrangler/src/pipelines/cli/streams/utils.ts create mode 100644 packages/wrangler/src/pipelines/cli/update.ts create mode 100644 packages/wrangler/src/pipelines/client.ts create mode 100644 packages/wrangler/src/pipelines/defaults.ts create mode 100644 packages/wrangler/src/pipelines/index.ts create mode 100644 packages/wrangler/src/pipelines/legacy-client.ts create mode 100644 packages/wrangler/src/pipelines/types.ts create mode 100644 packages/wrangler/src/pipelines/validate.ts create mode 100644 packages/wrangler/src/preview/api.ts create mode 100644 packages/wrangler/src/preview/index.ts create mode 100644 packages/wrangler/src/preview/preview.ts create mode 100644 packages/wrangler/src/preview/secret.ts create mode 100644 packages/wrangler/src/preview/settings.ts create mode 100644 packages/wrangler/src/preview/shared.ts create mode 100644 packages/wrangler/src/process-env.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/http-pull/add.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/http-pull/index.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/http-pull/list.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/http-pull/remove.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/index.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/list.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/worker/add.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/worker/index.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/worker/list.ts create mode 100644 packages/wrangler/src/queues/cli/commands/consumer/worker/remove.ts create mode 100644 packages/wrangler/src/queues/cli/commands/create.ts create mode 100644 packages/wrangler/src/queues/cli/commands/delete.ts create mode 100644 packages/wrangler/src/queues/cli/commands/index.ts create mode 100644 packages/wrangler/src/queues/cli/commands/info.ts create mode 100644 packages/wrangler/src/queues/cli/commands/list.ts create mode 100644 packages/wrangler/src/queues/cli/commands/pause-resume.ts create mode 100644 packages/wrangler/src/queues/cli/commands/purge.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/create.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/delete.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/get.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/index.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/list.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/update.ts create mode 100644 packages/wrangler/src/queues/cli/commands/subscription/utils.ts create mode 100644 packages/wrangler/src/queues/cli/commands/update.ts create mode 100644 packages/wrangler/src/queues/client.ts create mode 100644 packages/wrangler/src/queues/constants.ts create mode 100644 packages/wrangler/src/queues/subscription-types.ts create mode 100644 packages/wrangler/src/queues/utils.ts create mode 100644 packages/wrangler/src/r2/bucket.ts create mode 100644 packages/wrangler/src/r2/catalog.ts create mode 100644 packages/wrangler/src/r2/constants.ts create mode 100644 packages/wrangler/src/r2/cors.ts create mode 100644 packages/wrangler/src/r2/domain.ts create mode 100644 packages/wrangler/src/r2/helpers/bucket.ts create mode 100644 packages/wrangler/src/r2/helpers/bulk.ts create mode 100644 packages/wrangler/src/r2/helpers/catalog.ts create mode 100644 packages/wrangler/src/r2/helpers/domain.ts create mode 100644 packages/wrangler/src/r2/helpers/local-uploads.ts create mode 100644 packages/wrangler/src/r2/helpers/misc.ts create mode 100644 packages/wrangler/src/r2/helpers/notification.ts create mode 100644 packages/wrangler/src/r2/helpers/object.ts create mode 100644 packages/wrangler/src/r2/helpers/sippy.ts create mode 100644 packages/wrangler/src/r2/index.ts create mode 100644 packages/wrangler/src/r2/lifecycle.ts create mode 100644 packages/wrangler/src/r2/local-uploads.ts create mode 100644 packages/wrangler/src/r2/lock.ts create mode 100644 packages/wrangler/src/r2/notification.ts create mode 100644 packages/wrangler/src/r2/object.ts create mode 100644 packages/wrangler/src/r2/public-dev-url.ts create mode 100644 packages/wrangler/src/r2/sippy.ts create mode 100644 packages/wrangler/src/r2/sql.ts create mode 100644 packages/wrangler/src/secret/index.ts create mode 100644 packages/wrangler/src/secrets-store/client.ts create mode 100644 packages/wrangler/src/secrets-store/commands.ts create mode 100644 packages/wrangler/src/secrets-store/index.ts create mode 100644 packages/wrangler/src/sentry/index.ts create mode 100644 packages/wrangler/src/setup.ts create mode 100644 packages/wrangler/src/sites.ts create mode 100644 packages/wrangler/src/sourcemap.ts create mode 100644 packages/wrangler/src/streams.d.ts create mode 100644 packages/wrangler/src/tail/createTail.ts create mode 100644 packages/wrangler/src/tail/filters.ts create mode 100644 packages/wrangler/src/tail/index.ts create mode 100644 packages/wrangler/src/tail/printing.ts create mode 100644 packages/wrangler/src/triggers/index.ts create mode 100644 packages/wrangler/src/tsconfig-sanity.ts create mode 100644 packages/wrangler/src/tunnel/client.ts create mode 100644 packages/wrangler/src/tunnel/create.ts create mode 100644 packages/wrangler/src/tunnel/delete.ts create mode 100644 packages/wrangler/src/tunnel/dev.ts create mode 100644 packages/wrangler/src/tunnel/index.ts create mode 100644 packages/wrangler/src/tunnel/info.ts create mode 100644 packages/wrangler/src/tunnel/list.ts create mode 100644 packages/wrangler/src/tunnel/quick-start.ts create mode 100644 packages/wrangler/src/tunnel/run.ts create mode 100644 packages/wrangler/src/turnstile/client.ts create mode 100644 packages/wrangler/src/turnstile/create.ts create mode 100644 packages/wrangler/src/turnstile/delete.ts create mode 100644 packages/wrangler/src/turnstile/get.ts create mode 100644 packages/wrangler/src/turnstile/index.ts create mode 100644 packages/wrangler/src/turnstile/list.ts create mode 100644 packages/wrangler/src/turnstile/update.ts create mode 100644 packages/wrangler/src/type-generation/helpers.ts create mode 100644 packages/wrangler/src/type-generation/index.ts create mode 100644 packages/wrangler/src/type-generation/new-config.ts create mode 100644 packages/wrangler/src/type-generation/pipeline-schema.ts create mode 100644 packages/wrangler/src/type-generation/runtime/index.ts create mode 100644 packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts create mode 100644 packages/wrangler/src/update-check.ts create mode 100644 packages/wrangler/src/user/access.ts create mode 100644 packages/wrangler/src/user/auth-config-file.ts create mode 100644 packages/wrangler/src/user/auth-variables.ts create mode 100644 packages/wrangler/src/user/commands.ts create mode 100644 packages/wrangler/src/user/fetch-accounts.ts create mode 100644 packages/wrangler/src/user/generate-auth-url.ts create mode 100644 packages/wrangler/src/user/generate-random-state.ts create mode 100644 packages/wrangler/src/user/index.ts create mode 100644 packages/wrangler/src/user/keyring-preference.ts create mode 100644 packages/wrangler/src/user/membership.ts create mode 100644 packages/wrangler/src/user/preferences.ts create mode 100644 packages/wrangler/src/user/profile-store.ts create mode 100644 packages/wrangler/src/user/profiles.ts create mode 100644 packages/wrangler/src/user/shared.ts create mode 100644 packages/wrangler/src/user/temporary-account-path.ts create mode 100644 packages/wrangler/src/user/temporary-terms.ts create mode 100644 packages/wrangler/src/user/user.ts create mode 100644 packages/wrangler/src/user/whoami.ts create mode 100644 packages/wrangler/src/utils/add-created-resource-config.ts create mode 100644 packages/wrangler/src/utils/assert-never.ts create mode 100644 packages/wrangler/src/utils/box.ts create mode 100644 packages/wrangler/src/utils/collectKeyValues.ts create mode 100644 packages/wrangler/src/utils/constants.ts create mode 100644 packages/wrangler/src/utils/create-batches.ts create mode 100644 packages/wrangler/src/utils/debounce.ts create mode 100644 packages/wrangler/src/utils/dedent.ts create mode 100644 packages/wrangler/src/utils/detect-agent.ts create mode 100644 packages/wrangler/src/utils/did-you-mean.ts create mode 100644 packages/wrangler/src/utils/diff-json.ts create mode 100644 packages/wrangler/src/utils/diff.ts create mode 100644 packages/wrangler/src/utils/download-worker-config.ts create mode 100644 packages/wrangler/src/utils/error-codes.ts create mode 100644 packages/wrangler/src/utils/filesystem.ts create mode 100644 packages/wrangler/src/utils/format-message.ts create mode 100644 packages/wrangler/src/utils/friendly-validator-errors.ts create mode 100644 packages/wrangler/src/utils/getLegacyScriptName.ts create mode 100644 packages/wrangler/src/utils/getRules.ts create mode 100644 packages/wrangler/src/utils/getScriptName.ts create mode 100644 packages/wrangler/src/utils/getValidBindingName.ts create mode 100644 packages/wrangler/src/utils/is-local.ts create mode 100644 packages/wrangler/src/utils/isAbortError.ts create mode 100644 packages/wrangler/src/utils/levenshtein.ts create mode 100644 packages/wrangler/src/utils/log-file.ts create mode 100644 packages/wrangler/src/utils/logPossibleBugMessage.ts create mode 100644 packages/wrangler/src/utils/memoizeGetPort.ts create mode 100644 packages/wrangler/src/utils/merge-deep.ts create mode 100644 packages/wrangler/src/utils/mergeWithOverride.ts create mode 100644 packages/wrangler/src/utils/onKeyPress.ts create mode 100644 packages/wrangler/src/utils/placement.ts create mode 100644 packages/wrangler/src/utils/print-bindings.ts create mode 100644 packages/wrangler/src/utils/render-labelled-values.ts create mode 100644 packages/wrangler/src/utils/retry.ts create mode 100644 packages/wrangler/src/utils/shell-quote.ts create mode 100644 packages/wrangler/src/utils/sortObjectRecursive.ts create mode 100644 packages/wrangler/src/utils/std.ts create mode 100644 packages/wrangler/src/utils/strings.ts create mode 100644 packages/wrangler/src/utils/types.ts create mode 100644 packages/wrangler/src/utils/worker-not-found-error.ts create mode 100644 packages/wrangler/src/vectorize/client.ts create mode 100644 packages/wrangler/src/vectorize/common.ts create mode 100644 packages/wrangler/src/vectorize/create.ts create mode 100644 packages/wrangler/src/vectorize/createMetadataIndex.ts create mode 100644 packages/wrangler/src/vectorize/delete.ts create mode 100644 packages/wrangler/src/vectorize/deleteByIds.ts create mode 100644 packages/wrangler/src/vectorize/deleteMetadataIndex.ts create mode 100644 packages/wrangler/src/vectorize/get.ts create mode 100644 packages/wrangler/src/vectorize/getByIds.ts create mode 100644 packages/wrangler/src/vectorize/index.ts create mode 100644 packages/wrangler/src/vectorize/info.ts create mode 100644 packages/wrangler/src/vectorize/insert.ts create mode 100644 packages/wrangler/src/vectorize/list.ts create mode 100644 packages/wrangler/src/vectorize/listMetadataIndex.ts create mode 100644 packages/wrangler/src/vectorize/listVectors.ts create mode 100644 packages/wrangler/src/vectorize/query.ts create mode 100644 packages/wrangler/src/vectorize/types.ts create mode 100644 packages/wrangler/src/vectorize/upsert.ts create mode 100644 packages/wrangler/src/versions/api.ts create mode 100644 packages/wrangler/src/versions/deploy.ts create mode 100644 packages/wrangler/src/versions/deployments/index.ts create mode 100644 packages/wrangler/src/versions/deployments/list.ts create mode 100644 packages/wrangler/src/versions/deployments/status.ts create mode 100644 packages/wrangler/src/versions/deployments/view.ts create mode 100644 packages/wrangler/src/versions/index.ts create mode 100644 packages/wrangler/src/versions/list.ts create mode 100644 packages/wrangler/src/versions/rollback/index.ts create mode 100644 packages/wrangler/src/versions/secrets/bulk.ts create mode 100644 packages/wrangler/src/versions/secrets/delete.ts create mode 100644 packages/wrangler/src/versions/secrets/index.ts create mode 100644 packages/wrangler/src/versions/secrets/list.ts create mode 100644 packages/wrangler/src/versions/secrets/put.ts create mode 100644 packages/wrangler/src/versions/types.ts create mode 100644 packages/wrangler/src/versions/upload.ts create mode 100644 packages/wrangler/src/versions/view.ts create mode 100644 packages/wrangler/src/vitest.d.ts create mode 100644 packages/wrangler/src/vpc/client.ts create mode 100644 packages/wrangler/src/vpc/create.ts create mode 100644 packages/wrangler/src/vpc/delete.ts create mode 100644 packages/wrangler/src/vpc/get.ts create mode 100644 packages/wrangler/src/vpc/index.ts create mode 100644 packages/wrangler/src/vpc/list.ts create mode 100644 packages/wrangler/src/vpc/shared.ts create mode 100644 packages/wrangler/src/vpc/update.ts create mode 100644 packages/wrangler/src/vpc/validation.ts create mode 100644 packages/wrangler/src/websearch/client.ts create mode 100644 packages/wrangler/src/websearch/index.ts create mode 100644 packages/wrangler/src/websearch/search.ts create mode 100644 packages/wrangler/src/websearch/types.ts create mode 100644 packages/wrangler/src/worker.d.ts create mode 100644 packages/wrangler/src/workerd.d.ts create mode 100644 packages/wrangler/src/workflows/commands/delete.ts create mode 100644 packages/wrangler/src/workflows/commands/describe.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/describe.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/list.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/pause.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/restart.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/resume.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/send-event.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/terminate-all.ts create mode 100644 packages/wrangler/src/workflows/commands/instances/terminate.ts create mode 100644 packages/wrangler/src/workflows/commands/list.ts create mode 100644 packages/wrangler/src/workflows/commands/trigger.ts create mode 100644 packages/wrangler/src/workflows/index.ts create mode 100644 packages/wrangler/src/workflows/local.ts create mode 100644 packages/wrangler/src/workflows/types.ts create mode 100644 packages/wrangler/src/workflows/utils.ts create mode 100644 packages/wrangler/src/wrangler-banner.ts create mode 100644 packages/wrangler/src/xxhash-wasm.d.ts create mode 100644 packages/wrangler/src/yargs-types.ts create mode 100644 packages/wrangler/src/zones.ts create mode 100644 packages/wrangler/telemetry.md create mode 100644 packages/wrangler/templates/__tests__/pages-dev-util.test.ts create mode 100644 packages/wrangler/templates/__tests__/tsconfig-sanity.ts create mode 100644 packages/wrangler/templates/__tests__/tsconfig.json create mode 100644 packages/wrangler/templates/checked-fetch.js create mode 100644 packages/wrangler/templates/facade.d.ts create mode 100644 packages/wrangler/templates/middleware/common.ts create mode 100644 packages/wrangler/templates/middleware/loader-modules.ts create mode 100644 packages/wrangler/templates/middleware/loader-sw.ts create mode 100644 packages/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts create mode 100644 packages/wrangler/templates/middleware/middleware-miniflare3-json-error.ts create mode 100644 packages/wrangler/templates/middleware/middleware-patch-console-prefix.d.ts create mode 100644 packages/wrangler/templates/middleware/middleware-patch-console-prefix.ts create mode 100644 packages/wrangler/templates/middleware/middleware-pretty-error.ts create mode 100644 packages/wrangler/templates/middleware/middleware-scheduled.ts create mode 100644 packages/wrangler/templates/modules-watch-stub.js create mode 100644 packages/wrangler/templates/new-worker-scheduled.js create mode 100644 packages/wrangler/templates/new-worker-scheduled.ts create mode 100644 packages/wrangler/templates/new-worker.js create mode 100644 packages/wrangler/templates/new-worker.ts create mode 100644 packages/wrangler/templates/no-op-worker.js create mode 100644 packages/wrangler/templates/pages-dev-pipeline.ts create mode 100644 packages/wrangler/templates/pages-dev-util.ts create mode 100644 packages/wrangler/templates/pages-shim.ts create mode 100644 packages/wrangler/templates/pages-template-plugin.ts create mode 100644 packages/wrangler/templates/pages-template-worker.ts create mode 100644 packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts create mode 100644 packages/wrangler/templates/remoteBindings/wrangler.jsonc create mode 100644 packages/wrangler/templates/startDevWorker/InspectorProxyWorker.ts create mode 100644 packages/wrangler/templates/startDevWorker/ProxyWorker.ts create mode 100644 packages/wrangler/templates/tsconfig-sanity.ts create mode 100644 packages/wrangler/templates/tsconfig.init.json create mode 100644 packages/wrangler/templates/tsconfig.json create mode 100644 packages/wrangler/tsconfig.emit.json create mode 100644 packages/wrangler/tsconfig.experimental-config.json create mode 100644 packages/wrangler/tsconfig.json create mode 100644 packages/wrangler/tsdown.config.mts create mode 100644 packages/wrangler/tsup.config.ts create mode 100644 packages/wrangler/turbo.json create mode 100644 packages/wrangler/vitest.config.mts create mode 100644 patches/@cloudflare__component-listbox@1.10.6.patch create mode 100644 patches/@netlify__build-info.patch create mode 100644 patches/buffer-equal-constant-time@1.0.1.patch create mode 100644 patches/postal-mime.patch create mode 100644 patches/toucan-js@4.0.0.patch create mode 100644 patches/youch@4.1.0-beta.10.patch create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tools/README.md create mode 100644 tools/clean/__tests__/clean.test.ts create mode 100644 tools/clean/clean.ts create mode 100644 tools/dependabot/__tests__/generate-dependabot-pr-changesets.test.ts create mode 100644 tools/dependabot/generate-dependabot-pr-changesets.ts create mode 100644 tools/deployments/__tests__/changeset-version.test.ts create mode 100644 tools/deployments/__tests__/deploy-non-npm-packages.test.ts create mode 100644 tools/deployments/__tests__/validate-changesets.test.ts create mode 100644 tools/deployments/__tests__/validate-package-dependencies.test.ts create mode 100644 tools/deployments/__tests__/validate-pinned-dependencies.test.ts create mode 100644 tools/deployments/__tests__/validate-pr-description.test.ts create mode 100644 tools/deployments/alert-on-error.ts create mode 100644 tools/deployments/check-npm-version.ts create mode 100644 tools/deployments/deploy-non-npm-packages.ts create mode 100644 tools/deployments/validate-catalog-usage.ts create mode 100644 tools/deployments/validate-changesets.ts create mode 100644 tools/deployments/validate-fixtures.ts create mode 100644 tools/deployments/validate-package-dependencies.ts create mode 100644 tools/deployments/validate-pinned-dependencies.ts create mode 100644 tools/deployments/validate-pr-description.ts create mode 100644 tools/deployments/validate-private-packages.ts create mode 100644 tools/e2e/__tests__/common.test.ts create mode 100644 tools/e2e/common.ts create mode 100644 tools/e2e/e2eCleanup.ts create mode 100644 tools/e2e/runIndividualE2EFiles.ts create mode 100644 tools/github-workflow-helpers/__tests__/validate-action-pinning.test.ts create mode 100644 tools/github-workflow-helpers/validate-action-pinning.ts create mode 100644 tools/package.json create mode 100644 tools/test/run-test-file.ts create mode 100644 tools/tsconfig.json create mode 100644 tools/turbo.json create mode 100644 tools/vitest.config.mts create mode 100644 turbo.json create mode 100644 vitest.shared.ts create mode 100644 vitest.workspace.ts diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..4177918 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,127 @@ +# Changesets + +Every non-trivial change to the project - those that should appear in the changelog - must be captured in a "changeset". + +We use the [`changesets`](https://github.com/changesets/changesets/blob/main/README.md) tool for creating changesets, publishing versions and updating the changelog. + +## Creating a Changeset + +```sh +pnpm changeset +``` + +1. Select which packages are affected by the change +2. Choose whether the version requires a major, minor, or patch release +3. Write a description of the change (see format below) +4. Include the generated changeset in your commit: + ```sh + git add .changeset/*.md + ``` + +## Version Types + +- **patch**: Bug fixes, small improvements, documentation fixes +- **minor**: New features, new CLI commands, new configuration options, deprecations, and changes to experimental/beta/pre-1.0 features (including breaking changes to those features). When adding or changing experimental features, call this out explicitly in the changeset description. +- **major**: Breaking changes to stable features (when deprecations take effect, or functional breaking behavior is added). Note: breaking changes to experimental/beta features do NOT require a major version bump. + +**Important restrictions:** + +- Major versions for `wrangler` are currently **forbidden**. This rule will be removed when we are preparing for the next major release of `wrangler`. +- Major versions for other packages require strong justification +- If the change collects more analytics, it should be a minor even though there is no user-visible change. + +## Changeset Message Format + +``` + + +<BODY> +``` + +- **TITLE**: A single sentence with an imperative description of the change +- **BODY**: One or more paragraphs explaining the reason for the change and anything notable about the approach. Aim for more than one sentence but less than three paragraphs to keep it succinct and useful. Larger changes may warrant more detail. + +### Good Examples + +For a new feature (minor): + +```markdown +--- +"wrangler": minor +--- + +Add `wrangler d1 export` command for exporting D1 databases to SQL files + +You can now export your D1 database to a local SQL file: + +`wrangler d1 export my-database --output backup.sql` + +This is useful for creating backups or migrating data between databases. +``` + +For a bug fix (patch): + +```markdown +--- +"wrangler": patch +--- + +Fix `wrangler dev` failing to start when `wrangler.toml` contains Unicode characters + +Previously, projects with non-ASCII characters in configuration values would fail with "Invalid UTF-8 sequence". This is now handled correctly. +``` + +### Bad Examples (avoid these) + +- "fix bug" - What bug? What was the symptom? +- "update dependency" - Which one? Why? Any user impact? +- "Add new feature" - What feature? How do you use it? +- "refactor" - Why does this warrant a release? What's the user impact? + +## Formatting Rules + +### Markdown Headers + +Changeset descriptions must **NOT** use h1 (`#`), h2 (`##`), or h3 (`###`) headers. + +The changelog uses h3 for section headers, so any headers in changeset content must be h4 (`####`) or smaller. This prevents formatting issues in the generated changelog. + +### Code Examples + +For new features or significant changes, consider including a brief usage example. Examples can be helpful for users to understand new functionality, but they are not mandatory—use your judgment based on how self-explanatory the change is. + +When showing Wrangler configuration examples, use `wrangler.json` (with JSONC syntax for comments) rather than `wrangler.toml`. + +## Multiple Changesets + +If your PR makes multiple distinct user-facing changes, create separate changesets so each gets its own changelog entry. Don't lump unrelated changes together, and don't mix different types of changes (e.g., bug fix + new feature) in a single changeset. + +## Package Coverage + +Each changeset should reference all packages that have user-facing changes: + +- If a change affects multiple packages, list them all in the changeset +- Alternatively, create separate changesets for each package if the changes warrant different descriptions +- You do NOT need to include packages that will only be released because they depend on a changed package - changesets handles this automatically + +## When a Changeset is NOT Required + +- Changes that are purely internal refactoring with no user-facing impact +- Changes only to devDependencies +- Documentation-only changes within a package +- Test-only changes +- CI/workflow changes that don't affect package behavior + +## File Example + +Here's a complete example of a patch changeset file: + +```markdown +--- +"wrangler": patch +--- + +Replace the word "publish" with "deploy" everywhere + +We should be consistent with the word that describes how we get a worker to the edge. The command is `deploy`, so let's use that everywhere. +``` diff --git a/.changeset/bright-doors-evict.md b/.changeset/bright-doors-evict.md new file mode 100644 index 0000000..06f0ed8 --- /dev/null +++ b/.changeset/bright-doors-evict.md @@ -0,0 +1,7 @@ +--- +"miniflare": minor +--- + +Add `unsafeEvictDurableObject()` for targeted Durable Object eviction + +This lets users verify how a Durable Object recovers after its instance is torn down. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..12248e5 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@1.6.2/schema.json", + "changelog": [ + "@changesets/changelog-github", + { "repo": "cloudflare/workers-sdk" } + ], + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "bumpVersionsWithWorkspaceProtocolOnly": true, + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + }, + "ignore": [], + "privatePackages": { + "tag": true, + "version": true + } +} diff --git a/.changeset/fix-schema-editor-dirty-state.md b/.changeset/fix-schema-editor-dirty-state.md new file mode 100644 index 0000000..eae0ed4 --- /dev/null +++ b/.changeset/fix-schema-editor-dirty-state.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/local-explorer-ui": patch +--- + +Fix D1 schema editor tab not tracking unsaved changes + +The schema editor tab (edit table / create table) now correctly marks the tab as dirty when there are unsaved schema changes. This shows the unsaved changes indicator on the tab, triggers the browser's leave guard when navigating away, and prompts for confirmation when closing the tab. + +Additionally, column and constraint deletions now properly mark the schema as dirty. Previously, removing a column or constraint would filter the entry out of the state array entirely, causing the dirty-state check to miss the change. diff --git a/.changeset/fix-tanstack-no-install.md b/.changeset/fix-tanstack-no-install.md new file mode 100644 index 0000000..00d743c --- /dev/null +++ b/.changeset/fix-tanstack-no-install.md @@ -0,0 +1,7 @@ +--- +"create-cloudflare": patch +--- + +Pass `--no-install` to `@tanstack/cli create` during scaffolding + +Previously, the TanStack Start template did not pass `--no-install` to the framework CLI, causing dependencies to be installed twice: once by `@tanstack/cli create` and again by C3's own install step. This aligns the TanStack Start template with other framework templates that already skip the framework CLI's install. diff --git a/.changeset/gentle-lions-list-do-ids.md b/.changeset/gentle-lions-list-do-ids.md new file mode 100644 index 0000000..dc60f59 --- /dev/null +++ b/.changeset/gentle-lions-list-do-ids.md @@ -0,0 +1,5 @@ +--- +"miniflare": minor +--- + +Allow `listDurableObjectIds()` to accept Durable Object class names as well as binding names. diff --git a/.changeset/green-badgers-evict.md b/.changeset/green-badgers-evict.md new file mode 100644 index 0000000..9589046 --- /dev/null +++ b/.changeset/green-badgers-evict.md @@ -0,0 +1,12 @@ +--- +"wrangler": minor +--- + +Add Durable Object eviction support to `createTestHarness` + +You can now gracefully evict a running Durable Object by class name or binding name to verify how it recovers after its instance is torn down: + +```ts +const worker = server.getWorker(); +await worker.evictDurableObject("Counter", { name: "user-123" }); +``` diff --git a/.changeset/lazy-register-hooks.md b/.changeset/lazy-register-hooks.md new file mode 100644 index 0000000..c4d65a1 --- /dev/null +++ b/.changeset/lazy-register-hooks.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Fix load time crash on Node.js versions earlier than 22.15 + +The plugin eagerly imported `registerHooks` from `node:module`, which only exists on Node.js v22.15.0+. `registerHooks` is now read lazily, meaning that missing support is only surfaced when using `experimental.newConfig`. diff --git a/.changeset/pages-delegation-force-notice.md b/.changeset/pages-delegation-force-notice.md new file mode 100644 index 0000000..1b0a4db --- /dev/null +++ b/.changeset/pages-delegation-force-notice.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Improve the agent-facing `--force` guidance for Pages-to-Workers delegation + +When an AI agent opts out of the Pages-to-Workers delegation by passing `--force` to `wrangler pages deploy` or `wrangler pages project create`, Wrangler now prints a notice at the end of a successful command explaining that `--force` only needs to be passed once: the project then exists, so subsequent commands are no longer delegated and do not need the flag. diff --git a/.changeset/remove-service-environments.md b/.changeset/remove-service-environments.md new file mode 100644 index 0000000..e7fd8e3 --- /dev/null +++ b/.changeset/remove-service-environments.md @@ -0,0 +1,9 @@ +--- +"wrangler": minor +--- + +Remove support for service environments and the `legacy_env` configuration field + +Service environments have been removed. Wrangler now always deploys each environment as its own Worker named `<name>-<environment>`, which matches the behaviour of the previous default (`legacy_env = true`). The `--legacy-env` CLI flag has been removed, and the `legacy_env` configuration field is no longer supported — including it in your configuration file will now raise an error. + +Because `legacy_env = true` was already the default, removing the field will not change how your Worker is deployed. If you were relying on service environments (`legacy_env = false`), each environment will now be deployed as a standalone Worker instead of as an environment of a single Worker. See https://developers.cloudflare.com/workers/wrangler/environments/ for more information. diff --git a/.changeset/suggest-similar-commands-on-typo.md b/.changeset/suggest-similar-commands-on-typo.md new file mode 100644 index 0000000..d4eb587 --- /dev/null +++ b/.changeset/suggest-similar-commands-on-typo.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Suggest similar commands when a typo is detected + +When an unknown command or subcommand is entered, wrangler now suggests the closest matching command if one exists within a reasonable edit distance. For example, running `wrangler whoamio` will display `Did you mean "wrangler whoami"?`, and running `wrangler kv namespase` will display `Did you mean "wrangler kv namespace"?`. diff --git a/.codeowners b/.codeowners new file mode 100644 index 0000000..1fce657 --- /dev/null +++ b/.codeowners @@ -0,0 +1,81 @@ +# ============================================================ +# Codeowners Plus - Code Ownership Rules +# ============================================================ +# This file defines code ownership for the workers-sdk monorepo, +# enforced by Codeowners Plus (https://github.com/multimediallc/codeowners-plus). +# +# See CODEOWNERS.md for full documentation on how ownership works. +# +# Syntax: +# (no prefix) = primary owner (highest-priority match wins) +# & prefix = AND rule (additional required reviewer) +# ? prefix = optional/CC reviewer (non-blocking) +# Multiple teams on one line = OR (either can satisfy) +# +# Rules are relative to this file's directory (repo root). +# Unlike GitHub CODEOWNERS, `*.js` only matches in this directory; +# use `**/*.js` for recursive matching. +# +# Paths not matching any specific rule fall through to the default +# primary owner (currently @cloudflare/wrangler). +# ============================================================ + +# Default owner - ANT/Wrangler team owns everything +* @cloudflare/wrangler + +# ---------------------------------------------------------- +# Versioning & release files (wrangler-only, no AND rules) +# ---------------------------------------------------------- +# Only wrangler team approval is needed for releases. +# Product teams can request hold on release by reaching out. +**/CHANGELOG.md @cloudflare/wrangler +**/package.json @cloudflare/wrangler + +# ---------------------------------------------------------- +# D&C ownership (AND: requires wrangler + deploy-config) +# ---------------------------------------------------------- +# */** excludes root-level files (CHANGELOG.md, package.json) +# so releases only need wrangler approval +& packages/workers-shared/*/** @cloudflare/deploy-config + +# ---------------------------------------------------------- +# D1 ownership (AND: requires wrangler + d1) +# ---------------------------------------------------------- +& packages/wrangler/src/api/d1/** @cloudflare/d1 +& packages/wrangler/src/d1/** @cloudflare/d1 +& packages/wrangler/src/__tests__/d1/** @cloudflare/d1 + +# ---------------------------------------------------------- +# Cloudchamber ownership (AND: requires wrangler + cloudchamber) +# ---------------------------------------------------------- +& packages/wrangler/src/cloudchamber/** @cloudflare/cloudchamber +& packages/wrangler/src/containers/** @cloudflare/cloudchamber +& packages/wrangler/src/__tests__/containers/** @cloudflare/cloudchamber +# */** excludes root-level files (CHANGELOG.md, package.json) +& packages/containers-shared/*/** @cloudflare/cloudchamber + +# ---------------------------------------------------------- +# Workers KV ownership (AND: requires wrangler + workers-kv) +# ---------------------------------------------------------- +& packages/wrangler/src/kv/** @cloudflare/workers-kv +& packages/wrangler/src/__tests__/kv/** @cloudflare/workers-kv +& packages/miniflare/src/workers/kv/** @cloudflare/workers-kv +& packages/miniflare/test/plugins/kv/** @cloudflare/workers-kv + +# ---------------------------------------------------------- +# Workflows ownership (AND: requires wrangler + workflows) +# ---------------------------------------------------------- +# */** excludes root-level files (CHANGELOG.md, package.json) +& packages/workflows-shared/*/** @cloudflare/workflows + +# ---------------------------------------------------------- +# Adding a new product team +# ---------------------------------------------------------- +# Copy this template and fill in <feature> and <team>: +# +# # Product: <Name> (AND: requires wrangler + <team>) +# & packages/wrangler/src/<feature>/** @cloudflare/<team> +# & packages/wrangler/src/__tests__/<feature>/** @cloudflare/<team> +# & packages/miniflare/src/plugins/<feature>/** @cloudflare/<team> +# & packages/miniflare/src/workers/<feature>/** @cloudflare/<team> +# & packages/miniflare/test/plugins/<feature>/** @cloudflare/<team> diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e838df0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# https://editorconfig.org +root = true + +[*] +end_of_line = lf +indent_style = tab + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..d26ca6a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# Refactor from spaces to tabs +e5a6aca696108cda8c3890b8ce2ec44c6cc09a0e +# Prettier 2 -> 3 +f6300a71adb83e5f92cd9c9ddd56a85132e03f98 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c3e3a80 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +api-extractor.json linguist-language=JSON-with-Comments +tsconfig.emit.json linguist-language=JSON-with-Comments diff --git a/.gitguardian.yml b/.gitguardian.yml new file mode 100644 index 0000000..681691a --- /dev/null +++ b/.gitguardian.yml @@ -0,0 +1,4 @@ +version: 2 +secret: + ignored-paths: + - "packages/wrangler/src/__tests__/hyperdrive.test.ts" diff --git a/.github/ISSUE_TEMPLATE/bug-template.yaml b/.github/ISSUE_TEMPLATE/bug-template.yaml new file mode 100644 index 0000000..f286a08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-template.yaml @@ -0,0 +1,35 @@ +name: "\U0001F41B Bug Report" +description: Report an issue or possible bug +type: Bug + +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to file a bug report! Please fill out this form as completely as possible. + - type: textarea + attributes: + label: What versions & operating system are you using? + description: "Make sure you've checked that you meet our system requirements as listed here: https://developers.cloudflare.com/workers/wrangler/install-and-update/. Please provide the output of running `npx envinfo --system --npmPackages '{wrangler,create-cloudflare,miniflare,@cloudflare/*}' --binaries`" + placeholder: e.g. Wrangler v4.6, Node v20, Windows 11 etc... + validations: + required: true + - type: input + attributes: + label: Please provide a link to a minimal reproduction + placeholder: https://github.com/foobar-user/minimal-repro + validations: + required: true + - type: textarea + attributes: + label: Describe the Bug + description: Please describe the bug in as much detail as possible, including steps to reproduce, the behaviour you're seeing, and the behaviour you expect. + validations: + required: true + + - type: textarea + attributes: + label: Please provide any relevant error logs + description: Although not required, we often request logs to help troubleshoot the issue, so providing this up-front streamlines the process towards resolution. Please be careful to hide any sensitive information. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..fb7977d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Feature Request + url: https://github.com/cloudflare/workers-sdk/discussions/new?category=feature-requests + about: "Have a suggestion? Open a discussion to share your idea!" + - name: Support + url: https://discord.cloudflare.com + about: "Join us on Discord!" diff --git a/.github/actions/check-remote-tests/action.yml b/.github/actions/check-remote-tests/action.yml new file mode 100644 index 0000000..07f4802 --- /dev/null +++ b/.github/actions/check-remote-tests/action.yml @@ -0,0 +1,33 @@ +name: "Check Remote Tests" +description: > + Determines whether E2E tests should be given Cloudflare API credentials. + Returns true for merge-queue runs, Version Packages PRs, or when the + "ci:run-remote-tests" label is present on a pull request. +outputs: + run-remote: + description: "'true' when remote tests should run, 'false' otherwise" + value: ${{ steps.decide.outputs.run_remote }} +runs: + using: "composite" + steps: + - name: Decide whether to run remote tests + id: decide + shell: bash + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + HEAD_REF: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + if [ "$EVENT_NAME" = "merge_group" ] || \ + [ "$HEAD_REF" = "changeset-release/main" ]; then + echo "run_remote=true" >> "$GITHUB_OUTPUT" + elif [ "$EVENT_NAME" = "pull_request" ]; then + HAS_LABEL=$(gh pr view "$PR_NUMBER" \ + --repo "$REPO" --json labels \ + --jq '[.labels[].name] | any(. == "ci:run-remote-tests")') + echo "run_remote=${HAS_LABEL}" >> "$GITHUB_OUTPUT" + else + echo "run_remote=false" >> "$GITHUB_OUTPUT" + fi diff --git a/.github/actions/expose-actions-variables/action.yml b/.github/actions/expose-actions-variables/action.yml new file mode 100644 index 0000000..a44eeb0 --- /dev/null +++ b/.github/actions/expose-actions-variables/action.yml @@ -0,0 +1,5 @@ +name: "Expose Actions Variables" +description: "Expose ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL on GITHUB_ENV" +runs: + using: "node20" + main: "index.mjs" diff --git a/.github/actions/expose-actions-variables/index.mjs b/.github/actions/expose-actions-variables/index.mjs new file mode 100644 index 0000000..ea6b525 --- /dev/null +++ b/.github/actions/expose-actions-variables/index.mjs @@ -0,0 +1,7 @@ +import fs from "node:fs"; + +fs.appendFileSync( + process.env.GITHUB_ENV, + `GITHUB_ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}\n` + + `GITHUB_ACTIONS_RESULTS_URL=${process.env.ACTIONS_RESULTS_URL}\n` +); diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 0000000..59ca8a9 --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,62 @@ +name: "Install Dependencies" +description: "Install dependencies, fetching from cache when possible" +inputs: + # We run all jobs on Node.js 22 by default, as this is the most stable and supported version right now. + # + # Pinned to 22.23.1 (rather than floating `22`) until it's safe to float back: + # Node 22.23.0 regressed node-fetch@2 keep-alive responses with + # ERR_STREAM_PREMATURE_CLOSE (nodejs/node#63989, fixed in 22.23.1). + node-version: + description: the version of Node.js to install + default: 22.23.1 + turbo-api: + description: the api URL for connecting to the turbo remote cache + turbo-team: + description: the team identifier for connecting to the turbo remote cache + turbo-token: + description: the api token for connecting to the turbo remote cache + turbo-signature: + description: the cache signature key for connecting to the turbo remote cache + disable-cache: + description: when "true", skip the pnpm store cache on setup-node (defense in depth for release builds) + default: "false" +runs: + using: "composite" + steps: + - name: Install pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Install Node.js ${{ inputs.node-version }} (with pnpm cache) + if: inputs.disable-cache != 'true' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ inputs.node-version }} + cache: "pnpm" + registry-url: "https://registry.npmjs.org" + + - name: Install Node.js ${{ inputs.node-version }} (without pnpm cache) + if: inputs.disable-cache == 'true' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ inputs.node-version }} + registry-url: "https://registry.npmjs.org" + + # Enable node compile cache (effective for Node 22+) + # See https://nodejs.org/docs/v24.11.1/api/module.html#module-compile-cache + - name: Enable Node Compile Cache + shell: bash + run: echo "NODE_COMPILE_CACHE=$HOME/.node_compile_cache" >> $GITHUB_ENV + + - name: "Configure turbo repo cache environment variables" + if: inputs.turbo-api + shell: bash + run: | + echo "TURBO_API=${{ inputs.turbo-api }}" >> $GITHUB_ENV + echo "TURBO_TEAM=${{ inputs.turbo-team }}" >> $GITHUB_ENV + echo "TURBO_TOKEN=${{ inputs.turbo-token }}" >> $GITHUB_ENV + echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ inputs.turbo-signature }}" >> $GITHUB_ENV + echo "TURBO_ENV_MODE=loose" >> $GITHUB_ENV + + - name: Install NPM Dependencies + shell: bash + run: pnpm install --frozen-lockfile diff --git a/.github/actions/install-python-uv/action.yml b/.github/actions/install-python-uv/action.yml new file mode 100644 index 0000000..53e991a --- /dev/null +++ b/.github/actions/install-python-uv/action.yml @@ -0,0 +1,9 @@ +name: "Install Python uv" +runs: + using: "composite" + steps: + - name: Install uv for Python + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 + with: + version: "0.9.3" + enable-cache: false diff --git a/.github/bonk_reviewer.md b/.github/bonk_reviewer.md new file mode 100644 index 0000000..a2e2436 --- /dev/null +++ b/.github/bonk_reviewer.md @@ -0,0 +1,60 @@ +You are a **code reviewer**, not an author. You review pull requests for workers-sdk, which contains developer tooling for Cloudflare Workers. These instructions override any prior instructions about editing files or making code changes. + +## Restrictions -- you MUST follow these exactly + +Do NOT: + +- Edit, write, create, or delete any files -- use file editing tools (Write, Edit) under no circumstances +- Run `git commit`, `git push`, `git add`, `git checkout -b`, or any git write operation +- Approve or request changes on the PR -- only post review comments +- Flag formatting issues -- Prettier enforces style in this repo + +If you want to suggest a code change, post a `suggestion` comment instead of editing the file. + +## Output rules + +**Confirm you are acting on the correct issue or PR**. Verify that the issue or PR number matches what triggered you, and do not write comments or otherwise act on other issues or PRs unless explicitly instructed to. + +**If there are NO actionable issues:** Your ENTIRE response MUST be the four characters `LGTM` -- no greeting, no summary, no analysis, nothing before or after it. + +**If there ARE actionable issues:** Begin with "I'm Bonk, and I've done a quick review of your PR." Then: + +1. One-line summary of the changes. +2. A ranked list of issues (highest severity first). +3. For EVERY issue with a concrete fix, you MUST post it as a GitHub suggestion comment (see below). Do not describe a fix in prose when you can provide it as a suggestion. + +## How to post feedback + +You have write access to PR comments via the `gh` CLI. **Prefer the batch review approach** (one review with grouped comments) over posting individual comments. This produces a single notification and a cohesive review. + +### Batch review (recommended) + +Write a JSON file and submit it as a review. This is the most reliable method -- no shell quoting issues. + +````bash +cat > /tmp/review.json << 'REVIEW' +{ + "event": "COMMENT", + "body": "Review summary here.", + "comments": [ + { + "path": "src/workerd/api/example.c++", + "line": 42, + "side": "RIGHT", + "body": "Ownership issue -- `kj::Own` moved but still referenced:\n```suggestion\nauto result = kj::mv(owned);\n```" + } + ] +} +REVIEW +gh api repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews --input /tmp/review.json +```` + +Each comment needs `path`, `line`, `side`, and `body`. Use `suggestion` fences in `body` for applicable changes. + +- `side`: `"RIGHT"` for added or unchanged lines, `"LEFT"` for deleted lines +- For multi-line suggestions, add `start_line` and `start_side` to the comment object +- If `gh api` returns a 422 (wrong line number, stale commit), fall back to a top-level PR comment with `gh pr comment` instead of retrying + +## What counts as actionable + +Logic bugs, security issues (note a lot of this repo covers local development environments), backward compatibility violations, incorrect API behavior. Be pragmatic -- do not nitpick, do not flag subjective preferences. diff --git a/.github/changeset-version.js b/.github/changeset-version.js new file mode 100644 index 0000000..c5140cb --- /dev/null +++ b/.github/changeset-version.js @@ -0,0 +1,147 @@ +const assert = require("node:assert"); +const fs = require("node:fs"); +const path = require("node:path"); +const { execSync } = require("node:child_process"); + +// This script is used by the `release.yml` workflow to update the version of the packages being released. +// The standard step is only to run `changeset version` but this does not update the lockfile. +// So we also run `pnpm install`, which does this update. +// This is a workaround until this is handled automatically by `changeset version`. +// See https://github.com/changesets/changesets/issues/421. + +function getPkg(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} +function setPkg(filePath, newPkg) { + fs.writeFileSync(filePath, `${JSON.stringify(newPkg, null, "\t")}\n`); +} +function parseVersion(version) { + // Extract `<major>.<minor>.<patch>` from version (could be a constraint) + const match = /(\d+)\.(\d+)\.(\d+)/.exec(version); + assert(match !== null, `Expected ${version} to be <major>.<minor>.<patch>`); + return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]; +} + +const rootPath = path.resolve(__dirname, ".."); +const miniflarePath = path.join(rootPath, "packages/miniflare"); +const miniflarePkgPath = path.join(miniflarePath, "package.json"); + +function getWorkerdVersion() { + const pnpmWorkspacePath = path.join(rootPath, "pnpm-workspace.yaml"); + const match = /workerd: "(\d+\.\d+\.\d+)"/.exec( + fs.readFileSync(pnpmWorkspacePath, "utf8") + ); + assert(match !== null, `Expected ${match[1]} to be <major>.<minor>.<patch>`); + return match[1]; +} +/** + * Gets the correct version to bump `miniflare` to, ensuring the minor versions + * of `workerd` and `miniflare` match. Minor bumps in changesets will become + * patch bumps if the `workerd` version hasn't changed. + * See `changeset-version.test.js` for examples. + * + * @param workerdVersion `workerd` version constraint in `miniflare` package + * @param previousVersion `miniflare` version before running `changeset version` + * @param version `miniflare` version after running `changeset version` + */ +function getNextMiniflareVersion(workerdVersion, previousVersion, version) { + const [, workerdMinor] = parseVersion(workerdVersion); + const [, , previousPatch] = parseVersion(previousVersion); + const [major, minor] = parseVersion(version); + if (workerdMinor === minor) { + // If the minor versions match already, there's nothing we need to do + return version; + } else if (workerdMinor > minor) { + // If the workerd minor is greater than the miniflare minor, + // use the workerd minor and reset patch to 0 + return `${major}.${workerdMinor}.0`; + } else { + // Otherwise, if the workerd minor is less than the miniflare minor, + // use the workerd minor and bump the patch instead + return `${major}.${workerdMinor}.${previousPatch + 1}`; + } +} +exports.getNextMiniflareVersion = getNextMiniflareVersion; + +function main() { + // 1. Get `miniflare` version before applying changesets, so we know if the + // minor version was bumped + const previousMiniflarePkg = getPkg(miniflarePkgPath); + const previousMiniflareVersion = previousMiniflarePkg.version; + + // 2. Run standard `changeset version` command to apply changesets, bump + // versions, and update changelogs + console.log("Applying changesets and updating versions..."); + execSync("pnpm exec changeset version", { stdio: "inherit" }); + + // 3. Force `miniflare`'s minor version to be the same as `workerd` + console.log("Getting miniflare and workerd versions..."); + const miniflarePkg = getPkg(miniflarePkgPath); + const miniflareVersion = miniflarePkg.version; + const workerdVersion = getWorkerdVersion(); + const nextMiniflareVersion = getNextMiniflareVersion( + workerdVersion, + previousMiniflareVersion, + miniflareVersion + ); + if (nextMiniflareVersion !== miniflareVersion) { + // If `changeset version` didn't produce the correct version on its own... + + // ...update `miniflare`'s `package.json` version + console.log(`Updating miniflare version to ${nextMiniflareVersion}...`); + miniflarePkg.version = nextMiniflareVersion; + setPkg(miniflarePkgPath, miniflarePkg); + + const changedPathsBuffer = execSync("git ls-files --modified", { + cwd: rootPath, + }); + console.log("Checking modified files..."); + const changedPaths = changedPathsBuffer.toString().trim().split("\n"); + for (const relativeChangedPath of changedPaths) { + const changedPath = path.resolve(rootPath, relativeChangedPath); + const name = path.basename(changedPath); + if (name === "package.json") { + console.log("Updating dependencies in", changedPath); + // ...update `miniflare` version in dependencies of other packages + const pkg = getPkg(changedPath); + let changed = false; + for (const key of [ + "dependencies", + "devDependencies", + "peerDependencies", + "optionalDependencies", + ]) { + const constraint = pkg[key]?.["miniflare"]; + if (constraint === undefined) continue; + // Don't update `workspace:`-style constraints + if (constraint.startsWith("workspace:")) continue; + pkg[key]["miniflare"] = nextMiniflareVersion; + changed = true; + } + if (changed) setPkg(changedPath, pkg); + } else if (name === "CHANGELOG.md") { + console.log("Updating changelog in", changedPath); + // ...update `CHANGELOG.md`s with correct version + let changelog = fs.readFileSync(changedPath, "utf8"); + // Replace version header in `miniflare` `CHANGELOG.md` + changelog = changelog.replace( + `## ${miniflareVersion}`, + `## ${nextMiniflareVersion}` + ); + // Replace `Updated dependencies` line in other `CHANGELOG.md`s + changelog = changelog.replace( + `- miniflare@${miniflareVersion}`, + `- miniflare@${nextMiniflareVersion}` + ); + fs.writeFileSync(changedPath, changelog); + } + } + } + + // 4. Update the lockfile + console.log("Updating lockfile..."); + execSync("pnpm install --lockfile-only", { stdio: "inherit" }); + console.log("Done."); +} + +if (require.main === module) main(); diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4e5ecc6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,48 @@ +version: 2 +updates: + # Automatically check for updates in framework CLIs for C3 + - package-ecosystem: "npm" + open-pull-requests-limit: 10 + directory: "/packages/create-cloudflare/src/frameworks" + schedule: + interval: "weekly" + # Don't run these at midday on Mondays, when we are busy trying to land PRs + day: "sunday" + time: "06:00" + versioning-strategy: increase + # Match the 24h minimumReleaseAge enforced by pnpm in pnpm-workspace.yaml + # so Dependabot doesn't open PRs that CI will then reject. + cooldown: + default-days: 1 + # the following is used to add the [C3] prefix to the PR title, + # we override the commit message but setting a prefix like this + # makes it so that also the PR title gets such prefix + commit-message: + prefix: "[C3] " + labels: + - "package:c3" + - "dependencies" + - "ci:skip-pr-description-validation" + + # Check for workerd & workers-types updates for Miniflare + - package-ecosystem: "npm" + # If you restrict the update to a directory that is not the root + # then it will not update the pnpm-lock.yaml. + directory: "/" + groups: + # We want to keep workerd and workers-types updates in lock-step + workerd-and-workers-types: + patterns: + - "workerd" + - "@cloudflare/workers-types" + schedule: + interval: "daily" + time: "06:00" + versioning-strategy: increase + labels: + - "miniflare" + - "dependencies" + - "ci:skip-pr-description-validation" + allow: + - dependency-name: "workerd" + - dependency-name: "@cloudflare/workers-types" diff --git a/.github/extract-pr-and-workflow-id.js b/.github/extract-pr-and-workflow-id.js new file mode 100644 index 0000000..55810e7 --- /dev/null +++ b/.github/extract-pr-and-workflow-id.js @@ -0,0 +1,21 @@ +// This file is not used directly. +// Instead its contents are used in `.github/workflows/write-prerelease-comment.yml` +// Any changes here should be copied into the CI step there. +const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, +}); + +for (const artifact of allArtifacts.data.artifacts) { + // Extract the PR number from the artifact name + const match = /^npm-package-(.+)-(\d+)$/.exec(artifact.name); + if (match) { + const packageName = match[1].toUpperCase(); + require("fs").appendFileSync( + process.env.GITHUB_ENV, + `\nWORKFLOW_RUN_PR_FOR_${packageName}=${match[2]}` + + `\nWORKFLOW_RUN_ID_FOR_${packageName}=${context.payload.workflow_run.id}` + ); + } +} diff --git a/.github/get-c3-dependabot-bumped-framework.cjs b/.github/get-c3-dependabot-bumped-framework.cjs new file mode 100644 index 0000000..8bdde7f --- /dev/null +++ b/.github/get-c3-dependabot-bumped-framework.cjs @@ -0,0 +1,26 @@ +const { execSync } = require("child_process"); + +module.exports = function () { + const diff = execSync( + `git diff origin/${process.env.GITHUB_BASE_REF} -- packages/create-cloudflare/src/frameworks/package.json` + ).toString(); + + const changedPackages = diff + .match(/-\s*".*?":\s".*?",?/g) + .map((match) => match.match(/-\s*"(.*)":/)?.[1]) + .filter(Boolean); + + if (changedPackages.length === 0) { + console.warn("No changes detected!"); + return null; + } else if (changedPackages.length > 1) { + console.warn( + `More then one package has changed (${changedPackages.join( + ", " + )}), that's not currently supported` + ); + throw new Error("More than one package bump detected"); + } else { + return changedPackages[0]; + } +}; diff --git a/.github/holopin.yml b/.github/holopin.yml new file mode 100644 index 0000000..6c269f6 --- /dev/null +++ b/.github/holopin.yml @@ -0,0 +1,9 @@ +organization: cloudflare +stickers: + - id: cltg4w6r612040fl2r7vweuvv + alias: "ci:outstanding-contribution" + +holobytes: + - evolvingStickerId: cltg4rlqc39020fl51scnguhb + alias: contribution + in: true diff --git a/.github/opencode.json b/.github/opencode.json new file mode 100644 index 0000000..9b765bb --- /dev/null +++ b/.github/opencode.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://opencode.ai/config.json", + "disabled_providers": ["opencode"], + "enabled_providers": ["cloudflare-ai-gateway"], + "provider": { + "cloudflare-ai-gateway": { + "models": { + "anthropic/claude-opus-4-8": {}, + "anthropic/claude-sonnet-4-5": {}, + "workers-ai/@cf/moonshotai/kimi-k2.6": {} + } + } + }, + "permission": { + "read": "allow", + "edit": "allow", + "glob": "allow", + "grep": "allow", + "bash": "deny", + "task": "allow", + "skill": "allow", + "todoread": "allow", + "todowrite": "allow", + "webfetch": "deny" + } +} diff --git a/.github/prereleases/upload.mjs b/.github/prereleases/upload.mjs new file mode 100644 index 0000000..1648821 --- /dev/null +++ b/.github/prereleases/upload.mjs @@ -0,0 +1,50 @@ +import { execSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, "../.."); + +function getPackagePaths() { + const stdout = execSync( + 'pnpm list --filter="./packages/*" --recursive --depth=-1 --parseable', + { cwd: projectRoot, encoding: "utf8" } + ); + return stdout.split("\n").filter((pkgPath) => path.isAbsolute(pkgPath)); +} + +function getPackage(pkgPath) { + const json = fs.readFileSync(path.join(pkgPath, "package.json"), "utf8"); + return { + path: pkgPath, + json: JSON.parse(json), + }; +} + +function getPackages() { + return getPackagePaths().map(getPackage); +} + +export function getPackagesForPrerelease() { + return getPackages().filter((pkg) => pkg.json["workers-sdk"]?.prerelease); +} + +const pkgs = getPackagesForPrerelease(); + +spawnSync( + "pnpm", + [ + "dlx", + "pkg-pr-new", + "publish", + "--pnpm", + "--compact", + "--no-template", + ...pkgs.map((pkg) => pkg.path), + ], + { + stdio: "inherit", + } +); diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c4d9e9d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +Fixes #[insert GH or internal issue link(s)]. + +_Describe your change..._ + +--- + +<!-- +Please don't delete the checkboxes <3 +The following selections do not need to be completed if this PR only contains changes to .md files +--> + +- Tests + - [ ] Tests included/updated + - [ ] Automated tests not possible - manual testing has been completed as follows: + - [ ] Additional testing not necessary because: +- Public documentation + - [ ] Cloudflare docs PR(s): <!--e.g. <https://github.com/cloudflare/cloudflare-docs/pull/>...--> + - [ ] Documentation not necessary because: + +*A picture of a cute animal (not mandatory, but encouraged)* + +<!-- +Have you read our [Contributing guide](https://github.com/cloudflare/workers-sdk/blob/main/CONTRIBUTING.md)? +In particular, for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code. +--> diff --git a/.github/skills/issue-review.md b/.github/skills/issue-review.md new file mode 100644 index 0000000..4424519 --- /dev/null +++ b/.github/skills/issue-review.md @@ -0,0 +1,376 @@ +--- +name: github-issue-review +description: Triage a GitHub issue on cloudflare/workers-sdk. Assesses quality, identifies the component, checks for duplicates, and recommends next steps. +--- + +# GitHub Issue Triage Skill + +This skill triages a GitHub issue to assess its quality, identify the affected component, and recommend next steps for the maintainers. + +**Important:** This skill runs without bash or network access. All analysis is based on the pre-fetched issue data in `context.json`. Use only `read`, `glob`, `grep`, `edit`, and `write` tools. + +## Input + +Pre-fetched issue data at `data/<issue_number>/context.json` containing the full issue details from the GitHub API (title, body, comments, labels, author, dates). + +## Triage Process + +Perform the following steps in order. **Stop and skip to Output as soon as you have sufficient evidence for a recommendation.** + +### Step 1: Load Issue Details + +Read the pre-fetched `context.json` file using the `read` tool. + +Extract and note: + +- Title and description +- Issue type (bug report vs feature request vs question) +- Product/component affected +- Version reported against (if any) +- Operating system (if any) +- Reproduction steps or reproduction link (if any) +- Error messages (if any) +- Comment count and any relevant comment content +- Labels currently applied +- Issue age (created date) and last activity date +- `state_reason` if the issue is already closed + +### Step 2: Check for Closeable Issues + +Check each of the following categories in order. **STOP at the first match** and skip to Output. + +> **Note:** The `state_reason` values in this section (`completed`, `not_planned`) are informational — they indicate the appropriate GitHub close reason for maintainer reference. They are not captured as a field in the output report. + +#### 2a: Spam or Junk + +Recommend **CLOSE** (state_reason: `not_planned`, no comment needed) if: + +- The body contains random characters, test text, or nonsensical content (e.g. "bhhg", "ssssssssss", "aloll") +- The issue is clearly spam (e.g. loan advertisements, SEO spam, unrelated product promotions) +- The issue is a blank/empty test issue (e.g. title is "test" with no meaningful body) +- The body is entirely empty or contains only template headers with no content filled in + +#### 2b: Reporter Confirmed Resolved + +Recommend **CLOSE** (state_reason: `completed`) if: + +- The reporter explicitly confirmed the issue is resolved in comments +- The reporter said they found a workaround and no longer need a fix +- A maintainer indicated it should be closed in comments + +#### 2c: Already Fixed in a Prior Release + +Recommend **CLOSE** (state_reason: `completed`) if, based on references within the issue comments or linked PRs: + +- A comment references a PR or release that addresses this issue +- The issue describes a bug that was fixed in a version newer than the reporter's version +- The issue's symptoms match a fix described in a linked PR or comment + +When closing, cite the specific PR number and release version if known. + +**Template:** + +> This issue has been fixed in PR #XXXX, which was released in **wrangler X.Y.Z** (or the relevant package version). Please update and let us know if you're still seeing the issue. + +Or for older issues where the fix can't be pinpointed: + +> This is a fairly old issue, and from testing with the latest version, it appears to have been resolved. I'm going to close it for now — if you're still running into problems on the latest version, feel free to open a new issue with more details and we can investigate further. + +#### 2d: Duplicate + +Recommend **CLOSE** (state_reason: `not_planned`) if a comment or linked reference in the issue identifies it as a duplicate of another issue — for example, a maintainer has already commented pointing to a canonical issue, or the reporter themselves links to an existing report. + +When closing, link to the canonical issue. + +**Template:** + +> Closing as a duplicate of #XXXX. Please follow that issue for updates. + +If the duplicate is in another repo: + +> I'm closing this as a duplicate of <owner/repo>#XXXX, which is where this is being tracked. + +#### 2e: Not workers-sdk / Wrong Repo + +Recommend **CLOSE** (state_reason: `not_planned`) if: + +- The issue describes Workers **runtime** behavior (fetch API quirks, V8 issues, compatibility flags, crypto APIs) — belongs in [cloudflare/workerd](https://github.com/cloudflare/workerd) +- The issue is about a third-party framework (Nuxt, SvelteKit, Remix, Hono, React Router) and the bug is in that framework's code, not in Wrangler or the Vite plugin +- The issue is about an upstream tool (esbuild, Bun, Vite core) rather than Cloudflare's integration +- The issue is an account/billing/abuse problem — belongs in Cloudflare Support + +**Templates:** + +For framework issues: + +> It looks like this issue is coming from <framework>, rather than from workers-sdk. I'd recommend opening an issue on that project's repo: <link>. If it turns out to be a workers-sdk issue after all, feel free to open a new issue here with more details. + +For runtime issues: + +> This looks like a Workers runtime issue rather than a tooling issue. The right place to track this is [cloudflare/workerd](https://github.com/cloudflare/workerd) — could a maintainer transfer this issue there? + +(Set Action field to: "Transfer to cloudflare/workerd, then post this comment.") + +For account/support issues: + +> Unfortunately, we're unable to provide support for account-level issues via GitHub. Please contact Cloudflare Support at https://dash.cloudflare.com/?to=/:account/support or the email address mentioned in the error message. + +#### 2f: Stale / No Response + +Recommend **CLOSE** (state_reason: `not_planned`) if: + +- The issue has the `awaiting reporter response` or `needs reproduction` label AND the last activity was >30 days ago +- The issue is >12 months old with no activity in the last 6 months +- A maintainer asked for a reproduction or clarification and the reporter never responded + +**Template:** + +> We haven't heard from you in a while so I'm going to close this issue for now. If you're still running into problems, feel free to open a new issue with more details and we can investigate further. + +For very old issues: + +> This is a very old issue so I'm going to close it for now. If you're still running into problems on the latest version, feel free to comment with more details and we can investigate further. + +#### 2g: Transient Platform Issue + +Recommend **CLOSE** (state_reason: `completed`) if: + +- The issue describes a one-time API error (500, 503, auth failures) that appears to have been a service incident +- Multiple users reported the same error around the same time, and it has since stopped +- The error is from Cloudflare's API (not from Wrangler itself) and there's no way to fix it in workers-sdk + +**Template:** + +> This was a transient API issue, and should be resolved now. If you're still seeing this error, please open a new issue with the latest details and we can investigate further. + +Or for old transient issues: + +> This appears to have been a transient server-side API issue. Similar reports were caused by Cloudflare API incidents that have since been resolved. I'm going to close this for now. + +#### 2h: User Error / Misunderstanding + +Recommend **CLOSE** (state_reason: `not_planned`) if: + +- The issue describes expected behavior that the reporter misunderstands +- The issue is caused by incorrect configuration, wrong SQL syntax, or misuse of an API +- The fix is a documentation pointer rather than a code change + +When closing, explain the correct approach and link to relevant documentation. Be helpful, not dismissive. + +**Template:** + +> This isn't a bug — <brief explanation of the correct behavior>. You can find more details in the docs: <link>. I'm going to close this for now, but feel free to ask on our [Discord](https://discord.cloudflare.com) if you have more questions. + +#### 2i: Breaking Change + +Recommend **KEEP OPEN** if: + +- The requested change would be a breaking change in the current major version + +The issue should remain open as a tracking item. Do not close it — the team hasn't decided against the change, it's deferred to a future major version. + +**Suggested labels:** `breaking change` + +**Template:** + +> Changing this behavior at this stage would be a breaking change, so it will need to wait for the next major version. I've added the `breaking change` label to track this. + +(Set Action field to: "Apply `breaking change` label, then post this comment.") + +#### 2j: Won't Fix / By Design + +Recommend **CLOSE** (state_reason: `not_planned`) if: + +- The behavior is intentional and documented +- The team has explicitly decided not to implement this feature +- The cost/complexity of the change outweighs the benefit + +**Templates:** + +For design decisions: + +> This is intentional behavior — <brief explanation>. I don't think we're likely to change this in the near future, but we appreciate the feedback. + +For feature requests that won't happen: + +> Thanks for the suggestion. We don't intend to implement this right now, but it may be revisited in future. <Optional: brief explanation of why, or pointer to an alternative approach.> + +#### 2k: Feature Superseded / Deprecated + +Recommend **CLOSE** (state_reason: `completed`) if: + +- The requested feature has been implemented via a different mechanism than proposed +- The issue relates to a deprecated feature that has a replacement +- A newer feature or package addresses the underlying need + +**Template:** + +> This has been addressed by <feature/package>. <Brief explanation of how it solves the original request.> I'm going to close this for now. + +### Step 3: Check for Insufficient Information + +If the issue wasn't caught by Step 2, recommend **NEEDS MORE INFO** if: + +- Bug report has no reproduction steps or link (the bug template requires one) +- Bug report is missing version information +- The description is too vague to act on (e.g. "it doesn't work" with no details) +- The error message is missing or truncated + +**Template:** + +> Thanks for reporting this. Could you provide <specific missing information>? In particular: +> +> - <specific question 1> +> - <specific question 2> +> +> A minimal reproduction (a GitHub repo or link we can clone and run) would also help us investigate. Without more details, we won't be able to look into this. + +**Suggested label:** `awaiting reporter response` (and optionally `needs reproduction`) + +**STOP HERE if** the issue clearly needs more info. Skip to Output. + +### Step 4: Identify Component + +Map the issue to a package based on labels, title, and body content: + +| Signal | Package | +| ------------------------------------------------------------------------ | ----------------------------------------------- | +| `wrangler` label, wrangler CLI commands, `wrangler.toml`/`wrangler.json` | `packages/wrangler` | +| `miniflare` label, local dev simulation | `packages/miniflare` | +| `d1` label, D1 database, `d1 execute`, migrations | `packages/wrangler` (D1 code is in wrangler) | +| `vitest` label, worker tests, `vitest-pool-workers` | `packages/vitest-pool-workers` | +| `vite-plugin` label, vite dev, `@cloudflare/vite-plugin` | `packages/vite-plugin-cloudflare` | +| `c3` label, `create-cloudflare`, project scaffolding | `packages/create-cloudflare` | +| `pages` label, Pages deployment, `_routes.json`, `_headers` | `packages/wrangler` (Pages code is in wrangler) | +| `Workers + Assets` label, static asset serving | `packages/wrangler` | +| `containers` label, container registry | `packages/wrangler` | +| `workflows` label, Workflows API | `packages/wrangler` | +| `workers-builds` label | Workers Builds (may be internal) | +| `python` label, Python Workers | `packages/wrangler` | +| `workers for platforms` label, dispatch namespaces | `packages/wrangler` | +| `kv-asset-handler` label | `packages/kv-asset-handler` | +| `types` label, `wrangler types` command | `packages/wrangler` | +| R2, KV, Queues, Durable Objects, Vectorize bindings | `packages/wrangler` | +| `node compat`/`nodejs compat` label, Node.js APIs | May be workerd or wrangler depending on context | +| Workers runtime behavior (not tooling) | Likely belongs in cloudflare/workerd | +| Cloudflare dashboard, API behavior | Likely a platform issue, not workers-sdk | + +### Step 5: Assess Reproducibility and Severity + +For **bug reports**, evaluate: + +- **Has reproduction?** Does the issue include a minimal repro link or clear steps? +- **Severity estimate:** Is this a crash, data loss, incorrect behavior, or cosmetic issue? +- **Scope:** Does it affect all users or a specific configuration? +- **Workaround available?** Did the reporter or comments mention one? +- **Version gap:** Is the reporter on an old version? Could updating fix this? + +For **feature requests**, evaluate: + +- **Clarity:** Is the proposed solution clearly described? +- **Use case:** Is the motivation explained? +- **Scope:** Small enhancement vs large new feature? +- **Existing alternatives:** Is there already a way to achieve this (even if less convenient)? + +## Output Format + +**How the output is used:** `report.md` is posted verbatim as a maintainer-facing triage comment on the issue (attributed to an automated bot), and the labels in `summary.json` are applied automatically. Because of this: + +- Write `report.md` for maintainers, not the reporter. The **Suggested Comment** section is a _draft_ for a maintainer to send to the reporter — it is **not** posted automatically, so do not phrase the surrounding report as though a reply has already been made. +- Only put labels in the **Suggested Labels** fields that already exist in the repository. If unsure whether a label exists, omit it rather than guess — non-existent labels are dropped before applying, so inventing labels only weakens the report's accuracy. + +### Report Directory Structure + +``` +./data/<issue_number>/ +├── report.md # Full detailed report +└── summary.json # Structured JSON summary +``` + +### Output Step 1: Write Full Report + +Write the full report to `./data/<issue_number>/report.md`: + +```markdown +# Issue Triage: <owner/repo>#<number> + +## Summary + +<One-line summary of the issue> + +## Classification + +- **Type:** <Bug | Feature Request | Question | Discussion> +- **Component:** <package name or "unknown"> +- **Severity:** <Critical | High | Medium | Low | N/A> +- **Has Reproduction:** <Yes (with link) | No | N/A> +- **Quality:** <Complete | Partial | Incomplete> + +## Findings + +- **Created:** <date> +- **Author:** <username> +- **Version:** <reported version, or "not specified"> +- **Labels:** <labels, or "none"> +- **Comments:** <count> + +### Analysis + +<2-5 bullet points covering what you found — component identification, +reproducibility assessment, severity reasoning, staleness indicators, +duplicate candidates. Only include what's relevant.> + +## Recommendation + +**Status:** <CLOSE | KEEP OPEN | NEEDS MORE INFO | NEEDS VERIFICATION> + +**Reasoning:** <2-3 sentences explaining why> + +**Action:** <What a maintainer should do next> + +**Suggested Labels:** <labels to add, if any. Use existing repo labels only.> + +### Suggested Comment + +> <The exact comment to post on the issue. Follow the templates above for the +> matching closure category. Key principles: +> +> - Always offer an escape hatch ("feel free to open a new issue") +> - Link to specific PRs, releases, or docs when available +> - Be concise for straightforward closures, detailed for design decisions +> - Never be dismissive or snarky, even for spam (just close silently) or user error +> - For NEEDS MORE INFO, ask specific questions (not generic "please provide more details") +> Omit this section entirely for spam (close without comment) or if no comment is needed.> +``` + +### Output Step 2: Write Summary File + +Write a machine-readable summary as **JSON** to `./data/<issue_number>/summary.json`. This file is parsed by the triage workflow (to build the dashboard payload and apply labels), so it must be valid JSON with exactly these keys: + +```json +{ + "issueNumber": <issue_number as a number>, + "title": "<issue title, with emoji/template prefixes like 'Bug:' removed>", + "githubUrl": "https://github.com/<owner>/<repo>/issues/<issue_number>", + "recommendation": "<CLOSE | KEEP OPEN | NEEDS MORE INFO | NEEDS VERIFICATION>", + "difficulty": "<easy | medium | hard | n/a>", + "reasoning": "<brief reasoning, 1-2 sentences>", + "suggestedAction": "<brief description of next steps>", + "hasSuggestedComment": <true if a Suggested Comment section is present in the report, false otherwise>, + "suggestedLabels": ["<label>", "..."] +} +``` + +**Key definitions:** + +- **issueNumber**: The issue number as a JSON number (not a string). +- **title**: Issue title with any emoji prefixes (e.g. "🐛 Bug:") or template prefixes removed. +- **githubUrl**: Full URL to the issue. +- **recommendation**: One of `CLOSE`, `KEEP OPEN`, `NEEDS MORE INFO`, `NEEDS VERIFICATION`. +- **difficulty**: Estimated fix difficulty — `easy`, `medium`, `hard`, or `n/a` (for feature requests or closures). +- **reasoning**: Brief summary of why (1-2 sentences). +- **suggestedAction**: Brief description of next steps. +- **hasSuggestedComment**: Boolean — `true` if the report includes a Suggested Comment, `false` otherwise. +- **suggestedLabels**: JSON array of labels to apply, matching the **Suggested Labels** field in the report. Use existing repo labels only (see above). Use an empty array `[]` if there are no labels to apply. + +**CRITICAL:** Write valid, parseable JSON only (no trailing commas, no comments, no surrounding markdown fences). Because JSON strings are escaped, `reasoning`/`suggestedAction` may safely contain any punctuation. diff --git a/.github/version-script.js b/.github/version-script.js new file mode 100644 index 0000000..fb13807 --- /dev/null +++ b/.github/version-script.js @@ -0,0 +1,35 @@ +/** + * Update the package.json version property for the given package + * + * Usage: + * + * ``` + * node ./.github/version-script.js <package-name> + * ``` + * + * `<package-name>` defaults to `wrangler` if not provided. + */ + +const { readFileSync, writeFileSync } = require("fs"); +const { execSync } = require("child_process"); + +try { + const packageName = getArgs()[0] ?? "wrangler"; + const packageJsonPath = `./packages/${packageName}/package.json`; + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + const stdout = execSync("git rev-parse --short HEAD", { encoding: "utf8" }); + pkg.version = "0.0.0-" + stdout.trim(); + writeFileSync(packageJsonPath, JSON.stringify(pkg, null, "\t") + "\n"); +} catch (error) { + console.error(error); + process.exit(1); +} + +/** + * Get the command line args, stripping `node` and script filename, etc. + */ +function getArgs() { + const args = Array.from(process.argv); + while (args.shift() !== module.filename) {} + return args; +} diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..ee5eaff --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,151 @@ +# GitHub Actions + +See below for a summary of this repo's Actions + +- _Actions marked with "⚠️" are expected to sometimes fail._ + +## Security auditing + +We use [`zizmor`](https://docs.zizmor.sh/) to audit GitHub Actions workflow definitions and keep CI workflows as safe as possible. When changing files in this directory, run: + +```sh +zizmor .github/workflows/*.yml +``` + +Workflow changes should avoid unsuppressed `zizmor` findings. In particular: + +- Pin external actions to immutable commit SHAs, not tags. +- Use `actions/checkout` v6 or newer so persisted credentials are stored under `$RUNNER_TEMP`; set `persist-credentials: false` when a job does not need follow-up authenticated Git operations. +- Pass GitHub expression values into shell steps through `env` instead of expanding `${{ ... }}` directly inside `run` blocks. +- Treat privileged triggers such as `pull_request_target` and `workflow_run` as security-sensitive. If a privileged trigger is required, document the safety model and add a targeted `zizmor` ignore with a reason. + +## PR related actions + +### Tests + Checks (test-and-check.yml) + +- Triggers + - Updates to PRs. + - PRs in the merge queue. +- Actions + - Builds all the packages. + - Runs formatting, linting and type checks. + - Runs fixture tests, Wrangler unit tests, C3 unit tests, Miniflare unit tests, and ESLint + Prettier checks. + - Adds the PR to a GitHub project + - Makes sure that Wrangler's warning for old Node.js versions works. + +### Wrangler E2E tests (e2e-wrangler.yml) + +- Triggers + - Updates to PRs on the Cloudflare fork. + - PRs in the merge queue. +- Actions + - Runs the E2E tests for Wrangler. + - Cloudflare API credentials are only passed on Version Packages PRs (`changeset-release/main`), in the merge queue, or when the `ci:run-remote-tests` label is applied. Other PRs run the E2E suite without remote tests. + +### Vite Plugin E2E tests (e2e-vite.yml) + +- Triggers + - Updates to PRs on the Cloudflare fork. + - PRs in the merge queue. +- Actions + - Runs the E2E tests for the Vite plugin. + - Cloudflare API credentials are only passed on Version Packages PRs (`changeset-release/main`), in the merge queue, or when the `ci:run-remote-tests` label is applied. Other PRs run the E2E suite without remote tests. + +## Deploy Pages Previews (deploy-pages-preview.yml) + +- Triggers + - Updates to PRs that have one of the `preview:...` labels. +- Actions + - Deploy a preview of the matching Pages project to Cloudflare. + +## Deploy (to testing) and Test Playground Preview Worker (worker-playground-preview-testing-env-deploy-and-test.yml) + +- Triggers + - Commits merged to the `main` branch, on the Cloudflare fork, which touch files in the `packages/playground-preview-worker` directory. + - Updates to PRs, on the Cloudflare fork, with the `playground-worker` label applied. +- Actions + - Runs integrations tests to ensure the behaviour of the Worker powering the Workers Playground. + +## Create Pull Request Prerelease (prerelease.yml) + +- Triggers + - Updates to PRs. +- Actions + - Creates an installable pre-release of any package containing `{ "workers-sdk": { "prerelease": true } }` in its `package.json` (e.g. Wrangler, C3, and Miniflare) on every PR. + - Adds a comment to the PR with links to the pre-releases. + +## Housekeeping actions + +### Add issues to DevProd project (issues.yml) + +- Triggers + - Updates to issues. +- Actions + - Add the issue to a GitHub project. + +### Triage Issue (triage-issue.yml) + +- Triggers + - A new issue is opened (skips PRs and bot-authored issues). + - A new comment is created on an issue — re-triages with the latest context. Skips PRs, bot comments, and the triage bot's own sticky comment (matched by the sticky-comment marker) to avoid re-triage loops. + - Manual `workflow_dispatch` with an `issue-number` input. +- Actions + - Runs an OpenCode agent with the `.github/skills/issue-review.md` skill against the pre-fetched issue data (including existing comments) to produce a markdown triage report (`report.md`) and a structured JSON summary (`summary.json`). + - Uploads the report to the triage dashboard. + - Posts the report and structured summary as a maintainer-facing sticky comment on the issue and applies the suggested labels (validated against existing repo labels). Because the comment is sticky, re-triage updates the existing comment rather than posting a duplicate. Comments and labels are attributed to the workers-devprod bot via `GH_ACCESS_TOKEN`. + - The AI agent itself runs sandboxed (no shell or network access); all GitHub writes happen in workflow steps from the generated files. + +### Generate changesets for dependabot PRs (c3-dependabot-versioning-prs.yml and miniflare-dependabot-versioning-prs.yml) + +- Triggers + - Updates to PRs, by the dependabot user, which update one of: + - frameworks dependencies in C3, + - miniflare. +- Actions + - Generates changesets for the affected package. + +### E2E Project Cleanup (e2e-project-cleanup.yml) + +- Triggers + - Scheduled to run at 3am each day. +- Actions + - Deletes any Workers and Pages projects that were not properly cleaned up by the E2E tests. + +## Main branch actions + +### Handle Changesets (changesets.yml) + +- Triggers + - Commits merged to the `main` branch, on the Cloudflare fork. +- Actions + - If there are changeset in the working directory, create or update a "Version Packages" PR to prep for a release. + - If there are no changesets, release any packages that have a bump to their version in this change. + - Public packages are deployed to npm + - Private packages will run their `deploy` script, if they have one. + +## C3 related actions + +### C3 E2E Tests (c3-e2e.yml) + +- Triggers + - Updates to PRs. +- Actions + - Runs the E2E tests for C3. + - Cloudflare API credentials are only passed on Version Packages PRs (`changeset-release/main`), in the merge queue, or when the `ci:run-remote-tests` label is applied. Other PRs run the E2E suite without remote tests. + +### Rerun Code Owners (rerun-codeowners.yml + rerun-codeowners-privileged.yml) + +- Triggers + - A review is submitted or dismissed on a PR. +- Actions + - Re-runs the "Run Codeowners Plus" check so it re-evaluates approval status after the review change. + - Uses the `workflow_run` pattern: the trigger workflow exists solely to fire a `workflow_run` event; the privileged companion workflow (which has full permissions) reads the PR head SHA from `github.event.workflow_run.head_sha` and performs the re-run. This is necessary because `pull_request_review` gives a read-only token for fork PRs and has no `_target` variant. + +### Rerun Remote Tests (rerun-remote-tests.yml) + +- Triggers + - The `ci:run-remote-tests` or `run-c3-frameworks-tests` label is added to or removed from a PR. +- Actions + - Re-runs the E2E workflows for the PR so they pick up the label change and pass (or withhold) API credentials to the test steps. + - `ci:run-remote-tests` re-runs Wrangler, Vite, and C3 E2E workflows; `run-c3-frameworks-tests` re-runs only C3 E2E. + - Uses `pull_request_target` to get a privileged token even for fork PRs (safe because no untrusted code is checked out). diff --git a/.github/workflows/bonk-pr-review.yml b/.github/workflows/bonk-pr-review.yml new file mode 100644 index 0000000..a19e00b --- /dev/null +++ b/.github/workflows/bonk-pr-review.yml @@ -0,0 +1,69 @@ +name: New PR Review + +on: + pull_request: + types: [opened] + +jobs: + review: + # Skip Version Packages PRs (auto-generated by the changesets action) since they don't need a Bonk review + if: | + github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name && + !(github.event.pull_request.base.repo.owner.login == 'cloudflare' && github.event.pull_request.head.ref == 'changeset-release/main') + runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false + permissions: + id-token: write + contents: read + issues: write + pull-requests: write + steps: + - name: Check if PR author is Cloudflare org member + run: | + STATUS=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/orgs/cloudflare/members/${PR_AUTHOR}" \ + --silent -i 2>/dev/null | head -1 | awk '{print $2}') || true + if [ "$STATUS" != "204" ]; then + echo "User ${PR_AUTHOR} is not a member of the Cloudflare organization" + exit 1 + fi + echo "User ${PR_AUTHOR} is a Cloudflare org member" + env: + GH_TOKEN: ${{ secrets.READ_ONLY_ORG_GITHUB_TOKEN }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 30 + persist-credentials: false + + - name: Load review prompt + id: prompt + run: | + { + echo 'value<<EOF' + echo "You are reviewing PR #${{ github.event.pull_request.number }} on ${{ github.repository }}." + echo "" + cat .github/bonk_reviewer.md + echo EOF + } >> "$GITHUB_OUTPUT" + + - name: Run Bonk + uses: ask-bonk/ask-bonk/github@24832587005550860c8ad13fd96519e731ca632a # main + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }} + CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }} + with: + model: "cloudflare-ai-gateway/anthropic/claude-opus-4-8" + variant: "high" + forks: "false" + permissions: write + opencode_version: 1.15.13 # pin to this version as certain ones cause ProviderInitError issues + prompt: ${{ steps.prompt.outputs.value }} diff --git a/.github/workflows/bonk.yml b/.github/workflows/bonk.yml new file mode 100644 index 0000000..e26078f --- /dev/null +++ b/.github/workflows/bonk.yml @@ -0,0 +1,80 @@ +name: Bonk + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} + cancel-in-progress: false + +jobs: + bonk: + if: github.event.sender.type != 'Bot' && (contains(github.event.comment.body, '/bonk') || contains(github.event.comment.body, '@ask-bonk')) + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + id-token: write + contents: write + issues: write + pull-requests: write + steps: + - name: Check if comment author is Cloudflare org member + run: | + STATUS=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/orgs/cloudflare/members/${COMMENT_AUTHOR}" \ + --silent -i 2>/dev/null | head -1 | awk '{print $2}') || true + if [ "$STATUS" != "204" ]; then + echo "User ${COMMENT_AUTHOR} is not a member of the Cloudflare organization" + exit 1 + fi + echo "User ${COMMENT_AUTHOR} is a Cloudflare org member" + env: + GH_TOKEN: ${{ secrets.READ_ONLY_ORG_GITHUB_TOKEN }} + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Build prompt with triggering comment + id: prompt + run: | + { + DELIMITER=$(openssl rand -hex 16) + echo "value<<$DELIMITER" + echo "You were invoked by @${COMMENT_AUTHOR} on ${COMMENT_URL}" + echo "" + echo "Their comment:" + echo '```' + echo "$COMMENT_BODY" + echo '```' + echo "" + echo "This is your task. Read it carefully and act on it." + echo "$DELIMITER" + } >> "$GITHUB_OUTPUT" + env: + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + COMMENT_BODY: ${{ github.event.comment.body }} + COMMENT_URL: ${{ github.event.comment.html_url }} + + - name: Run Bonk + uses: ask-bonk/ask-bonk/github@c39e982defd0114385df54e72012a3fc4333c4d4 + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }} + CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }} + with: + model: "cloudflare-ai-gateway/anthropic/claude-opus-4-8" + variant: "high" + mentions: "/bonk,@ask-bonk" + permissions: write + agent: bonk + opencode_version: 1.15.13 # pin to this version as certain ones cause ProviderInitError issues + prompt: ${{ steps.prompt.outputs.value }} diff --git a/.github/workflows/c3-dependabot-versioning-prs.yml b/.github/workflows/c3-dependabot-versioning-prs.yml new file mode 100644 index 0000000..9e1964a --- /dev/null +++ b/.github/workflows/c3-dependabot-versioning-prs.yml @@ -0,0 +1,39 @@ +name: "C3 - Generate changesets for dependabot PRs" +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] dependabot-only job requires write access to push generated changesets; checkout v6 persists credentials under RUNNER_TEMP + paths: + - "packages/create-cloudflare/src/frameworks/package.json" + +permissions: + # content:write permission needed to update add changesets to dependabot PRs + # (see tools/dependabot/generate-dependabot-pr-changesets.ts) + contents: write + +jobs: + generate-changeset: + runs-on: ubuntu-slim + if: | + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 2 + persist-credentials: true + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_ACCESS_TOKEN }} + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Configure Git + run: | + git config --global user.email wrangler@cloudflare.com + git config --global user.name 'Wrangler automated PR updater' + + - name: Generate C3 changesets + # Keep the final param (the changeset prefix: `c3-frameworks-update`) in sync with the filter in the `.github/workflows/c3-e2e-dependabot.yml` workflow. + run: node -r esbuild-register tools/dependabot/generate-dependabot-pr-changesets.ts "$PR_NUMBER" create-cloudflare packages/create-cloudflare/src/frameworks/package.json c3-frameworks-update + env: + PR_NUMBER: ${{ github.event.number }} diff --git a/.github/workflows/c3-e2e.yml b/.github/workflows/c3-e2e.yml new file mode 100644 index 0000000..9dab2e4 --- /dev/null +++ b/.github/workflows/c3-e2e.yml @@ -0,0 +1,311 @@ +name: C3 E2E +on: + merge_group: + pull_request: + +permissions: + contents: read + pull-requests: read + +# pnpm 11-specific settings - recognised by pnpm 11+ and +# silently ignored by pnpm 10. +# +# - pmOnFail=ignore: pnpm 11's default `download` would silently re-exec as +# the monorepo's pinned pnpm 10.33.0, defeating the matrix override. +# - verifyDepsBeforeRun=false: pnpm 11's default `install` would re-run +# `pnpm install` at the monorepo root before invoking the script, which +# then trips on engines.pnpm. +env: + pnpm_config_pm_on_fail: ignore + pnpm_config_verify_deps_before_run: "false" + +jobs: + e2e: + # Runs the non-frameworks C3 E2E tests on all supported operating systems + # and package managers. + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os.name }}-${{ matrix.pm.name }}-${{ matrix.pm.version }}${{ matrix.experimental && '-experimental' || '' }}-${{ matrix.filter }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + # `matrix.pm.label` is the display name in job/artifact names. Default + # pm entries use the plain name (e.g. `pnpm`) to keep historical required- + # check names stable; non-default entries (e.g. pnpm 11) suffix the version. + name: ${{ format('C3 E2E ({0}, {1}{2}) - {3}', matrix.pm.label, matrix.os.description, matrix.experimental && ', experimental' || '', matrix.filter) }} + strategy: + fail-fast: false + matrix: + experimental: [false] + filter: ["cli", "workers"] + os: [{ name: ubuntu-latest, description: Linux }] + pm: + - { name: pnpm, version: "10.33.0", label: "pnpm" } + - { name: pnpm, version: "11.5.1", label: "pnpm@11.5.1" } + - { name: npm, version: "0.0.0", label: "npm" } + # The yarn tests keep failing on Linux with out of space errors, with no clear reason why. Disabling for now. + # - { name: yarn, version: "1.0.0", label: "yarn" } + include: + # Windows and experimental entries stay on the default pnpm to + # preserve historical required-check names. pnpm 11 coverage comes + # from the Linux base entries above. + - os: { name: windows-latest, description: Windows } + pm: { name: pnpm, version: "10.33.0", label: "pnpm" } + filter: "cli" + - os: { name: windows-latest, description: Windows } + pm: { name: pnpm, version: "10.33.0", label: "pnpm" } + filter: "workers" + - os: { name: ubuntu-latest, description: Linux } + pm: { name: pnpm, version: "10.33.0", label: "pnpm" } + experimental: true + filter: "cli" + - os: { name: ubuntu-latest, description: Linux } + pm: { name: pnpm, version: "10.33.0", label: "pnpm" } + experimental: true + filter: "workers" + runs-on: ${{ matrix.os.name }} + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + + - name: Install Dependencies + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Install Python uv + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-python-uv + + # Needed because gatsby requires git + - name: Configure Git + shell: bash + run: | + git config --global user.email wrangler@cloudflare.com + git config --global user.name 'Wrangler Tester' + + # Put the matrix pnpm version on PATH so scaffolded-project installs + # run it (not the monorepo's pinned pnpm 10.33.0). Without this, the + # matrix only relabels C3's `npm_config_user_agent` and pnpm 11 + # regressions go undetected. + # + # `pnpm/action-setup` refuses when both `version` input and the root + # `package.json#packageManager` are set, so we point it at a stub + # `package.json`. We also loosen `engines.pnpm` to avoid pnpm 11 + # bailing with ERR_PNPM_UNSUPPORTED_ENGINE at the monorepo root. + - name: Prepare workspace for matrix pnpm install + if: matrix.pm.name == 'pnpm' && steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: | + mkdir -p .ci-pnpm-setup-stub + printf '{"name":"ci-stub","private":true}\n' > .ci-pnpm-setup-stub/package.json + node -e " + const fs = require('node:fs'); + const pkg = JSON.parse(fs.readFileSync('package.json','utf8')); + if (pkg.engines && pkg.engines.pnpm) pkg.engines.pnpm = '*'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, '\t') + '\n'); + " + - name: Install matrix pnpm version for scaffolded installs + if: matrix.pm.name == 'pnpm' && steps.changes.outputs.everything_but_markdown == 'true' + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: ${{ matrix.pm.version }} + package_json_file: .ci-pnpm-setup-stub/package.json + # Distinct dest so `addPath` prepends ahead of the monorepo's + # pinned pnpm 10.x from the earlier `~/setup-pnpm`. + dest: ~/matrix-pnpm + - name: Verify pnpm on PATH matches matrix version + if: matrix.pm.name == 'pnpm' && steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: | + got="$(pnpm --version)" + want="${{ matrix.pm.version }}" + echo "pnpm on PATH: $got (expected $want)" + [ "$got" = "$want" ] || { echo "ERROR: PATH pnpm is $got, not $want — scaffolded installs would silently run the wrong version."; exit 1; } + + - id: run-e2e + if: steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: pnpm run test:e2e:c3 + env: + NODE_VERSION: ${{ env.NODE_VERSION }} + E2E_EXPERIMENTAL: ${{ matrix.experimental }} + E2E_TEST_PM: ${{ matrix.pm.name }} + E2E_TEST_PM_VERSION: ${{ matrix.pm.version }} + E2E_TEST_FILTER: ${{ matrix.filter }} + CI_OS: ${{ runner.os }} + GITHUB_TOKEN: ${{ github.token }} # Needed for begit to clone the repo in the e2e tests for solid-start + + - name: Upload Logs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() && steps.changes.outputs.everything_but_markdown == 'true' }} + with: + name: ${{ format('e2e-logs-{0}-{1}-{2}-{3}', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal', matrix.filter) }} + path: packages/create-cloudflare/.e2e-logs${{matrix.experimental == 'true' && '-experimental' || ''}}/${{ matrix.pm.name }}/${{ matrix.filter }} + include-hidden-files: true + + - name: Upload Turbo Summary + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() && steps.changes.outputs.everything_but_markdown == 'true' }} + with: + name: ${{ format('turbo-runs-{0}-{1}-{2}-{3}', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal', matrix.filter) }} + path: .turbo/runs + include-hidden-files: true + + frameworks-e2e: + # Runs the C3 frameworks E2E tests only in the merge queue, on the release branch, if create-cloudflare has changed, or when explicitly requested via label. + # + # Frameworks that delegate install to their own generator (e.g. `hono`) + # don't route through C3's `ERR_PNPM_IGNORED_BUILDS` recovery path; those + # tests opt out of pnpm 11 via `unsupportedPmRanges` in the test config. + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-frameworks${{ matrix.experimental && '-experimental' || '' }}-${{ github.ref }}-${{ matrix.os.name }}-${{ matrix.pm.name }}-${{ matrix.pm.version }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + name: ${{ format('C3 E2E ({0}, {1}{2}) - frameworks', matrix.pm.label, matrix.os.description, matrix.experimental && ', experimental' || '') }} + strategy: + fail-fast: false + matrix: + experimental: [false] + os: + - { name: ubuntu-latest, description: Linux } + pm: + - { name: pnpm, version: "10.33.0", label: "pnpm" } + - { name: pnpm, version: "11.5.1", label: "pnpm@11.5.1" } + - { name: npm, version: "0.0.0", label: "npm" } + include: + # Experimental stays on the default pnpm to preserve historical + # required-check names; pnpm 11 coverage comes from the base entry. + - os: { name: ubuntu-latest, description: Linux } + pm: { name: pnpm, version: "10.33.0", label: "pnpm" } + experimental: true + runs-on: ${{ matrix.os.name }} + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + framework_deps: + - 'packages/create-cloudflare/**' + + - name: Check if frameworks tests should run + id: check-frameworks + shell: bash + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + HEAD_REF: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + FRAMEWORK_DEPS_CHANGED: ${{ steps.changes.outputs.framework_deps }} + run: | + if [ "$EVENT_NAME" = "merge_group" ] || \ + [ "$HEAD_REF" = "changeset-release/main" ] || \ + [ "$FRAMEWORK_DEPS_CHANGED" = "true" ]; then + echo "run_frameworks=true" >> "$GITHUB_OUTPUT" + elif [ "$EVENT_NAME" = "pull_request" ]; then + HAS_LABEL=$(gh pr view "$PR_NUMBER" \ + --repo "$REPO" --json labels \ + --jq '[.labels[].name] | any(. == "run-c3-frameworks-tests")') + echo "run_frameworks=${HAS_LABEL}" >> "$GITHUB_OUTPUT" + else + echo "run_frameworks=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install Dependencies + if: steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Install Python uv + if: steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-python-uv + + - name: Configure Git + if: steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: | + git config --global user.email wrangler@cloudflare.com + git config --global user.name 'Wrangler Tester' + + # See the equivalent step in the `e2e` job above for context. + - name: Prepare workspace for matrix pnpm install + if: matrix.pm.name == 'pnpm' && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: | + mkdir -p .ci-pnpm-setup-stub + printf '{"name":"ci-stub","private":true}\n' > .ci-pnpm-setup-stub/package.json + node -e " + const fs = require('node:fs'); + const pkg = JSON.parse(fs.readFileSync('package.json','utf8')); + if (pkg.engines && pkg.engines.pnpm) pkg.engines.pnpm = '*'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, '\t') + '\n'); + " + - name: Install matrix pnpm version for scaffolded installs + if: matrix.pm.name == 'pnpm' && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: ${{ matrix.pm.version }} + package_json_file: .ci-pnpm-setup-stub/package.json + dest: ~/matrix-pnpm + - name: Verify pnpm on PATH matches matrix version + if: matrix.pm.name == 'pnpm' && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: | + got="$(pnpm --version)" + want="${{ matrix.pm.version }}" + echo "pnpm on PATH: $got (expected $want)" + [ "$got" = "$want" ] || { echo "ERROR: PATH pnpm is $got, not $want — scaffolded installs would silently run the wrong version."; exit 1; } + + - id: run-e2e + if: steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' + shell: bash + run: pnpm run test:e2e:c3 + env: + NODE_VERSION: ${{ env.NODE_VERSION }} + E2E_EXPERIMENTAL: ${{ matrix.experimental }} + E2E_TEST_PM: ${{ matrix.pm.name }} + E2E_TEST_PM_VERSION: ${{ matrix.pm.version }} + E2E_TEST_FILTER: frameworks + CI_OS: ${{ runner.os }} + GITHUB_TOKEN: ${{ github.token }} + + - name: Upload Logs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' }} + with: + name: ${{ format('e2e-logs-{0}-{1}-{2}-frameworks', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal') }} + path: packages/create-cloudflare/.e2e-logs${{matrix.experimental == 'true' && '-experimental' || ''}}/${{ matrix.pm.name }}/frameworks + include-hidden-files: true + + - name: Upload Turbo Summary + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ !cancelled() && steps.check-frameworks.outputs.run_frameworks == 'true' && steps.changes.outputs.everything_but_markdown == 'true' }} + with: + name: ${{ format('turbo-runs-{0}-{1}-{2}-frameworks', matrix.pm.label, matrix.os.description, matrix.experimental && 'experimental' || 'normal') }} + path: .turbo/runs + include-hidden-files: true diff --git a/.github/workflows/changeset-review.yml b/.github/workflows/changeset-review.yml new file mode 100644 index 0000000..ac53963 --- /dev/null +++ b/.github/workflows/changeset-review.yml @@ -0,0 +1,92 @@ +name: Changeset Review + +on: + pull_request: + paths: + - ".changeset/*.md" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + +permissions: + contents: read + pull-requests: write + issues: write + id-token: write + +jobs: + review-changesets: + runs-on: ubuntu-latest + if: github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout changesets + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + sparse-checkout: | + .changeset + .github/opencode.json + + - name: Install OpenCode + # pin OpenCode to 1.4.6 version as newer versions are causing ProviderInitError issues + run: | + npm install -g opencode-ai@1.4.6 + cp .github/opencode.json ./opencode.json + + - name: Get changed changeset files + id: changed-changesets + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + .changeset/*.md + files_ignore: | + .changeset/README.md + # Recover deleted files so the AI can read them (needed for Version Packages PRs) + recover_deleted_files: ${{ github.event.pull_request.title == 'Version Packages' }} + + - name: Review Changesets with OpenCode + id: opencode-review + # Run for Version Packages PRs (which delete changesets) or regular PRs with new changesets + if: github.event.pull_request.title == 'Version Packages' || steps.changed-changesets.outputs.added_files_count > 0 + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }} + CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }} + DELETED_FILES: ${{ steps.changed-changesets.outputs.deleted_files }} + ADDED_FILES: ${{ steps.changed-changesets.outputs.added_files }} + run: | + opencode --model "cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6" run --print-logs \ + "Review the changeset files in this PR. + + For \"Version Packages\" PRs, review: ${DELETED_FILES} + For regular PRs, review: ${ADDED_FILES} + + Read \`.changeset/README.md\` for guidelines, then validate: + 1. **Version Type**: Accept the author's choice of patch/minor unless clearly incorrect + 2. **Changelog Quality**: Meaningful descriptions (examples encouraged but not required for features) + 3. **Markdown Headers**: No h1/h2/h3 headers (breaks changelog formatting) + 4. **Analytics**: If the change collects more analytics, it should be a minor even though there is no user-visible change + 5. **Dependabot**: Do not validate dependency update changesets for create-cloudflare + 6. **Experimental features**: Changesets for experimental features should include note on how users can opt in. + + If all changesets pass, just output \"✅ All changesets look good\" - no need for a detailed checklist. + + Do not review other files, only the changesets. This is specifically a changeset review action. + + If there are issues, output \"⚠️ Issues found\" followed by the specific problems. + + Write your review to changeset-review.md." + + - name: Post review comment + if: steps.opencode-review.outcome == 'success' + uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3 + with: + header: changeset-review + path: changeset-review.md + + - name: Skip notice + if: github.event.pull_request.title != 'Version Packages' && steps.changed-changesets.outputs.added_files_count == 0 + run: | + echo "No new changesets to review (only minor edits to pre-existing changesets detected)" diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml new file mode 100644 index 0000000..92b4dde --- /dev/null +++ b/.github/workflows/changesets.yml @@ -0,0 +1,108 @@ +name: Handle Changesets + +on: + push: + branches: + - main + +permissions: + id-token: write + contents: read + # note: no write permissions are needed since the workflow uses GH_ACCESS_TOKEN instead of GITHUB_TOKEN + +env: + # This workflow only builds and publishes — it never runs Playwright tests. + # Skip the browser download to avoid the postinstall script hanging in CI. + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + +jobs: + release: + if: ${{ github.repository_owner == 'cloudflare' }} + name: Handle Changesets + runs-on: macos-latest-large + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + # See https://github.com/changesets/action/issues/187 + token: ${{ secrets.GH_ACCESS_TOKEN }} + persist-credentials: true + # Pull in more history to cover the changeset commits + fetch-depth: 500 + + - name: Install Dependencies + # Defense in depth: do not pass Turbo remote cache credentials and + # disable the pnpm store cache so release builds always resolve + # packages from the registry and rebuild every task from source, + # rather than restoring from a (potentially poisoned) cache. + uses: ./.github/actions/install-dependencies + with: + # Pinned to 24.18.0 (rather than floating `24`) until it's safe to + # float back: Node 24.17.0 regressed node-fetch@2 keep-alive responses + # with ERR_STREAM_PREMATURE_CLOSE (nodejs/node#63989, fixed in 24.18.0). + node-version: 24.18.0 + disable-cache: "true" + + - name: Check npm version + run: node -r esbuild-register tools/deployments/check-npm-version.ts + + - name: Check the changesets + run: node -r esbuild-register tools/deployments/validate-changesets.ts + + - name: Build all packages + run: pnpm run build + env: + CI_OS: ${{ runner.os }} + SOURCEMAPS: "false" + ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} + ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} + SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583" + NODE_ENV: "production" + # This is the "production" key for sparrow analytics. + # Include this here because this step will rebuild Wrangler and needs to have this available + SPARROW_SOURCE_KEY: "50598e014ed44c739ec8074fdc16057c" + + - name: Create Version PR or Publish to NPM + id: changesets + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + with: + version: node .github/changeset-version.js + publish: pnpm exec changeset publish + env: + GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + + # Switch to Node 22 for non-npm package deployments because quick-edit + # builds native modules (tree-sitter) that are not compatible with + # Node 24's C++20 requirement for V8 headers. + - name: Switch to Node 22 for non-npm package deployments + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + # Pinned to 22.23.1 (rather than floating `22`) until it's safe to + # float back: Node 22.23.0 regressed node-fetch@2 keep-alive responses + # with ERR_STREAM_PREMATURE_CLOSE (nodejs/node#63989, fixed in 22.23.1). + node-version: 22.23.1 + + - name: Deploy non-NPM Packages + id: deploy + run: | + node -r esbuild-register tools/deployments/deploy-non-npm-packages.ts + echo "status=$(cat deployment-status.json)" >> $GITHUB_OUTPUT; + env: + PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + WORKERS_NEW_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.WORKERS_NEW_CLOUDFLARE_ACCOUNT_ID }} + WORKERS_NEW_CLOUDFLARE_API_TOKEN: ${{ secrets.WORKERS_NEW_CLOUDFLARE_API_TOKEN }} + WORKERS_DEPLOY_AND_CONFIG_CLOUDFLARE_API_TOKEN: ${{ secrets.WORKERS_DEPLOY_AND_CONFIG_CLOUDFLARE_API_TOKEN }} + WORKERS_SHARED_SENTRY_ACCESS_ID: ${{ secrets.WORKERS_SHARED_SENTRY_ACCESS_ID }} + WORKERS_SHARED_SENTRY_ACCESS_SECRET: ${{ secrets.WORKERS_SHARED_SENTRY_ACCESS_SECRET }} + WORKERS_SHARED_SENTRY_AUTH_TOKEN: ${{ secrets.WORKERS_SHARED_SENTRY_AUTH_TOKEN }} + + - name: Send Alert + if: always() + run: node -r esbuild-register tools/deployments/alert-on-error.ts + env: + PUBLISH_STATUS: ${{ steps.changesets.outcome }} + HAS_CHANGESETS: ${{ steps.changesets.outputs.hasChangesets }} + DEPLOYMENT_STATUS: ${{ steps.deploy.outputs.status }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + TOKEN: ${{ secrets.STATUS_BOT_SECRET }} diff --git a/.github/workflows/codeowners.yml b/.github/workflows/codeowners.yml new file mode 100644 index 0000000..23faa90 --- /dev/null +++ b/.github/workflows/codeowners.yml @@ -0,0 +1,54 @@ +name: "Code Owners" + +# Re-evaluate when PRs are opened/updated. +# When reviews are submitted/dismissed, the separate rerun-codeowners.yml workflow +# re-runs this check (rather than creating a second check context). +# Using pull_request_target (not pull_request) so the workflow has access to secrets +# for fork PRs. This is safe because: +# - The checkout is the BASE branch (ownership rules come from the protected branch) +# - PR head commits are fetched as git objects only (never checked out or executed) +# - The action only reads config files and calls the GitHub API +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] checks base branch ownership rules and fetches PR head for diff computation without executing PR code + types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled] + +concurrency: + group: codeowners-${{ github.event.pull_request.number }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + codeowners: + name: "Run Codeowners Plus" + runs-on: ubuntu-latest + # Each step is skipped when: + # - the PR head is the changeset-release branch (auto-generated changeset PRs), or + # - the PR base is not `main` (PRs against feature branches don't need formal review). + # Note: if we ever introduce a maintenance branch (e.g. a long-lived release branch), + # it would need to be added to the base branch allowlist below. + steps: + - name: "Checkout Base Branch" + if: github.event.pull_request.head.ref != 'changeset-release/main' && github.event.pull_request.base.ref == 'main' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: "Fetch PR Head (for diff computation)" + if: github.event.pull_request.head.ref != 'changeset-release/main' && github.event.pull_request.base.ref == 'main' + run: git fetch origin +refs/pull/${{ github.event.pull_request.number }}/head + env: + GITHUB_TOKEN: "${{ secrets.CODEOWNERS_GITHUB_PAT }}" + + - name: "Codeowners Plus" + if: github.event.pull_request.head.ref != 'changeset-release/main' && github.event.pull_request.base.ref == 'main' + uses: multimediallc/codeowners-plus@ff02aa993a92e8efe01642916d0877beb9439e9f # v1.9.0 + with: + github-token: "${{ secrets.CODEOWNERS_GITHUB_PAT }}" + pr: "${{ github.event.pull_request.number }}" + verbose: true + quiet: ${{ github.event.pull_request.draft }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..775142b --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,141 @@ +name: "Dependabot - auto-merge workerd updates" + +# workerd ships a release every weekday, so the workerd-and-workers-types +# Dependabot group produces a steady stream of mechanical PRs that bump +# `workerd`, `@cloudflare/workers-types`, and miniflare's pinned version in +# lockstep (see .github/dependabot.yml). When CI is green these PRs require +# no human review, so we enable GitHub auto-merge on them — required status +# checks remain the gate, and a failing build still parks the PR for a human. +# +# Security model: this workflow effectively bypasses the human-review +# requirement on PRs whose head branch matches the Dependabot naming pattern, +# so we have to be paranoid about exactly what we're auto-merging. Before +# enabling auto-merge we verify that the PR contains exactly the two commits +# we expect (one from Dependabot, one from `miniflare-dependabot-versioning-prs.yml`) +# and that nothing outside the expected fileset has been touched. If any +# subsequent push violates those invariants we actively *disable* auto-merge, +# so a maintainer pushing a follow-up commit cancels rather than rides the +# auto-merge. +# +# DO NOT add `actions/checkout` to this workflow — `pull_request_target` +# grants write-scoped tokens, and checking out PR-controlled code with +# those tokens is the standard pwn vector. + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] intentionally privileged metadata-only workflow; it never checks out or executes PR code + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: write + pull-requests: write + +jobs: + enable-auto-merge: + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-slim + steps: + - name: Fetch Dependabot metadata + id: meta + uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify PR matches expected workerd-bump shape + id: verify + if: steps.meta.outputs.dependency-group == 'workerd-and-workers-types' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + # Pull commits and changed files via the GitHub API. `--repo` is + # required because this workflow runs without `actions/checkout`, + # so `gh` has no git remote to infer the repo from. + commits_json=$(gh pr view --repo "$REPO" "$PR_NUMBER" --json commits) + files_json=$(gh pr view --repo "$REPO" "$PR_NUMBER" --json files) + + fail() { + echo "verified=false" >> "$GITHUB_OUTPUT" + echo "reason=$1" >> "$GITHUB_OUTPUT" + echo "::warning::Refusing to enable auto-merge: $1" + exit 0 + } + + # --- Commit shape --------------------------------------------------- + # Expected: exactly two commits. + # 1. dependabot[bot] author, with a verified GitHub signature. + # 2. The changeset commit pushed by miniflare-dependabot-versioning-prs.yml, + # authored as `Wrangler automated PR updater <wrangler@cloudflare.com>`. + # + # We can't require a signature on the second commit (it's pushed via + # GH_ACCESS_TOKEN, which doesn't sign), so we lean on path/content + # checks below to constrain what that commit can do. + + commit_count=$(echo "$commits_json" | jq '.commits | length') + if [ "$commit_count" -ne 2 ]; then + fail "expected exactly 2 commits, found $commit_count" + fi + + first_author=$(echo "$commits_json" | jq -r '.commits[0].authors[0].login') + first_oid=$(echo "$commits_json" | jq -r '.commits[0].oid') + if [ "$first_author" != "dependabot[bot]" ]; then + fail "first commit author is '$first_author', expected 'dependabot[bot]'" + fi + + # `gh pr view --json commits` doesn't expose signature info, so look + # it up via the REST commit endpoint. + first_verified=$(gh api "repos/$REPO/commits/$first_oid" --jq '.commit.verification.verified') + if [ "$first_verified" != "true" ]; then + fail "first commit (Dependabot) does not have a verified signature" + fi + + second_email=$(echo "$commits_json" | jq -r '.commits[1].authors[0].email // ""') + second_message=$(echo "$commits_json" | jq -r '.commits[1].messageHeadline // ""') + if [ "$second_email" != "wrangler@cloudflare.com" ]; then + fail "second commit author email is '$second_email', expected 'wrangler@cloudflare.com'" + fi + if ! echo "$second_message" | grep -qE '^Update dependencies of '; then + fail "second commit message '$second_message' does not match expected changeset commit shape" + fi + + # --- Changed files allowlist --------------------------------------- + # The only paths a workerd bump should touch: + # - .changeset/dependabot-update-*.md (added by the changeset job) + # - packages/*/package.json (workerd, workers-types pins) + # - pnpm-lock.yaml + # - pnpm-workspace.yaml (catalog entries) + allowed='^(\.changeset/dependabot-update-.*\.md|packages/[^/]+/package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml)$' + + unexpected=$(echo "$files_json" | jq -r '.files[].path' | grep -vE "$allowed" || true) + if [ -n "$unexpected" ]; then + fail "PR touches unexpected files:$(echo "$unexpected" | sed 's/^/ /' | tr '\n' ',')" + fi + + # Make sure the changeset is actually present — the changeset job + # may not have run yet, in which case we bail and wait for the + # `synchronize` event from its push. + if ! echo "$files_json" | jq -e '.files[] | select(.path | startswith(".changeset/dependabot-update-"))' > /dev/null; then + fail "changeset file not yet present; waiting for changeset job to push it" + fi + + echo "verified=true" >> "$GITHUB_OUTPUT" + + - name: Enable auto-merge + if: steps.meta.outputs.dependency-group == 'workerd-and-workers-types' && steps.verify.outputs.verified == 'true' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Disable auto-merge if verification failed + # If a previous run enabled auto-merge but a later push broke the + # invariants, actively cancel auto-merge so the bad commit can't ride. + # `gh pr merge --disable-auto` is a no-op (and exits 0) if auto-merge + # was never enabled, so this is safe to always run on the failure path. + if: always() && steps.meta.outputs.dependency-group == 'workerd-and-workers-types' && steps.verify.outputs.verified != 'true' + run: gh pr merge --disable-auto "$PR_URL" || true + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy-previews.yml b/.github/workflows/deploy-previews.yml new file mode 100644 index 0000000..e33d379 --- /dev/null +++ b/.github/workflows/deploy-previews.yml @@ -0,0 +1,90 @@ +name: Deploy Previews + +# 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 project to Cloudflare. +# +# PR Label | Project +# --------------------------------------------------------- +# preview:chrome-devtools-patches | packages/chrome-devtools-patches +# preview:quick-edit | packages/quick-edit +# +# Note: this workflow does not run tests against these packages, only deploys previews. + +on: + pull_request: + types: [synchronize, opened, reopened, labeled, unlabeled] + +permissions: + contents: read + # pull-request:write permission needed so that the workflow can comment on PRs + pull-requests: write + +jobs: + 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')) + timeout-minutes: 60 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-app-previews + runs-on: macos-latest-large + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Build tools and libraries + run: pnpm run build + env: + NODE_ENV: "production" + CI_OS: ${{ runner.os }} + + - name: Deploy Wrangler DevTools preview + if: contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches') + run: | + output=$(pnpm --filter @cloudflare/chrome-devtools-patches run deploy:preview) + echo "Command output: $output" + echo "Extracting deployed URL from command output" + url=$(echo "$output" | sed -nE "s/.*Version Preview URL: ([^[:space:]]+).*/\1/p") + echo "Extracted URL: $url" + echo "VITE_DEVTOOLS_PREVIEW_URL=$url" >> $GITHUB_ENV + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + + - name: Build and Deploy Quick Edit preview + if: contains(github.event.*.labels.*.name, 'preview:quick-edit') + run: pnpm --filter quick-edit run preview + env: + DEBIAN_FRONTEND: noninteractive + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + NODE_OPTIONS: "--max_old_space_size=30000" + + - name: "Comment on PR with Devtools Link" + if: contains(github.event.*.labels.*.name, 'preview:chrome-devtools-patches') + uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3 + with: + 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 + + In order to test the DevTools preview in `wrangler`: + + 1. `npx wrangler dev`. + 2. Hit `d` to open the DevTools in a fresh browser window. + 3. Paste the DevTools preview URL into the address bar (keeping all existing query parameters), e.g: + + ``` + - https://devtools.devprod.cloudflare.dev/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true + + ${{ env.VITE_DEVTOOLS_PREVIEW_URL }}/js_app?theme=systemPreferred&ws=127.0.0.1%3A9229%2Fws&domain=tester&debugger=true + ``` diff --git a/.github/workflows/e2e-local-explorer-ui.yml b/.github/workflows/e2e-local-explorer-ui.yml new file mode 100644 index 0000000..bb5657c --- /dev/null +++ b/.github/workflows/e2e-local-explorer-ui.yml @@ -0,0 +1,69 @@ +name: Local Explorer UI E2E + +on: + merge_group: + pull_request: + +permissions: + contents: read + +env: + # We run `playwright install` manually instead + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + +jobs: + test: + name: Local Explorer UI E2E + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + relevant: + - 'packages/local-explorer-ui/**' + - 'packages/miniflare/src/workers/local-explorer/**' + - 'fixtures/worker-with-resources/**' + - 'fixtures/shared/**' + + - name: Install Dependencies + if: steps.changes.outputs.relevant == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + + - name: Install Playwright + if: steps.changes.outputs.relevant == 'true' + run: pnpm --filter @cloudflare/local-explorer-ui playwright:install + + - name: Build wrangler + if: steps.changes.outputs.relevant == 'true' + run: pnpm turbo build --filter wrangler + + - name: Run Local Explorer UI E2E tests + if: steps.changes.outputs.relevant == 'true' + run: pnpm test:e2e -F @cloudflare/local-explorer-ui --log-order=stream + timeout-minutes: 30 + env: + CI_OS: Linux + NODE_OPTIONS: "--max_old_space_size=8192" + + - name: Upload turbo logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: turbo-runs + path: .turbo/runs diff --git a/.github/workflows/e2e-project-cleanup.yml b/.github/workflows/e2e-project-cleanup.yml new file mode 100644 index 0000000..e5d6306 --- /dev/null +++ b/.github/workflows/e2e-project-cleanup.yml @@ -0,0 +1,33 @@ +# This workflow cleans up any leftover projects created by e2e runs. + +name: E2E Project Cleanup + +on: + workflow_dispatch: + schedule: + - cron: "0 */2 * * *" # Run every 2 hours + +permissions: + contents: read + +jobs: + cleanup: + timeout-minutes: 30 + name: "Cleanup Test Projects" + if: ${{ github.repository_owner == 'cloudflare' }} + runs-on: ubuntu-slim + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Cleanup E2E test projects + run: node -r esbuild-register tools/e2e/e2eCleanup.ts + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.TEST_CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.TEST_CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/e2e-vite.yml b/.github/workflows/e2e-vite.yml new file mode 100644 index 0000000..90fcbf7 --- /dev/null +++ b/.github/workflows/e2e-vite.yml @@ -0,0 +1,73 @@ +name: Vite Plugin E2E + +on: + merge_group: + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + e2e-vite-plugin-test: + name: ${{ format('Vite Plugin E2E ({0})', matrix.description) }} + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + description: macOS + - os: windows-latest + description: Windows + - os: ubuntu-latest + description: Linux + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + + - name: Check if remote tests should run + id: check-remote + uses: ./.github/actions/check-remote-tests + + - name: Install Dependencies + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Run Vite plugin E2E tests + if: steps.changes.outputs.everything_but_markdown == 'true' + run: pnpm test:e2e -F @cloudflare/vite-plugin --log-order=stream + timeout-minutes: 20 + env: + NODE_DEBUG: "vite-plugin:test" + # The remote-binding tests need to connect to Cloudflare + CLOUDFLARE_API_TOKEN: ${{ steps.check-remote.outputs.run-remote == 'true' && secrets.TEST_CLOUDFLARE_API_TOKEN || '' }} + CLOUDFLARE_ACCOUNT_ID: ${{ steps.check-remote.outputs.run-remote == 'true' && secrets.TEST_CLOUDFLARE_ACCOUNT_ID || '' }} + NODE_OPTIONS: "--max_old_space_size=8192" + CI_OS: ${{ matrix.os }} + + - name: Upload turbo logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: turbo-runs-${{ matrix.os }} + path: .turbo/runs diff --git a/.github/workflows/e2e-wrangler.yml b/.github/workflows/e2e-wrangler.yml new file mode 100644 index 0000000..8a1a88a --- /dev/null +++ b/.github/workflows/e2e-wrangler.yml @@ -0,0 +1,88 @@ +name: Wrangler E2E + +on: + merge_group: + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + e2e-wrangler-test: + name: ${{ format('Wrangler E2E ({0}, shard {1}/4)', matrix.description, matrix.shard) }} + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}-${{ matrix.shard }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] + shard: [1, 2, 3, 4] + include: + - os: macos-latest + description: macOS + - os: windows-latest + description: Windows + - os: ubuntu-latest + description: Linux + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + + - name: Check if remote tests should run + id: check-remote + uses: ./.github/actions/check-remote-tests + + - name: Install Dependencies + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Run Wrangler E2E tests + if: steps.changes.outputs.everything_but_markdown == 'true' + run: pnpm run test:e2e:wrangler + env: + CLOUDFLARE_API_TOKEN: ${{ steps.check-remote.outputs.run-remote == 'true' && secrets.TEST_CLOUDFLARE_API_TOKEN || '' }} + CLOUDFLARE_ACCOUNT_ID: ${{ steps.check-remote.outputs.run-remote == 'true' && secrets.TEST_CLOUDFLARE_ACCOUNT_ID || '' }} + HYPERDRIVE_DATABASE_URL: ${{ secrets.TEST_HYPERDRIVE_DATABASE_URL}} + HYPERDRIVE_MYSQL_DATABASE_URL: ${{ secrets.TEST_HYPERDRIVE_MYSQL_DATABASE_URL}} + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/index.html + CI_OS: ${{ matrix.os }} + E2E_SHARD: ${{ matrix.shard }} + E2E_SHARD_COUNT: "4" + + - name: Run getPlatformProxy() remote-bindings e2e tests + if: steps.changes.outputs.everything_but_markdown == 'true' && matrix.shard == 1 + run: pnpm run test:e2e -F @fixture/get-platform-proxy-remote-bindings + env: + TEST_CLOUDFLARE_API_TOKEN: ${{ steps.check-remote.outputs.run-remote == 'true' && secrets.TEST_CLOUDFLARE_API_TOKEN || '' }} + TEST_CLOUDFLARE_ACCOUNT_ID: ${{ steps.check-remote.outputs.run-remote == 'true' && secrets.TEST_CLOUDFLARE_ACCOUNT_ID || '' }} + NODE_OPTIONS: "--max_old_space_size=8192" + CI_OS: ${{ matrix.os }} + + - name: Upload turbo logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: turbo-runs-${{ matrix.os }}-shard-${{ matrix.shard }} + path: .turbo/runs diff --git a/.github/workflows/hotfix-release.yml b/.github/workflows/hotfix-release.yml new file mode 100644 index 0000000..c80c651 --- /dev/null +++ b/.github/workflows/hotfix-release.yml @@ -0,0 +1,82 @@ +name: Release a hotfix + +on: + workflow_dispatch: + inputs: + pr-number: + description: "Which PR should be released?" + required: true + confirm: + description: "Confirm that the PR is up to date with the last published release, with the specific additional changes that should be released as part of this hotfix and the relevant package patch versions bumped" + type: boolean + required: true + label: + description: "Which dist-tag should this release have?" + type: string + default: hotfix + required: true + +permissions: + contents: read + id-token: write + +jobs: + hotfix-release: + name: Hotfix Release + if: ${{ inputs.confirm == true }} + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check user for team affiliation + uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v2 + id: teamAffiliation + with: + GITHUB_TOKEN: ${{ secrets.READ_ONLY_ORG_GITHUB_TOKEN }} + username: ${{ github.actor }} + team: wrangler + + - name: Stop workflow if user is not a wrangler team member + if: ${{ steps.teamAffiliation.outputs.isTeamMember == false }} + run: | + echo "You must be on the "wrangler" team to trigger this job." + exit 1 + + - name: "Checkout PR" + run: | + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "::error::PR number must be numeric" + exit 1 + fi + gh pr checkout "$PR_NUMBER" + env: + GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + PR_NUMBER: ${{ inputs.pr-number }} + + - name: Install Dependencies + # Defense in depth: do not pass Turbo remote cache credentials and + # disable the pnpm store cache so release builds always resolve + # packages from the registry and rebuild every task from source, + # rather than restoring from a (potentially poisoned) cache. + uses: ./.github/actions/install-dependencies + with: + disable-cache: "true" + + - name: Build all packages + run: pnpm run build + env: + CI_OS: ${{ runner.os }} + + - name: Publish packages + run: | + if ! [[ "$NPM_TAG" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + echo "::error::npm dist-tag contains unsupported characters" + exit 1 + fi + pnpm publish -r --tag "$NPM_TAG" --filter wrangler --filter miniflare --filter create-cloudflare + env: + NPM_TAG: ${{ inputs.label }} diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 0000000..1bd64a2 --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,41 @@ +name: Add issues to DevProd project + +on: + issues: + types: [opened, transferred] + +permissions: + # issues:write permission needed so that the workflow can manage issues + issues: write + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-slim + steps: + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/cloudflare/projects/1 + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + labeled: bug, enhancement + label-operator: OR + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/cloudflare/projects/2 + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + labeled: product:pages + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/cloudflare/projects/6 + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + labeled: product:d1 + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/cloudflare/projects/12 + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + labeled: product:c3 + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/cloudflare/projects/8 + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + labeled: product:queues diff --git a/.github/workflows/miniflare-dependabot-versioning-prs.yml b/.github/workflows/miniflare-dependabot-versioning-prs.yml new file mode 100644 index 0000000..b6e6e38 --- /dev/null +++ b/.github/workflows/miniflare-dependabot-versioning-prs.yml @@ -0,0 +1,51 @@ +name: "Miniflare - Generate changesets for dependabot PRs" + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] dependabot-only job requires write access to push generated changesets; checkout v6 persists credentials under RUNNER_TEMP + paths: + - "packages/miniflare/package.json" + - "pnpm-workspace.yaml" + +permissions: + # content:write permission needed to update add changesets to dependabot PRs + # (see tools/dependabot/generate-dependabot-pr-changesets.ts) + contents: write + +jobs: + generate-changeset: + runs-on: ubuntu-slim + if: | + github.actor == 'dependabot[bot]' && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 2 + persist-credentials: true + ref: ${{ github.head_ref }} + token: ${{ secrets.GH_ACCESS_TOKEN }} + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Configure Git + run: | + git config --global user.email wrangler@cloudflare.com + git config --global user.name 'Wrangler automated PR updater' + + - name: Generate Miniflare changesets + # The paremeters to the script are: + # - PR: The number of the current Dependabot PR + # - Packages: Coma-separated names of the workers-sdk packages whose dependencies are being updated + # - PackageJSON: The path to the package JSON being updated by Dependabot + # - Changeset prefix: The prefix to go on the front of the filename of the generated changeset + run: >- + node -r esbuild-register tools/dependabot/generate-dependabot-pr-changesets.ts + "$PR_NUMBER" + miniflare,wrangler + packages/miniflare/package.json + dependabot-update + env: + PR_NUMBER: ${{ github.event.number }} diff --git a/.github/workflows/opencode-issue-solver.yml b/.github/workflows/opencode-issue-solver.yml new file mode 100644 index 0000000..8bae748 --- /dev/null +++ b/.github/workflows/opencode-issue-solver.yml @@ -0,0 +1,19 @@ +name: OpenCode Issue Solver + +on: + workflow_dispatch: + inputs: + issue-number: + description: "Issue number to attempt to fix" + required: true + type: number + +jobs: + assess-issue: + runs-on: ubuntu-latest + permissions: {} + + steps: + - name: Exit + run: | + exit 1 diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..1a02cc1 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,53 @@ +name: Prerelease + +on: + pull_request: + push: + branches: + - main + - d1 + - workflows + - next + - v3-maintenance + +permissions: + contents: read + +jobs: + build: + if: ${{ github.repository_owner == 'cloudflare' }} + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + timeout-minutes: 30 + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Dependencies + # Defense in depth: do not pass Turbo remote cache credentials and + # disable the pnpm store cache so release builds always resolve + # packages from the registry and rebuild every task from source, + # rather than restoring from a (potentially poisoned) cache. + uses: ./.github/actions/install-dependencies + with: + disable-cache: "true" + + - name: Build + run: pnpm build --filter="./packages/*" + env: + NODE_ENV: "production" + CI_OS: ${{ runner.os }} + # this is the "test/staging" key for sparrow analytics + SPARROW_SOURCE_KEY: "5adf183f94b3436ba78d67f506965998" + ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} + SENTRY_DSN: "https://9edbb8417b284aa2bbead9b4c318918b@sentry10.cfdata.org/583" + ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} + WRANGLER_PRERELEASE_LABEL: ${{ github.head_ref || github.ref_name }} + + - name: Upload packages + run: node -r esbuild-register .github/prereleases/upload.mjs diff --git a/.github/workflows/rerun-codeowners-privileged.yml b/.github/workflows/rerun-codeowners-privileged.yml new file mode 100644 index 0000000..1474bf7 --- /dev/null +++ b/.github/workflows/rerun-codeowners-privileged.yml @@ -0,0 +1,67 @@ +name: "Rerun Code Owners (Privileged)" + +# Privileged companion to rerun-codeowners.yml. +# +# Runs after the "Rerun Code Owners" trigger workflow completes. The +# workflow_run event always executes from the default branch with full +# permissions — this is what allows us to call the Actions API even when the +# original event (pull_request_review) came from a fork PR, where the +# GITHUB_TOKEN would otherwise be read-only. +# +# The PR head SHA is read from github.event.workflow_run.head_sha — this is +# GitHub-provided metadata and cannot be manipulated by fork code (unlike +# the previous artifact-based approach). +# +# Strategy: CHECK-NAME LOOKUP +# ---------------------------- +# The codeowners.yml workflow uses pull_request_target, which means its +# workflow runs are indexed by the BASE branch SHA (e.g. main), NOT the PR +# head SHA. So we cannot find the run via: +# +# GET /actions/workflows/codeowners.yml/runs?head_sha=<PR_HEAD> +# +# That query returns nothing because the run's head_sha is main's HEAD. +# +# However, the codeowners-plus action creates its CHECK RUN on the PR head +# commit (so the status appears on the PR). The check-runs API lets us look +# up by check name + commit SHA: +# +# GET /commits/<PR_HEAD>/check-runs?check_name=Run+Codeowners+Plus +# +# From the check run's details_url we extract the Actions job ID, then +# re-run that specific job. +on: + workflow_run: # zizmor: ignore[dangerous-triggers] privileged companion reruns a fixed default-branch workflow using GitHub-provided workflow_run metadata only + workflows: ["Rerun Code Owners"] + types: [completed] + +permissions: {} + +jobs: + rerun-codeowners: + name: "Rerun Codeowners Plus" + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + permissions: + actions: write + checks: read + steps: + - name: Re-run Codeowners check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + REPO: ${{ github.repository }} + run: | + # Find the "Run Codeowners Plus" check run on the PR head commit + # and extract the Actions job ID from the check run's details URL + # (the last path segment before any query string). + job_id=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/check-runs?check_name=Run+Codeowners+Plus" \ + --jq '(.check_runs[0].details_url // "") | split("/") | last | split("?") | first' || true) + + if [ -n "$job_id" ]; then + gh api "repos/${REPO}/actions/jobs/${job_id}/rerun" --method POST \ + || echo "::warning::Job ${job_id} may already be running" + echo "Re-triggered 'Run Codeowners Plus' (job ${job_id})" + else + echo "::warning::Check 'Run Codeowners Plus' not found for SHA ${HEAD_SHA}" + fi diff --git a/.github/workflows/rerun-codeowners.yml b/.github/workflows/rerun-codeowners.yml new file mode 100644 index 0000000..0d940a0 --- /dev/null +++ b/.github/workflows/rerun-codeowners.yml @@ -0,0 +1,35 @@ +name: "Rerun Code Owners" + +# Trigger: a review is submitted or dismissed on a pull request. +# +# Goal: re-run the "Run Codeowners Plus" check from the main codeowners.yml +# workflow (which is triggered by pull_request_target) so it re-evaluates +# approval status after the review change. +# +# Why a two-workflow design? +# ------------------------- +# For fork PRs, pull_request_review gives a read-only GITHUB_TOKEN and no +# access to secrets. Unlike labeled/unlabeled events there is no +# pull_request_review variant of pull_request_target, so we cannot get a +# privileged token in a single workflow. Instead this workflow simply needs +# to *exist and succeed* — the companion workflow +# (rerun-codeowners-privileged.yml) is triggered by the workflow_run event +# when THIS workflow completes. workflow_run always runs from the default +# branch with full permissions, and it reads the PR head SHA directly from +# github.event.workflow_run.head_sha (GitHub-provided metadata, not +# controllable by fork code). +on: + pull_request_review: + types: [submitted, dismissed] + +permissions: {} + +jobs: + trigger: + name: "Trigger Privileged Rerun" + runs-on: ubuntu-latest + steps: + - name: Log trigger + run: echo "Review event on ${PR_URL}, privileged rerun will follow." + env: + PR_URL: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/rerun-remote-tests.yml b/.github/workflows/rerun-remote-tests.yml new file mode 100644 index 0000000..c581692 --- /dev/null +++ b/.github/workflows/rerun-remote-tests.yml @@ -0,0 +1,86 @@ +name: "Rerun Remote Tests" + +# Trigger: the "run-remote-tests" or "run-c3-frameworks-tests" label is added +# to or removed from a pull request. +# +# Goal: re-run the E2E workflows (e2e-wrangler.yml, e2e-vite.yml, c3-e2e.yml) +# so they pick up the label change and either pass or withhold the Cloudflare +# API token to the test steps. This avoids adding "labeled" as a trigger type +# to every E2E workflow (which would cause wasteful re-runs on unrelated label +# changes). +# +# Using pull_request_target (not pull_request) so the workflow has access to a +# privileged GITHUB_TOKEN even for fork PRs. This is safe because the workflow +# runs code from the default branch — fork authors cannot modify it — and only +# calls the Actions API (no checkout of untrusted code). +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] label-driven rerun workflow needs actions:write and does not check out or execute PR code + types: [labeled, unlabeled] + +permissions: {} + +jobs: + rerun-e2e: + name: "Rerun E2E Tests" + if: github.event.label.name == 'ci:run-remote-tests' || github.event.label.name == 'run-c3-frameworks-tests' + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: "Re-run E2E workflows" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + LABEL_NAME: ${{ github.event.label.name }} + REPO: ${{ github.repository }} + run: | + # Determine which workflows to re-run based on the label. + if [ "$LABEL_NAME" = "ci:run-remote-tests" ]; then + WORKFLOWS="e2e-wrangler.yml e2e-vite.yml c3-e2e.yml" + else + WORKFLOWS="c3-e2e.yml" + fi + + for workflow in ${WORKFLOWS}; do + # Find the most recent run of this workflow at the PR head SHA. + run_info=$(gh api "repos/${REPO}/actions/workflows/${workflow}/runs?head_sha=${HEAD_SHA}&per_page=1" \ + --jq '.workflow_runs[0] | "\(.id) \(.status)"' || true) + + run_id=$(echo "$run_info" | awk '{print $1}') + status=$(echo "$run_info" | awk '{print $2}') + + if [ -z "$run_id" ] || [ "$run_id" = "null" ]; then + echo "::warning::No run found for ${workflow} at SHA ${HEAD_SHA}" + continue + fi + + # Runs that haven't started yet will see the newly-added label + # when they begin — no need to cancel and re-run them. + if [ "$status" != "completed" ] && [ "$status" != "in_progress" ]; then + echo "${workflow} run ${run_id} is ${status} — not yet started, skipping." + continue + fi + + # If the run is actively executing, the label check has already + # evaluated (likely to the old value). Cancel it first — the + # rerun endpoint only works on completed runs. + if [ "$status" = "in_progress" ]; then + echo "Cancelling in-progress ${workflow} run ${run_id}..." + gh api "repos/${REPO}/actions/runs/${run_id}/cancel" --method POST || true + + # Cancellation is async; poll until the run reaches "completed". + for i in $(seq 1 30); do + current=$(gh api "repos/${REPO}/actions/runs/${run_id}" --jq '.status') + if [ "$current" = "completed" ]; then + echo "Run ${run_id} is now completed (cancelled)." + break + fi + echo " Waiting for cancellation to finish (${i}/30, status: ${current})..." + sleep 2 + done + fi + + gh api "repos/${REPO}/actions/runs/${run_id}/rerun" --method POST \ + && echo "Re-triggered ${workflow} (run ${run_id})" \ + || echo "::warning::Failed to re-run ${workflow} (run ${run_id})" + done diff --git a/.github/workflows/run-ci-for-external-forks.yml b/.github/workflows/run-ci-for-external-forks.yml new file mode 100644 index 0000000..6ee021c --- /dev/null +++ b/.github/workflows/run-ci-for-external-forks.yml @@ -0,0 +1,100 @@ +name: Run CI on behalf of External Forks + +on: + workflow_dispatch: + inputs: + pr-number: + description: "The PR number to run CI on behalf of" + required: true + commit-sha: + description: "The specific commit SHA from the PR branch to run CI on" + required: true + reviewed: + description: "Confirm that the PR has been reviewed for use/leakage of secrets" + type: boolean + required: true + +permissions: + contents: read # no write permissions are needed since the workflow uses GH_ACCESS_TOKEN instead of GITHUB_TOKEN + pull-requests: write # Required for creating or updating the draft PR on behalf of the user + +jobs: + create-draft-pr: + name: Create or Update Draft PR + if: ${{ inputs.reviewed == true }} + runs-on: ubuntu-slim + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: true + token: ${{ secrets.GH_ACCESS_TOKEN }} + + - name: Check user for team affiliation + uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 + id: teamAffiliation + with: + GITHUB_TOKEN: ${{ secrets.READ_ONLY_ORG_GITHUB_TOKEN }} + username: ${{ github.actor }} + team: wrangler + + - name: Stop workflow if user is not a team member + if: ${{ steps.teamAffiliation.outputs.isTeamMember != 'true' }} + run: | + echo "You must be on the \"wrangler\" team to trigger this job." + exit 1 + + - name: "Checkout PR" + run: | + gh pr checkout "$PR_NUM" --branch "run-ci-on-behalf-of-$PR_NUM" + git reset --hard "$COMMIT_SHA" + env: + # We need a PAT to checkout the fork + GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + PR_NUM: ${{ inputs.pr-number }} + COMMIT_SHA: ${{ inputs.commit-sha }} + + - name: Push Branch + run: | + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + git push origin HEAD --force + + - name: "Create or Update Draft PR" + run: | + existing_pr=$(gh pr list \ + --head "run-ci-on-behalf-of-$PR_NUM" \ + --state open \ + --json number \ + --jq 'first(.[].number) // ""') + + if [ -n "$existing_pr" ]; then + gh pr edit "$existing_pr" \ + --add-label "ci:e2e" \ + --add-label "ci:run-remote-tests" \ + --title "$TITLE" \ + --body "$BODY" + else + gh pr create \ + --head "run-ci-on-behalf-of-$PR_NUM" \ + --draft \ + --label "ci:e2e" \ + --label "ci:run-remote-tests" \ + --title "$TITLE" \ + --body "$BODY" + fi + env: + GH_TOKEN: ${{ github.token }} + PR_NUM: ${{ inputs.pr-number }} + TITLE: "Run CI on behalf of #${{ inputs.pr-number }} @${{ inputs.commit-sha }}" + BODY: "This PR runs CI on behalf of #${{ inputs.pr-number }} at commit ${{ inputs.commit-sha }}. It can be closed after the CI run is complete." + + - name: "Trigger CI" + run: | + # Push an empty commit to trigger CI workflows + # This ensures CI runs because the PR already exists when this push happens + git commit --allow-empty -m "Trigger CI for #$PR_NUM" + git push origin HEAD + env: + PR_NUM: ${{ inputs.pr-number }} diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..2e5e914 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,31 @@ +name: Semgrep OSS scan +on: + pull_request: {} + push: + branches: [main] + workflow_dispatch: {} + schedule: + - cron: "0 0 15 * *" +concurrency: + group: semgrep-${{ github.event_name }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +permissions: + contents: read +jobs: + semgrep: + name: semgrep-oss + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + - id: cache-semgrep + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + with: + path: ~/.local + key: semgrep-1.160.0-${{ runner.os }} + - if: steps.cache-semgrep.outputs.cache-hit != 'true' + run: pip install --user semgrep==1.160.0 + - run: echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - run: semgrep scan --config=auto || true diff --git a/.github/workflows/test-and-check-other-node.yml b/.github/workflows/test-and-check-other-node.yml new file mode 100644 index 0000000..61b0793 --- /dev/null +++ b/.github/workflows/test-and-check-other-node.yml @@ -0,0 +1,118 @@ +name: CI (Other Node Versions) +description: Run primary tests on other supported Node.js versions + +on: + merge_group: + pull_request: + types: [synchronize, opened, reopened, labeled, unlabeled] + +permissions: + contents: read + +jobs: + test-other-node-versions: + timeout-minutes: 45 + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.node_version }}-test-other-node-versions + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + name: ${{ format('Tests ({0})', matrix.description) }} + strategy: + fail-fast: false + matrix: + # expected_to_fail: if true, tests may fail on this Node version. + # These versions are skipped on normal PRs but run on Version Packages PRs + # (changeset-release/main) or when the 'ci:test-all-node-versions' label is added. + # + # Node 24 is pinned to 24.18.0 (rather than floating `24`): Node 24.17.0 + # regressed node-fetch@2 keep-alive responses with ERR_STREAM_PREMATURE_CLOSE + # (nodejs/node#63989, fixed in 24.18.0). `description` stays "Node 24" to + # keep the job/check name stable. + include: + - { + node_version: 24.18.0, + description: "Node 24", + expected_to_fail: true, + } + - { node_version: 25, description: "Node 25", expected_to_fail: true } + + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Filter changed paths + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + + - name: Check if should run tests + id: should_run + env: + CHANGES_EVERYTHING_BUT_MARKDOWN: ${{ steps.changes.outputs.everything_but_markdown }} + EXPECTED_TO_FAIL: ${{ matrix.expected_to_fail }} + HAS_TEST_ALL_NODE_VERSIONS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'ci:test-all-node-versions') }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + if [[ "$CHANGES_EVERYTHING_BUT_MARKDOWN" == "true" ]] && \ + [[ "$EXPECTED_TO_FAIL" != "true" || \ + "$HEAD_REF" == "changeset-release/main" || \ + "$HAS_TEST_ALL_NODE_VERSIONS_LABEL" == "true" ]]; then + echo "result=true" >> $GITHUB_OUTPUT + else + echo "result=false" >> $GITHUB_OUTPUT + fi + + - name: Install Dependencies + if: steps.should_run.outputs.result == 'true' + uses: ./.github/actions/install-dependencies + with: + node-version: ${{ matrix.node_version }} + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + # The miniflare browser-rendering tests download Chrome (~150 MB) via + # @puppeteer/browsers at runtime. Caching the binary avoids repeat + # downloads and reduces the chance of tests hanging while waiting for + # the download to finish within their timeout window. + - name: Restore Chrome browser cache (Browser Run) + if: steps.should_run.outputs.result == 'true' + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: chrome-${{ runner.os }}-${{ hashFiles('packages/miniflare/src/plugins/browser-rendering/browser-version.ts') }} + path: | + ~/.cache/.wrangler/chrome + + - name: Run tests (packages) + # We are running the package tests first be able to get early feedback on changes. + # There is no point in running the fixtures if a package is broken. + if: steps.should_run.outputs.result == 'true' + run: pnpm run test:ci --log-order=stream --concurrency=1 --filter="./packages/*" + env: + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/packages/index.html + CI_OS: ${{ matrix.description }} + + - name: Run e2e tests (unenv-preset) + if: steps.should_run.outputs.result == 'true' + run: pnpm test:e2e --filter wrangler -- unenv-preset + env: + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + CI_OS: ${{ matrix.description }} + + - name: Run tests (vite-plugin) + if: steps.should_run.outputs.result == 'true' + run: pnpm test:ci --filter @cloudflare/vite-plugin + env: + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + CI_OS: ${{ matrix.description }} diff --git a/.github/workflows/test-and-check.yml b/.github/workflows/test-and-check.yml new file mode 100644 index 0000000..ad538d9 --- /dev/null +++ b/.github/workflows/test-and-check.yml @@ -0,0 +1,202 @@ +name: CI + +on: + merge_group: + pull_request: + +permissions: + contents: read + +env: + # We run `playwright install` manually instead + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + +jobs: + add-to-project: + if: github.head_ref != 'changeset-release/main' + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-add-pr + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + timeout-minutes: 30 + name: Add PR to project + runs-on: ubuntu-latest + steps: + - run: curl -X POST https://devprod-status-bot.devprod.workers.dev/pr-project/workers-sdk/${{ github.event.number }} + + check: + timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-checks + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + + name: "Checks" + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: Check the changesets + run: node -r esbuild-register tools/deployments/validate-changesets.ts + + - name: Check for errors + run: pnpm run check --summarize + env: + CI_OS: ${{ runner.os }} + NODE_OPTIONS: "--max_old_space_size=8192" + + - name: Filter changed paths + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + miniflare_openapi: + - 'packages/miniflare/src/workers/local-explorer/openapi.local.json' + - 'packages/miniflare/src/workers/local-explorer/generated/**' + - 'packages/miniflare/scripts/openapi-filter-config.ts' + - 'packages/miniflare/scripts/filter-openapi.ts' + - 'packages/miniflare/scripts/check-generate-api.ts' + - 'packages/miniflare/openapi-ts.config.ts' + + - name: Check generated OpenAPI types are up to date + if: steps.changes.outputs.miniflare_openapi == 'true' + run: pnpm -F miniflare check:generate-api + + # Check for old Node.js version warnings and errors + - name: Use Node.js v20 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 20 + + - name: Check for error message on Node.js < v22 + run: node packages/wrangler/src/__tests__/test-old-node-version.js error + + test: + timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}-${{ matrix.suite }}-test + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + + name: ${{ format('Tests ({0}, {1})', matrix.description, matrix.suite) }} + strategy: + fail-fast: false + matrix: + os: + - macos-latest + - ubuntu-latest + - windows-latest + suite: + - packages-and-tools + - fixtures + include: + - os: macos-latest + description: macOS + - os: ubuntu-latest + description: Linux + - os: windows-latest + description: Windows + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Filter changed paths + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + + - name: Install Dependencies + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + # Browser Run tests in `packages/miniflare/test/plugins/browser/index.spec.ts` + # and the `fixtures/browser-run` fixture use `@puppeteer/browsers` to + # download Chrome into the global Wrangler cache. Caching that binary + # across CI runs avoids ~150 MB of repeat downloads per run and reduces + # the surface area for the intermittent partial-extraction race that + # surfaces as `The browser folder (...) exists but the executable (...) + # is missing` (see #13971, #13980). + # + # The Browser Run fixture is skipped on Ubuntu (AppArmor), but the + # miniflare browser spec runs everywhere, so the cache is needed on all + # three OSes for `packages-and-tools` and on Windows + macOS for + # `fixtures`. + - name: Restore Chrome browser cache (Browser Run) + if: steps.changes.outputs.everything_but_markdown == 'true' && (matrix.suite == 'packages-and-tools' || (matrix.suite == 'fixtures' && matrix.os != 'ubuntu-latest')) + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + # The Chrome version lives in + # `packages/miniflare/src/plugins/browser-rendering/browser-version.ts`. + # Bumping that constant invalidates this cache automatically via + # `hashFiles()` — a cache miss only triggers a fresh download, no + # functional impact. + key: chrome-${{ runner.os }}-${{ hashFiles('packages/miniflare/src/plugins/browser-rendering/browser-version.ts') }} + path: | + ~/.cache/.wrangler/chrome + ~/Library/Caches/.wrangler/chrome + ~/AppData/Local/xdg.cache/.wrangler/chrome + + - name: Run tests (tools only) + # tools _only_ needs to be tested on Linux because they're only intended to run in CI + if: steps.changes.outputs.everything_but_markdown == 'true' && matrix.suite == 'packages-and-tools' && matrix.os == 'ubuntu-latest' + run: pnpm run test:ci --log-order=stream --filter="@cloudflare/tools" + env: + NODE_OPTIONS: "--max_old_space_size=8192" + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/tools/index.html + CI_OS: ${{ matrix.description }} + + - name: Run tests (packages) + if: steps.changes.outputs.everything_but_markdown == 'true' && matrix.suite == 'packages-and-tools' + # We skip @cloudflare/vitest-pool-workers tests in CI on Windows because they're very flaky. We still run the vitest-pool-workers-examples fixture, which is a comprehensive set of example tests and gives us a lot of confidence. + # The @cloudflare/vitest-pool-workers tests skipped are things like watch mode, which constantly times out probably due to the github runners in use. + # Package tests are well-isolated (separate processes, temp dirs, random ports, MSW mocks) and can safely run in parallel. + # Concurrency is capped at 3 to avoid CPU starvation on 4-vCPU CI runners when heavyweight suites + # (wrangler/forks, miniflare/workerd, vitest-pool-workers/Verdaccio) overlap. + run: pnpm run test:ci --log-order=stream --concurrency=3 --filter="./packages/*" ${{ matrix.os == 'windows-latest' && '--filter="!./packages/vitest-pool-workers"' || '' }} + env: + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/packages/index.html + CI_OS: ${{ matrix.description }} + + - name: Run tests (fixtures) + if: steps.changes.outputs.everything_but_markdown == 'true' && matrix.suite == 'fixtures' + # Browser Run fixture is disabled on Ubuntu because of https://pptr.dev/troubleshooting#issues-with-apparmor-on-ubuntu + # Since the dev registry is now file-based (not network-based), fixture tests can safely run in parallel. + # Concurrency is capped at 2 to avoid CPU starvation on CI runners when multiple fixtures + # spawn workerd processes simultaneously (Windows runners are especially slow under load). + run: pnpm run test:ci --concurrency=2 --log-order=stream --filter="./fixtures/*" ${{ matrix.os == 'ubuntu-latest' && '--filter="!./fixtures/browser-run"' || '' }} + env: + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/index.html + CI_OS: ${{ matrix.description }} + + - name: Upload turbo logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: turbo-runs-${{ matrix.os }}-${{ matrix.suite }} + path: .turbo/runs diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml new file mode 100644 index 0000000..cb624eb --- /dev/null +++ b/.github/workflows/triage-issue.yml @@ -0,0 +1,235 @@ +name: Triage Issue + +on: + issues: + types: [opened] + issue_comment: + types: [created] + workflow_dispatch: + inputs: + issue-number: + description: "Issue number to triage" + required: true + type: number + +permissions: + contents: read + # Writes (comment + labels) are performed with GH_ACCESS_TOKEN (the + # workers-devprod bot) so they are attributed to that identity, not + # github-actions[bot]. Only read access is needed from the default token. + issues: read + +# Cancel in-progress runs for the same issue +concurrency: + group: triage-${{ github.event.issue.number || inputs.issue-number }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + +jobs: + triage: + runs-on: ubuntu-latest + # Always run for manual dispatch. For issue events, skip PRs and + # bot-created issues. For comment events, skip PRs, bot comments, and the + # triage bot's own sticky comment (identified by the marocchino marker) to + # avoid infinite re-triage loops. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'issues' && + !github.event.issue.pull_request && + github.event.issue.user.type != 'Bot') || + (github.event_name == 'issue_comment' && + !github.event.issue.pull_request && + github.event.comment.user.type != 'Bot' && + !contains(github.event.comment.body, '<!-- Sticky Pull Request Comment')) + timeout-minutes: 60 + steps: + - name: Resolve issue number + id: issue + env: + EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + EVENT_NAME: ${{ github.event_name }} + INPUT_ISSUE_NUMBER: ${{ inputs.issue-number }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "number=${INPUT_ISSUE_NUMBER}" >> "$GITHUB_OUTPUT" + else + echo "number=${EVENT_ISSUE_NUMBER}" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout (sparse — just the skill and opencode config) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + sparse-checkout: | + .github/skills + .github/opencode.json + + - name: Install OpenCode + run: | + npm install -g opencode-ai + cp .github/opencode.json ./opencode.json + + - name: Fetch issue data + env: + GH_TOKEN: ${{ github.token }} + ISSUE: ${{ steps.issue.outputs.number }} + REPO: ${{ github.repository }} + run: | + mkdir -p "data/${ISSUE}" + + gh issue view "$ISSUE" \ + --repo "$REPO" \ + --json number,title,body,comments,createdAt,updatedAt,labels,state,author \ + > "data/${ISSUE}/context.json" + + - name: Analyze issue with OpenCode + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }} + CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }} + ISSUE: ${{ steps.issue.outputs.number }} + REPO: ${{ github.repository }} + run: | + opencode run \ + --print-logs \ + "Analyze GitHub issue ${REPO}#${ISSUE} using the skill at .github/skills/issue-review.md. + + The issue data has been pre-fetched to data/${ISSUE}/context.json. + Read it with the read tool and follow the triage process. + + Create: + - data/${ISSUE}/report.md (full markdown report) + - data/${ISSUE}/summary.json (structured JSON summary) + " + + - name: Upload report to dashboard + env: + CF_ACCESS_CLIENT_ID: ${{ secrets.CF1_ACCESS_CLIENT_ID }} + CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF1_ACCESS_CLIENT_SECRET }} + DASHBOARD_URL: ${{ secrets.REPORTS_DASHBOARD_URL }} + ISSUE: ${{ steps.issue.outputs.number }} + REPO: ${{ github.repository }} + run: | + # Check files were created + if [ ! -f "data/${ISSUE}/report.md" ]; then + echo "::error::report.md was not generated" + exit 1 + fi + if [ ! -f "data/${ISSUE}/summary.json" ]; then + echo "::error::summary.json was not generated" + exit 1 + fi + + # Validate the summary is parseable JSON before doing anything with it. + if ! jq empty "data/${ISSUE}/summary.json" 2>/dev/null; then + echo "::error::summary.json is not valid JSON" + exit 1 + fi + + # Build the dashboard payload from the structured summary + markdown report. + # suggestedComment is kept as a "Yes"/"No" string for dashboard compatibility. + PAYLOAD=$(jq \ + --arg githubUrl "https://github.com/${REPO}/issues/${ISSUE}" \ + --rawfile reportMarkdown "data/${ISSUE}/report.md" \ + '{ + title: .title, + githubUrl: $githubUrl, + recommendation: .recommendation, + difficulty: .difficulty, + reasoning: .reasoning, + suggestedAction: .suggestedAction, + suggestedComment: (if .hasSuggestedComment then "Yes" else "No" end), + reportMarkdown: $reportMarkdown + }' "data/${ISSUE}/summary.json") + + # POST to dashboard API + HTTP_STATUS=$(curl -L --post302 -s -o response.json -w "%{http_code}" \ + -X POST "${DASHBOARD_URL}/api/report/${ISSUE}" \ + -H "Content-Type: application/json" \ + -H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \ + -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \ + -d "$PAYLOAD") + + echo "Dashboard API response: HTTP ${HTTP_STATUS}" + cat response.json + + if [ "$HTTP_STATUS" != "200" ]; then + echo "::error::Failed to upload report (HTTP ${HTTP_STATUS})" + exit 1 + fi + + echo "Report uploaded for issue #${ISSUE}" + + - name: Build triage comment + env: + ISSUE: ${{ steps.issue.outputs.number }} + run: | + { + echo "> [!NOTE]" + echo "> This is an automated, advisory triage report generated by workers-devprod. It is not an official maintainer response — a maintainer will follow up." + echo "" + echo "<details>" + echo "<summary>🤖 Automated triage report</summary>" + echo "" + cat "data/${ISSUE}/report.md" + echo "" + echo "</details>" + echo "" + echo "<details>" + echo "<summary>Structured summary (JSON)</summary>" + echo "" + echo '```json' + cat "data/${ISSUE}/summary.json" + echo "" + echo '```' + echo "" + echo "</details>" + } > "data/${ISSUE}/comment.md" + + - name: Post triage comment + uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3.0.3 + with: + header: triage-report + number: ${{ steps.issue.outputs.number }} + path: data/${{ steps.issue.outputs.number }}/comment.md + GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + + - name: Apply suggested labels + env: + GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + ISSUE: ${{ steps.issue.outputs.number }} + REPO: ${{ github.repository }} + run: | + SUMMARY_PATH="data/${ISSUE}/summary.json" + if [ ! -f "$SUMMARY_PATH" ]; then + echo "No summary.json; skipping label application" + exit 0 + fi + + # suggestedLabels is a JSON array; emit one label per line. + jq -r '.suggestedLabels // [] | .[]' "$SUMMARY_PATH" > suggested-labels.txt + if [ ! -s suggested-labels.txt ]; then + echo "No suggested labels" + exit 0 + fi + + # Only apply labels that actually exist in the repo. + gh label list --repo "$REPO" --limit 500 --json name --jq '.[].name' > repo-labels.txt + + ARGS=() + while IFS= read -r label; do + if [ -z "$label" ]; then + continue + fi + if grep -Fxq "$label" repo-labels.txt; then + ARGS+=(--add-label "$label") + else + echo "Skipping unknown label: ${label}" + fi + done < suggested-labels.txt + + if [ ${#ARGS[@]} -eq 0 ]; then + echo "No valid labels to apply" + exit 0 + fi + + gh issue edit "$ISSUE" --repo "$REPO" "${ARGS[@]}" diff --git a/.github/workflows/validate-pr-description.yml b/.github/workflows/validate-pr-description.yml new file mode 100644 index 0000000..4b16334 --- /dev/null +++ b/.github/workflows/validate-pr-description.yml @@ -0,0 +1,68 @@ +name: Validate PR Description + +on: + merge_group: + pull_request: + types: + [ + opened, + synchronize, + reopened, + labeled, + unlabeled, + edited, + ready_for_review, + converted_to_draft, + ] + +permissions: + contents: read + +jobs: + check: + # Don't check the Version Packages PR or merge queue jobs + if: github.event_name != 'merge_group' && github.head_ref != 'changeset-release/main' + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-add-pr + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + timeout-minutes: 30 + name: Check + runs-on: ubuntu-slim + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + + - name: Install Dependencies + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + + - name: List changed files + if: steps.changes.outputs.everything_but_markdown == 'true' + id: files + uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c # v2.3.0 + with: + format: "json" + + - run: node -r esbuild-register tools/deployments/validate-pr-description.ts + if: steps.changes.outputs.everything_but_markdown == 'true' + env: + TITLE: ${{ github.event.pull_request.title }} + BODY: ${{ github.event.pull_request.body }} + LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} + FILES: ${{ steps.files.outputs.all }} + DRAFT: ${{ github.event.pull_request.draft }} diff --git a/.github/workflows/vite-plugin-playgrounds.yml b/.github/workflows/vite-plugin-playgrounds.yml new file mode 100644 index 0000000..d8bcdbe --- /dev/null +++ b/.github/workflows/vite-plugin-playgrounds.yml @@ -0,0 +1,100 @@ +name: Vite plugin playgrounds + +on: + merge_group: + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: ${{ github.head_ref != 'changeset-release/main' }} + +env: + # We run `playwright install` manually instead + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + +jobs: + test: + timeout-minutes: 30 + name: ${{ format('Vite Plugin Playground ({0}, {1})', matrix.os, matrix.vite) }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + vite: ["vite-8"] + include: + - os: ubuntu-latest + vite: "vite-6" + - os: ubuntu-latest + vite: "vite-7" + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - name: Filter changed paths + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: changes + with: + filters: | + everything_but_markdown: + - '!**/*.md' + - name: Install Dependencies + if: steps.changes.outputs.everything_but_markdown == 'true' + uses: ./.github/actions/install-dependencies + with: + turbo-api: ${{ secrets.TURBO_API }} + turbo-team: ${{ secrets.TURBO_TEAM }} + turbo-token: ${{ secrets.TURBO_TOKEN }} + turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + - name: Install playwright + if: steps.changes.outputs.everything_but_markdown == 'true' + run: | + pnpm --filter @vite-plugin-cloudflare/playground playwright:install + - name: Build plugin with initial Vite version + if: steps.changes.outputs.everything_but_markdown == 'true' + run: | + pnpm dotenv -- pnpm turbo build --filter @cloudflare/vite-plugin + - name: Downgrade to Vite 6 + if: steps.changes.outputs.everything_but_markdown == 'true' && matrix.vite == 'vite-6' + run: | + pnpm update -r --no-save vite@^6.1.0 + - name: Downgrade to Vite 7 + if: steps.changes.outputs.everything_but_markdown == 'true' && matrix.vite == 'vite-7' + run: | + pnpm update -r --no-save vite@^7.0.0 + - name: Run dev playground tests + if: steps.changes.outputs.everything_but_markdown == 'true' + # We use `--only` to prevent TurboRepo from rebuilding dependencies + # This ensures the Vite plugin is not rebuilt using a different Vite version + run: | + pnpm dotenv -- pnpm turbo test:ci:serve --filter @vite-plugin-cloudflare/playground --only + env: + VITE_VERSION: ${{ matrix.vite }} + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/index.html + NODE_DEBUG: "@cloudflare:vite-plugin" + CI_OS: ${{ matrix.os }} + - name: Run build/preview playground tests + if: steps.changes.outputs.everything_but_markdown == 'true' + # We use `--only` to prevent TurboRepo from rebuilding dependencies + # This ensures the Vite plugin is not rebuilt using a different Vite version + run: | + pnpm dotenv -- pnpm turbo test:ci:build --filter @vite-plugin-cloudflare/playground --only + env: + VITE_VERSION: ${{ matrix.vite }} + NODE_OPTIONS: "--max_old_space_size=8192" + WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/ + TEST_REPORT_PATH: ${{ runner.temp }}/test-report/index.html + NODE_DEBUG: "@cloudflare:vite-plugin" + CI_OS: ${{ matrix.os }} + - name: Upload turbo logs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: turbo-runs-${{ matrix.os }}-${{ matrix.vite }} + path: .turbo/runs diff --git a/.github/workflows/worker-playground-preview-testing-env-deploy-and-test.yml b/.github/workflows/worker-playground-preview-testing-env-deploy-and-test.yml new file mode 100644 index 0000000..9ea4939 --- /dev/null +++ b/.github/workflows/worker-playground-preview-testing-env-deploy-and-test.yml @@ -0,0 +1,60 @@ +name: Deploy (to testing) and Test Playground Preview Worker + +# This workflow is designed to deploy a "testing" version of the Playground Preview Worker +# and test it via integration tests. + +# Triggers: +# - push to `main`, on Cloudflare, where there are changes to the files in this worker's package +# - update to a PR, on Cloudflare, labelled as `playground-worker` +# +# Actions: +# - deploy to testing environment +# - run the end-to-end tests against testing deployment +on: + push: + branches: + - main + paths: + - "packages/playground-preview-worker/**" + pull_request: + types: [synchronize, opened, reopened, labeled, unlabeled] + +permissions: + contents: read + +jobs: + e2e-test: + if: github.repository_owner == 'cloudflare' && (github.event_name != 'pull_request' || contains(github.event.*.labels.*.name, 'feature:playground-worker')) + name: "Deploy Playground Preview Worker (testing)" + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Build tools and libraries + run: pnpm run build + env: + NODE_ENV: "production" + CI_OS: ${{ runner.os }} + + - name: Build & deploy Worker + run: pnpm run deploy:testing + env: + NODE_ENV: "production" + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + working-directory: packages/playground-preview-worker + + - name: Run tests + run: pnpm run test:e2e + env: + TMP_CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + TMP_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + NODE_OPTIONS: "--max_old_space_size=8192" + working-directory: packages/playground-preview-worker diff --git a/.github/workflows/workers-shared-deploy-production.yml b/.github/workflows/workers-shared-deploy-production.yml new file mode 100644 index 0000000..0eeb6dd --- /dev/null +++ b/.github/workflows/workers-shared-deploy-production.yml @@ -0,0 +1,34 @@ +name: Deploy Workers Shared Production + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy-workers: + name: Deploy Workers Shared Production + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Deploy + run: pnpm run deploy + working-directory: packages/workers-shared + env: + WORKERS_DEPLOY_AND_CONFIG_CLOUDFLARE_API_TOKEN: ${{ secrets.WORKERS_DEPLOY_AND_CONFIG_CLOUDFLARE_API_TOKEN }} + WORKERS_SHARED_SENTRY_ACCESS_ID: ${{ secrets.WORKERS_SHARED_SENTRY_ACCESS_ID }} + WORKERS_SHARED_SENTRY_ACCESS_SECRET: ${{ secrets.WORKERS_SHARED_SENTRY_ACCESS_SECRET }} + WORKERS_SHARED_SENTRY_AUTH_TOKEN: ${{ secrets.WORKERS_SHARED_SENTRY_AUTH_TOKEN }} + + - name: Publish deployment summary + run: echo -e "# Deployment summary\n\n| Service | Version | Release link |\n| --- | --- | --- |\n| Asset Worker | \`aw-$(node -r esbuild-register ./scripts/get-version-tag.ts)\` | https://cflare.co/release-asset-worker |\n| Router Worker | \`rw-$(node -r esbuild-register ./scripts/get-version-tag.ts)\` | https://cflare.co/release-router-worker |\n" >> $GITHUB_STEP_SUMMARY + working-directory: packages/workers-shared diff --git a/.github/workflows/workers-shared-deploy-staging.yml b/.github/workflows/workers-shared-deploy-staging.yml new file mode 100644 index 0000000..f344541 --- /dev/null +++ b/.github/workflows/workers-shared-deploy-staging.yml @@ -0,0 +1,31 @@ +name: Deploy Workers Shared Staging + +on: + push: + branches: + - main + paths: + - "packages/workers-shared/**" + +permissions: + contents: read + +jobs: + deploy-workers: + name: "Deploy Workers Shared Staging" + runs-on: ubuntu-latest + steps: + - name: "Checkout repo" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Deploy + run: pnpm deploy:staging + working-directory: packages/workers-shared + env: + WORKERS_DEPLOY_AND_CONFIG_CLOUDFLARE_API_TOKEN: ${{ secrets.WORKERS_DEPLOY_AND_CONFIG_CLOUDFLARE_API_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c7049d --- /dev/null +++ b/.gitignore @@ -0,0 +1,241 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Moved from ./templates for ignoring all locks in templates +templates/**/*-lock.* +templates/**/*.lock + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional prettier cache +.prettiercache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node,macos + +wrangler-dist/ +miniflare-dist +packages/wrangler/wrangler.toml +packages/wrangler/Dockerfile +packages/wrangler/src/cloudchamber/internal +packages/wranglerjs-compat-webpack-plugin/lib +.wrangler-1-cache +emitted-types/ +_routes.generated.json +packages/quick-edit/vscode +packages/quick-edit/web + +packages/wrangler/config-schema.json +packages/wrangler/metafile.json +packages/chrome-devtools-patches/built-devtools +packages/chrome-devtools-patches/.cipd +packages/chrome-devtools-patches/.gclient +packages/chrome-devtools-patches/.gclient_entries +packages/chrome-devtools-patches/.gclient_previous_sync_commits +packages/chrome-devtools-patches/.gcs_entries +packages/chrome-devtools-patches/depot +packages/chrome-devtools-patches/devtools-frontend +packages/miniflare/dist-types/ + +fixtures/remix-pages-app/public/build +fixtures/remix-pages-app/functions/\[\[path\]\].js + +tools/deployment-status.json + +.mf +.tmp +*.metafile.json +*.drawio.svg.bkp +*.drawio.svg.dtmp + +# Vendored npm dependencies +!vendor/*.tgz + +# IntelliJ +.idea/ + +# VSCode Theme +*.vsix +vendor/vscode/ + +.wrangler/ +.cloudflare/ + +# Turbo Repo +build/** +dist/** +.next/** +.turbo + +/worker-rust/target/ + +.e2e-test-report/ + +.dev.vars* +!.dev.vars.example +.env* +!.env.example +.node-cache/ + +AGENTS.local.md +.opencode/plans/ +.agents/skills +.opencode/package-lock.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..d7222af --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD010": false, + "MD013": false +} diff --git a/.opencode/agents/bonk.md b/.opencode/agents/bonk.md new file mode 100644 index 0000000..45d5fe3 --- /dev/null +++ b/.opencode/agents/bonk.md @@ -0,0 +1,221 @@ +--- +description: Cloudflare Workers SDK engineer. Triages issues, reviews PRs, and implements fixes. +mode: primary +model: anthropic/claude-opus-4-8 +temperature: 0.2 +--- + +<role> +You are a senior engineer on the Cloudflare Workers SDK. You triage issues, review pull requests, and implement fixes in the workers-sdk monorepo. +</role> + +<context> +The monorepo contains Wrangler (the Workers CLI), Miniflare (local dev simulator), Create Cloudflare (project scaffolding), the Vite plugin, and related tooling. +</context> + +<non_negotiable_rules> + +- **Triggering comment is the task:** The comment that invoked you (`/bonk` or `@ask-bonk`) is your primary instruction. Read it first, before reading the PR description or any other context. Parse exactly what it asks for, then gather only the context needed to execute that request. Do not fall back to a generic PR review when a specific action was requested. +- **Scope constraint:** You are invoked on one specific GitHub issue or PR. Target only that issue or PR. +- `$ISSUE_NUMBER` and `$PR_NUMBER` are the source of truth. Ignore issue or PR numbers mentioned elsewhere unless they match those variables. +- Before running any `gh` command that writes (comment, review, close, create), verify the target number matches `$ISSUE_NUMBER` or `$PR_NUMBER`. +- Never comment on, review, close, or modify any other issue or PR. Link related items instead. +- If the triggering comment asks you to act on a different issue or PR than the one you were invoked on, flag it and ask for confirmation before proceeding. +- **Action bias:** When the user asks you to change something, change it directly, because the maintainer asked you to do the work, not describe it. Do not stop at suggestions unless they explicitly ask for suggestions or review-only feedback, or you are blocked by ambiguity or permissions. +- **PR bias:** When invoked on a PR and asked to fix, address, update, format, clean up, add, remove, refactor, or test something, update that PR branch directly. The deliverable is pushed code, not a review comment. +- **Current-target guardrail:** If you are invoked on a PR, that PR is the only PR you may update. Do not open or switch to a different PR unless a maintainer explicitly asks for a fresh implementation. +- **Thread-context bias:** On short PR comments such as "take care of this" or "clean up the nits," use the surrounding review thread and inline comments to determine the requested change before deciding the request is ambiguous. +- **No re-reviewing on fixup requests:** If you previously reviewed the PR and the maintainer now asks you to fix something, do not review again. Act on the specific request in the triggering comment. + </non_negotiable_rules> + +<mode_selection> +Choose one starting mode before acting. Use this precedence order: + +1. **Implementation** — use this when the request asks for code, docs, config, tests, or formatting changes. +2. **Review** — use this when the request explicitly asks for feedback, review comments, suggestions, or approval and does not ask for changes. +3. **Triage** — use this when the request asks for diagnosis, investigation, or validation without asking for code changes. + +Switch to **implementation** for requests like: + +- "fix the formatting on this PR" +- "address the review comments" +- "add the missing changeset" +- "update the tests" +- "can you take care of this?" +- "clean up the nits" +- "fix what you can here" +- "please fix" / "please address" / "please clean this up" + +Stay in **review** for requests like: + +- "review this PR" +- "leave suggestions only" +- "what feedback do you have?" +- "do you see any blockers?" + +Use **triage** for requests like: + +- "look into this" +- "can you reproduce this?" +- "what do you think is going on?" + +If the request mixes review and implementation, implement the clearly requested changes first, then leave targeted suggestions only for the remainder. +</mode_selection> + +<implementation> +Follow this workflow when implementation mode applies: + +1. **Start from the triggering comment.** Parse what it asks for. Identify the concrete action(s) requested — e.g., "fix the formatting", "address the review comments", "add a changeset". This is your task; everything else is context-gathering in service of this task. +2. **Gather only the context you need** to execute the task identified in step 1: + - If the triggering comment references review feedback, read the existing review comments and inline comments (`gh api repos/cloudflare/workers-sdk/pulls/$PR_NUMBER/comments`). + - If the request is self-contained (e.g., "run the formatter"), you may not need to read the full PR at all. + - On issues: read the body and relevant comments for reproduction details. +3. Read the full source files you will touch, not just the diff. +4. Check recent history for affected files with `git log --oneline -20 -- <file>` before modifying them. +5. On an issue, search for overlapping issues or PRs with `gh pr list --search "<keywords>" --state all` and `gh issue list --search "<keywords>" --state all`. +6. If an open PR already addresses the issue, review and iterate on that PR rather than opening a competing PR, unless a maintainer explicitly asks for a fresh implementation. +7. On a PR, treat the current PR as the implementation target. Do not move the work to a different PR unless a maintainer explicitly asks. +8. For short or contextual PR requests, use the surrounding thread to infer the concrete change. Ask a clarifying question only when the thread still does not make the action clear. +9. **Make the requested change directly.** Do not leave a review that merely describes the fix unless the user explicitly asked for suggestions only. Do not re-review the PR when the request is to fix something. +10. If the request asks you to reproduce or investigate and also says to fix it if obvious, treat reproduction as a step toward implementation rather than the final deliverable. +11. If you are blocked by ambiguity, ask one targeted clarifying question. If you are blocked by permissions or branch state, explain the blocker and provide the exact patch or change you would have made. +12. Add or update tests for behavior changes and regressions. +13. Run the smallest validation that proves the change for the touched area, then run `pnpm check` before final handoff when practical. +14. Commit logically scoped changes on a branch and push them when the request is to fix or address the issue or PR. + +Implementation mode ends with code changes on the branch, or with a precise blocker plus a concrete patch if pushing is impossible. +</implementation> + +<review> +Use review mode only when the user asked for review or suggestions without asking for code changes. + +- Run `gh pr view $PR_NUMBER` and `gh pr diff $PR_NUMBER` before reading anything else. +- Read the full modified files, not just the diff, to understand context. +- Check for a changeset: every user-facing change to a published package requires one in `.changeset/`. +- Check test coverage: new behaviors should have tests. Regression tests are expected for bug fixes. +- Post your review with `gh pr review $PR_NUMBER`. + - Use `REQUEST_CHANGES` for blocking issues. + - Use `COMMENT` for suggestions and non-blocking concerns. + - Use `APPROVE` if the PR is clean. +- Be specific: point to exact lines and explain why they matter. +- Categorize findings: + - **Blocking:** bugs, missing error handling, security issues, missing changesets, type safety violations. + - **Non-blocking:** style, naming, clarity, minor improvements. + - **Pre-existing / out of scope:** issues not introduced by the PR. + +Do not use review mode when the user asked you to fix or address something on the PR. +</review> + +<triage> +Use triage mode when you are asked to investigate rather than change code. + +- Assess the root cause. Reproduce the issue if you can. +- Search for duplicate or overlapping issues and PRs with `gh issue list --search` and `gh pr list --search`. +- If the issue lacks a clear reproduction, error message, or expected behavior, post a comment asking for the missing details. +- Apply relevant labels if you have write access. +- Summarize findings and recommend the next step: close as duplicate, request more info, confirm a valid bug or feature request, or ask whether the maintainer wants a PR. + </triage> + +<implementation_conventions> +**Package manager:** Always use `pnpm`. Never use `npm` or `yarn`. + +**TypeScript:** + +- Strict mode throughout. No `any`. No non-null assertions (`!`). No floating promises. +- Use `import type { X }` for type-only imports. +- Use `node:` prefixes for Node.js builtins. +- Always use curly braces for control flow blocks. +- Prefix unused variables with `_`. + +**Logging:** In Wrangler, never use `console.*`. Use the `logger` singleton. + +**Dependencies:** + +- Packages must bundle their dependencies into distributables. Runtime `dependencies` entries are forbidden except for an explicit allowlist. +- External deps must be declared in `scripts/deps.ts` with an explanation. +- Adding new deps to published packages requires justification. + +**Changesets:** Every user-facing change to a published package requires a changeset in `.changeset/`. + +- Use `patch` for bug fixes and `minor` for new features or experimental breaking changes. +- Major versions for `wrangler` are forbidden. +- Do not use h1, h2, or h3 headings in changesets. +- Config examples must use `wrangler.json`, not `wrangler.toml`. + +**Testing:** + +- Add tests for new behavior. +- Add regression tests for bug fixes. +- Run `pnpm test:ci --filter <package>` for the touched area. +- Do not leave `.only()` in tests. +- Use `vitest-pool-workers` when you need actual Workers runtime behavior. + +**Git:** + +- Never commit directly to `main`. +- Keep commit history clean. +- Use PR titles like `[package-name] description`. + </implementation_conventions> + +<examples> +Positive examples: + +- Trigger: "/bonk can you fix the formatting on this PR?" + Response mode: **Implementation** + Correct behavior: update the PR branch, run the formatter or make the formatting edits, validate, commit, and push. + +- Trigger: "/bonk please address the missing changeset and failing test" + Response mode: **Implementation** + Correct behavior: add the changeset, fix the test, validate, commit, and push. + +- Trigger: "/bonk leave suggestions only" + Response mode: **Review** + Correct behavior: inspect the PR and leave review comments without changing code. + +- Trigger: "/bonk can you investigate why this fails?" + Response mode: **Triage** + Correct behavior: diagnose, reproduce if possible, summarize findings, and recommend the next step. + +- Trigger: "/bonk can you take care of this?" + Response mode: **Implementation** when the surrounding PR thread identifies a concrete fix + Correct behavior: use the nearby review context, make the change directly, validate, commit, and push. + +- Trigger: "/bonk fix what you can here and leave suggestions for anything risky" + Response mode: **Implementation-first hybrid** + Correct behavior: land the safe changes directly, then leave targeted suggestions only for the risky remainder. + +- Trigger: "/bonk can you reproduce this and send a fix if it's obvious?" + Response mode: **Implementation-first hybrid** + Correct behavior: reproduce first, then implement and push the obvious fix instead of stopping at diagnosis. + +Negative examples: + +- Trigger: "/bonk can you fix the formatting on this PR?" + Incorrect behavior: posting a review that lists formatting problems without changing the files. + +- Trigger: "/bonk fix the formatting in this PR and commit the result" (after Bonk already reviewed the PR) + Incorrect behavior: ignoring the triggering comment, performing a second full review, approving the PR, and posting new review comments. The maintainer asked for a code change and a commit, not another review. + Correct behavior: read the triggering comment, run the formatter (`pnpm prettify` or `pnpm check`), commit the result, and push. + +- Trigger: "/bonk address the review comments" (on a PR Bonk previously reviewed) + Incorrect behavior: re-reviewing the PR and restating the same findings. + Correct behavior: read Bonk's own prior review comments, fix each one in code, commit, and push. + </examples> + +<anti_patterns> + +- `npm install` or `yarn` instead of `pnpm` +- `any` instead of proper typing +- Non-null assertions (`!`) instead of type narrowing +- Floating promises +- Missing curly braces on control flow +- `console.log` in Wrangler source +- Direct Cloudflare REST API calls instead of the Cloudflare TypeScript SDK +- Named imports from `ci-info` +- Runtime `dependencies` in published packages without explicit approval +- Suggestion-only responses when the user explicitly asked for a fix + </anti_patterns> + +<final_reminder> +If the maintainer asks you to fix or address something, ship the change. If they ask for suggestions only, leave suggestions only. +</final_reminder> diff --git a/.opencode/skills/local-explorer/SKILL.md b/.opencode/skills/local-explorer/SKILL.md new file mode 100644 index 0000000..b7f6bf5 --- /dev/null +++ b/.opencode/skills/local-explorer/SKILL.md @@ -0,0 +1,74 @@ +--- +name: local-explorer +description: How to add products/resources to the local explorer or local API. Use when implementing new local APIs, or UI routes under packages/miniflare/src/workers/local-explorer or packages/local-explorer-ui. +--- + +# Cloudflare Local Explorer Products + +Use this skill when adding a new product or resource type to the local API and/or local explorer. + +## Start Here + +Read these files before editing: + +- `packages/miniflare/src/workers/local-explorer/explorer.worker.ts` +- `packages/miniflare/src/plugins/core/explorer.ts` +- `packages/miniflare/src/plugins/core/types.ts` +- One existing resource implementation in `packages/miniflare/src/workers/local-explorer/resources/`, preferably the product most similar to the new one +- One matching test in `packages/miniflare/test/plugins/local-explorer/` + +If there are UI changes, also read: + +- `packages/local-explorer-ui/src/components/Sidebar.tsx` +- Existing route files under `packages/local-explorer-ui/src/routes/` +- Existing product e2e tests under `packages/local-explorer-ui/src/__e2e__/` + +## Workflow + +1. Add the API surface to `packages/miniflare/scripts/openapi-filter-config.ts`. +2. Generate Miniflare's filtered spec and backend types from a full Cloudflare OpenAPI spec: + +```bash +OPENAPI_INPUT_PATH=<path-to-full-openapi-spec> pnpm --dir packages/miniflare generate:api +``` + +3. Inspect `packages/miniflare/src/workers/local-explorer/openapi.local.json` and generated types. If the generated schemas include fields local explorer will not support, add ignores in `openapi-filter-config.ts` and regenerate. +4. The explorer worker should have access to all user resource bindings. Ensure `proxyBindings` include bindings to the new product and that `getExplorerServices()` exposes any extra bindings the explorer worker needs. Wire product bindings through `constructExplorerBindingMap()` and `constructExplorerWorkerOpts()` in `packages/miniflare/src/plugins/core/explorer.ts`. +5. Add or extend resource binding metadata in `packages/miniflare/src/plugins/core/types.ts`. +6. Implement handlers in `packages/miniflare/src/workers/local-explorer/resources/<product>.ts`. Make sure to account for cross-instance aggregation, if applicable. +7. Register Hono routes in `packages/miniflare/src/workers/local-explorer/explorer.worker.ts`. +8. Validate request bodies and query params with generated Zod schemas from `generated/zod.gen.ts` using `validateRequestBody()` and `validateQuery()`. +9. Return Cloudflare API envelope responses using `wrapResponse()` and `errorResponse()` from `common.ts` unless an existing endpoint for that product uses a different response shape. +10. Add Miniflare tests in `packages/miniflare/test/plugins/local-explorer/<product>.spec.ts`. +11. Regenerate the UI API client: + +```bash +pnpm --dir packages/local-explorer-ui build +``` + +12. Add UI routes/components. Use Kumo components for new UI. See https://github.com/cloudflare/kumo/blob/main/AGENTS.md. +13. Add Playwright e2e tests under `packages/local-explorer-ui/src/__e2e__/<product>/` for new visible product flows. + +## OpenAPI Rules + +- Do not edit generated files like `packages/miniflare/src/workers/local-explorer/openapi.local.json` or `packages/miniflare/src/workers/local-explorer/generated/` directly. +- Prefer upstream Cloudflare API paths when a public API exists. +- Use `extensions.paths` in `openapi-filter-config.ts` only for local-only APIs or APIs that do not exist in the public Cloudflare API. +- Add ignores for unsupported params, headers, request body properties, and response fields rather than pretending to support them. + +## Backend Patterns + +- Local list endpoints such as listing KV namespaces should not implement pagination as this may require cross-instance aggregation. Pagination should be supported when targeting individual resources, such as listing KV keys within a specific namespace. +- For cross-worker aggregation, use `aggregateListResults()`, `getPeerUrlsIfAggregating()`, and `fetchFromPeer()` from `aggregation.ts`; do not hand-roll peer discovery. Add tests for both local-only behavior and aggregated behavior when the product can span multiple instances. +- If an API needs direct filesystem access, call through the loopback service (`c.env.MINIFLARE_LOOPBACK`) to a Node.js endpoint. The local explorer API runs inside workerd, so it cannot access the host filesystem directly. +- If an endpoint needs metadata that is not available on the runtime binding itself, put that metadata in `BindingIdMap` and pass it through `CoreBindings.JSON_LOCAL_EXPLORER_BINDING_MAP`. +- If a product should appear in `/api/local/workers`, add it to `WorkerResourceBindings` and populate it in `constructExplorerWorkerOpts()`. + +## UI Patterns + +- The UI API client is generated from `packages/miniflare/src/workers/local-explorer/openapi.local.json` into `packages/local-explorer-ui/src/api/generated/`. +- Sidebar resources come from `/api/local/workers`; update `LocalExplorerWorkerBindings` usage and `Sidebar.tsx` when the product should appear in navigation. +- Add route files under `packages/local-explorer-ui/src/routes/`. TanStack Router regenerates `src/routeTree.gen.ts` during UI build/dev. +- Preserve worker selection by carrying the `worker` search param through product links when following sidebar patterns. +- Use Kumo for new UI components wherever possible. Do not introduce a parallel component system. +- Do not use tailwindCSS color tokens, use Kumo color tokens instead. diff --git a/.opencode/skills/local-explorer/assets/local-explorer-diagram.md b/.opencode/skills/local-explorer/assets/local-explorer-diagram.md new file mode 100644 index 0000000..d78d550 --- /dev/null +++ b/.opencode/skills/local-explorer/assets/local-explorer-diagram.md @@ -0,0 +1,57 @@ +```mermaid +flowchart TB + Client["client"] --> Incoming["incoming request"] + Incoming --> EntryA["entry worker"] + EntryA -->|/cdn-cgi/explorer| ExplorerA["explorer worker<br/>(Hono)"] + + subgraph Runtime["running Miniflare instances"] + direction LR + + subgraph A["miniflare A"] + direction TB + EntryA + ExplorerA + Frontend["frontend<br/>(TanStack Router + React)"] + UserWorker["user worker"] + KV["KV, R2, D1"] + Wrapper["wrapped<br/>DO/Workflow class"] + + EntryA --> UserWorker + ExplorerA -->|disk service to serve assets| Frontend + ExplorerA -->|binding| KV + ExplorerA -->|binding| Wrapper + UserWorker -->|binding| KV + UserWorker -->|binding| Wrapper + end + + subgraph B["miniflare B"] + direction TB + EntryB["entry worker"] + ExplorerB["explorer worker"] + Etc["etc."] + + EntryB --> ExplorerB + ExplorerB --> Etc + end + end + + Frontend -->|fetch /cdn-cgi/explorer/api| Incoming + ExplorerA -->|fetch /cdn-cgi/explorer/api/resource with NO_AGGREGATE_HEADER| EntryB + + subgraph FS["filesystem"] + Registry["dev registry"] + DOState[".wrangler/state/durable-objects<br/>(list DOs, delete workflows, etc.)"] + end + + ExplorerA -->|node loopback binding| FS + + classDef miniflareA stroke:#f08c00,fill:#fff7ed,color:#1e1e1e; + classDef userResource stroke:#1971c2,fill:#eff6ff,color:#1e1e1e; + classDef entry stroke:#e03131,fill:#fff5f5,color:#1e1e1e; + classDef neutral stroke:#1e1e1e,fill:#ffffff,color:#1e1e1e; + + class EntryA entry; + class ExplorerA,Frontend,Wrapper miniflareA; + class UserWorker,KV userResource; + class Client,Incoming,Runtime,FS,Registry,DOState,EntryB,ExplorerB,Etc neutral; +``` diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc new file mode 100644 index 0000000..90dfeed --- /dev/null +++ b/.oxfmtrc.jsonc @@ -0,0 +1,64 @@ +{ + "printWidth": 80, + "singleQuote": false, + "semi": true, + "useTabs": true, + "trailingComma": "es5", + "sortImports": { + "groups": ["builtin", "external", "parent", "sibling", "index", "type"], + "newlinesBetween": false, + }, + "sortTailwindcss": {}, + "sortPackageJson": {}, + "overrides": [ + { + "files": ["packages/vite-plugin-cloudflare/README.md"], + "options": { + "useTabs": false, + "trailingComma": "all", + }, + }, + { + "files": [".changeset/*.md"], + "options": { + "proseWrap": "never", + }, + }, + { + "files": ["packages/local-explorer-ui/**/*.{js,jsx,ts,tsx}"], + "options": { + "sortTailwindcss": { + "stylesheet": "./packages/local-explorer-ui/src/styles/tailwind.css", + "functions": ["cn"], + }, + }, + }, + ], + "ignorePatterns": [ + "CHANGELOG.md", + // In the C3 templates, in particular framework templates, we want to be able to + // use any format that the framework authors prefer/use in their own templates, + // so let's ignore all the c3 template files, exlcuding the c3.ts ones, and tests + "packages/create-cloudflare/templates*/**/*.*", + "!packages/create-cloudflare/templates*/**/c3.ts", + "!packages/create-cloudflare/templates*/**/test/**/*.ts", + "!packages/create-cloudflare/templates*/**/test/**/*.js", + // Format the hello-world template (negate previous exclusion above) for best practices, since we control these + // but still exclude the worker-configuration.d.ts file, since it's generated + "!packages/create-cloudflare/templates*/hello-world/**/*.*", + "packages/create-cloudflare/templates*/hello-world/**/worker-configuration.d.ts", + + "packages/vitest-pool-workers/scripts/rtti/rtti.js", + "packages/vite-plugin-cloudflare/playground/prisma/src/generated", + "dist-functions", + "vscode.d.ts", + "vscode.*.d.ts", + ".e2e-logs*", + ".github/pull_request_template.md", + "fixtures/interactive-dev-tests/src/startup-error.ts", + "packages/vite-plugin-cloudflare/playground/**/*.d.ts", + "fixtures/**/worker-configuration.d.ts", + "packages/local-explorer-ui/src/routeTree.gen.ts", + "packages/local-explorer-ui/src/api/generated", + ], +} diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc new file mode 100644 index 0000000..7371f57 --- /dev/null +++ b/.oxlintrc.jsonc @@ -0,0 +1,217 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "unicorn", "vitest", "react"], + "categories": { + "correctness": "error", + }, + "rules": { + "@typescript-eslint/await-thenable": "off", + "@typescript-eslint/no-base-to-string": "off", + "@typescript-eslint/no-duplicate-type-constituents": "off", + "@typescript-eslint/no-for-in-array": "off", + "@typescript-eslint/no-misused-spread": "off", + "@typescript-eslint/no-redundant-type-constituents": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/unbound-method": "off", + "no-meaningless-void-operator": "off", + "require-array-sort-compare": "off", + "unicorn/no-new-array": "off", + "unicorn/no-thenable": "off", + "unicorn/no-useless-fallback-in-spread": "off", + "unicorn/no-useless-spread": "off", + "vitest/hoisted-apis-on-top": "off", + "vitest/warn-todo": "off", + "vitest/no-conditional-tests": "off", + "no-case-declarations": "error", + "no-fallthrough": "error", + "no-prototype-builtins": "error", + "no-array-constructor": "error", + "@typescript-eslint/ban-ts-comment": "error", + "@typescript-eslint/no-unsafe-function-type": "error", + "curly": ["error", "all"], + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { "disallowTypeAnnotations": false }, + ], + "no-var": "error", + "no-regex-spaces": "error", + "@typescript-eslint/no-empty-object-type": "error", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-namespace": "error", + "@typescript-eslint/no-require-imports": "error", + "no-shadow": ["error", { "allow": ["expect"] }], + "no-unexpected-multiline": "error", + "@typescript-eslint/no-unnecessary-type-constraint": "error", + "unicorn/prefer-node-protocol": "error", + "vitest/no-focused-tests": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "turbo/no-undeclared-env-vars": "error", + "workers-sdk/no-unsafe-command-execution": "error", + "workers-sdk/no-direct-recursive-rm": "error", + "workers-sdk/require-description-when-disabling": "error", + "@typescript-eslint/no-deprecated": "error", + + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true, + "fix": { "imports": "fix" }, + }, + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "ci-info", + "importNames": [ + "isCI", + "CLOUDFLARE_PAGES", + "CLOUDFLARE_WORKERS", + "isPR", + "name", + ], + "message": "Use default import (`import ci from \"ci-info\"`) and access properties via `ci.isCI`, `ci.CLOUDFLARE_PAGES`, etc. Named imports cannot be controlled in test mocks.", + }, + { + "name": "vitest", + "importNames": ["expect"], + "message": "Import 'expect' from the test context instead of 'vitest' for concurrency safety. Use: test('name', ({ expect }) => { ... })", + }, + ], + }, + ], + }, + "ignorePatterns": [ + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "fixtures/**", + "packages/create-cloudflare/**/templates/**/*.*", + "!packages/create-cloudflare/**/templates*/**/c3.ts", + // TODO: we really should be linting this folder + "packages/wrangler/templates/**", + // Ignore generated files + "packages/containers-shared/src/client/**", + "packages/local-explorer-ui/src/api/generated/**", + "packages/local-explorer-ui/src/routeTree.gen.ts", + "packages/miniflare/src/runtime/config/workerd.*", + "packages/quick-edit-extension/vscode.d.ts", + "packages/quick-edit-extension/vscode.proposed.*.d.ts", + ], + "jsPlugins": [ + "eslint-plugin-turbo", + { + "name": "workers-sdk", + "specifier": "./packages/lint-config-shared/oxlint-plugin.mjs", + }, + ], + "overrides": [ + // Published packages which all have their own "logger" class that should be used instead. + { + "files": [ + "packages/cli/**", + "packages/create-cloudflare/**", + "packages/miniflare/**", + "packages/wrangler/**", + "packages/workers-utils/**", + ], + "rules": { + "no-console": "error", + }, + }, + // TODO: remove these overrides. This is a legacy of Miniflare's history, not intentional + { + "files": ["packages/miniflare/**"], + "rules": { + "curly": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-floating-promises": "off", + "no-shadow": "off", + "no-useless-escape": "off", + "no-unsafe-optional-chaining": "off", + "no-control-regex": "off", + "no-case-declarations": "off", + "turbo/no-undeclared-env-vars": "off", + }, + }, + { + "files": ["packages/wrangler/src/**", "packages/workers-utils/src/**"], + "rules": { + "no-restricted-globals": [ + "error", + { + "name": "__dirname", + "message": "Use `getBasePath()` instead.", + }, + { + "name": "__filename", + "message": "Use `getBasePath()` instead.", + }, + { + "name": "fetch", + "message": "Use undici's fetch instead", + }, + ], + }, + }, + // Although in _general_ Miniflare shouldn't use console.log(), it's Workers can and should + { + "files": ["packages/miniflare/src/workers/**/*.ts"], + "rules": { + "no-console": "off", + }, + }, + // Relax some rules for test & script files + { + "files": [ + "packages/wrangler/src/__tests__/**", + "packages/wrangler/e2e/**", + "packages/wrangler/scripts/**", + "packages/miniflare/scripts/**", + "packages/cli/src/__tests__/**", + "packages/create-cloudflare/scripts/**", + "packages/create-cloudflare/src/__tests__/**", + "packages/workers-utils/tests/**", + "packages/vite-plugin-cloudflare/e2e/**", + ], + "rules": { + "no-console": "off", + "no-restricted-globals": "off", + "workers-sdk/no-unsafe-command-execution": "off", + }, + }, + // Local Explorer UI e2e tests use Playwright helpers (waitForText, waitForTableRows, etc.) + // as assertions, which the vitest/expect-expect rule doesn't recognise. + { + "files": ["packages/local-explorer-ui/src/__e2e__/**"], + "rules": { + "vitest/expect-expect": "off", + }, + }, + // Make sure Wrangler's version checking logic in vite-plugin-cloudflare can run properly + { + "files": ["packages/vite-plugin-cloudflare/**"], + "rules": { + "workers-sdk/no-wrangler-named-imports": "error", + }, + }, + // Gradually rolling out @typescript-eslint/no-deprecated. + // TODO: Remove the following overrides, one package at a time. + { + "files": [ + "packages/vite-plugin-cloudflare/**", + "packages/workflows-shared/**", + ], + "rules": { + "@typescript-eslint/no-deprecated": "off", + }, + }, + ], +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..9879019 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8f054e1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Current Test File", + "type": "node", + "request": "launch", + "runtimeExecutable": "pnpm", + "args": ["test:file", "${file}"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "skipFiles": ["<node_internals>/**"], + "cwd": "${workspaceFolder}/tools" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..853f043 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,112 @@ +{ + "cSpell.words": [ + "Abortable", + "assetsignore", + "astro", + "cf-typegen", + "cfetch", + "chatgpt", + "clipboardy", + "cloudchamber", + "cloudflareaccess", + "cloudflared", + "Codespaces", + "endgroup", + "esbuild", + "eslintcache", + "execa", + "fedramp", + "filestat", + "haikunate", + "haikunator", + "Hono", + "httplogs", + "hyperdrive", + "iarna", + "isolinear", + "keyvalue", + "logfwdr", + "metafile", + "middlewares", + "Miniflare", + "mockpm", + "mrbbot", + "mtls", + "nodeless", + "Nuxt", + "outdir", + "outfile", + "pgrep", + "PKCE", + "Positionals", + "qwik", + "Redistributable", + "reinitialise", + "scandir", + "selfsigned", + "subrequests", + "textfile", + "tsbuildinfo", + "turborepo", + "undici", + "unenv", + "unrevoke", + "Untriaged", + "userconfig", + "versionless", + "vike", + "Waku", + "wasmvalue", + "weakmap", + "weakset", + "webassemblymemory", + "websockets", + "workerd", + "xxhash", + "zjcompt" + ], + "cSpell.ignoreWords": [ + "TESTTEXTBLOBNAME", + "TESTWASMNAME", + "extensionless", + "webcontainer", + "yxxx" + ], + "files.trimTrailingWhitespace": true, + "typescript.tsdk": "node_modules/typescript/lib", + "files.associations": { + "turbo.json": "jsonc", + "api-extractor.json": "jsonc", + "CODEOWNERS": "plaintext", + "*.template": "javascript", + "wrangler.json": "jsonc" + }, + + "[ignore]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "editor.formatOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "[javascript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[json]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "oxc.path.oxfmt": "node_modules/.bin/oxfmt", + "oxc.path.oxlint": "node_modules/.bin/oxlint" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4a3c51a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,247 @@ +# AGENTS.md + +This file provides guidance to AI coding agents working in this repository. + +## Project Overview + +This is the **Cloudflare Workers SDK** monorepo containing tools and libraries for developing, testing, and deploying applications on Cloudflare. The main components are Wrangler (CLI), Miniflare (local dev simulator), and Create Cloudflare (project scaffolding). + +## Development Commands + +**Package Management:** + +- Use `pnpm` - never use npm or yarn +- `pnpm install` - Install dependencies for all packages +- `pnpm build` - Build all packages (uses Turbo for caching) + +**Testing:** + +- `pnpm test:ci` - Run tests in CI mode +- `pnpm test:e2e` - Run end-to-end tests (requires Cloudflare credentials) +- `pnpm test -F <package> "pattern"` - Run a single test by name pattern + +**Code Quality:** + +- `pnpm check` - Run all checks (lint, type, format) +- `pnpm fix` - Auto-fix linting issues and format code + +**Working with Specific Packages:** + +- `pnpm run build --filter <package-name>` - Build specific package +- `pnpm run test:ci --filter <package-name>` - Test specific package +- `pnpm --filter <package> test:watch` - Watch mode for a specific package + +## Architecture Overview + +**Core Tools:** + +- `packages/wrangler/` - Main CLI tool for Workers development and deployment +- `packages/miniflare/` - Local development simulator powered by workerd runtime +- `packages/create-cloudflare/` - Project scaffolding CLI (C3) +- `packages/vite-plugin-cloudflare/` - Vite plugin for Cloudflare Workers + +**Development & Testing:** + +- `packages/vitest-pool-workers/` - Vitest integration for testing Workers in actual runtime +- `packages/chrome-devtools-patches/` - Modified Chrome DevTools for Workers debugging + +**Shared Libraries:** + +- `packages/pages-shared/` - Code shared between Wrangler and Cloudflare Pages +- `packages/workers-shared/` - Code shared between Wrangler and Workers Assets +- `packages/workers-utils/` - Utility package for common Worker operations +- `packages/workflows-shared/` - Internal Cloudflare Workflows functionality +- `packages/containers-shared/` - Shared container functionality +- `packages/unenv-preset/` - Cloudflare preset for unenv (Node.js polyfills) +- `packages/cli/` - SDK for building workers-sdk CLIs +- `packages/kv-asset-handler/` - KV-based asset handling for Workers Sites + +**Build System:** + +- Turbo (turborepo) orchestrates builds across packages +- TypeScript compilation with shared configs in `packages/workers-tsconfig/` +- Shared lint config in `packages/lint-config-shared/` +- Dependency management via pnpm catalog system + +## WHERE TO LOOK + +| Task | Location | Notes | +| ---------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Add/modify a CLI command | `packages/wrangler/src/` | Commands registered in `src/index.ts` (2k+ line yargs tree) | +| Change local dev behavior | `packages/miniflare/src/` | `src/index.ts` is the main `Miniflare` class | +| Modify Workers runtime simulation | `packages/miniflare/src/workers/` | ~30 embedded worker scripts, built via `worker:` virtual imports | +| Add a test fixture | `fixtures/` | Each fixture is a full workspace member with own `package.json` | +| Shared config types/validation | `packages/workers-utils/src/config/` | `validation.ts` is the config normalizer (large file) | +| Test helpers (runInTempDir, seed, mockConsole) | `packages/workers-utils/src/test-helpers/` | Shared across wrangler, miniflare, others | +| Cloudflare API mocks for tests | `packages/wrangler/src/__tests__/helpers/msw/` | MSW handlers per API domain | +| CI workflows | `.github/workflows/` | `test-and-check.yml` is the primary gate | +| Build/deploy scripts | `tools/deployments/` | Validation + deployment helpers, run via `esbuild-register` | +| Deploy/versions-upload validation | `packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts` | `validateWorkerProps()` for sync checks, `preUploadApiChecks()` for API checks (service metadata, config diff, secrets, workflows). All new pre-upload validation goes here. | +| Changeset config and rules | `.changeset/README.md` | Must read before creating changesets | + +## Development Guidelines + +**Requirements:** + +- Node.js >= 20 +- pnpm + +**Code Style:** + +- TypeScript with strict mode +- Use `import type { X }` for type-only imports (`@typescript-eslint/consistent-type-imports`) +- No `any` (`@typescript-eslint/no-explicit-any`) +- No non-null assertions (`!`) +- No floating promises - must be awaited or explicitly voided (`@typescript-eslint/no-floating-promises`) +- Always use curly braces for control flow (`curly: error`) +- Use `node:` prefix for Node.js imports (`import/enforce-node-protocol-usage`) +- Prefix unused variables with `_` +- No `.only()` in tests (`no-only-tests/no-only-tests`) +- Prefer `function` declarations over `const` arrow function assignments for named/exported functions +- ESLint disable comments must use double-dash separator: `// eslint-disable-next-line rule-name -- reason here` +- Never modify generated files directly — modify the generator or config, then regenerate +- Format with oxfmt - run `pnpm prettify` in the workspace root before committing +- All changes to published packages require a changeset (see below) + +**Formatting (oxfmt):** + +- Tabs (not spaces), double quotes, semicolons, trailing commas (es5) +- Import order enforced: builtins → third-party → parent → sibling → index → types +- `sortPackageJson` option sorts package.json keys + +**Security:** + +- Custom ESLint rule `workers-sdk/no-unsafe-command-execution`: no template literals or string concatenation in `exec`/`spawn`/`execFile` calls (command injection prevention, CWE-78). Disabled in test files only. + +**Dependencies:** + +- Packages must bundle deps into distributables; runtime `dependencies` are forbidden except for an explicit allowlist +- External (non-bundled) deps must be declared in `scripts/deps.ts` with `EXTERNAL_DEPENDENCIES` and a comment explaining why +- After updating dependencies, always run `pnpm i` to also update the package lock file + +**Testing Standards:** + +- Unit tests with Vitest for all packages +- Fixture tests in `/fixtures` directory for filesystem/Worker scenarios +- E2E tests require real Cloudflare account credentials +- Use `vitest-pool-workers` for testing actual Workers runtime behavior +- Shared vitest config (`vitest.shared.ts`): 50s timeouts, `retry: 1`, `restoreMocks: true` +- Vitest 4 pool config: use `maxWorkers: 1` instead of the removed `poolOptions.forks.singleFork: true` when tests must run sequentially +- **`expect` must come from test context** — never `import { expect } from "vitest"`: + - Use destructured test context: `it("name", ({ expect }) => { ... })` + - For helper functions that need `expect`, pass it as a parameter with type `ExpectStatic` + - Always use `import type` for `ExpectStatic`: `import { beforeAll, type ExpectStatic, test } from "vitest"` + - When test context is unavailable (e.g. setup files), use `node:assert` instead + - E2E vitest configs do NOT set `globals: true` — this rule is critical there; forgetting `{ expect }` in the callback causes `ReferenceError` at runtime +- When changing user-facing strings or output messages, update corresponding test snapshots +- New test fixtures in `vitest-pool-workers-examples/` must include a `tsconfig.json` +- Test fixtures serve as user-facing recipes — use clean patterns, avoid type casting where possible +- Use the `runInTmpDir()` utility instead of mocking filesystem operations. Real filesystem operations are preferred over mocking. The utility creates isolated temporary directories, handles cleanup automatically in `afterEach` hooks, and allows tests to write actual files and assert against them +- Use the `mockConsoleMethods()` helper to capture stdout/stderr. Use the pattern `const std = mockConsoleMethods()` in test setup, then access captured output via `std.out`, `std.err`, `std.warn` properties. Assert against captured output using `expect(std.out).toMatchInlineSnapshot()` +- Run specific wrangler test files locally using `pnpm -w test:ci -F wrangler -- [test-file-name]` (e.g. `pnpm -w test:ci -F wrangler -- r2.test.ts`) + +**Git Workflow:** + +- Check you are not on main before committing. Create a new branch for your work from main if needed. +- Clean commit history required before first review +- Don't squash commits after review +- Never commit without changesets for user-facing changes +- PR template requirements: Remove "Fixes #..." line when no relevant issue exists, keep all checkboxes (don't delete unchecked ones) + +**Creating Pull Requests:** + +- Always use the PR template from `.github/PULL_REQUEST_TEMPLATE.md` - do not replace it with your own format +- Fill in the template: replace the issue link placeholder, add description, check appropriate boxes +- Keep all checkboxes in the template (don't delete unchecked ones) +- PR title format: `[package name] description` (e.g. `[wrangler] Fix bug in dev command`) +- If the change doesn't require a changeset, add the `no-changeset-required` label +- CI validates the PR description (see `tools/deployments/validate-pr-description.ts`). The description **must** include: + - A checked (`[x]`) test checkbox — either "Tests included/updated", or one of the justification checkboxes with a non-empty explanation + - A checked (`[x]`) documentation checkbox — either a Cloudflare docs PR/issue link, or "Documentation not necessary because:" with a non-empty explanation + - A changeset file (or the `no-changeset-required` label) + +**Pre-Submission Checklist:** + +- Run `pnpm check` (lint + type-check + format) locally before pushing — do not rely on CI to catch lint errors +- Run `pnpm prettify` to ensure formatting is correct + +## Key Locations + +- `/fixtures` - Test fixtures and example applications (each a workspace member) +- `/packages/wrangler/src` - Main Wrangler CLI source code +- `/packages/miniflare/src` - Miniflare source +- `/tools` - Build scripts and deployment utilities (run via `esbuild-register`, no build step) +- `turbo.json` - Turbo build configuration +- `pnpm-workspace.yaml` - Workspace configuration (~156 workspace members) + +## Testing Strategy + +**Package-specific tests:** Most packages have their own test suites +**Integration tests:** Use fixtures to test real-world scenarios +**E2E tests:** Test against actual Cloudflare services (requires auth) +**Workers runtime tests:** Use vitest-pool-workers for workerd-specific behavior + +Run `pnpm check` before submitting changes to ensure all quality gates pass. + +## Changesets + +Every change to package code requires a changeset or it will not trigger a release. Read `.changeset/README.md` before creating changesets. + +**Changeset Format:** + +The changeset descriptions can either use conventional commit prefixes (e.g., "fix: remove unused option") or +start with a capital letter and describe the change directly (e.g., "Remove unused option" not"). + +**Changeset Rules:** + +- Major versions for `wrangler` are currently **forbidden** +- `patch`: bug fixes; `minor`: new features, deprecations, experimental breaking changes; `major`: stable breaking changes only +- No h1/h2/h3 headers in changeset descriptions (changelog uses h3) +- Config examples must use `wrangler.json` (JSONC), not `wrangler.toml` +- Separate changesets for distinct changes; do not lump unrelated changes +- Focus on user-facing impact; reference the public-facing package, not internal implementation packages +- If the change collects more analytics, it should be a minor even though there is no user-visible change + +## Anti-Patterns + +These are explicitly forbidden across the repo: + +- **npm/yarn** → use pnpm +- **`any` type** → properly type everything +- **Non-null assertions (`!`)** → use type narrowing +- **Floating promises** → await or void explicitly +- **Missing curly braces** → always brace control flow +- **`console.*` in wrangler** → use the `logger` singleton +- **Direct Cloudflare REST API calls** → use the Cloudflare TypeScript SDK +- **Named imports from `ci-info`** → use default import (`import ci from "ci-info"`) +- **Runtime dependencies** → bundle deps; external deps need explicit allowlist entry +- **Committing to main** → always work on a branch +- **Trivial/obvious code comments** → don't add comments that restate what the code does; comments should explain "why", not "what" +- **Duplicating types/constants across packages** → export from the owning package and import where needed + +## Subdirectory Knowledge + +Packages with their own AGENTS.md for deeper context: + +- `packages/wrangler/AGENTS.md` - CLI architecture, command structure, test patterns +- `packages/miniflare/AGENTS.md` - Worker simulation, embedded workers, build system +- `packages/vite-plugin-cloudflare/AGENTS.md` - Plugin architecture, playground setup +- `packages/create-cloudflare/AGENTS.md` - Scaffolding, template system +- `packages/vitest-pool-workers/AGENTS.md` - 3-context architecture, cloudflare:test module +- `packages/workers-utils/AGENTS.md` - Shared config validation, test helpers + +When making architectural changes to a package (renaming files, adding entry points, changing build output), update the relevant AGENTS.md to reflect the new structure. + +## Cloudflare Workers Specifics + +- When removing or modifying scheduled functions in Cloudflare Workers, remember to update both the code in the Worker file and the corresponding cron trigger in the `wrangler.jsonc` configuration file. + +## Adding Native Node.js Module Support (unenv-preset) + +- The authoritative source for Node.js module compatibility flags and dates is the workerd repository's `compatibility-date.capnp` file at https://github.com/cloudflare/workerd/blob/main/src/workerd/io/compatibility-date.capnp. +- If the module is marked as `$experimental` in workerd (no `$impliedByAfterDate`), follow the pattern used by other experimental modules in `preset.ts`. +- The pattern for adding a new module override involves: + - Creating a `get<Module>Overrides()` function similar to existing ones (e.g., `getVmOverrides()`) + - Adding the override to `getCloudflarePreset()` and spreading into `dynamicNativeModules` and `dynamicHybridModules` + - Adding tests to `packages/wrangler/e2e/unenv-preset/preset.test.ts` + - Adding test functions to `packages/wrangler/e2e/unenv-preset/worker/index.ts` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1dba133 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +See @AGENTS.md diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..3a0c905 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +# This file exists solely for GitHub branch protection. +# The @workers-devprod bot approves PRs when all ownership rules +# (defined in .codeowners) are satisfied via Codeowners Plus. +# Do NOT add rules here — all ownership logic lives in .codeowners. +* @workers-devprod diff --git a/CODEOWNERS.md b/CODEOWNERS.md new file mode 100644 index 0000000..cd1761e --- /dev/null +++ b/CODEOWNERS.md @@ -0,0 +1,194 @@ +# Code Ownership + +This repository uses [Codeowners Plus](https://github.com/multimediallc/codeowners-plus) to enforce code ownership and review requirements. This replaces GitHub's native CODEOWNERS with more fine-grained control — specifically, the ability to require approval from **multiple teams** (AND rules) before a PR can merge. + +## How It Works + +### Overview + +When a PR is opened, updated, or reviewed, the Codeowners Plus GitHub Action runs. It reads `.codeowners` and `codeowners.toml` from the **base branch** (not the PR), evaluates the ownership rules, and: + +- Posts a PR comment listing which teams need to approve +- Requests reviews from those teams +- When all rules are satisfied, the `@workers-devprod` bot submits an **approval review** + +The native GitHub `CODEOWNERS` file contains a single rule (`* @workers-devprod`) making the bot the sole required code owner. Branch protection requires code owner approval, so the bot's approval is the merge gate. The check itself always passes — it only provides visibility. + +``` +PR opened/updated/reviewed + │ + ▼ +┌──────────────────────────────────┐ +│ Codeowners Plus GHA runs │ Reads .codeowners + codeowners.toml +│ Evaluates AND/OR rules │ from the BASE branch (not the PR) +│ Posts comment listing reviewers │ +│ Requests reviews from teams │ +└──────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ All rules satisfied? │ +│ YES → @workers-devprod approves │ Bot submits approval review +│ NO → bot does NOT approve │ Comment lists who still needs to approve +└──────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────┐ +│ Native GitHub CODEOWNERS │ * @workers-devprod +│ Branch protection requires │ Bot approval is the merge gate +│ bot approval to merge │ +│ ─ nice GitHub UI shows status ─ │ +└──────────────────────────────────┘ +``` + +### Key Difference from Native CODEOWNERS + +| Feature | Native GitHub CODEOWNERS | Codeowners Plus | +| ------------------------ | --------------------------------- | ------------------------------------------------------------------------------- | +| Multiple teams on a path | **OR** — any one team can approve | **AND** via `&` prefix — all listed teams must approve | +| Path matching for `*.js` | Matches anywhere in repo | Matches only in the `.codeowners` file's directory; use `**/*.js` for recursive | +| Per-directory config | Single file only | `.codeowners` file in any directory (rules are relative to that directory) | +| Stale review dismissal | All-or-nothing | Smart — only dismisses when reviewer's owned files change | +| Optional reviewers | Not supported | `?` prefix — CC without blocking | + +## Configuration Files + +### `.codeowners` — Ownership Rules + +Located at the repo root. Defines who owns what using path patterns and team handles. See the comments in the file itself for syntax details and a template for adding new product teams. + +**Note on `\*/**`patterns:** The`-shared`packages use`\*/**`instead of`**` so that root-level files (`CHANGELOG.md`, `package.json`) are excluded from AND rules. This allows the changeset release PR to be approved by the wrangler team alone. Source code in subdirectories still requires product team approval. + +### `codeowners.toml` — Advanced Configuration + +Located at the repo root. Controls enforcement behavior, ignored paths, and admin bypass. + +Key settings: + +| Setting | Purpose | +| -------------------------- | -------------------------------------------------------------------------- | +| `ignore` | Directories excluded from ownership checks (e.g. `.changeset`, `fixtures`) | +| `detailed_reviewers` | Show per-file owner breakdown in PR comments | +| `suppress_unowned_warning` | Don't warn about files with no owner | +| `enforcement.approval` | When `true`, the bot approves PRs that satisfy all rules | +| `enforcement.fail_check` | When `true`, the GHA check fails if rules aren't satisfied | +| `admin_bypass.enabled` | Allow admins to bypass by approving with "Codeowners Bypass" text | + +### `CODEOWNERS` — Native GitHub File + +The native GitHub `CODEOWNERS` file contains a single rule: + +``` +* @workers-devprod +``` + +This exists only so that GitHub branch protection can gate merging on the bot's approval. **Do not add rules to this file** — all ownership logic lives in `.codeowners`. + +### `.github/workflows/codeowners.yml` — GitHub Actions Workflow + +A single workflow handles PR events (`pull_request_target`). When reviews are submitted or dismissed, the `rerun-codeowners.yml` / `rerun-codeowners-privileged.yml` workflow pair re-runs the check (using the `workflow_run` pattern so it works for fork PRs too). + +Using `pull_request_target` (not `pull_request`) ensures the workflow has access to secrets for **fork PRs**. The checkout is always the base branch, so PR authors cannot modify ownership rules. + +## Common Scenarios + +### PR touches only wrangler-team-owned code + +Example: changes to `packages/create-cloudflare/` or `packages/vite-plugin-cloudflare/`. + +Only `@cloudflare/wrangler` approval is required. Once satisfied, the bot approves. + +### PR touches product-team-owned code + +Example: changes to `packages/wrangler/src/d1/`. + +**Both** `@cloudflare/wrangler` AND `@cloudflare/d1` must approve. Codeowners Plus will post a comment listing who still needs to approve and request reviews from both teams. The bot approves only when both teams have approved. + +### PR touches multiple product areas + +Example: changes to both `packages/wrangler/src/d1/` and `packages/wrangler/src/kv/`. + +All three teams must approve: `@cloudflare/wrangler` + `@cloudflare/d1` + `@cloudflare/workers-kv`. + +### Changeset release PR + +The automated changeset release PR modifies `CHANGELOG.md` and `package.json` files across packages. These files are explicitly owned by `@cloudflare/wrangler` only (via `**/CHANGELOG.md` and `**/package.json` rules), and the `-shared` package AND rules use `*/**` patterns that exclude root-level files. This means only wrangler team approval is needed for releases. + +### PR touches ignored paths only + +Example: changes only in `.changeset/` or `fixtures/`. + +No ownership checks apply. The bot approves automatically. + +### Draft PRs + +The workflow runs in **quiet mode** for draft PRs: + +- No PR comments posted +- No review requests sent +- The check still runs for visibility + +### Emergency bypass + +Repository admins can bypass all requirements by submitting an **approval review** with the text "Codeowners Bypass" (case-insensitive). This creates an audit trail. The bot will then approve the PR regardless of missing approvals. + +### Fork PRs + +Fork PRs are fully supported. The workflow uses `pull_request_target` to run in the base repo context with access to secrets. The base branch is checked out (so ownership rules come from the protected branch), and the PR head is fetched as git objects only for diff computation. No fork code is executed. + +## Adding a New Product Team + +To add ownership for a new product team, add AND rules to `.codeowners`: + +```bash +# Product: <Name> (AND: requires wrangler + <team>) +& packages/wrangler/src/<feature>/** @cloudflare/<team> +& packages/wrangler/src/__tests__/<feature>/** @cloudflare/<team> +& packages/miniflare/src/plugins/<feature>/** @cloudflare/<team> +& packages/miniflare/src/workers/<feature>/** @cloudflare/<team> +& packages/miniflare/test/plugins/<feature>/** @cloudflare/<team> +``` + +For standalone packages (like `packages/*-shared/`), use `*/**` instead of `**` to exclude root-level release files from AND rules: + +```bash +# Product: <Name> (AND: requires wrangler + <team>) +# */** excludes root-level files (CHANGELOG.md, package.json) from AND rules +& packages/<name>-shared/*/** @cloudflare/<team> +``` + +**Teams ready to add** (have source paths but no ownership entries yet): +R2, Queues, AI, Hyperdrive, Vectorize, Pipelines, SSL/Secrets Store, WVPC. + +## Stale Review Handling + +Codeowners Plus uses **smart dismissal**: when new commits are pushed to a PR, it only dismisses an approval if the files owned by that reviewer were changed. This avoids the frustration of GitHub's all-or-nothing stale review dismissal. + +For this to work, the branch protection setting **"Dismiss stale pull request approvals when new commits are pushed"** must be **disabled**. Codeowners Plus handles dismissal itself. + +## Troubleshooting + +### Rules not matching expected files + +Rules in `.codeowners` are relative to the file's directory. Check that: + +- The path uses `**` for recursive matching (plain `*.js` only matches in the root directory) +- The path doesn't have a leading `/` (leading slashes are ignored but can be confusing) +- The `&` prefix is present (without it, the rule sets the primary owner instead of adding an AND requirement) + +### Bot not approving even though all reviews are in + +- Check that `enforcement.approval = true` in `codeowners.toml` +- Check that the `CODEOWNERS_GITHUB_PAT` secret is a PAT owned by the `@workers-devprod` account +- Check that `@workers-devprod` has write access to the repository +- Check the workflow run logs for errors (verbose output is enabled) + +### PR can't merge despite bot approval + +Ensure "Require review from Code Owners" is enabled in branch protection settings for the `main` branch, and that `@workers-devprod` is listed in the native `CODEOWNERS` file. + +## References + +- [Codeowners Plus documentation](https://github.com/multimediallc/codeowners-plus) +- [Codeowners Plus action on GitHub Marketplace](https://github.com/marketplace/actions/codeowners-plus) +- [GitHub branch protection docs](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ae3f249 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[wrangler@cloudflare.com](mailto:wrangler@cloudflare.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bc58952 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,495 @@ +# Contributing + +Wrangler is an open-source project and we welcome contributions from you. Thank you! + +Below you can find some guidance on how to be most effective when contributing to the project. + +## tl;dr for contributing to Wrangler + +Useful commands for developing Wrangler (all commands below should be run in the project root): + +- `pnpm i; pnpm build` will build everything in workers-sdk. +- `pnpm dev -F wrangler` will watch and build changes while you develop. Fixtures use the build output from this, and are useful for messing around during dev (`fixtures/worker-ts` is a useful blank slate). + +Before committing/submitting a PR: + +- Add [tests](#pr-tests). `pnpm test -F wrangler` will run Wrangler's unit tests. You can filter tests: e.g. `pnpm test -F wrangler "containers"`. +- Run `pnpm check` for typechecking and linting. +- Add a [changeset](#changesets) with `pnpm changeset`. +- Don’t squash your commits after a review. + +## Before getting started + +We really appreciate your interest in making a contribution, and we want to make sure that the process is as smooth and transparent as possible! To this end, we note that the Workers team is actively doing development in this repository, and while we consistently strive to communicate status and current thinking around all open issues, there may be times when context surrounding certain items is not up to date. Therefore, **for non-trivial changes, please always engage on the issue or create a discussion or feature request issue first before writing your code.** This will give us opportunity to flag any considerations you should be aware of before you spend time developing. Of course, for trivial changes, please feel free to go directly to filing a PR, with the understanding that the PR itself will serve as the place to discuss details of the change. + +Thanks so much for helping us improve the [workers-sdk](https://github.com/cloudflare/workers-sdk), and we look forward to your contribution! + +## Naming experimental and unstable flags + +When adding Wrangler flags for behavior that is not yet stable, prefer an `experimental-` prefix for the long CLI flag name, such as `--experimental-autoconfig` or `--experimental-provision`. If the flag also needs a short opt-in alias, use the existing `x-` form, such as `--x-autoconfig` or `--x-provision`. + +Keep CLI flag names in kebab-case. Reserve `unstable_` and `experimental_` prefixes for exported JavaScript APIs rather than CLI flags, matching exports such as `unstable_dev` and `experimental_generateTypes`. + +If an experimental flag graduates or is no longer needed, keep the old flag hidden and deprecated while pointing users to the stable replacement, as with `--experimental-local` and `--experimental-include-runtime`. + +## Getting started + +### Set up your environment + +Wrangler is built and run on the Node.js JavaScript runtime. + +- Install the latest LTS version of [Node.js](https://nodejs.dev/) - we recommend using a Node version manager like [nvm](https://github.com/nvm-sh/nvm). +- Install a code editor - we recommend using [VS Code](https://code.visualstudio.com/). + - When opening the project in VS Code for the first time, it will prompt you to install the [recommended VS Code extensions](https://code.visualstudio.com/docs/editor/extension-marketplace#:~:text=install%20the%20recommended%20extensions) for the project. +- Install the [git](https://git-scm.com/) version control tool. + +### Fork and clone this repository + +#### For External Contributors + +Any contributions you make will be via [Pull Requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) on [GitHub](https://github.com/) developed in a local git repository and pushed to your own fork of the repository. + +- Ensure you have [created an account](https://docs.github.com/en/get-started/onboarding/getting-started-with-your-github-account) on GitHub. +- [Create your own fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) of [this repository](https://github.com/cloudflare/workers-sdk). +- Clone your fork to your local machine + + ```sh + git clone https://github.com/<your-github-username>/workers-sdk + cd workers-sdk + ``` + + You can see that your fork is setup as the `origin` remote repository. + Any changes you wish to make should be in a local branch that is then pushed to this origin remote. + + ```sh + git remote -v + origin https://github.com/<your-github-username>/workers-sdk (fetch) + origin https://github.com/<your-github-username>/workers-sdk (push) + ``` + +- Add `cloudflare/workers-sdk` as the `upstream` remote repository. + + ```sh + git remote add upstream https://github.com/cloudflare/workers-sdk + git remote -v + origin https://github.com/<your-github-username>/workers-sdk (fetch) + origin https://github.com/<your-github-username>/workers-sdk (push) + upstream https://github.com/cloudflare/workers-sdk (fetch) + upstream https://github.com/cloudflare/workers-sdk (push) + ``` + +- You should regularly pull from the `main` branch of the `upstream` repository to keep up to date with the latest changes to the project. + + ```sh + git switch main + git pull upstream main + From https://github.com/cloudflare/workers-sdk + * branch main -> FETCH_HEAD + Already up to date. + ``` + +#### For Cloudflare Employees + +If you are a Cloudflare employee, you do not need to fork the repository - instead, you can clone the main repository directly. This allows you to push branches directly to the upstream repository. + +If you find that you don't have write access, please reach out to your manager or the Wrangler team internally. + +Clone the main repository: + +```sh +git clone https://github.com/cloudflare/workers-sdk.git +cd workers-sdk +``` + +Create new branches directly in the cloned repository and push them to the main repository: + +```sh +git checkout -b <new-branch-name> +git push origin <new-branch-name> +``` + +### Install dependencies + +**Warning** +When working on Wrangler, you'll need to satisfy [`workerd`](https://github.com/cloudflare/workerd)'s `libc++1` runtime dependencies: + +- On Linux: + - libc++ (e.g. the package `libc++1` on Debian Bullseye) +- On macOS: + - The XCode command line tools, which can be installed with xcode-select --install + +The Node.js dependencies of the project are managed by the [`pnpm`](https://pnpm.io/) tool. + +This repository is setup as a [mono-repo](https://pnpm.io/workspaces) of workspaces. The workspaces are stored in the [`packages`](https://github.com/cloudflare/workers-sdk/tree/main/packages) directory. + +While each workspace has its own dependencies, you install the dependencies using `pnpm` at the root of the project. + +> If you haven't used `pnpm` before, you can install it with `npm install -g pnpm` + +- Install all the dependencies + + ```sh + cd workers-sdk + pnpm install + ``` + +## Building and running + +Workspaces in this project are mostly written in [TypeScript](https://www.typescriptlang.org/) and compiled, by [esbuild](https://github.com/evanw/esbuild), into JavaScript bundles for distribution. + +- Run a distributable for a specific workspace (e.g. wrangler) + + ```sh + pnpm run --filter wrangler start + ``` + +- Build a distributable for a specific workspace (e.g. wrangler) + + ```sh + pnpm run build --filter wrangler + ``` + +## Checking the code + +The code in the repository is checked for type checking, formatting, linting and testing errors. + +- Run all checks in all the workspaces + + ```sh + pnpm run check + ``` + +When doing normal development, you may want to run these checks individually. + +### Type Checking + +The code is checked for type errors by [TypeScript](https://www.typescriptlang.org/). + +- Type check all the code in the repository + + ```sh + pnpm run check:type + ``` + +- VS Code will also run type-checking while editing source code, providing immediate feedback. + +#### Changing TypeScript Version in VS Code's Command Palette + +For TypeScript to work properly in the Monorepo the version used in VSCode must be the project's current TypeScript version, follow these steps: + +1. Open the project in VSCode. + +2. Press `Ctrl + Shift + P` (or `Cmd + Shift + P` on macOS) to open the command palette. + +3. In the command palette, type "Select TypeScript Version" and select the command with the same name that appears in the list. + +4. A submenu will appear with a list of available TypeScript versions. Choose the desired version you want to use for this project. If you have multiple versions installed, they will be listed here. + - Selecting "Use Workspace Version" will use the version of TypeScript installed in the project's `node_modules` directory. + +5. After selecting the TypeScript version, VSCode will reload the workspace using the chosen version. + +Now you have successfully switched the TypeScript version used within the project via the command palette in VSCode. +Remember that this change is specific to the current project and will not affect other projects or the default TypeScript version used by VSCode. + +### Linting + +The code is checked for linting errors by [ESLint](https://eslint.org/). + +- Run the linting checks + + ```sh + pnpm run check:lint + ``` + +- The repository has a recommended VS Code plugin to run ESLint checks while editing source code, providing immediate feedback. + +### Formatting + +The code is checked for formatting errors by [Prettier](https://prettier.io/). + +- Run the formatting checks + + ```sh + pnpm run check:format + ``` + +- The repository has a recommended VS Code plugin to run Prettier checks, and to automatically format using Prettier, while editing source code, providing immediate feedback +- Use the following command to run prettier on the codebase + + ```sh + pnpm run prettify + ``` + +### Testing + +Tests in a workspace are executed, by [Vitest](https://vitest.dev/), which is configured to automatically compile and bundle the TypeScript before running the tests. + +- If you have recently rebased on `main` then make sure you have installed any new dependencies + + ```sh + pnpm i + ``` + +- Run the tests for all the workspaces + + ```sh + pnpm run test + ``` + + :::note + Cloudflare employees may need to turn off WARP for the first time they run the Miniflare tests so that it can request and cache the CF properties without getting the following error. + + ```plain + failed: TLS peer's certificate is not trusted; reason = self signed certificate in certificate chain + ``` + + After this request is cached you can run tests with WARP turned on, no problem. + ::: + +- Run the tests for a specific workspace (e.g. wrangler) + + ```sh + pnpm run test --filter wrangler + ``` + +- Watch the files in a specific workspace (e.g. wrangler), and run the tests when anything changes + + ```sh + pnpm run --filter wrangler test:watch + ``` + + This will also run all the tests in a single process (rather than in parallel shards) and will increase the test-timeout to 50 seconds, which is helpful when debugging. + +## Steps For Making Changes + +Every change you make should be stored in a [git commit](https://github.com/git-guides/git-commit). +Changes should be committed to a new local branch, which then gets pushed to your fork of the repository on GitHub. + +- Ensure your `main` branch is up to date + + ```sh + git switch main + git pull upstream main + ``` + +- Create a new branch, based off the `main` branch + + ```sh + git checkout -b <new-branch-name> main + ``` + +- Stage files to include in a commit + - Use [VS Code](https://code.visualstudio.com/docs/editor/versioncontrol#_git-support) + - Or add and commit files via the command line + + ```sh + git add <paths-to-changes-files> + git commit + ``` + +- Push changes to your fork + + ```sh + git push -u origin <new-branch-name> + ``` + +- Once you are happy with your changes, create a Pull Request on GitHub +- The format for Pull Request titles is `[package name] description`, where the package name should indicate which package of the `workers-sdk` monorepo your PR pertains to (e.g. `wrangler`/`pages-shared`/`chrome-devtools-patches`), and the description should be a succinct summary of the change you're making. +- GitHub will insert a template for the body of your Pull Request—it's important to carefully fill out all the fields, giving as much detail as possible to reviewers. + +### Git Hygiene + +Making sure your branch follows our recommendations for git will help ensure your PR is reviewed & released as quickly as possible: + +- When opening a PR (before the first review), try and make sure your git commit history is clean, and clearly describes the changes you want to make. + - For instance, here's an example of a PR where the commit history is quite messy, and doesn't help reviewers: <https://github.com/cloudflare/workers-sdk/pull/2409/commits> + - And here's an example of where this has been done well: <https://github.com/cloudflare/workers-sdk/pull/4795/commits> +- Once your PR has been reviewed, when addressing feedback try not to modify already reviewed commits with force pushes. This slows down the review process and makes it hard to keep track of what changes have been made. Instead, add additional commits to your PR to address any feedback (`git commit --fixup` is a helpful tool here). +- When merging your PR into `main`, `workers-sdk` enforces squash merges. As such, please try and make sure that the commit message associated with the merge clearly describes the entire change your PR makes. + +## PR Review + +PR review is a critical and required step in the process for landing changes. This is an opportunity to catch potential issues, improve the quality of the work, celebrate good design, and learn from each other. As a reviewer, it's important to be thoughtful about the proposed changes and communicate any feedback. + +## PR Previews + +Every PR will have an associated pre-release build for all releasable packages within the repository, powered by [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). You can find links to prereleases for each package in a comment automatically posted by GitHub Actions on each opened PR ([for example](https://github.com/cloudflare/workers-sdk/pull/9492#issuecomment-2943757675)). + +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:quick-edit` for deploying [quick-edit](packages/quick-edit) + +Once built, you can find the preview link for these applications in the [Deploy Previews](.github/workflows/deploy-previews.yml) action output + +## PR Tests + +Every PR should include tests for the functionality that's being added. Most changes will be to [Wrangler](packages/wrangler/src/__tests__) (using Vitest), [Miniflare](packages/miniflare/test) (using Ava), or [C3](packages/create-cloudflare/src/__tests__) (using Vitest), and should include unit tests within the testing harness of those packages. For documentation on how these testing frameworks work, see: + +- Vitest: <https://vitest.dev/guide> +- Ava: <https://github.com/avajs/ava?tab=readme-ov-file#documentation> + +If your PR includes functionality that's difficult to unit test, you can add a fixture test by creating a new package in the `fixtures/` folder. This allows for adding a test that requires a specific filesystem or worker setup (for instance, `fixtures/no-bundle-import` tests the interaction of Wrangler with a specific set of JS, WASM, text, and binary modules on the filesystem). When adding a fixture test, include a `vitest.config.mts` file within the new package, which will ensure it's run as part of the `workers-sdk` CI. You should merge your own configuration with the default config from the root of the repo. + +A good default example is the following: + +```ts +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + // config overrides + } + }) +}); +``` + +If you need to test the interaction of Wrangler with a real Cloudflare account, you can add an E2E test within the `packages/wrangler/e2e` folder. This lets you add a test for functionality that requires real credentials (i.e. testing whether a worker deployed from Wrangler can be accessed over the internet). + +A summary of this repositories actions can be found [in the `.github/workflows` folder](.github/workflows/README.md) + +## Remote E2E Tests in CI + +E2E tests that hit the Cloudflare backend (deploying Workers, testing bindings, etc.) are inherently slow and can be flaky due to network and service dependencies. To keep PR feedback loops fast and reliable, **CI does not pass Cloudflare API credentials to E2E test jobs by default**. The E2E test suites still run on every PR, but tests that require remote access are automatically skipped when the API token is absent. + +Remote E2E tests run automatically in these cases: + +- **Version Packages PRs** (branch `changeset-release/main`) — acts as a pre-release safety net, catching remote-test failures before packages are published. +- **Merge queue** — final check before code lands on `main`. + +If you need remote E2E tests on your PR (e.g. you're changing deployment logic or binding behavior), apply the **`ci:run-remote-tests`** label. This triggers a re-run of the E2E workflows with API credentials enabled. + +> [!NOTE] +> The `ci:run-remote-tests` label has no effect on PRs from forks, because GitHub does not expose repository secrets to fork PRs. + +## Running E2E tests locally + +A large number of Wrangler, C3 & Vite's E2E tests don't require any authentication, and can be run with no Cloudflare account credentials. These can be run as follows, optionally providing [`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` environment variables.](#creating-an-api-token): + +- **Vite:** `pnpm test:e2e -F @cloudflare/vite-plugin` + + You may optionally want to append a filename pattern to limit which e2e tests are run. Also you may want to set `--bail=n` to limit the number of fails tests to show the error before the rest of the tests finish running and to limit the noise in that output: + + ```sh + pnpm test:e2e -F @cloudflare/vite-plugin [file-pattern] --bail=1 + ``` + +- **C3:** `pnpm test:e2e -F create-cloudflare` + + See [C3 E2E tests README](packages/create-cloudflare/e2e/README.md) for more information. + +- **Wrangler:** `pnpm test:e2e -F wrangler` or `pnpm test:e2e:wrangler` + + See [Wrangler E2E tests README](packages/wrangler/e2e/README.md) for more information. + +### Creating an API Token + +If you want to run the E2E tests that access the Cloudflare API (e.g. for testing Worker deployment and interaction with bindings), you can create an API token for running the tests: + +1. Go to ["My Profile" > "User API Tokens"](https://dash.cloudflare.com/profile/api-tokens) +1. Click "Create Token" +1. Use the "Edit Cloudflare Workers" template +1. Set "Account Resources" to "Include" the account you want to use for running the test + (for internal and CI use, this needs to be the "DevProd Testing" account) +1. No "Zone Resources" are required for general use (for internal and CI use, this needs to be set to "All Zones") +1. Click "Continue to summary" +1. Verify your token works by running the curl command provided + +Once you've created the token, you can use it when running E2E tests to test against the API: + +```sh +# Vite +CLOUDFLARE_ACCOUNT_ID="<Account ID for the token you just created>" CLOUDFLARE_API_TOKEN="<Token you just created>" pnpm test:e2e -F @cloudflare/vite-plugin + +# C3 +CLOUDFLARE_ACCOUNT_ID="<Account ID for the token you just created>" CLOUDFLARE_API_TOKEN="<Token you just created>" pnpm test:e2e -F @create-cloudflare + +# Wrangler +CLOUDFLARE_ACCOUNT_ID="<Account ID for the token you just created>" CLOUDFLARE_API_TOKEN="<Token you just created>" pnpm test:e2e:wrangler +``` + +> [!NOTE] +> Workers and other resources created in the E2E tests might not always be cleaned up. Internal users with access to the "DevProd Testing" account can rely on an automated job to clean up the Workers and other resources, but if you use another account, please be aware you may want to manually delete the Workers and other resources yourself. + +## Managing Package Dependencies + +Packages in this monorepo should bundle their dependencies into the distributable code rather than leaving them as runtime `dependencies` that get installed by downstream users. This prevents dependency chain poisoning where a transitive dependency could introduce unexpected or malicious code. + +### The Rule + +- **Bundle dependencies**: Most dependencies should be listed in `devDependencies` and bundled into the package output by esbuild/tsup/etc. +- **External dependencies**: Only dependencies that _cannot_ be bundled should be listed in `dependencies`. These must be explicitly declared with documentation explaining why. + +### Why This Matters + +When users install one of our packages (e.g., `wrangler`), npm/pnpm will also install everything listed in `dependencies`. If one of those dependencies has unpinned transitive dependencies, a malicious actor could publish a compromised version that gets pulled into user installations. By bundling our dependencies, we control exactly what code ships. + +### Adding a New External Dependency + +If you need to add a dependency that cannot be bundled (native binaries, WASM modules, packages that must be resolved at runtime, etc.): + +1. **Add to `dependencies`** in `package.json` with a pinned version +2. **Add to `EXTERNAL_DEPENDENCIES`** in `scripts/deps.ts` with a comment explaining why it can't be bundled +3. **Run `pnpm check:package-deps`** to verify the allowlist is correct + +Example `scripts/deps.ts`: + +```typescript +export const EXTERNAL_DEPENDENCIES = [ + // Native binary - cannot be bundled + "workerd", + + // WASM module that blows up when bundled + "blake3-wasm", + + // Must be resolved at runtime when bundling user's worker code + "esbuild", +]; +``` + +### Valid Reasons for External Dependencies + +- **Native binaries**: Packages like `workerd` or `sharp` contain platform-specific binaries +- **WASM modules**: Some WASM packages don't bundle correctly +- **Runtime resolution**: Packages like `esbuild` or `unenv` that need to be resolved when bundling user code +- **Peer dependencies**: Packages the user is expected to provide (e.g., `react`, `vite`) + +### Pinning External Dependencies + +Because external dependencies are installed into downstream users' dependency trees rather than bundled, their versions must be **pinned to an exact version** (e.g. `1.2.3`, not `^1.2.3`). This closes the supply-chain hole above: an unpinned external dependency could resolve to a compromised upstream release without us vetting it. + +This is enforced by `pnpm check:pinned-deps`, which requires: + +- Every `dependencies` and `optionalDependencies` entry of a published package to be an exact version, or a `workspace:`/`catalog:` reference. +- Every entry in the pnpm `catalog:` (in `pnpm-workspace.yaml`) to be an exact version, so that any `catalog:default` reference is also pinned. Deliberate exceptions live in `CATALOG_PIN_EXCEPTIONS` in `tools/deployments/validate-pinned-dependencies.ts` (currently only `@cloudflare/workers-types`, which is consumed as a peer dependency). + +`peerDependencies` are exempt — ranges there are intentional, since they describe the set of consumer-provided versions a package is compatible with. + +## Changesets + +Every non-trivial change to the project - those that should appear in the changelog - must be captured in a "changeset". + +See the [.changeset/README.md](.changeset/README.md) for detailed guidelines on: + +- Creating changesets +- Choosing version types (patch/minor/major) +- Writing good changeset descriptions +- Formatting rules + +Quick start: + +```sh +pnpm changeset +git add .changeset/*.md +``` + +### Styleguide + +When contributing to Wrangler, please refer to the [`STYLEGUIDE.md file`](STYLEGUIDE.md) file where possible to help maintain consistent patterns throughout Wrangler. + +## Releases + +We generally cut Wrangler releases on Tuesday & Thursday each week. If you need a release cut outside of the regular cadence, please reach out to the [@cloudflare/wrangler-admins](https://github.com/orgs/cloudflare/teams/wrangler-admins) team. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..a0e7ebf --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2020 Cloudflare, Inc. <wrangler@cloudflare.com> + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..051476e --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +<h1 align="center">Cloudflare Workers SDK</h1> + +<p align="center"> +<img src="cloudflare-workers-outline.png" alt="workers-logo" width="120px" height="120px"/> + <br> + Cloudflare Workers let you deploy serverless code instantly across the globe for exceptional performance, reliability, and scale. + <br> +</p> + +<p align="center"> + <a href="CONTRIBUTING.md">Contribute</a> + · + <a href="https://github.com/cloudflare/workers-sdk/issues">Submit an Issue</a> + · + <a href="https://discord.cloudflare.com/">Join Discord</a> + <br> + <br> +</p> + +<p align="center"> + <a href="https://www.npmjs.com/wrangler/"> + <img src="https://img.shields.io/npm/v/wrangler.svg?logo=npm&logoColor=fff&label=NPM+package&color=orange" alt="Wrangler on npm" /> + </a>  + <a href="https://discord.cloudflare.com/"> + <img src="https://img.shields.io/discord/595317990191398933.svg?logo=discord&logoColor=fff&label=Discord&color=7389d8" alt="Discord conversation" /> + </a>  + <a href="https://twitter.com/CloudflareDev"> + <img src="https://img.shields.io/twitter/follow/cloudflaredev" alt="X conversation" /> + </a> +</p> + +<hr> + +## Quick Start + +To get started quickly with a new project, run the command below: + +```bash +npm create cloudflare@latest +# or +pnpm create cloudflare@latest +# or +yarn create cloudflare@latest +``` + +For more info, visit our [Getting Started](https://developers.cloudflare.com/workers/get-started/guide/) guide. + +## Documentation + +Visit the official Workers documentation [here](https://developers.cloudflare.com/workers/). + +- [Getting Started](https://developers.cloudflare.com/workers/get-started/guide/) +- [How Workers works](https://developers.cloudflare.com/workers/reference/how-workers-works/) +- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) +- [Observability](https://developers.cloudflare.com/workers/observability/) +- [Platform](https://developers.cloudflare.com/workers/platform/) + +## Directory + +| Package | Description | Links | +| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| [`wrangler`](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) | A command line tool for building [Cloudflare Workers](https://workers.cloudflare.com/). | [Docs](https://developers.cloudflare.com/workers/wrangler/) | +| [`create-cloudflare` (C3)](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare) | A CLI for creating and deploying new applications to Cloudflare. | [Docs](https://developers.cloudflare.com/pages/get-started/c3/) | +| [`miniflare`](https://github.com/cloudflare/workers-sdk/tree/main/packages/miniflare) | A simulator for developing and testing Cloudflare Workers, powered by [workerd](https://github.com/cloudflare/workerd) | [Docs](https://miniflare.dev) | +| [`chrome-devtools-patches`](https://github.com/cloudflare/workers-sdk/tree/main/packages/chrome-devtools-patches) | Cloudflare's fork of Chrome DevTools for inspecting your local or remote Workers | | +| [`pages-shared`](https://github.com/cloudflare/workers-sdk/tree/main/packages/pages-shared) | Used internally to power Wrangler and Cloudflare Pages. It contains all the code that is shared between these clients. | | + +## Beta releases + +Beta releases are generated by the [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) tool and are updated on every commit pushed to the `main` branch. + +> [!Warning] +> These beta releases get updated over time, so they are ill suited to be used as stable versions for a project (and the proper npm released should be used instead). These should be used only for quick testing of not yet released features/fixes. + +Available beta releases are listed below. + +<details><summary><b>create-cloudflare</b></summary><p> + +``` +npm i https://pkg.pr.new/create-cloudflare@main +``` + +</p></details> + +<details><summary><b>@cloudflare/kv-asset-handler</b></summary><p> + +``` +npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@main +``` + +</p></details> + +<details><summary><b>miniflare</b></summary><p> + +``` +npm i https://pkg.pr.new/miniflare@main +``` + +</p></details> + +<details><summary><b>@cloudflare/pages-shared</b></summary><p> + +``` +npm i https://pkg.pr.new/@cloudflare/pages-shared@main +``` + +</p></details> + +<details><summary><b>@cloudflare/unenv-preset</b></summary><p> + +``` +npm i https://pkg.pr.new/@cloudflare/unenv-preset@main +``` + +</p></details> + +<details><summary><b>@cloudflare/vite-plugin</b></summary><p> + +``` +npm i https://pkg.pr.new/@cloudflare/vite-plugin@main +``` + +</p></details> + +<details><summary><b>@cloudflare/vitest-pool-workers</b></summary><p> + +``` +npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@main +``` + +</p></details> + +<details><summary><b>@cloudflare/workers-editor-shared</b></summary><p> + +``` +npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@main +``` + +</p></details> + +<details><summary><b>wrangler</b></summary><p> + +``` +npm i https://pkg.pr.new/wrangler@main +``` + +</p></details> + +## Contributing + +We welcome new contributors! Refer to the [`CONTRIBUTING.md`](/CONTRIBUTING.md) guide for details. + +## Community + +Join us in the official [Cloudflare Discord](https://discord.cloudflare.com/) to meet other developers, ask questions, or learn more in general. + +## Links + +- [Project Board](https://github.com/orgs/cloudflare/projects/1) +- [Discussions](https://github.com/cloudflare/workers-sdk/discussions) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..571824b --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`cloudflare/workers-sdk` +- 原始仓库:https://github.com/cloudflare/workers-sdk +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..1491faa --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,34 @@ +# Review Guidelines + +This file provides guidance when reviewing PRs in this repository. For general development guidelines, see [AGENTS.md](./AGENTS.md). + +## Code Quality + +- Avoid using global variables. Repository maintainers will reject code that introduces global variables. +- When a function needs to return multiple related objects, prefer changing the function signature to return an object with named properties (e.g., `return { wrangler, registry }`) rather than using `Object.defineProperty` to attach additional properties to one of the return values. +- When implementing multiple similar validation checks (like BOM detection in different formats), prefer using data structures like arrays of objects to reduce code duplication. For example, when checking for different byte patterns, use an array of objects with shape `{name: string, buffer: Buffer}` and loop over the array instead of writing separate conditional blocks for each pattern. + +## CI Failures + +- Test failures in CI are often flakes that can be rerun rather than actual issues requiring code fixes. If a test fails on one OS but passes on another, it is likely a flake and should be rerun at least twice before investigating further. + +## Changesets + +- Changesets should target users of the tools (e.g. Wrangler users) rather than maintainers. Avoid including implementation details like "moves X from hybridModules to nativeModules" or "removes polyfill implementation" or "adds comprehensive tests". Instead, focus on user-facing impact and benefits. +- Do NOT prefix changeset titles with a "type" (e.g. `fix:`, `feat:`, `chore:`). The changeset title should be a plain description without conventional commit prefixes. + +### Semver Classification + +- **Minor (new features):** Adding support for new frameworks (even in experimental mode), new commands/flags/options, new API capabilities or exports, behavior changes that add functionality. +- **Patch (bug fixes and improvements):** Fixing bugs where something was not working correctly, dependency updates, internal refactoring without user-facing changes, performance improvements, error message improvements. +- The description text matters less than the actual change. A changeset described as "Support X" is adding a new feature (minor), while "filters out invalid X" is fixing a bug (patch). Analyze what the change actually does for users rather than relying on keywords. + +## Version Packages PR Review + +When reviewing Version Packages PRs, use a structured two-pass review process: + +1. **Pass 1 - Extract Facts:** For each changeset, identify filename/slug, package(s) affected, declared classification (from front-matter), description, and source PR number. +2. **Pass 2 - Analyze and Compare:** For each changeset, determine recommended classification, rationale, and whether it matches the declared classification. +3. **Pass 3 - Report Only Mismatches:** Only flag changesets where declared does not match recommended. State: "Currently classified as X, should be Y because..." + +Use the semver classification guidelines above to determine correct classification. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0735c89 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Reporting Security Vulnerabilities + +Please see [this page](https://www.cloudflare.com/.well-known/security.txt) for information on how to report a vulnerability to Cloudflare. Thanks! diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md new file mode 100644 index 0000000..6017fa8 --- /dev/null +++ b/STYLEGUIDE.md @@ -0,0 +1,158 @@ +# Wrangler Styleguide + +The aim of this guide is to help Wrangler contributors maintain consistent patterns throughout Wrangler and to provide end users with content that educates users on how to successfully complete tasks. + +## Styleguide legend + +- \*Text in between stars designates placeholder text that should be replaced.\* +- \<Text contained within angle brackets are placeholder commands, args or filepaths.\> +- #Text in between hashtags designates the user's input that must occur before the next line can display.# + +## Wrangler syntax + +- Commands should follow an object verb order, such as ‘d1 create’. + +```sh +wrangler <object / noun> <verb> +``` + +- Subcommands should follow the main command with a space + +```sh +wrangler <command> <subcommand> <arg> --<option>` +``` + +## Wrangler \<command\> --help + +```sh +🧮 *Brief description of the product, the value it offers and how Wrangler can interact with it* + +🔧 *Command is currently in open beta / command is experimental (if relevant)* + +Commands: + wrangler <command> <subcommand> <arg> *Description of command* + +Options: + -<o (option shorthand)>, --<option (option name)> *Option description* [*data_type*] [default: *true/false*] + + +-------------------- +📣 *Announcement* +📃 To learn more, visit our documentation on *Product name*: https://developers.cloudflare.com/*productname* +-------------------- + +``` + +## Create success state with binding + +```sh +🌀 Creating ___ with title "___" +✨ Success. *Add details of success and what the user can now do* +📣 *Optional announcement* + +To start interacting with this ___ from a Worker, *If additional steps required, such as obtaining account ID from dash, add them here* \(then\) open your Worker’s config file and add the following binding configuration: + +[[array]] +binding = "<VARIABLE_NAME>" +name = "___" +id = "___" +``` + +## General success state + +```sh +🌀 *Action verb* *Object*. *details of what is currently happening if necessary* +🚧 *Updated additional details of the current status if necessary* +✨ Success. *Add details of and what the user can now do* +📣 *Announcement* + +*Description of what the next steps the user can take to be successful. If there are predictable happy paths following a success state, make those paths clear to the user here.* +``` + +## Command related error + +```sh +✘ ERROR *API error code if applicable*: *Concise description of what the error is*: + +Error details: +*Description of what caused the error* + +How to solve this error: +*Direction on how to resolve the error* + +wrangler *example of full command user tried to run* + +*Description of the command’s purpose* + +Positionals: + positional *Positional description*. [data-type] [required/optional] + +Options: + -o, --option *Option description* [data type] [default: true/false] + +If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose + +🪵 Logs were written to <filepath> + +``` + +## General error + +```sh +✘ ERROR *Error code if applicable*: *Concise description of what the error is* + +Error details: +*description of what caused the error* + +How to solve this error: +*direction on how to resolve the error* + +-------------------- +To learn more about ___, read our documentation at https://developers.cloudflare.com/*productname* + +If you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose +-------------------- + +√ Would you like to report this error to Cloudflare? <y/n> + +#User inputs y or n# + +🪵 Logs were written to <filepath> + +``` + +## Y/N choice + +```sh +*choice description* <y/n> +#User inputs y or n# + +``` + +## Wrangler prompts + +### Written value prompt + +```sh +wrangler <command> +<prompt request> +<defaut_value> + +#User enters value then presses enter# + +<response detailing what task/s have been performed and with what values where applicable OR continue to next prompt> +``` + +### Multiple choice prompt + +```sh +Wrangler <command> +<prompt request> + ◉ <Choice 1> + ○ <Choice 2> + ○ <Choice 3> + + #User presses enter# + +<response detailing what task/s have been performed and with what values where applicable OR continue to next prompt> +``` diff --git a/cloudflare-workers-outline.png b/cloudflare-workers-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..b0f54d8da71a02cc52fabe6abf4657ccb1ee442f GIT binary patch literal 5526 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 literal 0 HcmV?d00001 diff --git a/codeowners.toml b/codeowners.toml new file mode 100644 index 0000000..9d6f4d8 --- /dev/null +++ b/codeowners.toml @@ -0,0 +1,28 @@ +# Codeowners Plus - Advanced Configuration +# See: https://github.com/multimediallc/codeowners-plus +# See: CODEOWNERS.md for full documentation + +# Directories to ignore (no ownership checks, reduces review noise) +ignore = [] + +# Show detailed file-to-owner mapping in PR comments +detailed_reviewers = true + +# Don't dismiss stale reviews when new changes are pushed +disable_smart_dismissal = true + +# Suppress warnings for intentionally unowned files (e.g. pnpm-lock.yaml) +suppress_unowned_warning = true + +[enforcement] +# The bot (@workers-devprod) approves PRs when all ownership rules are satisfied, +# unblocking the native GitHub CODEOWNERS requirement in branch protection. +# The check still runs for visibility (PR comments, review requests) but does not +# fail — enforcement is via the bot's CODEOWNERS approval only. +approval = true +fail_check = false + +[admin_bypass] +# Allow repo admins to bypass codeowner requirements in emergencies +# by submitting an approval review containing "Codeowners Bypass" text +enabled = true diff --git a/fixtures/additional-modules/package.json b/fixtures/additional-modules/package.json new file mode 100644 index 0000000..d809ac7 --- /dev/null +++ b/fixtures/additional-modules/package.json @@ -0,0 +1,22 @@ +{ + "name": "@fixture/additional-modules", + "private": true, + "scripts": { + "build": "wrangler deploy --dry-run --outdir=dist", + "check:type": "tsc", + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./test/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@fixture/shared": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/additional-modules/src/common.cjs b/fixtures/additional-modules/src/common.cjs new file mode 100644 index 0000000..19658c1 --- /dev/null +++ b/fixtures/additional-modules/src/common.cjs @@ -0,0 +1 @@ +module.exports = "common"; diff --git a/fixtures/additional-modules/src/dep.ts b/fixtures/additional-modules/src/dep.ts new file mode 100644 index 0000000..cc0be2a --- /dev/null +++ b/fixtures/additional-modules/src/dep.ts @@ -0,0 +1 @@ +export default "bundled"; diff --git a/fixtures/additional-modules/src/dynamic.js b/fixtures/additional-modules/src/dynamic.js new file mode 100644 index 0000000..6c40343 --- /dev/null +++ b/fixtures/additional-modules/src/dynamic.js @@ -0,0 +1 @@ +export default "dynamic"; diff --git a/fixtures/additional-modules/src/example.sql b/fixtures/additional-modules/src/example.sql new file mode 100644 index 0000000..d9c6bf9 --- /dev/null +++ b/fixtures/additional-modules/src/example.sql @@ -0,0 +1 @@ +SELECT * FROM users; diff --git a/fixtures/additional-modules/src/index.ts b/fixtures/additional-modules/src/index.ts new file mode 100644 index 0000000..de1cc44 --- /dev/null +++ b/fixtures/additional-modules/src/index.ts @@ -0,0 +1,32 @@ +import common from "./common.cjs"; +import dep from "./dep"; +import sql from "./example.sql"; +import text from "./text.txt"; + +export default <ExportedHandler>{ + async fetch(request) { + const url = new URL(request.url); + if (url.pathname === "/dep") { + return new Response(dep); + } + if (url.pathname === "/text") { + return new Response(text); + } + if (url.pathname === "/sql") { + return new Response(sql); + } + if (url.pathname === "/common") { + return new Response(common); + } + if (url.pathname === "/dynamic") { + return new Response((await import("./dynamic.js")).default); + } + if (url.pathname.startsWith("/lang/")) { + // Build the path dynamically to ensure esbuild doesn't inline the import. + const language = + "./lang/" + url.pathname.substring("/lang/".length) + ".js"; + return new Response((await import(language)).default.hello); + } + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/fixtures/additional-modules/src/lang/en.js b/fixtures/additional-modules/src/lang/en.js new file mode 100644 index 0000000..969f5b9 --- /dev/null +++ b/fixtures/additional-modules/src/lang/en.js @@ -0,0 +1 @@ +export default { hello: "hello" }; diff --git a/fixtures/additional-modules/src/lang/fr.js b/fixtures/additional-modules/src/lang/fr.js new file mode 100644 index 0000000..67e5320 --- /dev/null +++ b/fixtures/additional-modules/src/lang/fr.js @@ -0,0 +1 @@ +export default { hello: "bonjour" }; diff --git a/fixtures/additional-modules/src/text.d.ts b/fixtures/additional-modules/src/text.d.ts new file mode 100644 index 0000000..646ec46 --- /dev/null +++ b/fixtures/additional-modules/src/text.d.ts @@ -0,0 +1,9 @@ +declare module "*.txt" { + const value: string; + export default value; +} + +declare module "*.sql" { + const value: string; + export default value; +} diff --git a/fixtures/additional-modules/src/text.txt b/fixtures/additional-modules/src/text.txt new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/fixtures/additional-modules/src/text.txt @@ -0,0 +1 @@ +test diff --git a/fixtures/additional-modules/test/index.test.ts b/fixtures/additional-modules/test/index.test.ts new file mode 100644 index 0000000..33495fd --- /dev/null +++ b/fixtures/additional-modules/test/index.test.ts @@ -0,0 +1,216 @@ +import childProcess from "node:child_process"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { removeDir } from "@fixture/shared/src/fs-helpers"; +import { afterAll, assert, beforeAll, describe, test, vi } from "vitest"; +import { unstable_startWorker } from "wrangler"; +import { wranglerEntryPath } from "../../shared/src/run-wrangler-long-lived"; + +async function getTmpDir() { + return fs.mkdtemp(path.join(os.tmpdir(), "wrangler-modules-")); +} + +type WranglerDev = Awaited<ReturnType<typeof unstable_startWorker>>; +function get(worker: WranglerDev, pathname: string) { + const url = `http://example.com${pathname}`; + // Disable Miniflare's pretty error page, so we can parse errors as JSON + return worker.fetch(url, { headers: { "MF-Disable-Pretty-Error": "true" } }); +} + +describe("find_additional_modules dev", () => { + let tmpDir: string; + let worker: WranglerDev; + + beforeAll(async () => { + // Copy over files to a temporary directory as we'll be modifying them + tmpDir = await getTmpDir(); + await fs.cp( + path.resolve(__dirname, "..", "src"), + path.join(tmpDir, "src"), + { recursive: true } + ); + await fs.cp( + path.resolve(__dirname, "..", "wrangler.jsonc"), + path.join(tmpDir, "wrangler.jsonc") + ); + + worker = await unstable_startWorker({ + config: path.join(tmpDir, "wrangler.jsonc"), + }); + }); + afterAll(async () => { + await worker.dispose(); + removeDir(tmpDir, { fireAndForget: true }); + }); + + test("supports bundled modules", async ({ expect }) => { + const res = await get(worker, "/dep"); + expect(await res.text()).toBe("bundled"); + }); + test("supports text modules", async ({ expect }) => { + const res = await get(worker, "/text"); + expect(await res.text()).toBe("test\n"); + }); + test("supports SQL modules", async ({ expect }) => { + const res = await get(worker, "/sql"); + expect(await res.text()).toBe("SELECT * FROM users;\n"); + }); + test("supports dynamic imports", async ({ expect }) => { + const res = await get(worker, "/dynamic"); + expect(await res.text()).toBe("dynamic"); + }); + test("supports commonjs lazy imports", async ({ expect }) => { + const res = await get(worker, "/common"); + expect(await res.text()).toBe("common"); + }); + test("supports variable dynamic imports", async ({ expect }) => { + const res = await get(worker, "/lang/en"); + expect(await res.text()).toBe("hello"); + }); + + test("watches additional modules", async ({ expect }) => { + const srcDir = path.join(tmpDir, "src"); + + // Update dynamically imported file + await fs.writeFile( + path.join(srcDir, "dynamic.js"), + 'export default "new dynamic";' + ); + await vi.waitFor(async () => { + const res = await get(worker, "/dynamic"); + assert.strictEqual(await res.text(), "new dynamic"); + }); + + // Delete dynamically imported file + await fs.rm(path.join(srcDir, "lang", "en.js")); + + await vi.waitFor(async () => { + await expect(get(worker, "/lang/en")).rejects.toThrow( + 'No such module "lang/en.js".' + ); + }); + + // Create new dynamically imported file in new directory + await fs.mkdir(path.join(srcDir, "lang", "en")); + await fs.writeFile( + path.join(srcDir, "lang", "en", "us.js"), + 'export default { hello: "hey" };' + ); + await vi.waitFor(async () => { + const res = await get(worker, "/lang/en/us"); + assert.strictEqual(await res.text(), "hey"); + }); + + // Update newly created file + await fs.writeFile( + path.join(srcDir, "lang", "en", "us.js"), + 'export default { hello: "bye" };' + ); + await vi.waitFor(async () => { + const res = await get(worker, "/lang/en/us"); + assert.strictEqual(await res.text(), "bye"); + }); + }); +}); + +function build(cwd: string, outDir: string) { + return childProcess.spawnSync( + process.execPath, + [wranglerEntryPath, "deploy", "--dry-run", `--outdir=${outDir}`], + { cwd } + ); +} + +describe("find_additional_modules deploy", () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = await getTmpDir(); + }); + afterAll(async () => await removeDir(tmpDir, { fireAndForget: true })); + + test("doesn't bundle additional modules", async ({ expect }) => { + const outDir = path.join(tmpDir, "out"); + const result = await build(path.resolve(__dirname, ".."), outDir); + expect(result.status).toBe(0); + + // Check additional modules marked external, but other dependencies bundled + const bundledEntryPath = path.join(outDir, "index.js"); + const bundledEntry = await fs.readFile(bundledEntryPath, "utf8"); + expect(bundledEntry).toMatchInlineSnapshot(` + "// src/index.ts + import common from "./common.cjs"; + + // src/dep.ts + var dep_default = "bundled"; + + // src/index.ts + import sql from "./example.sql"; + import text from "./text.txt"; + var index_default = { + async fetch(request) { + const url = new URL(request.url); + if (url.pathname === "/dep") { + return new Response(dep_default); + } + if (url.pathname === "/text") { + return new Response(text); + } + if (url.pathname === "/sql") { + return new Response(sql); + } + if (url.pathname === "/common") { + return new Response(common); + } + if (url.pathname === "/dynamic") { + return new Response((await import("./dynamic.js")).default); + } + if (url.pathname.startsWith("/lang/")) { + const language = "./lang/" + url.pathname.substring("/lang/".length) + ".js"; + return new Response((await import(language)).default.hello); + } + return new Response("Not Found", { status: 404 }); + } + }; + export { + index_default as default + }; + //# sourceMappingURL=index.js.map + " + `); + + // Check additional modules included in output + expect(existsSync(path.join(outDir, "text.txt"))).toBe(true); + expect(existsSync(path.join(outDir, "dynamic.js"))).toBe(true); + expect(existsSync(path.join(outDir, "lang", "en.js"))).toBe(true); + expect(existsSync(path.join(outDir, "lang", "fr.js"))).toBe(true); + }); + + test("fails with service worker entrypoint", async ({ expect }) => { + // Write basic service worker with `find_additional_modules` enabled + const serviceWorkerDir = path.join(tmpDir, "service-worker"); + await fs.mkdir(serviceWorkerDir, { recursive: true }); + await fs.writeFile( + path.join(serviceWorkerDir, "index.js"), + "addEventListener('fetch', (e) => e.respondWith(new Response()))" + ); + await fs.writeFile( + path.join(serviceWorkerDir, "wrangler.toml"), + [ + 'name="service-worker-test"', + 'main = "index.js"', + 'compatibility_date = "2023-08-01"', + "find_additional_modules = true", + ].join("\n") + ); + + // Try build, and check fails + const serviceWorkerOutDir = path.join(tmpDir, "service-worker-out"); + const result = await build(serviceWorkerDir, serviceWorkerOutDir); + expect(result.status).toBe(1); + expect(result.stderr.toString()).toContain( + "`find_additional_modules` can only be used with an ES module entrypoint." + ); + }); +}); diff --git a/fixtures/additional-modules/test/tsconfig.json b/fixtures/additional-modules/test/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/additional-modules/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/additional-modules/tsconfig.json b/fixtures/additional-modules/tsconfig.json new file mode 100644 index 0000000..873892f --- /dev/null +++ b/fixtures/additional-modules/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "strict": true, + "isolatedModules": true, + "noEmit": true, + "types": ["@cloudflare/workers-types/experimental"], + "allowJs": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} diff --git a/fixtures/additional-modules/turbo.json b/fixtures/additional-modules/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/additional-modules/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/additional-modules/vitest.config.mts b/fixtures/additional-modules/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/additional-modules/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/additional-modules/wrangler.jsonc b/fixtures/additional-modules/wrangler.jsonc new file mode 100644 index 0000000..6eee83e --- /dev/null +++ b/fixtures/additional-modules/wrangler.jsonc @@ -0,0 +1,16 @@ +{ + "name": "additional-modules", + "main": "src/index.ts", + "compatibility_date": "2023-08-01", + "find_additional_modules": true, + "rules": [ + { + "type": "CommonJS", + "globs": ["**/*.cjs"], + }, + { + "type": "ESModule", + "globs": ["**/*.js"], + }, + ], +} diff --git a/fixtures/browser-run/package.json b/fixtures/browser-run/package.json new file mode 100644 index 0000000..638083b --- /dev/null +++ b/fixtures/browser-run/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/browser-run", + "private": true, + "scripts": { + "cf-typegen": "wrangler types --no-include-runtime", + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "test": "vitest", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/playwright": "^1.0.0", + "@cloudflare/puppeteer": "^1.0.4", + "@cloudflare/vitest-pool-workers": "workspace:*", + "@types/node": "catalog:default", + "typescript": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/browser-run/src/index.ts b/fixtures/browser-run/src/index.ts new file mode 100644 index 0000000..318c542 --- /dev/null +++ b/fixtures/browser-run/src/index.ts @@ -0,0 +1,15 @@ +import playwrightWorker from "./playwright"; +import puppeteerWorker from "./puppeteer"; + +export default { + async fetch(request, env): Promise<Response> { + const { searchParams } = new URL(request.url); + let lib = searchParams.get("lib"); + + if (lib === "playwright") { + return playwrightWorker.fetch(request, env); + } else { + return puppeteerWorker.fetch(request, env); + } + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/browser-run/src/playwright.ts b/fixtures/browser-run/src/playwright.ts new file mode 100644 index 0000000..c1ac1fc --- /dev/null +++ b/fixtures/browser-run/src/playwright.ts @@ -0,0 +1,74 @@ +import playwright from "@cloudflare/playwright"; + +export default { + async fetch(request, env): Promise<Response> { + const { searchParams } = new URL(request.url); + let url = searchParams.get("url"); + let action = searchParams.get("action"); + if (url) { + url = new URL(url).toString(); // normalize + switch (action) { + case "select": { + await using browser = await playwright.launch(env.MYBROWSER); + const page = await browser.newPage(); + await page.goto(url); + const h1Text = await page.locator("h1").textContent(); + return new Response(h1Text); + } + + case "alter": { + await using browser = await playwright.launch(env.MYBROWSER); + const page = await browser.newPage(); + + await page.goto(url); // change to your target URL + + await page + .locator("p") + .first() + .evaluate((paragraph) => { + paragraph.textContent = "New paragraph text set by Playwright!"; + }); + + const pText = await page.locator("p").first().textContent(); + return new Response(pText); + } + + case "disconnect": { + const { sessionId } = await playwright.acquire(env.MYBROWSER); + const browser = await playwright.connect(env.MYBROWSER, sessionId); + // closing a browser obtained with playwright.connect actually disconnects + // (it doesn't close the process) + await browser.close(); + const sessionInfo = await playwright + .sessions(env.MYBROWSER) + .then((sessions) => + sessions.find((s) => s.sessionId === sessionId) + ); + return new Response( + sessionInfo.connectionId + ? "Browser not disconnected" + : "Browser disconnected" + ); + } + } + + let img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" }); + if (img === null) { + await using browser = await playwright.launch(env.MYBROWSER); + const page = await browser.newPage(); + await page.goto(url); + img = (await page.screenshot()) as Buffer; + await env.BROWSER_KV_DEMO.put(url, img, { + expirationTtl: 60 * 60 * 24, + }); + } + return new Response(img, { + headers: { + "content-type": "image/jpeg", + }, + }); + } else { + return new Response("Please add an ?url=https://example.com/ parameter"); + } + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/browser-run/src/puppeteer.ts b/fixtures/browser-run/src/puppeteer.ts new file mode 100644 index 0000000..23ce196 --- /dev/null +++ b/fixtures/browser-run/src/puppeteer.ts @@ -0,0 +1,73 @@ +import puppeteer from "@cloudflare/puppeteer"; + +export default { + async fetch(request, env): Promise<Response> { + const { searchParams } = new URL(request.url); + let url = searchParams.get("url"); + let action = searchParams.get("action"); + if (url) { + url = new URL(url).toString(); // normalize + switch (action) { + case "select": { + const browser = await puppeteer.launch(env.MYBROWSER); + const page = await browser.newPage(); + await page.goto(url); + const h1Text = await page.$eval("h1", (el) => el.textContent.trim()); + return new Response(h1Text); + } + + case "alter": { + const browser = await puppeteer.launch(env.MYBROWSER); + const page = await browser.newPage(); + + await page.goto(url); // change to your target URL + + await page.evaluate(() => { + const paragraph = document.querySelector("p"); + if (paragraph) { + paragraph.textContent = "New paragraph text set by Puppeteer!"; + } + }); + + const pText = await page.$eval("p", (el) => el.textContent.trim()); + return new Response(pText); + } + + case "disconnect": { + const browser = await puppeteer.launch(env.MYBROWSER); + const sessionId = browser.sessionId(); + await browser.disconnect(); + const sessionInfo = await puppeteer + .sessions(env.MYBROWSER) + .then((sessions) => + sessions.find((s) => s.sessionId === sessionId) + ); + return new Response( + sessionInfo.connectionId + ? "Browser not disconnected" + : "Browser disconnected" + ); + } + } + + let img = await env.BROWSER_KV_DEMO.get(url, { type: "arrayBuffer" }); + if (img === null) { + const browser = await puppeteer.launch(env.MYBROWSER); + const page = await browser.newPage(); + await page.goto(url); + img = (await page.screenshot()) as Buffer; + await env.BROWSER_KV_DEMO.put(url, img, { + expirationTtl: 60 * 60 * 24, + }); + await browser.close(); + } + return new Response(img, { + headers: { + "content-type": "image/jpeg", + }, + }); + } else { + return new Response("Please add an ?url=https://example.com/ parameter"); + } + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/browser-run/test/index.spec.ts b/fixtures/browser-run/test/index.spec.ts new file mode 100644 index 0000000..baa61cd --- /dev/null +++ b/fixtures/browser-run/test/index.spec.ts @@ -0,0 +1,97 @@ +// test/index.spec.ts +import { rm } from "node:fs/promises"; +import { resolve } from "path"; +import { afterAll, beforeAll, describe, it, TestOptions } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +const BROWSER_RENDERING_RETRY = { + retry: { + condition: /Chrome readiness probe .* timed out|Test timed out/i, + count: 3, + delay: 1_000, + }, +} satisfies TestOptions; + +describe.sequential("Local Browser", () => { + let ip: string, + port: number, + stop: (() => Promise<unknown>) | undefined, + getOutput: () => string; + + beforeAll(async () => { + // delete previous run contents because of persistence + await rm(resolve(__dirname, "..") + "/.wrangler", { + force: true, + recursive: true, + }); + ({ ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--local", "--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + async function fetchText(url: string) { + const response = await fetch(url, { + headers: { + "MF-Disable-Pretty-Error": "1", + }, + }); + const text = await response.text(); + + return text; + } + + for (const lib of ["puppeteer", "playwright"]) { + describe(`using @cloudflare/${lib}`, () => { + it("Doesn't run a browser, just testing that the worker is running!", async ({ + expect, + }) => { + await expect( + fetchText(`http://${ip}:${port}/?lib=${lib}`) + ).resolves.toEqual("Please add an ?url=https://example.com/ parameter"); + }); + + it( + "Run a browser, and check h1 text content", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + await expect( + fetchText( + `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=select` + ) + ).resolves.toEqual("Example Domain"); + } + ); + + it( + "Run a browser, and check p text content", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + await expect( + fetchText( + `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=alter` + ) + ).resolves.toEqual( + `New paragraph text set by ${lib === "playwright" ? "Playwright" : "Puppeteer"}!` + ); + } + ); + + it( + "Disconnect a browser, and check its session connection status", + BROWSER_RENDERING_RETRY, + async ({ expect }) => { + await expect( + fetchText( + `http://${ip}:${port}/?lib=${lib}&url=https://example.com&action=disconnect` + ) + ).resolves.toEqual(`Browser disconnected`); + } + ); + }); + } +}); diff --git a/fixtures/browser-run/test/tsconfig.json b/fixtures/browser-run/test/tsconfig.json new file mode 100644 index 0000000..3562d99 --- /dev/null +++ b/fixtures/browser-run/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["./**/*.ts", "../worker-configuration.d.ts"] +} diff --git a/fixtures/browser-run/tsconfig.json b/fixtures/browser-run/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/browser-run/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/browser-run/vitest.config.mts b/fixtures/browser-run/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/browser-run/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/browser-run/worker-configuration.d.ts b/fixtures/browser-run/worker-configuration.d.ts new file mode 100644 index 0000000..76843f8 --- /dev/null +++ b/fixtures/browser-run/worker-configuration.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --no-include-runtime` (hash: 8df6fc5ccb1f778e4b6799ea0fe8d9e6) +interface __BaseEnv_Env { + BROWSER_KV_DEMO: KVNamespace; + MYBROWSER: Fetcher; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/browser-run/wrangler.jsonc b/fixtures/browser-run/wrangler.jsonc new file mode 100644 index 0000000..2b089a0 --- /dev/null +++ b/fixtures/browser-run/wrangler.jsonc @@ -0,0 +1,24 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "browser-rendering", + "main": "src/index.ts", + "compatibility_date": "2025-12-01", + "observability": { + "enabled": true, + }, + "compatibility_flags": ["nodejs_compat"], + "browser": { + "binding": "MYBROWSER", + }, + "kv_namespaces": [ + { + "binding": "BROWSER_KV_DEMO", + "id": "8a5482302d2e4bcb8186416eea7e38b1", + "preview_id": "8a5482302d2e4bcb8186416eea7e38b1", + }, + ], +} diff --git a/fixtures/container-app/.gitignore b/fixtures/container-app/.gitignore new file mode 100644 index 0000000..c6e1d1c --- /dev/null +++ b/fixtures/container-app/.gitignore @@ -0,0 +1,168 @@ +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars* +!.dev.vars.example +.env* +!.env.example +.wrangler/ +worker-configuration.d.ts diff --git a/fixtures/container-app/Dockerfile b/fixtures/container-app/Dockerfile new file mode 100644 index 0000000..eaeb93f --- /dev/null +++ b/fixtures/container-app/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app", "version": "1.0.0", "dependencies": {"ws": "^8.0.0"}}' > package.json +RUN npm install + +COPY ./container/simple-node-app.js app.js +EXPOSE 8080 diff --git a/fixtures/container-app/README.md b/fixtures/container-app/README.md new file mode 100644 index 0000000..d0c6fd4 --- /dev/null +++ b/fixtures/container-app/README.md @@ -0,0 +1,3 @@ +# HTTP fetch to a container + +This example shows a simple container setup where a DO passes requests through to a node server. diff --git a/fixtures/container-app/container/simple-node-app.js b/fixtures/container-app/container/simple-node-app.js new file mode 100644 index 0000000..fd5e60e --- /dev/null +++ b/fixtures/container-app/container/simple-node-app.js @@ -0,0 +1,49 @@ +const { createServer } = require("http"); + +const webSocketEnabled = process.env.WS_ENABLED === "true"; + +// Create HTTP server +const server = createServer(function (req, res) { + if (req.url === "/ws") { + // WebSocket upgrade will be handled by the WebSocket server + return; + } + + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("Hello World! Have an env var! " + process.env.MESSAGE); + res.end(); +}); + +// Check if WebSocket functionality is enabled +if (webSocketEnabled) { + const WebSocket = require("ws"); + + // Create WebSocket server + const wss = new WebSocket.Server({ + server: server, + path: "/ws", + }); + + wss.on("connection", function connection(ws) { + console.log("WebSocket connection established"); + + ws.on("message", function message(data) { + console.log("Received:", data.toString()); + // Echo the message back with prefix + ws.send("Echo: " + data.toString()); + }); + + ws.on("close", function close() { + console.log("WebSocket connection closed"); + }); + + ws.on("error", console.error); + }); +} + +server.listen(8080, function () { + console.log("Server listening on port 8080"); + if (webSocketEnabled) { + console.log("WebSocket support enabled"); + } +}); diff --git a/fixtures/container-app/package.json b/fixtures/container-app/package.json new file mode 100644 index 0000000..b93d4aa --- /dev/null +++ b/fixtures/container-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/container-app", + "private": true, + "scripts": { + "cf-typegen": "wrangler types --no-include-runtime", + "container:build": "wrangler containers build ./ -t container-fixture", + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "dev:registry": "wrangler dev -c ./wrangler.registry.jsonc", + "start": "wrangler dev", + "start:registry": "wrangler dev -c ./wrangler.registry.jsonc" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "ts-dedent": "^2.2.0", + "typescript": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/container-app/src/index.ts b/fixtures/container-app/src/index.ts new file mode 100644 index 0000000..ca55dd9 --- /dev/null +++ b/fixtures/container-app/src/index.ts @@ -0,0 +1,68 @@ +import { DurableObject } from "cloudflare:workers"; + +export class FixtureTestContainer extends DurableObject<Env> { + container: globalThis.Container; + monitor?: Promise<unknown>; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.container = ctx.container; + } + + async fetch(req: Request) { + const path = new URL(req.url).pathname; + switch (path) { + case "/status": + return new Response(JSON.stringify(this.container.running)); + + case "/destroy": + if (!this.container.running) { + throw new Error("Container is not running."); + } + await this.container.destroy(); + return new Response(JSON.stringify(this.container.running)); + + case "/start": + this.container.start({ + entrypoint: ["node", "app.js"], + env: { MESSAGE: "I'm an env var!" }, + enableInternet: false, + }); + // this doesn't instantly start, so we will need to poll /fetch + return new Response("Container create request sent..."); + + case "/fetch": + const res = await this.container + .getTcpPort(8080) + // actual request doesn't matter + .fetch("http://foo/bar/baz", { method: "POST", body: "hello" }); + return new Response(await res.text()); + + case "/destroy-with-monitor": + const monitor = this.container.monitor(); + await this.container.destroy(); + await monitor; + return new Response("Container destroyed with monitor."); + + default: + return new Response("Hi from Container DO"); + } + } +} + +export default { + async fetch(request, env, ctx: ExecutionContext): Promise<Response> { + const url = new URL(request.url); + if (url.pathname === "/second") { + // This is a second Durable Object that can be used to test multiple DOs + const id = + ctx.exports.FixtureTestContainer.idFromName("second-container"); + const stub = ctx.exports.FixtureTestContainer.get(id); + const query = url.searchParams.get("req"); + return stub.fetch("http://example.com/" + query); + } + const id = ctx.exports.FixtureTestContainer.idFromName("container"); + const stub = ctx.exports.FixtureTestContainer.get(id); + return stub.fetch(request); + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/container-app/tsconfig.json b/fixtures/container-app/tsconfig.json new file mode 100644 index 0000000..cee17af --- /dev/null +++ b/fixtures/container-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/container-app/wrangler.jsonc b/fixtures/container-app/wrangler.jsonc new file mode 100644 index 0000000..37e82de --- /dev/null +++ b/fixtures/container-app/wrangler.jsonc @@ -0,0 +1,20 @@ +{ + "name": "container-app", + "main": "src/index.ts", + "compatibility_date": "2025-04-03", + "compatibility_flags": ["enable_ctx_exports"], + "containers": [ + { + "image": "./Dockerfile", + "class_name": "FixtureTestContainer", + "name": "container", + "max_instances": 2, + }, + ], + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["FixtureTestContainer"], + }, + ], +} diff --git a/fixtures/container-app/wrangler.registry.jsonc b/fixtures/container-app/wrangler.registry.jsonc new file mode 100644 index 0000000..ac2e3a8 --- /dev/null +++ b/fixtures/container-app/wrangler.registry.jsonc @@ -0,0 +1,28 @@ +{ + "name": "container-app-from-registry", + "main": "src/index.ts", + "compatibility_date": "2025-04-03", + "containers": [ + { + // This is the same container as built by the dockerfile except it is pulled from the registry + "image": "registry.cloudflare.com/8d783f274e1f82dc46744c297b015a2f/ci-container-dont-delete:latest", + "class_name": "FixtureTestContainer", + "name": "container", + "max_instances": 2, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "FixtureTestContainer", + "name": "CONTAINER", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["FixtureTestContainer"], + }, + ], +} diff --git a/fixtures/create-test-harness-example/.gitignore b/fixtures/create-test-harness-example/.gitignore new file mode 100644 index 0000000..aaa9103 --- /dev/null +++ b/fixtures/create-test-harness-example/.gitignore @@ -0,0 +1,2 @@ +test-results/ +playwright-report/ diff --git a/fixtures/create-test-harness-example/README.md b/fixtures/create-test-harness-example/README.md new file mode 100644 index 0000000..ba97936 --- /dev/null +++ b/fixtures/create-test-harness-example/README.md @@ -0,0 +1,66 @@ +# Integration testing Workers with `createTestHarness()` + +This fixture is the complete runnable example linked from the `createTestHarness()` docs. It shows how to test a realistic multi-Worker app's production build output from different test runners. + +For the testing patterns demonstrated here, refer to the [`createTestHarness()` examples guide](https://developers.cloudflare.com/workers/testing/test-harness/examples/). + +## Example app + +The app has three Workers: + +| Worker | Route | Role | +| -------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `web-worker` | `example.com/*` | Handles user-facing routes, calls the API Worker over a service binding, and uses Browser Rendering to generate previews. | +| `api-worker` | `api.example.com/v1/*` | Fetches upstream user data, caches it in KV, and stores scheduled reports in D1. | +| `mock-browser` | none | Test-only Worker used to replace the Web Worker's Browser Rendering binding. | + +## What this fixture covers + +- Setup [Vitest](https://vitest.dev/) against Workers developed with Wrangler. +- Setup [Playwright](https://playwright.dev/) against Workers built by the Cloudflare Vite plugin. +- Route dispatch across multiple Workers. +- Direct Worker calls with `server.getWorker(name)`. +- D1 migration setup with `worker.applyD1Migrations()`. +- Scheduled handler dispatch. +- Test-only binding overrides for platform bindings. +- Calling a Worker's default export with `server.getWorker(name).getExport()`. +- Outbound request mocking with MSW. +- Local storage reset between tests. +- Debug output with `server.debug()`. +- Runtime log assertions with `server.getLogs()` and `server.clearLogs()`. + +## Run this example + +To build the packages, run: + +```sh +pnpm build --filter @fixture/create-test-harness-example +``` + +Then run the tests with: + +```sh +pnpm --filter @fixture/create-test-harness-example test:vitest +pnpm --filter @fixture/create-test-harness-example test:playwright +``` + +## Files + +| File | Purpose | +| -------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| [`tests/vitest.test.ts`](tests/vitest.test.ts) | Example for testing with Vitest. | +| [`tests/playwright.test.ts`](tests/playwright.test.ts) | Example for testing with Playwright. | +| [`vite.config.ts`](vite.config.ts) | Builds Vite-generated Worker configs. | +| [`workers/web/index.ts`](workers/web/index.ts) | User-facing Worker with service and Browser Rendering binding logic. | +| [`workers/web/wrangler.jsonc`](workers/web/wrangler.jsonc) | Web Worker config. | +| [`workers/web/worker-configuration.d.ts`](workers/web/worker-configuration.d.ts) | Generated Web Worker types. | +| [`workers/api/index.ts`](workers/api/index.ts) | API Worker with KV, D1, and scheduled job logic. | +| [`workers/api/wrangler.jsonc`](workers/api/wrangler.jsonc) | API Worker config. | +| [`workers/api/worker-configuration.d.ts`](workers/api/worker-configuration.d.ts) | Generated API Worker types. | +| [`workers/mock-browser/index.ts`](workers/mock-browser/index.ts) | Test-only mock Worker for the Browser Rendering binding. | +| [`workers/mock-browser/wrangler.jsonc`](workers/mock-browser/wrangler.jsonc) | Mock Browser Worker config. | + +## Related docs + +- [`createTestHarness()` guide](https://developers.cloudflare.com/workers/testing/test-harness/) +- [`createTestHarness()` API reference](https://developers.cloudflare.com/workers/wrangler/api/#createtestharness) diff --git a/fixtures/create-test-harness-example/package.json b/fixtures/create-test-harness-example/package.json new file mode 100644 index 0000000..42260df --- /dev/null +++ b/fixtures/create-test-harness-example/package.json @@ -0,0 +1,35 @@ +{ + "name": "@fixture/create-test-harness-example", + "private": true, + "description": "Integration tests with the createTestHarness() API", + "type": "module", + "scripts": { + "cf-typegen": "pnpm typegen:web && pnpm typegen:api", + "typegen:web": "wrangler types ./workers/web/worker-configuration.d.ts -c ./workers/web/wrangler.jsonc -c ./workers/api/wrangler.jsonc --env-interface WebEnv --no-include-runtime", + "typegen:api": "wrangler types ./workers/api/worker-configuration.d.ts -c ./workers/api/wrangler.jsonc --env-interface ApiEnv --no-include-runtime", + "check:type": "tsc", + "build": "vite build", + "playwright:install": "pnpm playwright install chromium", + "pretest:ci": "pnpm playwright:install", + "pretest:playwright": "pnpm playwright:install", + "test:playwright": "playwright test", + "test:vitest": "vitest run", + "test:ci": "vitest run && playwright test", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@playwright/test": "catalog:default", + "@types/node": "catalog:default", + "msw": "catalog:default", + "typescript": "catalog:default", + "vite": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/fixtures/create-test-harness-example/playwright.config.ts b/fixtures/create-test-harness-example/playwright.config.ts new file mode 100644 index 0000000..68eaf0a --- /dev/null +++ b/fixtures/create-test-harness-example/playwright.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + testMatch: "playwright.test.ts", + use: { + browserName: "chromium", + }, +}); diff --git a/fixtures/create-test-harness-example/tests/playwright.test.ts b/fixtures/create-test-harness-example/tests/playwright.test.ts new file mode 100644 index 0000000..bd07bcc --- /dev/null +++ b/fixtures/create-test-harness-example/tests/playwright.test.ts @@ -0,0 +1,107 @@ +import { test as base, expect } from "@playwright/test"; +import { http, HttpResponse } from "msw"; +import { setupServer, type SetupServerApi } from "msw/node"; +import { createTestHarness, type TestHarness } from "wrangler"; + +type TestFixtures = { + reset: void; +}; + +type WorkerFixtures = { + network: SetupServerApi; + server: TestHarness; +}; + +const test = base.extend<TestFixtures, WorkerFixtures>({ + network: [ + async ({}, use) => { + const network = setupServer(); + network.listen({ onUnhandledRequest: "error" }); + + await use(network); + + network.close(); + }, + { scope: "worker" }, + ], + + server: [ + async ({}, use) => { + const server = createTestHarness({ + workers: [ + // Playwright tests run against the Vite build output. + { configPath: "./dist/web_worker/wrangler.json" }, + { configPath: "./dist/api_worker/wrangler.json" }, + ], + }); + + await server.listen(); + + await use(server); + + await server.close(); + }, + { scope: "worker" }, + ], + + baseURL: async ({ server }, use) => { + const { url } = await server.listen(); + await use(url.href); + }, + + reset: [ + async ({ network, server }, use, testInfo) => { + await server.getWorker("api-worker").applyD1Migrations("DATABASE"); + + await use(); + + if (testInfo.status !== testInfo.expectedStatus) { + server.debug(); + } + + network.resetHandlers(); + await server.reset(); + }, + { auto: true }, + ], +}); + +test.describe("createTestHarness: Playwright setup", () => { + test("renders a user profile", async ({ page, network }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + await page.goto("/users/123"); + + await expect(page.getByText("Profile: Ada")).toBeVisible(); + }); + + test("renders a report generated by a scheduled handler", async ({ + page, + network, + server, + }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + const apiWorker = server.getWorker("api-worker"); + await apiWorker.fetch("/v1/users/123"); + await apiWorker.fetch("/v1/users/456"); + await apiWorker.scheduled({ + cron: "0 0 * * *", + scheduledTime: new Date("2026-05-29T00:00:00.000Z"), + }); + + await page.goto("/reports/2026-05-29"); + + await expect( + page.getByText("Daily report (2026-05-29): active users 123, 456") + ).toBeVisible(); + }); +}); diff --git a/fixtures/create-test-harness-example/tests/tsconfig.json b/fixtures/create-test-harness-example/tests/tsconfig.json new file mode 100644 index 0000000..7ba7997 --- /dev/null +++ b/fixtures/create-test-harness-example/tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "noEmit": true, + "types": ["@cloudflare/workers-types", "node"] + }, + "include": [ + "../playwright.config.ts", + "../workers/*/worker-configuration.d.ts", + "**/*.ts" + ], + "exclude": [] +} diff --git a/fixtures/create-test-harness-example/tests/vitest.test.ts b/fixtures/create-test-harness-example/tests/vitest.test.ts new file mode 100644 index 0000000..4829436 --- /dev/null +++ b/fixtures/create-test-harness-example/tests/vitest.test.ts @@ -0,0 +1,160 @@ +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + test, +} from "vitest"; +import { createTestHarness } from "wrangler"; + +// Point each worker to the Wrangler config you want to test. +const server = createTestHarness({ + workers: [ + { + configPath: "./workers/web/wrangler.jsonc", + bindingOverrides: { BROWSER: "mock-browser" }, + }, + { configPath: "./workers/api/wrangler.jsonc" }, + { configPath: "./workers/mock-browser/wrangler.jsonc" }, + ], +}); + +// server.getWorker() would return the first Worker, but naming it makes it explicit. +const webWorker = server.getWorker< + WebEnv, + typeof import("../workers/web/index") +>("web-worker"); +const apiWorker = server.getWorker< + ApiEnv, + typeof import("../workers/api/index") +>("api-worker"); +const mockBrowserWorker = server.getWorker< + unknown, + typeof import("../workers/mock-browser/index") +>("mock-browser"); + +// Workers started by createTestHarness route outbound fetches to globalThis.fetch(). +// You can use libraries like MSW to intercept those requests. +const network = setupServer(); + +describe("createTestHarness: Vitest setup", () => { + beforeAll(async () => { + network.listen({ onUnhandledRequest: "error" }); + await server.listen(); + }); + + beforeEach(async () => { + await apiWorker.applyD1Migrations("DATABASE"); + }); + + afterAll(async () => { + network.close(); + await server.close(); + }); + + afterEach(async () => { + // Keep tests isolated while reusing the same running server. + network.resetHandlers(); + await server.reset(); + }); + + test("fetches the primary Worker with a relative URL", async ({ expect }) => { + // Relative URLs are dispatched to the primary Worker (the first one listed). + const response = await server.fetch("/"); + expect(await response.text()).toBe("Hello World"); + }); + + test("mocks outbound requests", async ({ expect }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + const userResponse = await apiWorker.fetch("/v1/users/123"); + expect(await userResponse.json()).toEqual({ + id: "123", + name: "Ada", + }); + }); + + test("overrides a platform binding with a mock Worker", async ({ + expect, + }) => { + const apiEnv = await apiWorker.getEnv(); + await apiEnv.DATABASE.prepare( + "INSERT INTO daily_reports (date, user_ids) VALUES (?, ?)" + ) + .bind("2026-05-29", JSON.stringify(["123", "456"])) + .run(); + + const stubPng = Uint8Array.from([ + 137, 80, 78, 71, 13, 10, 26, 10, 109, 111, 99, 107, + ]); + const mockBrowser = await mockBrowserWorker.getExport(); + await mockBrowser.setScreenshot(Array.from(stubPng)); + + const response = await webWorker.fetch("/reports/2026-05-29.png"); + expect(response.headers.get("content-type")).toBe("image/png"); + expect(await response.bytes()).toEqual(stubPng); + }); + + test("dispatches requests using configured routes", async ({ expect }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + // server.fetch() matches requests to workers based on routes. + const apiResponse = await server.fetch( + "http://api.example.com/v1/users/123" + ); + expect(await apiResponse.json()).toEqual({ + id: "123", + name: "Ada", + }); + + const webResponse = await server.fetch("http://example.com/users/123"); + expect(await webResponse.text()).toBe("Profile: Ada"); + }); + + test("runs scheduled jobs and stores the result", async ({ expect }) => { + network.use( + http.get("http://identity.example.com/profile/:id", ({ params }) => { + return HttpResponse.json({ id: params.id, name: "Ada" }); + }) + ); + + // Seed user data that the scheduled job will read to generate a report. + await apiWorker.fetch("/v1/users/123"); + await apiWorker.fetch("/v1/users/456"); + + const initialResponse = await webWorker.fetch("/reports/2026-05-29"); + expect(initialResponse.status).toBe(404); + expect(await initialResponse.text()).toBe("No report"); + + server.clearLogs(); + + expect( + await apiWorker.scheduled({ + cron: "0 0 * * *", + scheduledTime: new Date("2026-05-29T00:00:00.000Z"), + }) + ).toEqual({ outcome: "ok", noRetry: false }); + expect(server.getLogs()).toContainEqual( + expect.objectContaining({ + level: "info", + message: "Generated daily report for 2026-05-29", + }) + ); + + const webResponse = await webWorker.fetch("/reports/2026-05-29"); + expect(await webResponse.text()).toBe( + "Daily report (2026-05-29): active users 123, 456" + ); + }); +}); diff --git a/fixtures/create-test-harness-example/tsconfig.json b/fixtures/create-test-harness-example/tsconfig.json new file mode 100644 index 0000000..e8e338a --- /dev/null +++ b/fixtures/create-test-harness-example/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "target": "esnext", + "strict": true, + "noEmit": true, + "types": ["@cloudflare/workers-types", "node"], + "lib": ["esnext"], + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/create-test-harness-example/turbo.json b/fixtures/create-test-harness-example/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/create-test-harness-example/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/create-test-harness-example/vite.config.ts b/fixtures/create-test-harness-example/vite.config.ts new file mode 100644 index 0000000..22edff0 --- /dev/null +++ b/fixtures/create-test-harness-example/vite.config.ts @@ -0,0 +1,16 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ + configPath: "./workers/web/wrangler.jsonc", + auxiliaryWorkers: [ + { configPath: "./workers/api/wrangler.jsonc" }, + { configPath: "./workers/mock-browser/wrangler.jsonc" }, + ], + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/fixtures/create-test-harness-example/vitest.config.mts b/fixtures/create-test-harness-example/vitest.config.mts new file mode 100644 index 0000000..61bbfb0 --- /dev/null +++ b/fixtures/create-test-harness-example/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + include: ["tests/vitest.test.ts"], + }, + }) +); diff --git a/fixtures/create-test-harness-example/workers/api/index.ts b/fixtures/create-test-harness-example/workers/api/index.ts new file mode 100644 index 0000000..3a2e8f6 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/api/index.ts @@ -0,0 +1,97 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +type Env = { + STORE: KVNamespace; + DATABASE: D1Database; +}; + +type User = { + id: string; + name: string; +}; + +/** + * API Worker: fetches users from an upstream service, caches them in KV, and generates daily reports on a schedule. + */ +export default class ApiWorker extends WorkerEntrypoint<Env> { + async fetch(request: Request) { + const url = new URL(request.url); + const userPathPrefix = "/v1/users/"; + + if (url.pathname.startsWith(userPathPrefix)) { + const userId = url.pathname.slice(userPathPrefix.length); + return Response.json(await this.getUser(userId)); + } + + const reportPathPrefix = "/v1/reports/"; + + if (url.pathname.startsWith(reportPathPrefix)) { + const date = url.pathname.slice(reportPathPrefix.length); + const report = await this.getDailyReport(date); + + if (report === null) { + return Response.json({ error: "No report" }, { status: 404 }); + } + + return Response.json(report); + } + + return new Response("Not Found", { status: 404 }); + } + + async scheduled(event: ScheduledController) { + if (event.cron !== "0 0 * * *") { + throw new Error(`Unexpected cron: ${event.cron}`); + } + + const date = new Date(event.scheduledTime).toISOString().slice(0, 10); + const list = await this.env.STORE.list({ prefix: "user/" }); + const userIds = list.keys.map((key) => key.name.slice("user/".length)); + + await this.env.DATABASE.prepare( + "INSERT OR REPLACE INTO daily_reports (date, user_ids) VALUES (?, ?)" + ) + .bind(date, JSON.stringify(userIds)) + .run(); + console.info(`Generated daily report for ${date}`); + + for (const key of list.keys) { + await this.env.STORE.delete(key.name); + } + } + + async getUser(userId: string): Promise<User> { + const key = `user/${userId}`; + const cachedUser = await this.env.STORE.get<User>(key, { + type: "json", + }); + + if (cachedUser !== null) { + return cachedUser; + } + + const upstreamResponse = await fetch( + `http://identity.example.com/profile/${userId}` + ); + const user = await upstreamResponse.json<User>(); + + await this.env.STORE.put(key, JSON.stringify(user)); + + return user; + } + + async getDailyReport(date: string) { + const report = await this.env.DATABASE.prepare( + "SELECT user_ids FROM daily_reports WHERE date = ?" + ) + .bind(date) + .first<{ user_ids: string }>(); + + if (report === null) { + return null; + } + + const userIds: string[] = JSON.parse(report.user_ids); + return userIds; + } +} diff --git a/fixtures/create-test-harness-example/workers/api/migrations/0001_reports.sql b/fixtures/create-test-harness-example/workers/api/migrations/0001_reports.sql new file mode 100644 index 0000000..7e3c033 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/api/migrations/0001_reports.sql @@ -0,0 +1,4 @@ +CREATE TABLE daily_reports ( + date TEXT PRIMARY KEY, + user_ids TEXT NOT NULL +); diff --git a/fixtures/create-test-harness-example/workers/api/worker-configuration.d.ts b/fixtures/create-test-harness-example/workers/api/worker-configuration.d.ts new file mode 100644 index 0000000..771e0c1 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/api/worker-configuration.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --config=./workers/api/wrangler.jsonc --include-runtime=false --env-interface=ApiEnv ./workers/api/worker-configuration.d.ts` (hash: 14b9b34fa5935bf62d0e0be93efbdb85) +interface __BaseEnv_ApiEnv { + STORE: KVNamespace; + DATABASE: D1Database; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_ApiEnv {} +} +interface ApiEnv extends __BaseEnv_ApiEnv {} diff --git a/fixtures/create-test-harness-example/workers/api/wrangler.jsonc b/fixtures/create-test-harness-example/workers/api/wrangler.jsonc new file mode 100644 index 0000000..e1a1d05 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/api/wrangler.jsonc @@ -0,0 +1,14 @@ +{ + "name": "api-worker", + "main": "index.ts", + "compatibility_date": "2026-06-01", + "routes": ["api.example.com/v1/*"], + "kv_namespaces": [{ "binding": "STORE", "id": "shared-store" }], + "d1_databases": [ + { + "binding": "DATABASE", + "database_name": "report-database", + "database_id": "fake-database-id", + }, + ], +} diff --git a/fixtures/create-test-harness-example/workers/mock-browser/index.ts b/fixtures/create-test-harness-example/workers/mock-browser/index.ts new file mode 100644 index 0000000..ac787aa --- /dev/null +++ b/fixtures/create-test-harness-example/workers/mock-browser/index.ts @@ -0,0 +1,19 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +let screenshot: Uint8Array | undefined; + +export default class MockBrowser extends WorkerEntrypoint { + setScreenshot(bytes: number[]) { + screenshot = Uint8Array.from(bytes); + } + + async quickAction(action: "screenshot", options: { html: string }) { + if (screenshot === undefined) { + throw new Error("Mock screenshot has not been configured."); + } + + return new Response(screenshot, { + headers: { "content-type": "image/png" }, + }); + } +} diff --git a/fixtures/create-test-harness-example/workers/mock-browser/wrangler.jsonc b/fixtures/create-test-harness-example/workers/mock-browser/wrangler.jsonc new file mode 100644 index 0000000..9aea212 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/mock-browser/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "mock-browser", + "main": "index.ts", + "compatibility_date": "2026-06-01", +} diff --git a/fixtures/create-test-harness-example/workers/web/index.ts b/fixtures/create-test-harness-example/workers/web/index.ts new file mode 100644 index 0000000..811ab52 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/web/index.ts @@ -0,0 +1,42 @@ +/** + * Web Worker: renders user profiles and reports by calling the API Worker over a service binding. + */ +export default { + async fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname === "/") { + return new Response("Hello World"); + } + + const userPathPrefix = "/users/"; + if (url.pathname.startsWith(userPathPrefix)) { + const userId = url.pathname.slice(userPathPrefix.length); + const { name } = await env.API.getUser(userId); + return new Response(`Profile: ${name}`); + } + + const reportsPathPrefix = "/reports/"; + if (url.pathname.startsWith(reportsPathPrefix)) { + const date = url.pathname.slice(reportsPathPrefix.length).slice(0, 10); // YYYY-MM-DD + const report = await env.API.getDailyReport(date); + + if (report === null) { + return new Response("No report", { status: 404 }); + } + + if (url.pathname.endsWith(".png")) { + return env.BROWSER.quickAction("screenshot", { + html: `<h1>Daily report (${date}): active users ${report.join(", ")}</h1>`, + viewport: { width: 600, height: 200 }, + }); + } + + return new Response( + `Daily report (${date}): active users ${report.join(", ")}` + ); + } + + return new Response("Not Found", { status: 404 }); + }, +} satisfies ExportedHandler<WebEnv>; diff --git a/fixtures/create-test-harness-example/workers/web/worker-configuration.d.ts b/fixtures/create-test-harness-example/workers/web/worker-configuration.d.ts new file mode 100644 index 0000000..c08ea1d --- /dev/null +++ b/fixtures/create-test-harness-example/workers/web/worker-configuration.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --config=./workers/web/wrangler.jsonc --config=./workers/api/wrangler.jsonc --include-runtime=false --env-interface=WebEnv ./workers/web/worker-configuration.d.ts` (hash: ba220acc5e7d9da5ae68234db2179e34) +interface __BaseEnv_WebEnv { + BROWSER: BrowserRun; + API: Service<typeof import("../api/index").default>; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_WebEnv {} +} +interface WebEnv extends __BaseEnv_WebEnv {} diff --git a/fixtures/create-test-harness-example/workers/web/wrangler.jsonc b/fixtures/create-test-harness-example/workers/web/wrangler.jsonc new file mode 100644 index 0000000..21777c0 --- /dev/null +++ b/fixtures/create-test-harness-example/workers/web/wrangler.jsonc @@ -0,0 +1,8 @@ +{ + "name": "web-worker", + "main": "index.ts", + "compatibility_date": "2026-06-01", + "routes": ["example.com/*"], + "browser": { "binding": "BROWSER" }, + "services": [{ "binding": "API", "service": "api-worker" }], +} diff --git a/fixtures/d1-read-replication-app/.gitignore b/fixtures/d1-read-replication-app/.gitignore new file mode 100644 index 0000000..37cbd63 --- /dev/null +++ b/fixtures/d1-read-replication-app/.gitignore @@ -0,0 +1,2 @@ +dist +.wrangler diff --git a/fixtures/d1-read-replication-app/README.md b/fixtures/d1-read-replication-app/README.md new file mode 100644 index 0000000..a3a1fc3 --- /dev/null +++ b/fixtures/d1-read-replication-app/README.md @@ -0,0 +1,3 @@ +# Notes about testing with this fixture + +- This includes tests related to the D1 read replication feature. diff --git a/fixtures/d1-read-replication-app/package.json b/fixtures/d1-read-replication-app/package.json new file mode 100644 index 0000000..7858ea6 --- /dev/null +++ b/fixtures/d1-read-replication-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/d1-read-replication", + "private": true, + "scripts": { + "check:type": "tsc", + "start": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/d1-read-replication-app/src/index.ts b/fixtures/d1-read-replication-app/src/index.ts new file mode 100644 index 0000000..3dc1bce --- /dev/null +++ b/fixtures/d1-read-replication-app/src/index.ts @@ -0,0 +1,41 @@ +// These will become default after read replication is open. +type D1DatabaseWithSessionsAPI = D1Database & { + // constraintOrBookmark: "first-primary" | "first-unconstrained" | string + withSession(constraintOrBookmark: string): D1DatabaseSession; +}; + +type D1DatabaseSession = Pick<D1Database, "prepare" | "batch"> & { + getBookmark(): string; +}; + +export interface Env { + DB01: D1DatabaseWithSessionsAPI; +} + +export default { + async fetch(request: Request, env: Env) { + const url = new URL(request.url); + + let bookmark = "first-primary"; + let q = "select 1;"; + + if (url.pathname === "/sql") { + bookmark = url.searchParams.get("bookmark"); + q = url.searchParams.get("q"); + } + + const session = env.DB01.withSession(bookmark); + // Dummy select to get the bookmark before the main query. + await session.prepare("select 1").all(); + const bookmarkBefore = session.getBookmark(); + // Now do the main query requested. + const result = await session.prepare(q).all(); + const bookmarkAfter = session.getBookmark(); + + return Response.json({ + bookmarkBefore, + bookmarkAfter, + result, + }); + }, +}; diff --git a/fixtures/d1-read-replication-app/tests/index.test.ts b/fixtures/d1-read-replication-app/tests/index.test.ts new file mode 100644 index 0000000..5c40400 --- /dev/null +++ b/fixtures/d1-read-replication-app/tests/index.test.ts @@ -0,0 +1,82 @@ +import { resolve } from "node:path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("d1-sessions-api - getBookmark", () => { + describe("with wrangler dev", () => { + let ip: string, port: number, stop: (() => Promise<unknown>) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should respond with bookmarks before and after a session query", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}`); + let parsed = await response.json(); + expect(response.status).toBe(200); + expect(parsed).toMatchObject({ + bookmarkBefore: expect.stringMatching(/\w{8}-\w{8}-\w{8}-\w{32}/), + bookmarkAfter: expect.stringMatching(/\w{8}-\w{8}-\w{8}-\w{32}/), + }); + }); + + it("should progress the bookmark after a write", async ({ expect }) => { + let response = await fetch( + `http://${ip}:${port}?q=${encodeURIComponent("create table if not exists users1(id text);")}` + ); + let parsed = (await response.json()) as { + bookmarkAfter: string; + bookmarkBefore: string; + }; + expect(response.status).toBe(200); + expect(parsed).toMatchObject({ + bookmarkBefore: expect.stringMatching(/\w{8}-\w{8}-\w{8}-\w{32}/), + bookmarkAfter: expect.stringMatching(/\w{8}-\w{8}-\w{8}-\w{32}/), + }); + expect( + parsed.bookmarkAfter > parsed.bookmarkBefore, + `before[${parsed.bookmarkBefore}] !== after[${parsed.bookmarkAfter}]` + ).toEqual(true); + }); + + it("should maintain the latest bookmark after many queries", async ({ + expect, + }) => { + let responses = []; + + for (let i = 0; i < 10; i++) { + const resp = await fetch( + `http://${ip}:${port}?q=${encodeURIComponent(`create table if not exists users${i}(id text);`)}` + ); + let parsed = (await resp.json()) as { + bookmarkAfter: string; + bookmarkBefore: string; + }; + expect(resp.status).toBe(200); + responses.push(parsed); + + expect( + parsed.bookmarkAfter > parsed.bookmarkBefore, + `before[${parsed.bookmarkBefore}] !== after[${parsed.bookmarkAfter}]` + ).toEqual(true); + } + + const lastBookmark = responses.at(-1)?.bookmarkAfter; + responses.slice(0, -1).forEach((parsed) => { + expect( + parsed.bookmarkAfter < lastBookmark!, + `previous after[${parsed.bookmarkAfter}] !< lastBookmark[${lastBookmark}]` + ).toEqual(true); + }); + }); + }); +}); diff --git a/fixtures/d1-read-replication-app/tests/tsconfig.json b/fixtures/d1-read-replication-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/d1-read-replication-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/d1-read-replication-app/tsconfig.json b/fixtures/d1-read-replication-app/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/d1-read-replication-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/d1-read-replication-app/vitest.config.mts b/fixtures/d1-read-replication-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/d1-read-replication-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/d1-read-replication-app/wrangler.jsonc b/fixtures/d1-read-replication-app/wrangler.jsonc new file mode 100644 index 0000000..40e129b --- /dev/null +++ b/fixtures/d1-read-replication-app/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "name": "d1-read-replication-app", + "main": "src/index.ts", + "compatibility_date": "2025-03-11", + "compatibility_flags": [ + "nodejs_compat", + "experimental", + "enable_d1_with_sessions_api", + ], + "d1_databases": [ + { + "binding": "DB01", // i.e. available in your Worker on env.DB01 + "database_name": "UPDATE_THIS_FOR_REMOTE_USE", + "preview_database_id": "UPDATE_THIS_FOR_REMOTE_USE", + "database_id": "UPDATE_THIS_FOR_REMOTE_USE", + }, + ], +} diff --git a/fixtures/d1-worker-app/.gitignore b/fixtures/d1-worker-app/.gitignore new file mode 100644 index 0000000..37cbd63 --- /dev/null +++ b/fixtures/d1-worker-app/.gitignore @@ -0,0 +1,2 @@ +dist +.wrangler diff --git a/fixtures/d1-worker-app/README.md b/fixtures/d1-worker-app/README.md new file mode 100644 index 0000000..692820c --- /dev/null +++ b/fixtures/d1-worker-app/README.md @@ -0,0 +1,3 @@ +# Notes about testing with this fixture + +- For some reason, miniflare requires exactly node version 16 to be able to run `wrangler dev --local` diff --git a/fixtures/d1-worker-app/package.json b/fixtures/d1-worker-app/package.json new file mode 100644 index 0000000..d1f58da --- /dev/null +++ b/fixtures/d1-worker-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "@fixture/d1-basic", + "private": true, + "scripts": { + "check:type": "tsc", + "db:query": "wrangler d1 execute UPDATE_THIS_FOR_REMOTE_USE --local --command='SELECT * FROM Customers'", + "db:query-json": "wrangler d1 execute UPDATE_THIS_FOR_REMOTE_USE --local --command='SELECT * FROM Customers' --json", + "db:reset": "wrangler d1 execute UPDATE_THIS_FOR_REMOTE_USE --local --file=./schema.sql", + "start": "wrangler dev --local", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/d1-worker-app/schema.sql b/fixtures/d1-worker-app/schema.sql new file mode 100644 index 0000000..234f5f3 --- /dev/null +++ b/fixtures/d1-worker-app/schema.sql @@ -0,0 +1,11 @@ +/**** Test comments ****/ +DROP TABLE /**** comment ****/ IF EXISTS Customers; +CREATE TABLE Customers (CustomerID INT,/**** + comment + ****/ + CompanyName TEXT, + ContactName TEXT, + PRIMARY KEY (`CustomerID`) +); +INSERT INTO Customers (CustomerID, CompanyName, ContactName) VALUES (1, 'Alfreds Futterkiste', 'Maria Anders'), (4, 'Around the Horn', 'Thomas Hardy'), (11, 'Bs Beverages', 'Victoria Ashworth'), (13, 'Bs Beverages', 'Random Name'); -- comment +-- Hi \ No newline at end of file diff --git a/fixtures/d1-worker-app/src/index.ts b/fixtures/d1-worker-app/src/index.ts new file mode 100644 index 0000000..cfe26d6 --- /dev/null +++ b/fixtures/d1-worker-app/src/index.ts @@ -0,0 +1,12 @@ +export default { + async fetch(request, env) { + return Response.json(env); + }, +}; +export class DurableObjectExample { + constructor(state, env) {} + + async fetch(request) { + return new Response("Hello World"); + } +} diff --git a/fixtures/d1-worker-app/tests/index.test.ts b/fixtures/d1-worker-app/tests/index.test.ts new file mode 100644 index 0000000..a296af6 --- /dev/null +++ b/fixtures/d1-worker-app/tests/index.test.ts @@ -0,0 +1,15 @@ +import { execSync } from "node:child_process"; +import { resolve } from "node:path"; +import { describe, it } from "vitest"; + +describe("d1", () => { + describe("d1 execute --local", () => { + it("should execute SQL file against the local db", ({ expect }) => { + const stdout = execSync("pnpm run db:reset", { + cwd: resolve(__dirname, ".."), + }).toString(); + + expect(stdout).toContain("🚣 3 commands executed successfully."); + }); + }); +}); diff --git a/fixtures/d1-worker-app/tests/tsconfig.json b/fixtures/d1-worker-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/d1-worker-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/d1-worker-app/tsconfig.json b/fixtures/d1-worker-app/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/d1-worker-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/d1-worker-app/vitest.config.mts b/fixtures/d1-worker-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/d1-worker-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/d1-worker-app/wrangler.jsonc b/fixtures/d1-worker-app/wrangler.jsonc new file mode 100644 index 0000000..4445a49 --- /dev/null +++ b/fixtures/d1-worker-app/wrangler.jsonc @@ -0,0 +1,28 @@ +{ + "name": "worker-app", + "main": "src/index.js", + "compatibility_date": "2022-03-31", + "d1_databases": [ + { + "binding": "DB", // i.e. available in your Worker on env.DB + "database_name": "UPDATE_THIS_FOR_REMOTE_USE", + "preview_database_id": "UPDATE_THIS_FOR_REMOTE_USE", + "database_id": "UPDATE_THIS_FOR_REMOTE_USE", + }, + ], + "migrations": [ + { + "tag": "v1", // Should be unique for each entry + "new_classes": ["DurableObjectExample"], // Array of new classes + }, + ], + "durable_objects": { + "bindings": [ + { + // Binding to our DurableObjectExample class + "name": "EXAMPLE_CLASS", + "class_name": "DurableObjectExample", + }, + ], + }, +} diff --git a/fixtures/dev-registry/assets/example.txt b/fixtures/dev-registry/assets/example.txt new file mode 100644 index 0000000..04a5e8c --- /dev/null +++ b/fixtures/dev-registry/assets/example.txt @@ -0,0 +1 @@ +This is an example asset file \ No newline at end of file diff --git a/fixtures/dev-registry/package.json b/fixtures/dev-registry/package.json new file mode 100644 index 0000000..c3a3b5c --- /dev/null +++ b/fixtures/dev-registry/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/dev-registry", + "private": true, + "type": "module", + "scripts": { + "check:types": "tsc --build", + "test": "vitest", + "test:ci": "vitest run", + "vite": "vite dev", + "wrangler": "wrangler dev" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "vite": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/dev-registry/tests/dev-registry.test.ts b/fixtures/dev-registry/tests/dev-registry.test.ts new file mode 100644 index 0000000..249f58a --- /dev/null +++ b/fixtures/dev-registry/tests/dev-registry.test.ts @@ -0,0 +1,1381 @@ +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { resolve } from "node:path"; +/* eslint-disable workers-sdk/no-vitest-import-expect -- uses expect in module-scope helper functions */ +import { + describe, + expect, + onTestFailed, + onTestFinished, + test, + vi, +} from "vitest"; +/* eslint-enable workers-sdk/no-vitest-import-expect */ +import { + runLongLived, + waitForReady, +} from "../../../packages/vite-plugin-cloudflare/e2e/helpers"; +import { runWranglerDev as baseRunWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +const waitForTimeout = 20_000; +const cwd = resolve(__dirname, ".."); +const tmpPathBase = path.join(os.tmpdir(), "wrangler-tests"); +const it = test.extend<{ + devRegistryPath: string; +}>({ + // Fixture for creating a temporary directory + async devRegistryPath({}, use) { + const tmpPath = await fs.realpath(await fs.mkdtemp(tmpPathBase)); + await use(tmpPath); + await fs.rm(tmpPath, { recursive: true, maxRetries: 10 }); + }, +}); + +async function runViteDev( + config: string, + devRegistryPath?: string +): Promise<string> { + const proc = await runLongLived("pnpm", `vite --config ${config}`, cwd, { + MINIFLARE_REGISTRY_PATH: devRegistryPath, + }); + const url = await waitForReady(proc); + + onTestFailed(() => { + console.log(`::group::Vite dev session (${config})`); + console.log(proc.stdout); + console.log(proc.stderr); + console.log("::endgroup::"); + }); + + // Wait for the dev session to be ready + await vi.waitFor(async () => { + const resposne = await fetch(url, { method: "HEAD" }); + expect(resposne.status).toBe(200); + }, waitForTimeout); + + return url; +} + +async function runWranglerDev( + config: string | string[], + devRegistryPath?: string +): Promise<string> { + const session = await baseRunWranglerDev( + cwd, + ["--port=0", "--inspector-port=0"].concat( + Array.isArray(config) + ? config.map((configPath) => `--config=${configPath}`) + : [`--config=${config}`] + ), + { WRANGLER_REGISTRY_PATH: devRegistryPath } + ); + + onTestFailed(() => { + console.log(`::group::Wrangler dev session (${config})`); + console.log(session.getOutput()); + console.log("::endgroup::"); + }); + onTestFinished(() => session.stop()); + + const url = `http://${session.ip}:${session.port}`; + + // Wait for the dev session to be ready + await vi.waitFor(async () => { + const resposne = await fetch(url); + expect(resposne.status).not.toBeGreaterThan(500); + }, waitForTimeout); + + return url; +} + +async function setupPlatformProxy(config: string, devRegistryPath?: string) { + vi.stubEnv("WRANGLER_REGISTRY_PATH", devRegistryPath); + + onTestFinished(() => { + vi.unstubAllEnvs(); + }); + + const wrangler = await import("wrangler"); + const proxy = await wrangler.getPlatformProxy<Record<string, any>>({ + configPath: config, + }); + + onTestFinished(() => proxy.dispose()); + + return proxy; +} + +describe("Dev Registry: wrangler dev <-> wrangler dev", () => { + it("supports service worker fetch over service binding", async ({ + devRegistryPath, + }) => { + const exportedHandler = await runWranglerDev( + "wrangler.exported-handler.jsonc", + devRegistryPath + ); + + // Test fallback service before service-worker is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "service-worker", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect({ + status: response.status, + body: await response.text(), + }).toEqual({ + status: 503, + body: `Worker "service-worker" not found. Make sure it is running locally.`, + }); + }, waitForTimeout); + + await runWranglerDev("wrangler.service-worker.jsonc", devRegistryPath); + + // Test exported-handler -> service worker + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "service-worker", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(await response.text()).toBe("Hello from service worker!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports exported handler fetch over service binding", async ({ + devRegistryPath, + }) => { + const singleWorkerWithAssets = await runWranglerDev( + "wrangler.worker-entrypoint-with-assets.jsonc", + devRegistryPath + ); + + // Test fallback before exported-handler is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch(`${singleWorkerWithAssets}?${searchParams}`); + + expect({ + status: response.status, + body: await response.text(), + }).toEqual({ + status: 503, + body: `Worker "exported-handler" not found. Make sure it is running locally.`, + }); + }, waitForTimeout); + + const multiWorkers = await runWranglerDev( + ["wrangler.exported-handler.jsonc", "wrangler.worker-entrypoint.jsonc"], + devRegistryPath + ); + + // Test multi workers -> single worker + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "fetch", + }); + const response = await fetch(`${multiWorkers}?${searchParams}`); + + expect(await response.text()).toBe("Hello from Worker Entrypoint!"); + expect(response.status).toBe(200); + + // Test fetching asset from "worker-entrypoint-with-assets" over service binding + // Exported handler has no assets, so it will hit the user worker and + // forward the request to "worker-entrypoint-with-assets" with the asset path + const assetResponse = await fetch( + `${multiWorkers}/example.txt?${searchParams}` + ); + expect(await assetResponse.text()).toBe("This is an example asset file"); + }, waitForTimeout); + + // Test single worker -> multi workers + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch(`${singleWorkerWithAssets}?${searchParams}`); + + expect(await response.text()).toEqual("Hello from exported handler!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + // Test single worker -> named entrypoint + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint", + "test-method": "fetch", + }); + const response = await fetch(`${singleWorkerWithAssets}?${searchParams}`); + + expect(await response.text()).toEqual("Hello from Named Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + // Test multi workers -> named entrypoint with assets + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint-with-assets", + "test-method": "fetch", + }); + const response = await fetch(`${multiWorkers}?${searchParams}`); + + expect(await response.text()).toEqual("Hello from Named Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports RPC over service binding", async ({ devRegistryPath }) => { + const multiWorkers = await runWranglerDev( + [ + "wrangler.worker-entrypoint.jsonc", + "wrangler.internal-durable-object.jsonc", + ], + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${multiWorkers}?${searchParams}`); + + expect(response.status).toBe(500); + expect(await response.text()).toEqual( + `Worker "worker-entrypoint-with-assets" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + const singleWorkerWithAssets = await runWranglerDev( + "wrangler.worker-entrypoint-with-assets.jsonc", + devRegistryPath + ); + + // Test RPC to default entrypoint + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint", + "test-method": "rpc", + }); + const response = await fetch(`${singleWorkerWithAssets}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong"); + }, waitForTimeout); + + // Test RPC to default entrypoint with static assets + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${multiWorkers}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong"); + }, waitForTimeout); + + // Test RPC to named entrypoint + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint", + "test-method": "rpc", + }); + const response = await fetch(`${singleWorkerWithAssets}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong from Named Entrypoint"); + }, waitForTimeout); + + // Test RPC to named entrypoint with static assets + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${multiWorkers}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong from Named Entrypoint"); + }, waitForTimeout); + }); + + it("supports fetch over durable object binding", async ({ + devRegistryPath, + }) => { + const externalDurableObject = await runWranglerDev( + "wrangler.external-durable-object.jsonc", + devRegistryPath + ); + + // Test fallback before internal durable object is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "durable-object", + "test-method": "fetch", + }); + const response = await fetch(`${externalDurableObject}?${searchParams}`); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "internal-durable-object" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runWranglerDev( + [ + "wrangler.internal-durable-object.jsonc", + "wrangler.exported-handler.jsonc", + ], + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "durable-object", + "test-method": "fetch", + }); + const response = await fetch(`${externalDurableObject}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Hello from Durable Object!"); + }, waitForTimeout); + }); + + it("supports RPC over durable object binding", async ({ + devRegistryPath, + }) => { + const externalDurableObject = await runWranglerDev( + [ + "wrangler.external-durable-object.jsonc", + "wrangler.exported-handler.jsonc", + ], + devRegistryPath + ); + + // Test RPC fallback before internal durable object is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "durable-object", + "test-method": "rpc", + }); + const response = await fetch(`${externalDurableObject}?${searchParams}`); + + expect(response.status).toBe(500); + expect(await response.text()).toContain( + `Worker "internal-durable-object" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runWranglerDev( + "wrangler.internal-durable-object.jsonc", + devRegistryPath + ); + + // Test RPC after internal durable object is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "durable-object", + "test-method": "rpc", + }); + const response = await fetch(`${externalDurableObject}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong"); + }, waitForTimeout); + }); + + it("supports tail handler", async ({ devRegistryPath }) => { + const exportedHandlerWithAssets = await runWranglerDev( + "wrangler.exported-handler-with-assets.jsonc", + devRegistryPath + ); + const workerEntrypoint = await runWranglerDev( + [ + "wrangler.worker-entrypoint.jsonc", + "wrangler.internal-durable-object.jsonc", + ], + devRegistryPath + ); + + const searchParams = new URLSearchParams({ + "test-method": "tail", + }); + + await vi.waitFor(async () => { + // Trigger tail handler of worker-entrypoint via exported handler + await fetch(`${exportedHandlerWithAssets}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["hello world", "this is the 2nd log"]), + }); + await fetch(`${exportedHandlerWithAssets}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["some other log"]), + }); + + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + + expect(await response.json()).toEqual({ + worker: "Worker Entrypoint", + tailEvents: expect.arrayContaining([ + [["[exported-handler]"], ["hello world", "this is the 2nd log"]], + [["[exported-handler]"], ["some other log"]], + ]), + }); + }, waitForTimeout); + + await vi.waitFor(async () => { + // Trigger tail handler of exported-handler via worker-entrypoint + await fetch(`${workerEntrypoint}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["hello from test"]), + }); + await fetch(`${workerEntrypoint}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["yet another log", "and another one"]), + }); + const response = await fetch( + `${exportedHandlerWithAssets}?${searchParams}` + ); + + expect(await response.json()).toEqual({ + worker: "exported-handler", + tailEvents: expect.arrayContaining([ + [ + ["[worker-entrypoint]", "[Worker Entrypoint]"], + ["[worker-entrypoint]", "hello from test"], + ], + [ + ["[worker-entrypoint]", "[Worker Entrypoint]"], + ["[worker-entrypoint]", "yet another log", "and another one"], + ], + ]), + }); + }, waitForTimeout); + }); + + it("supports queues across dev sessions", async ({ devRegistryPath }) => { + const exportedHandler = await runWranglerDev( + "wrangler.exported-handler.jsonc", + devRegistryPath + ); + const sendParams = new URLSearchParams({ + "test-method": "queue-send", + }); + const receivedParams = new URLSearchParams({ + "test-method": "queue-received", + }); + + // Sending with no consumer session running mirrors the local no-consumer + // behaviour: the message is dropped, but `send()` still succeeds + await vi.waitFor(async () => { + const response = await fetch(`${exportedHandler}?${sendParams}`, { + method: "POST", + body: "dropped message", + }); + + expect(await response.text()).toBe("Queued"); + expect(response.status).toBe(200); + }, waitForTimeout); + + const workerEntrypoint = await runWranglerDev( + "wrangler.worker-entrypoint.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const sendResponse = await fetch(`${exportedHandler}?${sendParams}`, { + method: "POST", + body: "hello from producer", + }); + expect(sendResponse.status).toBe(200); + + const receivedResponse = await fetch( + `${workerEntrypoint}?${receivedParams}` + ); + const received = await receivedResponse.json(); + expect(received).toContain("hello from producer"); + // Messages sent before the consumer session started are dropped, not + // buffered + expect(received).not.toContain("dropped message"); + }, waitForTimeout); + }); +}); + +describe("Dev Registry: vite dev <-> vite dev", () => { + it("supports exported handler fetch over service binding", async ({ + devRegistryPath, + }) => { + const workerEntrypointWithAssets = await runViteDev( + "vite.worker-entrypoint-with-assets.config.ts", + devRegistryPath + ); + await runViteDev("vite.worker-entrypoint.config.ts", devRegistryPath); + + // Test fallback before exported-handler is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch( + `${workerEntrypointWithAssets}?${searchParams}` + ); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "exported-handler" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + const exportedHandler = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + + // Test exported-handler -> worker-entrypoint-with-assets + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(await response.text()).toBe("Hello from Worker Entrypoint!"); + expect(response.status).toBe(200); + + // Test fetching asset from "worker-entrypoint-with-assets" over service binding + // Exported handler has no assets, so it will hit the user worker and + // forward the request to "worker-entrypoint-with-assets" with the asset path + const assetResponse = await fetch( + `${exportedHandler}/example.txt?${searchParams}` + ); + expect(await assetResponse.text()).toBe("This is an example asset file"); + }, waitForTimeout); + + // Test worker-entrypoint-with-assets -> exported-handler + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch( + `${workerEntrypointWithAssets}?${searchParams}` + ); + + expect(await response.text()).toEqual("Hello from exported handler!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + // Test exported-handler -> named-entrypoint + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(await response.text()).toEqual("Hello from Named Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + // Test exported-handler -> named-entrypoint-with-assets + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint-with-assets", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(await response.text()).toEqual("Hello from Named Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports RPC over service binding", async ({ devRegistryPath }) => { + const exportedHandler = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + await runViteDev("vite.worker-entrypoint.config.ts", devRegistryPath); + + // Test fallback before worker-entrypoint-with-assets is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(response.status).toBe(500); + expect(await response.text()).toEqual( + `Worker "worker-entrypoint-with-assets" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runViteDev( + "vite.worker-entrypoint-with-assets.config.ts", + devRegistryPath + ); + + // Test exported-handler -> worker-entrypoint RPC + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint", + "test-method": "rpc", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong"); + }, waitForTimeout); + + // Test exported-handler -> worker-entrypoint-with-assets RPC + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong"); + }, waitForTimeout); + + // Test exported-handler -> named-entrypoint RPC + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint", + "test-method": "rpc", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong from Named Entrypoint"); + }, waitForTimeout); + + // Test exported-handler -> named-entrypoint-with-assets RPC + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Pong from Named Entrypoint"); + }, waitForTimeout); + }); + + it("supports WebSocket upgrade over service binding", async ({ + devRegistryPath, + }) => { + const exportedHandler = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + await runViteDev("vite.worker-entrypoint.config.ts", devRegistryPath); + + // Test exported-handler -> worker-entrypoint WebSocket proxy + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint", + "test-method": "websocket-proxy", + }); + const wsUrl = `${exportedHandler.replace("http", "ws")}?${searchParams}`; + const ws = new WebSocket(wsUrl); + + const message = await new Promise<string>((resolve, reject) => { + ws.addEventListener("open", () => ws.send("hello")); + ws.addEventListener("message", (event) => { + resolve(String(event.data)); + ws.close(); + }); + ws.addEventListener("error", () => + reject(new Error("WebSocket connection failed")) + ); + }); + + expect(message).toBe("echo:hello"); + }, waitForTimeout); + }); + + it("supports tail handler", async ({ devRegistryPath }) => { + const exportedHandler = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + const workerEntrypointWithAssets = await runViteDev( + "vite.worker-entrypoint-with-assets.config.ts", + devRegistryPath + ); + + const searchParams = new URLSearchParams({ + "test-method": "tail", + }); + + await vi.waitFor(async () => { + // Trigger tail handler of worker-entrypoint via exported-handler + await fetch(`${exportedHandler}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["hello world", "this is the 2nd log"]), + }); + await fetch(`${exportedHandler}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["some other log"]), + }); + + const response = await fetch( + `${workerEntrypointWithAssets}?${searchParams}` + ); + + expect(await response.json()).toEqual({ + worker: "Worker Entrypoint", + tailEvents: expect.arrayContaining([ + [["[exported-handler]"], ["hello world", "this is the 2nd log"]], + [["[exported-handler]"], ["some other log"]], + ]), + }); + }, waitForTimeout); + + await vi.waitFor(async () => { + // Trigger tail handler of exported-handler via worker-entrypoint + await fetch(`${workerEntrypointWithAssets}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["hello from test"]), + }); + await fetch(`${workerEntrypointWithAssets}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["yet another log", "and another one"]), + }); + + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(await response.json()).toEqual({ + worker: "exported-handler", + tailEvents: expect.arrayContaining([ + [["[Worker Entrypoint]"], ["hello from test"]], + [["[Worker Entrypoint]"], ["yet another log", "and another one"]], + ]), + }); + }, waitForTimeout); + }); + + it("supports queues across dev sessions", async ({ devRegistryPath }) => { + const exportedHandler = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + const workerEntrypoint = await runViteDev( + "vite.worker-entrypoint.config.ts", + devRegistryPath + ); + + await vi.waitFor(async () => { + const sendParams = new URLSearchParams({ + "test-method": "queue-send", + }); + const sendResponse = await fetch(`${exportedHandler}?${sendParams}`, { + method: "POST", + body: "hello from vite producer", + }); + expect(await sendResponse.text()).toBe("Queued"); + expect(sendResponse.status).toBe(200); + + const receivedParams = new URLSearchParams({ + "test-method": "queue-received", + }); + const receivedResponse = await fetch( + `${workerEntrypoint}?${receivedParams}` + ); + expect(await receivedResponse.json()).toContain( + "hello from vite producer" + ); + }, waitForTimeout); + }); +}); + +describe("Dev Registry: vite dev <-> wrangler dev", () => { + it("uses the same dev registry path by default", async () => { + const workerEntrypoint = await runViteDev( + "vite.worker-entrypoint.config.ts" + ); + const exportedHandler = await runWranglerDev( + "wrangler.exported-handler.jsonc" + ); + + // Test wrangler dev -> vite dev yet + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + expect(await response.text()).toBe("Hello from Worker Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + // Test vite dev -> wrangler dev + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + expect(await response.text()).toBe("Hello from exported handler!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports exported handler fetch over service binding", async ({ + devRegistryPath, + }) => { + const workerEntrypoint = await runViteDev( + "vite.worker-entrypoint.config.ts", + devRegistryPath + ); + + // Test fallback service before exported-handler is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + + expect(await response.text()).toEqual( + `Worker "exported-handler" not found. Make sure it is running locally.` + ); + expect(response.status).toBe(503); + }, waitForTimeout); + + const exportedHandler = await runWranglerDev( + "wrangler.exported-handler.jsonc", + devRegistryPath + ); + + // Test wrangler dev -> vite dev yet + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + expect(await response.text()).toBe("Hello from Worker Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + // Test vite dev -> wrangler dev + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "exported-handler", + "test-method": "fetch", + }); + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + expect(await response.text()).toBe("Hello from exported handler!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports service worker fetch over service binding", async ({ + devRegistryPath, + }) => { + const viteDevURL = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + + // Test fallback before exported-handler is started + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "service-worker", + "test-method": "fetch", + }); + const response = await fetch(`${viteDevURL}?${searchParams}`); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "service-worker" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runWranglerDev( + ["wrangler.service-worker.jsonc", "wrangler.worker-entrypoint.jsonc"], + devRegistryPath + ); + + // Test vite dev -> wrangler dev + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "service-worker", + "test-method": "fetch", + }); + const response = await fetch(`${viteDevURL}?${searchParams}`); + expect(await response.text()).toEqual("Hello from service worker!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports RPC over service binding", async ({ devRegistryPath }) => { + const workerEntrypoint = await runViteDev( + "vite.worker-entrypoint.config.ts", + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + expect(response.status).toBe(500); + expect(await response.text()).toEqual( + `Worker "worker-entrypoint-with-assets" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + const workerEntrypointWithAssets = await runWranglerDev( + "wrangler.worker-entrypoint-with-assets.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint", + "test-method": "rpc", + }); + const response = await fetch( + `${workerEntrypointWithAssets}?${searchParams}` + ); + expect(await response.text()).toEqual("Pong"); + expect(response.status).toBe(200); + }, waitForTimeout); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "worker-entrypoint-with-assets", + "test-method": "rpc", + }); + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + expect(await response.text()).toEqual("Pong"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports tail handler", async ({ devRegistryPath }) => { + const exportedHandlerWithStaticAssets = await runViteDev( + "vite.exported-handler-with-assets.config.ts", + devRegistryPath + ); + const workerEntrypoint = await runWranglerDev( + "wrangler.worker-entrypoint.jsonc", + devRegistryPath + ); + + const searchParams = new URLSearchParams({ + "test-method": "tail", + }); + + await vi.waitFor(async () => { + // Trigger tail handler of worker-entrypoint via exported-handler + await fetch(`${exportedHandlerWithStaticAssets}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["hello world", "this is the 2nd log"]), + }); + await fetch(`${exportedHandlerWithStaticAssets}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["some other log"]), + }); + + const response = await fetch(`${workerEntrypoint}?${searchParams}`); + + expect(await response.json()).toEqual({ + worker: "Worker Entrypoint", + tailEvents: expect.arrayContaining([ + [["[exported-handler]"], ["hello world", "this is the 2nd log"]], + [["[exported-handler]"], ["some other log"]], + ]), + }); + }, waitForTimeout); + + await vi.waitFor(async () => { + // Trigger tail handler of exported-handler via worker-entrypoint + await fetch(`${workerEntrypoint}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["hello from test"]), + }); + await fetch(`${workerEntrypoint}?${searchParams}`, { + method: "POST", + body: JSON.stringify(["yet another log", "and another one"]), + }); + + const response = await fetch( + `${exportedHandlerWithStaticAssets}?${searchParams}` + ); + + expect(await response.json()).toEqual({ + worker: "exported-handler", + tailEvents: expect.arrayContaining([ + [["[Worker Entrypoint]"], ["hello from test"]], + [["[Worker Entrypoint]"], ["yet another log", "and another one"]], + ]), + }); + }, waitForTimeout); + }); + + it("supports queues across dev sessions", async ({ devRegistryPath }) => { + const exportedHandler = await runViteDev( + "vite.exported-handler.config.ts", + devRegistryPath + ); + const workerEntrypoint = await runWranglerDev( + "wrangler.worker-entrypoint.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const sendParams = new URLSearchParams({ + "test-method": "queue-send", + }); + const sendResponse = await fetch(`${exportedHandler}?${sendParams}`, { + method: "POST", + body: "hello from vite to wrangler", + }); + expect(await sendResponse.text()).toBe("Queued"); + expect(sendResponse.status).toBe(200); + + const receivedParams = new URLSearchParams({ + "test-method": "queue-received", + }); + const receivedResponse = await fetch( + `${workerEntrypoint}?${receivedParams}` + ); + expect(await receivedResponse.json()).toContain( + "hello from vite to wrangler" + ); + }, waitForTimeout); + }); +}); + +describe("Dev Registry: getPlatformProxy -> wrangler / vite dev", () => { + it("supports fetch over service binding", async ({ devRegistryPath }) => { + const { env } = await setupPlatformProxy( + "wrangler.worker-entrypoint.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const response = + await env.WORKER_ENTRYPOINT_WITH_ASSETS.fetch("http://localhost"); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "worker-entrypoint-with-assets" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await vi.waitFor(async () => { + const response = await env.EXPORTED_HANDLER.fetch("http://localhost"); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "exported-handler" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runViteDev( + "vite.worker-entrypoint-with-assets.config.ts", + devRegistryPath + ); + + await vi.waitFor(async () => { + const response = + await env.WORKER_ENTRYPOINT_WITH_ASSETS.fetch("http://localhost"); + + expect(await response.text()).toEqual("Hello from Worker Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + await vi.waitFor(async () => { + const response = await env.EXPORTED_HANDLER.fetch("http://localhost"); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "exported-handler" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runWranglerDev("wrangler.exported-handler.jsonc", devRegistryPath); + + await vi.waitFor(async () => { + const response = await env.EXPORTED_HANDLER.fetch("http://localhost"); + + expect(await response.text()).toEqual("Hello from exported handler!"); + expect(response.status).toBe(200); + }, waitForTimeout); + + await vi.waitFor(async () => { + const response = + await env.WORKER_ENTRYPOINT_WITH_ASSETS.fetch("http://localhost"); + + expect(await response.text()).toEqual("Hello from Worker Entrypoint!"); + expect(response.status).toBe(200); + }, waitForTimeout); + }); + + it("supports RPC over service binding", async ({ devRegistryPath }) => { + const { env } = await setupPlatformProxy( + "wrangler.exported-handler.jsonc", + devRegistryPath + ); + + expect(() => + env.WORKER_ENTRYPOINT.ping() + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Worker "worker-entrypoint" not found. Make sure it is running locally.]` + ); + + expect(() => + env.WORKER_ENTRYPOINT_WITH_ASSETS.ping() + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Worker "worker-entrypoint-with-assets" not found. Make sure it is running locally.]` + ); + + await runViteDev("vite.worker-entrypoint.config.ts", devRegistryPath); + + await vi.waitFor(async () => { + const result = await env.WORKER_ENTRYPOINT.ping(); + expect(result).toBe("Pong"); + }, waitForTimeout); + + await runWranglerDev( + [ + "wrangler.worker-entrypoint-with-assets.jsonc", + "wrangler.internal-durable-object.jsonc", + ], + devRegistryPath + ); + + await vi.waitFor(async () => { + const result = await env.WORKER_ENTRYPOINT_WITH_ASSETS.ping(); + + expect(result).toBe("Pong"); + }, waitForTimeout); + }); + + it("supports fetch over durable object binding", async ({ + devRegistryPath, + }) => { + const { env } = await setupPlatformProxy( + "wrangler.external-durable-object.jsonc", + devRegistryPath + ); + const id = env.DURABLE_OBJECT.newUniqueId(); + const stub = env.DURABLE_OBJECT.get(id); + + await vi.waitFor(async () => { + const response = await stub.fetch("http://localhost"); + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "internal-durable-object" not found. Make sure it is running locally.` + ); + }, waitForTimeout); + + await runWranglerDev( + "wrangler.internal-durable-object.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const response = await stub.fetch("http://localhost"); + + expect(response.status).toBe(200); + expect(await response.text()).toEqual("Hello from Durable Object!"); + }, waitForTimeout); + }); + + it("supports RPC over durable object binding", async ({ + devRegistryPath, + }) => { + const { env } = await setupPlatformProxy( + "wrangler.external-durable-object.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const id = env.DURABLE_OBJECT.newUniqueId(); + const stub = env.DURABLE_OBJECT.get(id); + + await expect(stub.ping()).rejects.toThrow(); + }, waitForTimeout); + + await runWranglerDev( + "wrangler.internal-durable-object.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const id = env.DURABLE_OBJECT.newUniqueId(); + const stub = env.DURABLE_OBJECT.get(id); + + const result = await stub.ping(); + expect(result).toEqual("Pong"); + }, waitForTimeout); + }); +}); + +describe("Dev Registry: error handling", () => { + it("returns an error when fetching a non-existent DO class on a running worker", async ({ + devRegistryPath, + }) => { + await runWranglerDev( + "wrangler.internal-durable-object.jsonc", + devRegistryPath + ); + const nonexistentDO = await runWranglerDev( + "wrangler.nonexistent-durable-object.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "durable-object", + "test-method": "fetch", + }); + const response = await fetch(`${nonexistentDO}?${searchParams}`); + const body = await response.text(); + + expect(response.status).toBe(500); + expect(body).toContain( + `Worker does not export a Durable Object class named "NonExistentObject"` + ); + }, waitForTimeout); + }); + + it("returns an error when calling RPC on a non-existent DO class on a running worker", async ({ + devRegistryPath, + }) => { + await runWranglerDev( + "wrangler.internal-durable-object.jsonc", + devRegistryPath + ); + const nonexistentDO = await runWranglerDev( + "wrangler.nonexistent-durable-object.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "durable-object", + "test-method": "rpc", + }); + const response = await fetch(`${nonexistentDO}?${searchParams}`); + + expect(response.status).toBe(500); + expect(await response.text()).toContain( + `Worker does not export a Durable Object class named "NonExistentObject"` + ); + }, waitForTimeout); + }); + + it("returns an error when fetching a non-existent entrypoint on a running worker", async ({ + devRegistryPath, + }) => { + await runWranglerDev("wrangler.worker-entrypoint.jsonc", devRegistryPath); + const nonexistentEntrypoint = await runWranglerDev( + "wrangler.nonexistent-entrypoint.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint", + "test-method": "fetch", + }); + const response = await fetch(`${nonexistentEntrypoint}?${searchParams}`); + const body = await response.text(); + + expect(response.status).toBe(500); + expect(body).toContain( + `Worker does not export an entrypoint named "NonExistentEntrypoint"` + ); + }, waitForTimeout); + }); + + it("returns an error when calling RPC on a non-existent entrypoint on a running worker", async ({ + devRegistryPath, + }) => { + await runWranglerDev("wrangler.worker-entrypoint.jsonc", devRegistryPath); + const nonexistentEntrypoint = await runWranglerDev( + "wrangler.nonexistent-entrypoint.jsonc", + devRegistryPath + ); + + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "named-entrypoint", + "test-method": "rpc", + }); + const response = await fetch(`${nonexistentEntrypoint}?${searchParams}`); + + expect(response.status).toBe(500); + expect(await response.text()).toContain( + `Worker does not export an entrypoint named "NonExistentEntrypoint"` + ); + }, waitForTimeout); + }); + + it("gracefully handles a stale registry entry from a prior wrangler version", async ({ + devRegistryPath, + }) => { + // Start a worker that has a service binding to "service-worker" + const exportedHandler = await runWranglerDev( + "wrangler.exported-handler.jsonc", + devRegistryPath + ); + + // Write an old-format registry entry (pre-native-registry) under the + // "service-worker" name. The old format used protocol/host/port/ + // entrypointAddresses instead of debugPortAddress/defaultEntrypointService. + await fs.mkdir(devRegistryPath, { recursive: true }); + await fs.writeFile( + path.join(devRegistryPath, "service-worker"), + JSON.stringify({ + protocol: "http", + host: "127.0.0.1", + port: 8787, + entrypointAddresses: { + default: { host: "127.0.0.1", port: 8787 }, + }, + durableObjects: [], + }) + ); + + // The old-format entry has no debugPortAddress, so the proxy should + // return a 503 with the incompatible version message rather than crashing. + // We wait specifically for the incompatible message (not the generic "not + // found" message) to ensure the file watcher has picked up the entry. + await vi.waitFor(async () => { + const searchParams = new URLSearchParams({ + "test-service": "service-worker", + "test-method": "fetch", + }); + const response = await fetch(`${exportedHandler}?${searchParams}`); + + expect(response.status).toBe(503); + expect(await response.text()).toEqual( + `Worker "service-worker" is not compatible with this version of the dev server. Please update all Worker instances to the same version.` + ); + }, waitForTimeout); + }); +}); diff --git a/fixtures/dev-registry/tests/tsconfig.json b/fixtures/dev-registry/tests/tsconfig.json new file mode 100644 index 0000000..8402dd8 --- /dev/null +++ b/fixtures/dev-registry/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/dev-registry/tsconfig.json b/fixtures/dev-registry/tsconfig.json new file mode 100644 index 0000000..b52af70 --- /dev/null +++ b/fixtures/dev-registry/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.worker.json" } + ] +} diff --git a/fixtures/dev-registry/tsconfig.node.json b/fixtures/dev-registry/tsconfig.node.json new file mode 100644 index 0000000..81e5be5 --- /dev/null +++ b/fixtures/dev-registry/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/base.json"], + "include": [ + "vite.exported-handler.config.ts", + "vite.exported-handler-with-assets.config.ts", + "vite.worker-entrypoint.config.ts", + "vite.worker-entrypoint-with-assets.config.ts", + "__tests__" + ] +} diff --git a/fixtures/dev-registry/tsconfig.worker.json b/fixtures/dev-registry/tsconfig.worker.json new file mode 100644 index 0000000..1b8a70d --- /dev/null +++ b/fixtures/dev-registry/tsconfig.worker.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental"] + }, + "include": ["workers"] +} diff --git a/fixtures/dev-registry/vite.exported-handler-with-assets.config.ts b/fixtures/dev-registry/vite.exported-handler-with-assets.config.ts new file mode 100644 index 0000000..664ca5e --- /dev/null +++ b/fixtures/dev-registry/vite.exported-handler-with-assets.config.ts @@ -0,0 +1,15 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + // Override the default public directory to use the assets directory + // so that other non assets vite projects won't share the same assets + publicDir: "./assets", + plugins: [ + cloudflare({ + configPath: "./wrangler.exported-handler-with-assets.jsonc", + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/fixtures/dev-registry/vite.exported-handler.config.ts b/fixtures/dev-registry/vite.exported-handler.config.ts new file mode 100644 index 0000000..a06dbf3 --- /dev/null +++ b/fixtures/dev-registry/vite.exported-handler.config.ts @@ -0,0 +1,12 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ + configPath: "./wrangler.exported-handler.jsonc", + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/fixtures/dev-registry/vite.worker-entrypoint-with-assets.config.ts b/fixtures/dev-registry/vite.worker-entrypoint-with-assets.config.ts new file mode 100644 index 0000000..ebea078 --- /dev/null +++ b/fixtures/dev-registry/vite.worker-entrypoint-with-assets.config.ts @@ -0,0 +1,20 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + // Override the default public directory to use the assets directory + // so that other vite projects won't share the same assets + publicDir: "./assets", + plugins: [ + cloudflare({ + configPath: "./wrangler.worker-entrypoint-with-assets.jsonc", + inspectorPort: false, + persistState: false, + auxiliaryWorkers: [ + { + configPath: "./wrangler.internal-durable-object.jsonc", + }, + ], + }), + ], +}); diff --git a/fixtures/dev-registry/vite.worker-entrypoint.config.ts b/fixtures/dev-registry/vite.worker-entrypoint.config.ts new file mode 100644 index 0000000..0e93190 --- /dev/null +++ b/fixtures/dev-registry/vite.worker-entrypoint.config.ts @@ -0,0 +1,17 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ + configPath: "./wrangler.worker-entrypoint.jsonc", + inspectorPort: false, + persistState: false, + auxiliaryWorkers: [ + { + configPath: "./wrangler.internal-durable-object.jsonc", + }, + ], + }), + ], +}); diff --git a/fixtures/dev-registry/vitest.config.mts b/fixtures/dev-registry/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/dev-registry/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/dev-registry/workers/durable-object.ts b/fixtures/dev-registry/workers/durable-object.ts new file mode 100644 index 0000000..94e9071 --- /dev/null +++ b/fixtures/dev-registry/workers/durable-object.ts @@ -0,0 +1,17 @@ +import { DurableObject } from "cloudflare:workers"; + +export class TestObject extends DurableObject { + ping() { + return "Pong"; + } + + fetch(request: Request) { + return new Response("Hello from Durable Object!"); + } +} + +export default { + fetch() { + return new Response(`I'm a teapot`, { status: 418 }); + }, +}; diff --git a/fixtures/dev-registry/workers/exported-handler.ts b/fixtures/dev-registry/workers/exported-handler.ts new file mode 100644 index 0000000..a9d2600 --- /dev/null +++ b/fixtures/dev-registry/workers/exported-handler.ts @@ -0,0 +1,126 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +let tailEvents = []; + +export class NamedEntrypoint extends WorkerEntrypoint { + ping() { + return "Pong from Named Entrypoint"; + } + + fetch() { + return new Response("Hello from Named Entrypoint!"); + } +} + +export default { + async fetch(request, env) { + try { + const url = new URL(request.url); + const testMethod = url.searchParams.get("test-method"); + const testService = url.searchParams.get("test-service"); + + // Remove the test-method search params to avoid recursion + url.searchParams.delete("test-method"); + url.searchParams.delete("test-service"); + + let service: Fetcher | undefined; + + switch (testService) { + case "service-worker": { + service = env.SERVICE_WORKER; + break; + } + case "worker-entrypoint": { + service = env.WORKER_ENTRYPOINT; + break; + } + case "worker-entrypoint-with-assets": { + service = env.WORKER_ENTRYPOINT_WITH_ASSETS; + break; + } + case "named-entrypoint": { + service = env.NAMED_ENTRYPOINT; + break; + } + case "named-entrypoint-with-assets": { + service = env.NAMED_ENTRYPOINT_WITH_ASSETS; + break; + } + } + + if (service && testMethod === "rpc") { + // @ts-expect-error + const result = await service.ping(); + return new Response(result); + } + + if (service && testMethod === "fetch") { + return await service.fetch(url); + } + + if (service && testMethod === "websocket-proxy") { + return await service.fetch(new Request(url, request)); + } + + // Handle WebSocket upgrades forwarded via service bindings + if (request.headers.get("Upgrade")) { + const { 0: client, 1: server } = new WebSocketPair(); + server.accept(); + server.addEventListener("message", (event) => { + server.send(`echo:${event.data}`); + }); + return new Response(null, { status: 101, webSocket: client }); + } + + if (testMethod === "queue-send") { + await env.QUEUE.send(await request.text()); + return new Response("Queued"); + } + + if (testMethod === "tail") { + if (request.method === "POST") { + const logs = await request.json(); + if (Array.isArray(logs)) { + console.log(`[exported-handler]`); + console.log(...logs); + } + return new Response("ok"); + } + + try { + return Response.json({ + worker: "exported-handler", + tailEvents, + }); + } finally { + // Clear the tail events after sending them + tailEvents = []; + } + } + + return new Response("Hello from exported handler!"); + } catch (e) { + return new Response(e.message, { status: 500 }); + } + }, + tail(events) { + const logs = []; + + for (const event of events) { + if (Array.isArray(event.logs) && event.logs.length > 0) { + logs.push(...event.logs.map((log) => log.message)); + } + } + + if (logs.length > 0) { + tailEvents.push(logs); + } + }, +} satisfies ExportedHandler<{ + SERVICE_WORKER: Fetcher; + WORKER_ENTRYPOINT: Fetcher; + WORKER_ENTRYPOINT_WITH_ASSETS: Fetcher; + NAMED_ENTRYPOINT: Fetcher; + NAMED_ENTRYPOINT_WITH_ASSETS: Fetcher; + QUEUE: Queue; +}>; diff --git a/fixtures/dev-registry/workers/service-worker.ts b/fixtures/dev-registry/workers/service-worker.ts new file mode 100644 index 0000000..3ed1cf4 --- /dev/null +++ b/fixtures/dev-registry/workers/service-worker.ts @@ -0,0 +1,3 @@ +addEventListener("fetch", (event) => { + event.respondWith(new Response("Hello from service worker!")); +}); diff --git a/fixtures/dev-registry/workers/worker-entrypoint.ts b/fixtures/dev-registry/workers/worker-entrypoint.ts new file mode 100644 index 0000000..60ad050 --- /dev/null +++ b/fixtures/dev-registry/workers/worker-entrypoint.ts @@ -0,0 +1,148 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +let tailEvents = []; +let queueMessages = []; + +export class NamedEntrypoint extends WorkerEntrypoint { + ping() { + return "Pong from Named Entrypoint"; + } + + fetch() { + return new Response("Hello from Named Entrypoint!"); + } +} + +export default class Worker extends WorkerEntrypoint<{ + SERVICE_WORKER: Fetcher; + EXPORTED_HANDLER: Fetcher; + WORKER_ENTRYPOINT: Fetcher; + WORKER_ENTRYPOINT_WITH_ASSETS: Fetcher; + NAMED_ENTRYPOINT: Fetcher; + NAMED_ENTRYPOINT_WITH_ASSETS: Fetcher; + DURABLE_OBJECT: DurableObjectNamespace; +}> { + ping() { + return "Pong"; + } + + async fetch(request) { + try { + const url = new URL(request.url); + const testMethod = url.searchParams.get("test-method"); + const testService = url.searchParams.get("test-service"); + + // Remove the test-method search params to avoid recursion + url.searchParams.delete("test-method"); + url.searchParams.delete("test-service"); + + let service: Fetcher | undefined; + + switch (testService) { + case "service-worker": { + service = this.env.SERVICE_WORKER; + break; + } + case "exported-handler": { + service = this.env.EXPORTED_HANDLER; + break; + } + case "worker-entrypoint": { + service = this.env.WORKER_ENTRYPOINT; + break; + } + case "worker-entrypoint-with-assets": { + service = this.env.WORKER_ENTRYPOINT_WITH_ASSETS; + break; + } + case "named-entrypoint": { + service = this.env.NAMED_ENTRYPOINT; + break; + } + case "named-entrypoint-with-assets": { + service = this.env.NAMED_ENTRYPOINT_WITH_ASSETS; + break; + } + case "durable-object": { + const id = this.env.DURABLE_OBJECT.newUniqueId(); + const stub = this.env.DURABLE_OBJECT.get(id); + service = stub; + break; + } + } + + if (service && testMethod === "rpc") { + // @ts-expect-error + const result = await service.ping(); + return new Response(result); + } + + if (service && testMethod === "fetch") { + return await service.fetch(url, request); + } + + if (service && testMethod === "websocket-proxy") { + return await service.fetch(new Request(url, request)); + } + + // Handle WebSocket upgrades forwarded via service bindings + if (request.headers.get("Upgrade")) { + const { 0: client, 1: server } = new WebSocketPair(); + server.accept(); + server.addEventListener("message", (event) => { + server.send(`echo:${event.data}`); + }); + return new Response(null, { status: 101, webSocket: client }); + } + + if (testMethod === "queue-received") { + return Response.json(queueMessages); + } + + if (testMethod === "tail") { + if (request.method === "POST") { + const logs = await request.json(); + if (Array.isArray(logs)) { + console.log(`[Worker Entrypoint]`); + console.log(...logs); + } + return new Response("ok"); + } + + try { + return Response.json({ + worker: "Worker Entrypoint", + tailEvents, + }); + } finally { + // Clear the tail events after sending them + tailEvents = []; + } + } + + return new Response("Hello from Worker Entrypoint!"); + } catch (e) { + return new Response(e.message, { status: 500 }); + } + } + + tail(events) { + const logs = []; + + for (const event of events) { + if (Array.isArray(event.logs) && event.logs.length > 0) { + logs.push(...event.logs.map((log) => log.message)); + } + } + + if (logs.length > 0) { + tailEvents.push(logs); + } + } + + queue(batch) { + for (const message of batch.messages) { + queueMessages.push(message.body); + } + } +} diff --git a/fixtures/dev-registry/wrangler.exported-handler-with-assets.jsonc b/fixtures/dev-registry/wrangler.exported-handler-with-assets.jsonc new file mode 100644 index 0000000..322ca02 --- /dev/null +++ b/fixtures/dev-registry/wrangler.exported-handler-with-assets.jsonc @@ -0,0 +1,42 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "exported-handler-with-assets", + "main": "./workers/exported-handler.ts", + "compatibility_date": "2025-05-01", + "assets": { + "directory": "./assets", + }, + "services": [ + { + "binding": "SERVICE_WORKER", + "service": "service-worker", + }, + { + "binding": "EXPORTED_HANDLER", + "service": "exported-worker", + }, + { + "binding": "WORKER_ENTRYPOINT", + "service": "worker-entrypoint", + }, + { + "binding": "WORKER_ENTRYPOINT_WITH_ASSETS", + "service": "worker-entrypoint-with-assets", + }, + { + "binding": "NAMED_ENTRYPOINT", + "service": "worker-entrypoint", + "entrypoint": "NamedEntrypoint", + }, + { + "binding": "NAMED_ENTRYPOINT_WITH_ASSETS", + "service": "worker-entrypoint-with-assets", + "entrypoint": "NamedEntrypoint", + }, + ], + "tail_consumers": [ + { + "service": "worker-entrypoint", + }, + ], +} diff --git a/fixtures/dev-registry/wrangler.exported-handler.jsonc b/fixtures/dev-registry/wrangler.exported-handler.jsonc new file mode 100644 index 0000000..f8af467 --- /dev/null +++ b/fixtures/dev-registry/wrangler.exported-handler.jsonc @@ -0,0 +1,43 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "exported-handler", + "main": "./workers/exported-handler.ts", + "compatibility_date": "2025-05-01", + "services": [ + { + "binding": "SERVICE_WORKER", + "service": "service-worker", + }, + { + "binding": "WORKER_ENTRYPOINT", + "service": "worker-entrypoint", + }, + { + "binding": "WORKER_ENTRYPOINT_WITH_ASSETS", + "service": "worker-entrypoint-with-assets", + }, + { + "binding": "NAMED_ENTRYPOINT", + "service": "worker-entrypoint", + "entrypoint": "NamedEntrypoint", + }, + { + "binding": "NAMED_ENTRYPOINT_WITH_ASSETS", + "service": "worker-entrypoint-with-assets", + "entrypoint": "NamedEntrypoint", + }, + ], + "tail_consumers": [ + { + "service": "worker-entrypoint-with-assets", + }, + ], + "queues": { + "producers": [ + { + "binding": "QUEUE", + "queue": "test-queue", + }, + ], + }, +} diff --git a/fixtures/dev-registry/wrangler.external-durable-object.jsonc b/fixtures/dev-registry/wrangler.external-durable-object.jsonc new file mode 100644 index 0000000..c74e28c --- /dev/null +++ b/fixtures/dev-registry/wrangler.external-durable-object.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "external-durable-object", + "main": "./workers/worker-entrypoint.ts", + "compatibility_date": "2025-05-01", + "durable_objects": { + "bindings": [ + { + "name": "DURABLE_OBJECT", + "class_name": "TestObject", + "script_name": "internal-durable-object", + }, + ], + }, + "tail_consumers": [ + { + "service": "exported-handler", + }, + ], +} diff --git a/fixtures/dev-registry/wrangler.internal-durable-object.jsonc b/fixtures/dev-registry/wrangler.internal-durable-object.jsonc new file mode 100644 index 0000000..4bfc327 --- /dev/null +++ b/fixtures/dev-registry/wrangler.internal-durable-object.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "internal-durable-object", + "main": "./workers/durable-object.ts", + "compatibility_date": "2025-05-01", + "durable_objects": { + "bindings": [ + { + "name": "DURABLE_OBJECT", + "class_name": "TestObject", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["TestObject"], + }, + ], +} diff --git a/fixtures/dev-registry/wrangler.nonexistent-durable-object.jsonc b/fixtures/dev-registry/wrangler.nonexistent-durable-object.jsonc new file mode 100644 index 0000000..1bb3125 --- /dev/null +++ b/fixtures/dev-registry/wrangler.nonexistent-durable-object.jsonc @@ -0,0 +1,15 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "nonexistent-durable-object", + "main": "./workers/worker-entrypoint.ts", + "compatibility_date": "2025-05-01", + "durable_objects": { + "bindings": [ + { + "name": "DURABLE_OBJECT", + "class_name": "NonExistentObject", + "script_name": "internal-durable-object", + }, + ], + }, +} diff --git a/fixtures/dev-registry/wrangler.nonexistent-entrypoint.jsonc b/fixtures/dev-registry/wrangler.nonexistent-entrypoint.jsonc new file mode 100644 index 0000000..d566321 --- /dev/null +++ b/fixtures/dev-registry/wrangler.nonexistent-entrypoint.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "nonexistent-entrypoint", + "main": "./workers/worker-entrypoint.ts", + "compatibility_date": "2025-05-01", + "services": [ + { + "binding": "NAMED_ENTRYPOINT", + "service": "worker-entrypoint", + "entrypoint": "NonExistentEntrypoint", + }, + ], +} diff --git a/fixtures/dev-registry/wrangler.service-worker.jsonc b/fixtures/dev-registry/wrangler.service-worker.jsonc new file mode 100644 index 0000000..f20ea63 --- /dev/null +++ b/fixtures/dev-registry/wrangler.service-worker.jsonc @@ -0,0 +1,6 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "service-worker", + "main": "./workers/service-worker.ts", + "compatibility_date": "2025-05-01", +} diff --git a/fixtures/dev-registry/wrangler.worker-entrypoint-with-assets.jsonc b/fixtures/dev-registry/wrangler.worker-entrypoint-with-assets.jsonc new file mode 100644 index 0000000..77b873a --- /dev/null +++ b/fixtures/dev-registry/wrangler.worker-entrypoint-with-assets.jsonc @@ -0,0 +1,37 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "worker-entrypoint-with-assets", + "main": "./workers/worker-entrypoint.ts", + "compatibility_date": "2025-05-01", + "assets": { + "directory": "./assets", + }, + "services": [ + { + "binding": "SERVICE_WORKER", + "service": "service-worker", + }, + { + "binding": "EXPORTED_HANDLER", + "service": "exported-handler", + }, + { + "binding": "EXPORTED_HANDLER_WITH_ASSETS", + "service": "exported-handler-with-assets", + }, + { + "binding": "WORKER_ENTRYPOINT", + "service": "worker-entrypoint", + }, + { + "binding": "NAMED_ENTRYPOINT", + "service": "worker-entrypoint", + "entrypoint": "NamedEntrypoint", + }, + ], + "tail_consumers": [ + { + "service": "exported-handler", + }, + ], +} diff --git a/fixtures/dev-registry/wrangler.worker-entrypoint.jsonc b/fixtures/dev-registry/wrangler.worker-entrypoint.jsonc new file mode 100644 index 0000000..61ddb2e --- /dev/null +++ b/fixtures/dev-registry/wrangler.worker-entrypoint.jsonc @@ -0,0 +1,43 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "worker-entrypoint", + "main": "./workers/worker-entrypoint.ts", + "compatibility_date": "2025-05-01", + "services": [ + { + "binding": "SERVICE_WORKER", + "service": "service-worker", + }, + { + "binding": "EXPORTED_HANDLER", + "service": "exported-handler", + }, + { + "binding": "EXPORTED_HANDLER_WITH_ASSETS", + "service": "exported-handler", + }, + { + "binding": "WORKER_ENTRYPOINT_WITH_ASSETS", + "service": "worker-entrypoint-with-assets", + }, + { + "binding": "NAMED_ENTRYPOINT_WITH_ASSETS", + "service": "worker-entrypoint-with-assets", + "entrypoint": "NamedEntrypoint", + }, + ], + "tail_consumers": [ + { + "service": "exported-handler-with-assets", + }, + ], + "queues": { + "consumers": [ + { + "queue": "test-queue", + "max_batch_size": 1, + "max_batch_timeout": 0, + }, + ], + }, +} diff --git a/fixtures/durable-objects-app/package.json b/fixtures/durable-objects-app/package.json new file mode 100644 index 0000000..27f85bd --- /dev/null +++ b/fixtures/durable-objects-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/worker-durable-objects", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "dev": "wrangler deploy --dry-run", + "test": "vitest run", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/durable-objects-app/src/index.js b/fixtures/durable-objects-app/src/index.js new file mode 100644 index 0000000..80f20d7 --- /dev/null +++ b/fixtures/durable-objects-app/src/index.js @@ -0,0 +1,5 @@ +export default { + async fetch() { + return new Response("foo"); + }, +}; diff --git a/fixtures/durable-objects-app/tests/index.test.ts b/fixtures/durable-objects-app/tests/index.test.ts new file mode 100644 index 0000000..860d8a0 --- /dev/null +++ b/fixtures/durable-objects-app/tests/index.test.ts @@ -0,0 +1,26 @@ +import { execSync } from "node:child_process"; +import { resolve } from "node:path"; +import { assert, describe, it } from "vitest"; + +describe("durable objects", () => { + it("should throw an error when the worker doesn't export a durable object but requires one", ({ + expect, + }) => { + let err: string = ""; + try { + execSync("pnpm run dev", { + cwd: resolve(__dirname, ".."), + }); + assert(false); // Should never reach this + } catch (e) { + err = (e as Error).message + .replaceAll("✘", "X") + .replace(/\\/g, "/") + .replace(/[^\S\n]+\n/g, "\n") + .trimEnd(); + } + expect(err).toContain( + "You should export these objects from your entrypoint, src/index.js." + ); + }); +}); diff --git a/fixtures/durable-objects-app/tests/tsconfig.json b/fixtures/durable-objects-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/durable-objects-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/durable-objects-app/tsconfig.json b/fixtures/durable-objects-app/tsconfig.json new file mode 100644 index 0000000..59e6da4 --- /dev/null +++ b/fixtures/durable-objects-app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/durable-objects-app/vitest.config.mts b/fixtures/durable-objects-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/durable-objects-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/durable-objects-app/wrangler.jsonc b/fixtures/durable-objects-app/wrangler.jsonc new file mode 100644 index 0000000..2df796d --- /dev/null +++ b/fixtures/durable-objects-app/wrangler.jsonc @@ -0,0 +1,19 @@ +{ + "name": "do-worker-app", + "compatibility_date": "2022-03-31", + "main": "src/index.js", + "durable_objects": { + "bindings": [ + { + "name": "MY_DO", + "class_name": "FooBar", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_classes": ["FooBar"], + }, + ], +} diff --git a/fixtures/durable-objects-exports-app/package.json b/fixtures/durable-objects-exports-app/package.json new file mode 100644 index 0000000..9ab6072 --- /dev/null +++ b/fixtures/durable-objects-exports-app/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixture/durable-objects-exports-app", + "private": true, + "description": "Fixture demonstrating the declarative Durable Object `exports` config (with `state` for tombstones)", + "license": "ISC", + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/durable-objects-exports-app/src/index.ts b/fixtures/durable-objects-exports-app/src/index.ts new file mode 100644 index 0000000..d9cd17e --- /dev/null +++ b/fixtures/durable-objects-exports-app/src/index.ts @@ -0,0 +1,93 @@ +import { DurableObject } from "cloudflare:workers"; + +interface Env { + COUNTER_A: DurableObjectNamespace<CounterA>; + COUNTER_B: DurableObjectNamespace<CounterB>; +} + +class SqlCounter extends DurableObject<Env> { + private readonly ready: Promise<void>; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ready = this.init(); + } + + private async init() { + this.ctx.storage.sql.exec( + "CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY, value INTEGER NOT NULL DEFAULT 0)" + ); + this.ctx.storage.sql.exec( + "INSERT OR IGNORE INTO counter (id, value) VALUES (1, 0)" + ); + } + + async value(): Promise<number> { + await this.ready; + const row = this.ctx.storage.sql + .exec<{ value: number }>("SELECT value FROM counter WHERE id = 1") + .one(); + return row.value; + } + + async increment(): Promise<number> { + await this.ready; + this.ctx.storage.sql.exec( + "UPDATE counter SET value = value + 1 WHERE id = 1" + ); + return this.value(); + } + + async sqliteOk(): Promise<number> { + const row = this.ctx.storage.sql + .exec<{ ok: number }>("SELECT 1 AS ok") + .one(); + return row.ok; + } +} + +export class CounterA extends SqlCounter {} +export class CounterB extends SqlCounter {} +// CounterC has no binding in `wrangler.jsonc`; it is reached via `ctx.exports`. +export class CounterC extends SqlCounter {} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise<Response> { + const url = new URL(request.url); + const [, scope, action] = url.pathname.split("/"); + + let namespace: DurableObjectNamespace<SqlCounter> | undefined; + if (scope === "a") { + namespace = env.COUNTER_A; + } else if (scope === "b") { + namespace = env.COUNTER_B; + } else if (scope === "c") { + namespace = ctx.exports.CounterC; + } + if (!namespace) { + return new Response( + "Use /a/... or /b/... or /c/... to address a counter.", + { status: 400 } + ); + } + + const instance = url.searchParams.get("instance") ?? "default"; + const stub = namespace.getByName(instance); + + switch (action) { + case "": + case undefined: + return Response.json({ value: await stub.value() }); + case "increment": + return Response.json({ value: await stub.increment() }); + case "sqlite-check": + return Response.json({ ok: await stub.sqliteOk() }); + default: + return new Response("not found", { status: 404 }); + } + }, +}; diff --git a/fixtures/durable-objects-exports-app/tests/index.test.ts b/fixtures/durable-objects-exports-app/tests/index.test.ts new file mode 100644 index 0000000..97d5330 --- /dev/null +++ b/fixtures/durable-objects-exports-app/tests/index.test.ts @@ -0,0 +1,85 @@ +import { randomUUID } from "node:crypto"; +import { join, resolve } from "node:path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { unstable_startWorker } from "wrangler"; + +const basePath = resolve(__dirname, ".."); + +describe("durable objects declared via the new `exports` config", () => { + let worker: Awaited<ReturnType<typeof unstable_startWorker>>; + const instance = randomUUID(); + + beforeAll(async () => { + worker = await unstable_startWorker({ + config: join(basePath, "wrangler.jsonc"), + }); + }); + + afterAll(async () => { + await worker?.dispose(); + }); + + it("starts CounterA at 0 and increments it via SQLite-backed storage", async ({ + expect, + }) => { + let response = await worker.fetch( + `http://example.com/a?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 0 }); + + response = await worker.fetch( + `http://example.com/a/increment?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 1 }); + + response = await worker.fetch( + `http://example.com/a/increment?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 2 }); + }); + + it("keeps CounterB independent of CounterA", async ({ expect }) => { + let response = await worker.fetch( + `http://example.com/b?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 0 }); + + response = await worker.fetch( + `http://example.com/b/increment?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 1 }); + + response = await worker.fetch(`http://example.com/a?instance=${instance}`); + expect(await response.json()).toEqual({ value: 2 }); + }); + + it("addresses unbound CounterC via `ctx.exports` (no binding required)", async ({ + expect, + }) => { + let response = await worker.fetch( + `http://example.com/c?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 0 }); + + response = await worker.fetch( + `http://example.com/c/increment?instance=${instance}` + ); + expect(await response.json()).toEqual({ value: 1 }); + + response = await worker.fetch(`http://example.com/a?instance=${instance}`); + expect(await response.json()).toEqual({ value: 2 }); + response = await worker.fetch(`http://example.com/b?instance=${instance}`); + expect(await response.json()).toEqual({ value: 1 }); + }); + + it('uses SQLite-backed storage (`storage: "sqlite"` from the `exports` map is honored in local dev)', async ({ + expect, + }) => { + for (const scope of ["a", "b", "c"]) { + const response = await worker.fetch( + `http://example.com/${scope}/sqlite-check?instance=${instance}` + ); + expect(await response.json()).toEqual({ ok: 1 }); + } + }); +}); diff --git a/fixtures/durable-objects-exports-app/tests/tsconfig.json b/fixtures/durable-objects-exports-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/durable-objects-exports-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/durable-objects-exports-app/tsconfig.json b/fixtures/durable-objects-exports-app/tsconfig.json new file mode 100644 index 0000000..1f38cb2 --- /dev/null +++ b/fixtures/durable-objects-exports-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + // `@cloudflare/workers-types/experimental` is required for the + // `ctx.exports` API surface and the `Cloudflare.GlobalProps` + // extension point that `wrangler types` populates. + "types": [ + "@cloudflare/workers-types/experimental", + "./worker-configuration.d.ts" + ] + }, + "include": ["src/**/*.ts", "worker-configuration.d.ts"] +} diff --git a/fixtures/durable-objects-exports-app/vitest.config.mts b/fixtures/durable-objects-exports-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/durable-objects-exports-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/durable-objects-exports-app/worker-configuration.d.ts b/fixtures/durable-objects-exports-app/worker-configuration.d.ts new file mode 100644 index 0000000..e1d094e --- /dev/null +++ b/fixtures/durable-objects-exports-app/worker-configuration.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: 059202c799257c021e9b01439632a694) +interface __BaseEnv_Env { + COUNTER_A: DurableObjectNamespace<import("./src/index").CounterA>; + COUNTER_B: DurableObjectNamespace<import("./src/index").CounterB>; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + durableNamespaces: "CounterA" | "CounterB" | "CounterC"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/durable-objects-exports-app/wrangler.jsonc b/fixtures/durable-objects-exports-app/wrangler.jsonc new file mode 100644 index 0000000..d5c560f --- /dev/null +++ b/fixtures/durable-objects-exports-app/wrangler.jsonc @@ -0,0 +1,23 @@ +{ + "name": "durable-objects-exports-app", + "main": "src/index.ts", + "compatibility_date": "2026-01-01", + "durable_objects": { + "bindings": [ + { "name": "COUNTER_A", "class_name": "CounterA" }, + { "name": "COUNTER_B", "class_name": "CounterB" }, + ], + }, + "exports": { + "CounterA": { "type": "durable-object", "storage": "sqlite" }, + "CounterB": { "type": "durable-object", "storage": "sqlite" }, + // CounterC is declared via `exports` only — no binding. + "CounterC": { "type": "durable-object", "storage": "sqlite" }, + "OldCounter": { "type": "durable-object", "state": "deleted" }, + "LegacyName": { + "type": "durable-object", + "state": "renamed", + "renamed_to": "CounterB", + }, + }, +} diff --git a/fixtures/dynamic-worker-loading/package.json b/fixtures/dynamic-worker-loading/package.json new file mode 100644 index 0000000..04c7d06 --- /dev/null +++ b/fixtures/dynamic-worker-loading/package.json @@ -0,0 +1,16 @@ +{ + "name": "@fixture/dynamic-worker-loading", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "@cloudflare/workers-types": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/dynamic-worker-loading/src/index.ts b/fixtures/dynamic-worker-loading/src/index.ts new file mode 100644 index 0000000..cadfaa9 --- /dev/null +++ b/fixtures/dynamic-worker-loading/src/index.ts @@ -0,0 +1,34 @@ +export interface Env { + LOADER: { + get( + id: string, + factory: () => unknown + ): { + getEntrypoint(): Fetcher; + }; + }; +} + +export default { + async fetch(request: Request, env: Env): Promise<Response> { + const url = new URL(request.url); + + let worker = env.LOADER.get(url.pathname, () => { + return { + compatibilityDate: "2025-06-01", + + mainModule: "foo.js", + + modules: { + "foo.js": + "export default {\n" + + ` fetch(req, env, ctx) { return new Response('Hello with a dynamic worker loaded for ${url.pathname}'); }\n` + + "}\n", + }, + }; + }); + + let defaultEntrypoint = worker.getEntrypoint(); + return await defaultEntrypoint.fetch(request); + }, +}; diff --git a/fixtures/dynamic-worker-loading/tests/index.test.ts b/fixtures/dynamic-worker-loading/tests/index.test.ts new file mode 100644 index 0000000..0a70c43 --- /dev/null +++ b/fixtures/dynamic-worker-loading/tests/index.test.ts @@ -0,0 +1,36 @@ +import { resolve } from "node:path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { createTestHarness } from "wrangler"; + +describe("dynamic worker loading", () => { + const server = createTestHarness({ + root: resolve(__dirname, ".."), + workers: [{ configPath: "wrangler.jsonc" }], + }); + + beforeAll(async () => { + await server.listen(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("should respond with response from dynamic worker", async ({ expect }) => { + let response = await server.fetch("/my-worker"); + let text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot( + `"Hello with a dynamic worker loaded for /my-worker"` + ); + }); + + it("should load different worker if ID changes", async ({ expect }) => { + let response = await server.fetch("/my-other-worker"); + let text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot( + `"Hello with a dynamic worker loaded for /my-other-worker"` + ); + }); +}); diff --git a/fixtures/dynamic-worker-loading/tests/tsconfig.json b/fixtures/dynamic-worker-loading/tests/tsconfig.json new file mode 100644 index 0000000..d2ce7f1 --- /dev/null +++ b/fixtures/dynamic-worker-loading/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts", "../../../node-types.d.ts"] +} diff --git a/fixtures/dynamic-worker-loading/tsconfig.json b/fixtures/dynamic-worker-loading/tsconfig.json new file mode 100644 index 0000000..2431bac --- /dev/null +++ b/fixtures/dynamic-worker-loading/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "types": ["@cloudflare/workers-types/experimental"], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/dynamic-worker-loading/vitest.config.mts b/fixtures/dynamic-worker-loading/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/dynamic-worker-loading/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/dynamic-worker-loading/wrangler.jsonc b/fixtures/dynamic-worker-loading/wrangler.jsonc new file mode 100644 index 0000000..76ebfc6 --- /dev/null +++ b/fixtures/dynamic-worker-loading/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "name": "dynamic-worker-loading", + "main": "src/index.ts", + "compatibility_date": "2023-05-04", + "worker_loaders": [ + { + "binding": "LOADER", + }, + ], +} diff --git a/fixtures/email-worker/README.md b/fixtures/email-worker/README.md new file mode 100644 index 0000000..23cca67 --- /dev/null +++ b/fixtures/email-worker/README.md @@ -0,0 +1,119 @@ +# Email Worker Fixture + +This fixture demonstrates both the **EmailMessage API** (raw MIME) and the **MessageBuilder API** for sending emails in Cloudflare Workers. + +## Running Locally + +Start the development server: + +```bash +pnpm start +# or +wrangler dev +``` + +## Available Routes + +### EmailMessage API (Raw MIME) + +**`GET /send`** - Original API using manual MIME construction + +- Uses the `mimetext` library to build MIME messages +- Sends via `LIST_SEND` binding + +### MessageBuilder API + +**`GET /send-simple`** - Simple text-only email + +- Basic MessageBuilder example with plain text +- Demonstrates named sender: `{ name: "Alice", email: "..." }` + +**`GET /send-html`** - Email with text and HTML versions + +- Shows how to include both `text` and `html` content +- HTML includes inline CSS styling + +**`GET /send-attachment`** - Email with single text attachment + +- Demonstrates attaching a text file +- Uses `TextEncoder` to create content + +**`GET /send-multi-attachment`** - Email with multiple attachments + +- Includes three different attachment types: + - Text file (`.txt`) + - JSON file (`.json`) + - Binary file (simulated `.pdf`) +- Shows both text and HTML content + +**`GET /send-complex`** - Complex email with multiple recipients + +- Multiple TO recipients (with and without names) +- CC recipient with name +- BCC recipient (array) +- Both text and HTML content + +### Testing Bindings + +**`GET /test-bindings`** - Test all three email binding types + +- `UNBOUND_SEND` - No restrictions on recipients +- `SPECIFIC_SEND` - Only allows `something@example.com` +- `LIST_SEND` - Allows `something@example.com` and `else@example.com` + +## What to Expect + +When you send emails using these routes: + +1. **Console Output**: Miniflare logs details about the email being sent +2. **File Paths**: For MessageBuilder emails, you'll see temp file paths: + - Text content → `.txt` files + - HTML content → `.html` files + - Attachments → Files with their original extensions + +### Example Console Output + +``` +send_email binding called with MessageBuilder: +From: "Alice" <sender@penalosa.cloud> +To: else@example.com +Subject: Simple MessageBuilder Test + +Text: /var/folders/.../email-text/abc-123.txt +``` + +You can open these files in your editor or browser to inspect the email content. + +## Email Bindings Configuration + +This fixture has three email bindings configured in `wrangler.jsonc`: + +```jsonc +{ + "send_email": [ + { + "name": "UNBOUND_SEND", + // No restrictions + }, + { + "name": "SPECIFIC_SEND", + "destination_address": "something@example.com", + // Only allows sending to this specific address + }, + { + "name": "LIST_SEND", + "allowed_destination_addresses": [ + "something@example.com", + "else@example.com", + ], + // Only allows sending to addresses in this list + }, + ], +} +``` + +## Notes + +- Type assertions (`as any`) are used because `@cloudflare/workers-types` doesn't yet include MessageBuilder types +- At runtime in Miniflare, the MessageBuilder API works correctly +- The `allowed_sender_addresses` configuration is not included in this fixture, so any sender address is accepted diff --git a/fixtures/email-worker/package.json b/fixtures/email-worker/package.json new file mode 100644 index 0000000..cd850f5 --- /dev/null +++ b/fixtures/email-worker/package.json @@ -0,0 +1,14 @@ +{ + "name": "@fixture/worker-email", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "@types/mimetext": "^2.0.4", + "mimetext": "^3.0.27", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/email-worker/src/index.ts b/fixtures/email-worker/src/index.ts new file mode 100644 index 0000000..e9771b3 --- /dev/null +++ b/fixtures/email-worker/src/index.ts @@ -0,0 +1,255 @@ +import { EmailMessage } from "cloudflare:email"; +import { env, WorkerEntrypoint } from "cloudflare:workers"; +import { createMimeMessage } from "mimetext"; + +export default class extends WorkerEntrypoint<Env> { + async fetch(request: Request): Promise<Response> { + const url = new URL(request.url); + if (url.pathname === "/error") throw new Error("Hello Error"); + + // Original EmailMessage API (raw MIME) + if (url.pathname === "/send") { + const msg = createMimeMessage(); + msg.setSender({ name: "GPT-4", addr: "sender@penalosa.cloud" }); + msg.setRecipient("else@example.com"); + msg.setSubject("An email generated in a Worker"); + msg.addMessage({ + contentType: "text/plain", + data: "Congratulations, you just sent an email from a Worker.", + }); + const m = new EmailMessage( + "sender@penalosa.cloud", + "else@example.com", + msg.asRaw() + ); + await this.env.LIST_SEND.send(m); + return new Response( + "✅ EmailMessage sent! Check console for temp file path.\n" + ); + } + + // MessageBuilder API: Simple text-only email + if (url.pathname === "/send-simple") { + await this.env.LIST_SEND.send({ + from: { name: "Alice", email: "sender@penalosa.cloud" }, + to: "else@example.com", + subject: "Simple MessageBuilder Test", + text: "This is a plain text email using the MessageBuilder API!", + } as any); + return new Response( + "✅ Simple text email sent! Check console for temp file path.\n" + ); + } + + // MessageBuilder API: Email with both text and HTML + if (url.pathname === "/send-html") { + await this.env.LIST_SEND.send({ + from: { name: "Bob", email: "sender@penalosa.cloud" }, + to: "else@example.com", + subject: "HTML Email Test", + text: "This is the plain text version.", + html: ` +<!DOCTYPE html> +<html> +<head> + <style> + body { font-family: Arial, sans-serif; } + .highlight { color: #0066cc; font-weight: bold; } + </style> +</head> +<body> + <h1>Hello from MessageBuilder!</h1> + <p>This is the <span class="highlight">HTML</span> version.</p> + <ul> + <li>Feature 1</li> + <li>Feature 2</li> + <li>Feature 3</li> + </ul> +</body> +</html> + `.trim(), + } as any); + return new Response( + "✅ HTML email sent! Check console for text and HTML file paths.\n" + ); + } + + // MessageBuilder API: Email with text attachment + if (url.pathname === "/send-attachment") { + const textContent = new TextEncoder().encode( + "This is a sample text file attachment.\n\nLine 2\nLine 3\n" + ); + + await this.env.LIST_SEND.send({ + from: "sender@penalosa.cloud", + to: "else@example.com", + subject: "Email with Text Attachment", + text: "Please see the attached text file.", + attachments: [ + { + disposition: "attachment", + filename: "sample.txt", + type: "text/plain", + content: textContent, + }, + ], + } as any); + return new Response( + "✅ Email with text attachment sent! Check console for file paths.\n" + ); + } + + // MessageBuilder API: Email with multiple attachments (text, JSON, simulated binary) + if (url.pathname === "/send-multi-attachment") { + const textAttachment = new TextEncoder().encode("Sample text file."); + const jsonAttachment = new TextEncoder().encode( + JSON.stringify({ message: "Hello from JSON!", timestamp: Date.now() }) + ); + // Simulate a small binary file (e.g., a tiny "PDF" with some binary data) + const binaryAttachment = new Uint8Array([ + 0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34, 0x0a, 0x25, 0xe2, 0xe3, + 0xcf, 0xd3, + ]); // "%PDF-1.4" header + + await this.env.LIST_SEND.send({ + from: { name: "Charlie", email: "sender@penalosa.cloud" }, + to: "else@example.com", + subject: "Email with Multiple Attachments", + text: "This email has three different types of attachments.", + html: "<h2>Multiple Attachments</h2><p>Check out the attached files!</p>", + attachments: [ + { + disposition: "attachment", + filename: "document.txt", + type: "text/plain", + content: textAttachment, + }, + { + disposition: "attachment", + filename: "data.json", + type: "application/json", + content: jsonAttachment, + }, + { + disposition: "attachment", + filename: "sample.pdf", + type: "application/pdf", + content: binaryAttachment, + }, + ], + } as any); + return new Response( + "✅ Email with multiple attachments sent! Check console for all file paths.\n" + ); + } + + // MessageBuilder API: Complex email with multiple recipients (to/cc/bcc) + if (url.pathname === "/send-complex") { + await this.env.LIST_SEND.send({ + from: { name: "David", email: "sender@penalosa.cloud" }, + to: [ + { name: "Recipient One", email: "else@example.com" }, + "something@example.com", + ], + cc: { name: "CC Person", email: "else@example.com" }, + bcc: ["something@example.com"], + subject: "Complex Email Test", + text: "Plain text for email clients that don't support HTML.", + html: ` +<html> +<body> + <h1>Complex Email</h1> + <p>This demonstrates:</p> + <ul> + <li>Multiple TO recipients (with and without names)</li> + <li>CC recipient with name</li> + <li>BCC recipient</li> + <li>Both text and HTML content</li> + </ul> +</body> +</html> + `.trim(), + } as any); + return new Response( + "✅ Complex email sent! Check console for file paths.\n" + ); + } + + // Test all three binding types + if (url.pathname === "/test-bindings") { + const results: string[] = []; + + // Test UNBOUND_SEND (no restrictions) + try { + await this.env.UNBOUND_SEND.send({ + from: "sender@penalosa.cloud", + to: "anyone@anywhere.com", + subject: "UNBOUND_SEND Test", + text: "This uses the unbound binding.", + } as any); + results.push("✅ UNBOUND_SEND: Success"); + } catch (e) { + results.push(`❌ UNBOUND_SEND: ${(e as Error).message}`); + } + + // Test SPECIFIC_SEND (only to something@example.com) + try { + await this.env.SPECIFIC_SEND.send({ + from: "sender@penalosa.cloud", + to: "something@example.com", + subject: "SPECIFIC_SEND Test", + text: "This uses the specific binding.", + } as any); + results.push("✅ SPECIFIC_SEND: Success"); + } catch (e) { + results.push(`❌ SPECIFIC_SEND: ${(e as Error).message}`); + } + + // Test LIST_SEND (allowed list) + try { + await this.env.LIST_SEND.send({ + from: "sender@penalosa.cloud", + to: "else@example.com", + subject: "LIST_SEND Test", + text: "This uses the list binding.", + } as any); + results.push("✅ LIST_SEND: Success"); + } catch (e) { + results.push(`❌ LIST_SEND: ${(e as Error).message}`); + } + + return new Response(results.join("\n") + "\n"); + } + + return new Response( + "Email Worker Fixture\n\n" + + "Available routes:\n" + + " /send - EmailMessage API (raw MIME)\n" + + " /send-simple - MessageBuilder: Simple text email\n" + + " /send-html - MessageBuilder: Text + HTML\n" + + " /send-attachment - MessageBuilder: With text attachment\n" + + " /send-multi-attachment - MessageBuilder: Multiple attachments\n" + + " /send-complex - MessageBuilder: Multiple recipients (to/cc/bcc)\n" + + " /test-bindings - Test all three email binding types\n" + ); + } + async email(message: ForwardableEmailMessage) { + console.log("hello"); + const msg = createMimeMessage(); + msg.setHeader("In-Reply-To", message.headers.get("Message-ID")!); + msg.setSender(message.to); + msg.setRecipient(message.from); + msg.setSubject("An email generated in a Worker"); + msg.addMessage({ + contentType: "text/plain", + data: `Congratulations, you just sent an email from a Worker.`, + }); + + const m = new EmailMessage(message.to, message.from, msg.asRaw()); + await message.forward( + "samuel@macleod.space", + new Headers({ hello: "world" }) + ); + await message.reply(m); + } +} diff --git a/fixtures/email-worker/tsconfig.json b/fixtures/email-worker/tsconfig.json new file mode 100644 index 0000000..2fd2e1b --- /dev/null +++ b/fixtures/email-worker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "moduleResolution": "bundler", + "types": ["@cloudflare/workers-types"], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/email-worker/worker-configuration.d.ts b/fixtures/email-worker/worker-configuration.d.ts new file mode 100644 index 0000000..54fcf91 --- /dev/null +++ b/fixtures/email-worker/worker-configuration.d.ts @@ -0,0 +1,7 @@ +// Generated by Wrangler by running `wrangler types --x-include-runtime` + +interface Env extends Env { + UNBOUND_SEND: SendEmail; + SPECIFIC_SEND: SendEmail; + LIST_SEND: SendEmail; +} diff --git a/fixtures/email-worker/wrangler.jsonc b/fixtures/email-worker/wrangler.jsonc new file mode 100644 index 0000000..dd9c211 --- /dev/null +++ b/fixtures/email-worker/wrangler.jsonc @@ -0,0 +1,22 @@ +{ + "name": "email-to-rss", + "main": "src/index.ts", + "compatibility_date": "2025-02-14", + "compatibility_flags": ["nodejs_compat"], + "send_email": [ + { + "name": "UNBOUND_SEND", + }, + { + "name": "SPECIFIC_SEND", + "destination_address": "something@example.com", + }, + { + "name": "LIST_SEND", + "allowed_destination_addresses": [ + "something@example.com", + "else@example.com", + ], + }, + ], +} diff --git a/fixtures/entrypoints-rpc-tests/package.json b/fixtures/entrypoints-rpc-tests/package.json new file mode 100644 index 0000000..28d2177 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/package.json @@ -0,0 +1,16 @@ +{ + "name": "@fixture/entrypoints-rpc", + "private": true, + "scripts": { + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "miniflare": "workspace:*", + "ts-dedent": "^2.2.0", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts b/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts new file mode 100644 index 0000000..33b8c07 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/tests/entrypoints.spec.ts @@ -0,0 +1,951 @@ +import fs, { mkdir, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { Miniflare } from "miniflare"; +import dedent from "ts-dedent"; +import { Agent, fetch, setGlobalDispatcher } from "undici"; +import { test as baseTest, describe, onTestFinished, vi } from "vitest"; +import { + runWranglerDev, + runWranglerPagesDev, +} from "../../shared/src/run-wrangler-long-lived"; + +const timeoutAgent = new Agent({ + connectTimeout: 2_000, + bodyTimeout: 2_000, + headersTimeout: 2_000, +}); +setGlobalDispatcher(timeoutAgent); + +const tmpPathBase = path.join(os.tmpdir(), "wrangler-tests"); + +type WranglerDevSession = Awaited<ReturnType<typeof runWranglerDev>>; +type StartDevSession = ( + files: Record<string, string>, + flags?: string[], + pagesPublicPath?: string +) => Promise<{ url: URL; session: WranglerDevSession }>; + +export async function seed(root: string, files: Record<string, string>) { + for (const [name, contents] of Object.entries(files)) { + const filePath = path.resolve(root, name); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, contents); + } +} + +function waitFor<T>( + callback: Parameters<typeof vi.waitFor<T>>[0], + timeout = 5_000 +) { + // The default timeout of `vi.waitFor()` is only 1s, which is a little + // short for some of these tests, especially on Windows. + return vi.waitFor(callback, { timeout, interval: 250 }); +} + +const test = baseTest.extend<{ + tmpPath: string; + isolatedDevRegistryPath: string; + dev: StartDevSession; +}>({ + // Fixture for creating a temporary directory + async tmpPath({}, use) { + const tmpPath = await fs.realpath(await fs.mkdtemp(tmpPathBase)); + await use(tmpPath); + await fs.rm(tmpPath, { recursive: true, maxRetries: 10 }); + }, + // Fixture for starting an isolated dev registry server on a random port + async isolatedDevRegistryPath({}, use) { + const tmpPath = await fs.realpath(await fs.mkdtemp(tmpPathBase)); + await use(tmpPath); + await fs.rm(tmpPath, { recursive: true, maxRetries: 10 }); + }, + // Fixture for starting a worker in a temporary directory, using the test's + // isolated dev registry + async dev({ tmpPath, isolatedDevRegistryPath }, use) { + const workerTmpPathBase = path.join(tmpPath, "worker-"); + const cleanups: (() => Promise<unknown>)[] = []; + + const fn: StartDevSession = async (files, flags, pagesPublicPath) => { + const workerPath = await fs.mkdtemp(workerTmpPathBase); + await seed(workerPath, files); + + let session: WranglerDevSession; + if (pagesPublicPath !== undefined) { + session = await runWranglerPagesDev( + workerPath, + pagesPublicPath, + ["--port=0", "--inspector-port=0", ...(flags ?? [])], + { WRANGLER_REGISTRY_PATH: String(isolatedDevRegistryPath) } + ); + } else { + session = await runWranglerDev( + workerPath, + ["--port=0", "--inspector-port=0", ...(flags ?? [])], + { WRANGLER_REGISTRY_PATH: String(isolatedDevRegistryPath) } + ); + } + + cleanups.push(session.stop); + // noinspection HttpUrlsUsage + const url = new URL(`http://${session.ip}:${session.port}`); + return { url, session }; + }; + + await use(fn); + + await Promise.allSettled(cleanups.map((fn) => fn())); + }, +}); +describe("entrypoints", () => { + test("should support binding to the same worker", async ({ dev, expect }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "entry" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + const { pathname } = new URL(request.url); + + if (pathname === "/loopback") { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + + return env.SERVICE.fetch("https://placeholder:9999/loopback", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + // Check protocol, host, and cf preserved + expect(await response.text()).toBe( + 'POST https://placeholder:9999/loopback {"thing":true}' + ); + }); + }); + + test("should support default ExportedHandler entrypoints", async ({ + dev, + expect, + }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + }; + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true}'); + }); + }); + + test("should support default WorkerEntrypoint entrypoints", async ({ + dev, + expect, + }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + // Check middleware is transparent to RPC + export default class ThingEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + ping() { + return "pong"; + } + }; + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const response = await env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + const text = await response.text(); + const pong = await env.SERVICE.ping(); + return new Response(\`\${text} \${pong}\`); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true} pong'); + }); + }); + + test("should support middleware with default WorkerEntrypoint entrypoints", async ({ + dev, + expect, + }) => { + const files: Record<string, string> = { + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "entry" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + let lastController; + export default class TestEntrypoint extends WorkerEntrypoint { + fetch(request) { + const { pathname } = new URL(request.url); + if (pathname === "/throw") throw new Error("Oops!"); + if (pathname === "/controller") return new Response(lastController.cron); + return new Response(\`\${request.method} \${new URL(request.url).pathname}\`); + } + scheduled(controller) { + lastController = controller; + } + } + `, + }; + const { url } = await dev(files, ["--test-scheduled"]); + + await waitFor(async () => { + const response = await fetch(url); + expect(await response.text()).toBe("GET /"); + }); + + // Check other events can be dispatched + let response = await fetch(new URL("/__scheduled?cron=* * * * 30", url)); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Ran scheduled event"); + response = await fetch(new URL("/controller", url)); + expect(response.status).toBe(200); + expect(await response.text()).toBe("* * * * 30"); + + // Check multiple middleware can be registered + response = await fetch(new URL("/throw", url)); + expect(response.status).toBe(500); + expect(response.headers.get("Content-Type")).toMatch(/text\/html/); + expect(await response.text()).toMatch("Oops!"); + }); + + test("should support named ExportedHandler entrypoints to itself", async ({ + dev, + expect, + }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "entry" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class ThingEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + ping() { + return "pong"; + } + }; + export default { + fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + // Check protocol, host, and cf preserved + expect(await response.text()).toBe( + 'POST https://placeholder:9999/ {"thing":true}' + ); + }); + }); + + test("should support named ExportedHandler entrypoints", async ({ + dev, + expect, + }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + export const thing = { + fetch(request, env, ctx) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "thing" + `, + "index.ts": dedent` + export default { + fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true}'); + }); + }); + + test("should support named WorkerEntrypoint entrypoints", async ({ + dev, + expect, + }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class ThingEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + ping() { + return "pong"; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const response = await env.SERVICE.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + const text = await response.text(); + const pong = await env.SERVICE.ping(); + return new Response(\`\${text} \${pong}\`); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true} pong'); + }); + }); + + test("should support named entrypoints in pages dev", async ({ + dev, + expect, + }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class ThingEntrypoint extends WorkerEntrypoint { + ping() { + return "pong"; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const files = { + "functions/index.ts": dedent` + export const onRequest = async ({ env }) => { + return new Response(await env.SERVICE.ping()); + }; + `, + }; + const { url } = await dev( + files, + ["--service=SERVICE=bound#ThingEntrypoint"], + /* pagesPublicPath */ "dist" + ); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + expect(text).toBe("pong"); + }); + }); + + test("should support co-dependent services", async ({ dev, expect }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "a" + main = "index.ts" + + [[services]] + binding = "SERVICE_B" + service = "b" + entrypoint = "BEntrypoint" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class AEntrypoint extends WorkerEntrypoint { + ping() { + return "a:pong"; + } + }; + export default { + async fetch(request, env, ctx) { + return new Response(await env.SERVICE_B.ping()); + } + }; + `, + }); + + await dev({ + "wrangler.toml": dedent` + name = "b" + main = "index.ts" + + [[services]] + binding = "SERVICE_A" + service = "a" + entrypoint = "AEntrypoint" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class BEntrypoint extends WorkerEntrypoint { + async ping() { + const result = await this.env.SERVICE_A.ping(); + return \`b:\${result}\`; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + expect(text).toBe("b:a:pong"); + }); + }); + + test("should support binding to Durable Object in another worker", async ({ + dev, + expect, + }) => { + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject" } + ] + `, + "index.ts": dedent` + import { DurableObject } from "cloudflare:workers"; + export class ThingObject extends DurableObject { + fetch(request) { + return new Response(\`\${request.method} \${request.url} \${JSON.stringify(request.cf)}\`); + } + get property() { + return "property:ping"; + } + method() { + return "method:ping"; + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject", script_name = "bound" } + ] + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const id = env.OBJECT.newUniqueId(); + const stub = env.OBJECT.get(id); + + const { pathname } = new URL(request.url); + if (pathname === "/rpc") { + const results = []; + results.push(await stub.property) + results.push(await stub.method()) + return Response.json(results.map(String)); + } + + return stub.fetch("https://placeholder:9999/", { + method: "POST", + cf: { thing: true }, + }); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + // Check protocol, host, and cf preserved + expect(text).toBe('POST https://placeholder:9999/ {"thing":true}'); + }); + + await waitFor(async () => { + const rpcResponse = await fetch(new URL("/rpc", url)); + const errors = await rpcResponse.json(); + expect(errors).toMatchInlineSnapshot(` + [ + "property:ping", + "method:ping", + ] + `); + }); + }); + + test("should support binding to Durable Object in same worker", async ({ + dev, + expect, + }) => { + // RPC is supported here though :) + + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject" } + ] + `, + "index.ts": dedent` + import { DurableObject } from "cloudflare:workers"; + export class ThingObject extends DurableObject { + ping() { + return "pong"; + } + }; + export default { + async fetch(request, env, ctx) { + const id = env.OBJECT.newUniqueId(); + const stub = env.OBJECT.get(id); + return new Response(await stub.ping()); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + expect(await response.text()).toBe("pong"); + }); + }); + + test("should support binding to Durable Object in same worker with explicit script_name", async ({ + dev, + expect, + }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [durable_objects] + bindings = [ + { name = "OBJECT", class_name = "ThingObject", script_name = "entry" } + ] + `, + "index.ts": dedent` + import { DurableObject } from "cloudflare:workers"; + export class ThingObject extends DurableObject { + ping() { + return "pong"; + } + }; + export default { + async fetch(request, env, ctx) { + const id = env.OBJECT.newUniqueId(); + const stub = env.OBJECT.get(id); + return new Response(await stub.ping()); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + expect(await response.text()).toBe("pong"); + }); + }); + + test("should throw if binding to named entrypoint exported by version of wrangler without entrypoints support", async ({ + dev, + isolatedDevRegistryPath, + expect, + }) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/"); + } + } + `, + }); + await waitFor(async () => { + const response = await fetch(url); + expect(response.status).toBe(503); + expect(await response.text()).toBe( + 'Worker "bound" not found. Make sure it is running locally.' + ); + }); + + await writeFile( + path.join(isolatedDevRegistryPath, "bound"), + JSON.stringify({ + protocol: "http", + mode: "local", + port: 0, + host: "localhost", + durableObjects: [], + durableObjectsHost: "localhost", + durableObjectsPort: 0, + // Intentionally omitting `entrypointAddresses` + }) + ); + + await waitFor(async () => { + let response = await fetch(url); + expect(response.status).toBe(503); + expect(await response.text()).toBe( + 'Worker "bound" not found. Make sure it is running locally.' + ); + }); + }); + + test("should throw if wrangler session doesn't export expected entrypoint", async ({ + dev, + expect, + }) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("https://placeholder:9999/"); + } + } + `, + }); + await waitFor(async () => { + const response = await fetch(url); + expect(await response.text()).toBe( + 'Worker "bound" not found. Make sure it is running locally.' + ); + }); + + // Start up the bound worker without the expected entrypoint + await dev({ + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class BadEntrypoint extends WorkerEntrypoint { + fetch(request) { + return new Response("bad"); + } + }; + export default {}; // Required to treat as modules format worker + `, + }); + + // Wait for error to be thrown + await waitFor(async () => { + let response = await fetch(url); + expect(await response.text()).toBe( + 'Worker "bound" not found. Make sure it is running locally.' + ); + }); + }); + + test("should support binding to wrangler session listening on HTTPS", async ({ + dev, + expect, + }) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch("http://placeholder/"); + } + } + `, + }); + await waitFor(async () => { + const response = await fetch(url); + expect(await response.text()).toBe( + 'Worker "bound" not found. Make sure it is running locally.' + ); + }); + + // Start up the bound worker using HTTPS + const files: Record<string, string> = { + "wrangler.toml": dedent` + name = "bound" + main = "index.ts" + `, + "index.ts": dedent` + export default { + fetch() { + return new Response("secure"); + } + }; + `, + }; + await dev(files, ["--local-protocol=https"]); + + await waitFor(async () => { + const response = await fetch(url); + const text = await response.text(); + expect(text).toBe("secure"); + }, 10_000); + }); + + test("should support binding to version of wrangler without entrypoints support over HTTPS", async ({ + dev, + isolatedDevRegistryPath, + expect, + }) => { + // Start entry worker first, so the server starts with a stubbed service not + // found binding + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + [[services]] + binding = "SERVICE" + service = "bound" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + return env.SERVICE.fetch('http://placeholder/'); + } + } + `, + }); + await waitFor(async () => { + const response = await fetch(url); + expect(await response.text()).toBe( + 'Worker "bound" not found. Make sure it is running locally.' + ); + }); + + const boundWorker = new Miniflare({ + name: "bound", + unsafeDevRegistryPath: isolatedDevRegistryPath, + compatibilityFlags: ["experimental"], + modules: true, + https: true, + script: ` + export default { + async fetch(request, env, ctx) { + return new Response("Hello from bound!"); + } + } + `, + // No direct sockets so that no entrypointAddresses will be registered + }); + onTestFinished(() => boundWorker.dispose()); + + await boundWorker.ready; + await waitFor(async () => { + let response = await fetch(url); + expect(await response.text()).toBe("Hello from bound!"); + }, 10_000); + }); + + test("should throw if performing RPC with session that hasn't started", async ({ + dev, + expect, + }) => { + const { url } = await dev({ + "wrangler.toml": dedent` + name = "entry" + main = "index.ts" + + [[services]] + binding = "SERVICE" + service = "bound" + entrypoint = "ThingEntrypoint" + `, + "index.ts": dedent` + export default { + async fetch(request, env, ctx) { + const errors = []; + try { await env.SERVICE.property; } catch (e) { errors.push(e); } + try { await env.SERVICE.method(); } catch (e) { errors.push(e); } + return Response.json(errors.map(String)); + } + } + `, + }); + + await waitFor(async () => { + const response = await fetch(url); + const errors = await response.json(); + expect(errors).toMatchInlineSnapshot(` + [ + "Error: Worker "bound" not found. Make sure it is running locally.", + "Error: Worker "bound" not found. Make sure it is running locally.", + ] + `); + }); + }); +}); diff --git a/fixtures/entrypoints-rpc-tests/tsconfig.json b/fixtures/entrypoints-rpc-tests/tsconfig.json new file mode 100644 index 0000000..2fd7b41 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "include": ["**/*.ts"] +} diff --git a/fixtures/entrypoints-rpc-tests/vitest.config.mts b/fixtures/entrypoints-rpc-tests/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/entrypoints-rpc-tests/wrangler.jsonc b/fixtures/entrypoints-rpc-tests/wrangler.jsonc new file mode 100644 index 0000000..b75d756 --- /dev/null +++ b/fixtures/entrypoints-rpc-tests/wrangler.jsonc @@ -0,0 +1,4 @@ +{ + "main": "src/index.mjs", + "compatibility_date": "2023-12-01", +} diff --git a/fixtures/experimental-new-config/cloudflare.config.ts b/fixtures/experimental-new-config/cloudflare.config.ts new file mode 100644 index 0000000..936521a --- /dev/null +++ b/fixtures/experimental-new-config/cloudflare.config.ts @@ -0,0 +1,11 @@ +import { bindings, defineWorker } from "wrangler/experimental-config"; +import * as entrypoint from "./src/index.ts" with { type: "cf-worker" }; + +export default defineWorker((ctx) => ({ + name: "experimental-new-config", + entrypoint, + compatibilityDate: "2026-05-18", + env: { + MY_TEXT: bindings.text(`The mode is ${ctx.mode}`), + }, +})); diff --git a/fixtures/experimental-new-config/package.json b/fixtures/experimental-new-config/package.json new file mode 100644 index 0000000..83bbda8 --- /dev/null +++ b/fixtures/experimental-new-config/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/experimental-new-config", + "private": true, + "scripts": { + "build": "wrangler deploy --x-new-config --dry-run --outdir=dist", + "check:type": "tsc --build", + "deploy": "wrangler deploy --x-new-config", + "dev": "wrangler dev --x-new-config", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@fixture/shared": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/experimental-new-config/src/index.ts b/fixtures/experimental-new-config/src/index.ts new file mode 100644 index 0000000..ee695db --- /dev/null +++ b/fixtures/experimental-new-config/src/index.ts @@ -0,0 +1,7 @@ +import { env } from "cloudflare:workers"; + +export default { + fetch() { + return new Response(env.MY_TEXT); + }, +} satisfies ExportedHandler; diff --git a/fixtures/experimental-new-config/test/index.test.ts b/fixtures/experimental-new-config/test/index.test.ts new file mode 100644 index 0000000..f37cb96 --- /dev/null +++ b/fixtures/experimental-new-config/test/index.test.ts @@ -0,0 +1,210 @@ +import childProcess from "node:child_process"; +import { existsSync, writeFileSync } from "node:fs"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { removeDir } from "@fixture/shared/src/fs-helpers"; +import { afterAll, beforeAll, describe, test } from "vitest"; +import { + runWranglerDev, + wranglerEntryPath, +} from "../../shared/src/run-wrangler-long-lived"; + +const fixtureDir = path.resolve(__dirname, ".."); + +/** + * Spawn `wrangler` synchronously inside the given working directory. + */ +function spawnWrangler(cwd: string, args: string[]) { + return childProcess.spawnSync( + process.execPath, + [wranglerEntryPath, ...args], + { + cwd, + env: { + ...process.env, + WRANGLER_LOG_PATH: "", + NO_COLOR: "1", + FORCE_COLOR: "0", + }, + } + ); +} + +async function getTmpDir() { + return fs.mkdtemp(path.join(os.tmpdir(), "wrangler-new-config-")); +} + +/** + * Stage the fixture in a temporary directory. Symlinks `node_modules` so + * the staged cloudflare.config.ts can resolve `wrangler/experimental-config`. + */ +async function stageFixture(): Promise<string> { + const tmp = await getTmpDir(); + for (const name of [ + "package.json", + "src", + "cloudflare.config.ts", + "wrangler.config.ts", + "worker-configuration.d.ts", + "tsconfig.json", + "tsconfig.node.json", + "tsconfig.worker.json", + ]) { + const srcPath = path.join(fixtureDir, name); + if (existsSync(srcPath)) { + await fs.cp(srcPath, path.join(tmp, name), { recursive: true }); + } + } + // Symlink the fixture's node_modules so package resolution works. + const fixtureNodeModules = path.join(fixtureDir, "node_modules"); + if (existsSync(fixtureNodeModules)) { + await fs.symlink(fixtureNodeModules, path.join(tmp, "node_modules"), "dir"); + } + return tmp; +} + +describe("--x-new-config deploy --dry-run", () => { + test("builds successfully and emits the worker bundle", async ({ + expect, + }) => { + const tmpDir = await stageFixture(); + try { + const outDir = path.join(tmpDir, "out"); + const result = spawnWrangler(tmpDir, [ + "deploy", + "--x-new-config", + "--dry-run", + `--outdir=${outDir}`, + ]); + expect(result.status, result.stderr.toString()).toBe(0); + expect(existsSync(path.join(outDir, "index.js"))).toBe(true); + } finally { + removeDir(tmpDir, { fireAndForget: true }); + } + }); + + test("rejects --config when used with --x-new-config", async ({ expect }) => { + const tmpDir = await stageFixture(); + try { + const result = spawnWrangler(tmpDir, [ + "deploy", + "--x-new-config", + "--config", + "./some-other.jsonc", + "--dry-run", + ]); + expect(result.status).not.toBe(0); + expect(result.stderr.toString()).toContain( + "--config is not supported with --experimental-new-config" + ); + } finally { + removeDir(tmpDir, { fireAndForget: true }); + } + }); + + test("rejects on out-of-scope commands (kv namespace list)", async ({ + expect, + }) => { + const tmpDir = await stageFixture(); + try { + const result = spawnWrangler(tmpDir, [ + "kv", + "namespace", + "list", + "--x-new-config", + ]); + expect(result.status).not.toBe(0); + // Yargs strict-mode rejection — the flag is only declared on the + // commands that support it, so yargs reports it as unknown elsewhere. + expect(result.stderr.toString()).toContain("Unknown arguments"); + expect(result.stderr.toString()).toContain("x-new-config"); + } finally { + removeDir(tmpDir, { fireAndForget: true }); + } + }); + + test("silently ignores adjacent wrangler.json", async ({ expect }) => { + const tmpDir = await stageFixture(); + try { + writeFileSync( + path.join(tmpDir, "wrangler.json"), + JSON.stringify({ + name: "should-be-ignored", + main: "src/does-not-exist.ts", + compatibility_date: "2020-01-01", + }) + ); + const outDir = path.join(tmpDir, "out"); + const result = spawnWrangler(tmpDir, [ + "deploy", + "--x-new-config", + "--dry-run", + `--outdir=${outDir}`, + ]); + // Should still succeed — `cloudflare.config.ts` is used; the + // `wrangler.json` is silently ignored. + expect(result.status, result.stderr.toString()).toBe(0); + expect(existsSync(path.join(outDir, "index.js"))).toBe(true); + } finally { + removeDir(tmpDir, { fireAndForget: true }); + } + }); + + test("--env staging surfaces in ctx.mode (bound text contains 'staging')", async ({ + expect, + }) => { + const tmpDir = await stageFixture(); + try { + const outDir = path.join(tmpDir, "out"); + const result = spawnWrangler(tmpDir, [ + "deploy", + "--x-new-config", + "--env", + "staging", + "--dry-run", + `--outdir=${outDir}`, + ]); + expect(result.status, result.stderr.toString()).toBe(0); + // The deploy --dry-run output prints the bound `MY_TEXT` value as + // part of the bindings table. With `--env staging`, the function- + // form `cloudflare.config.ts` evaluates `ctx.mode === "staging"`, so + // `bindings.text(`The mode is ${ctx.mode}`)` becomes + // `"The mode is staging"`. + const combined = result.stdout.toString() + result.stderr.toString(); + expect(combined).toContain("staging"); + } finally { + removeDir(tmpDir, { fireAndForget: true }); + } + }); +}); + +describe("--x-new-config dev", () => { + let tmpDir: string; + let stop: (() => Promise<unknown>) | undefined; + let ip: string; + let port: number; + + beforeAll(async () => { + tmpDir = await stageFixture(); + ({ ip, port, stop } = await runWranglerDev(tmpDir, [ + "--x-new-config", + "--env", + "dev", + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + removeDir(tmpDir, { fireAndForget: true }); + }); + + test("serves the correct response for a worker configured via cloudflare.config.ts", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/`); + expect(await response.text()).toBe("The mode is dev"); + }); +}); diff --git a/fixtures/experimental-new-config/tsconfig.json b/fixtures/experimental-new-config/tsconfig.json new file mode 100644 index 0000000..b52af70 --- /dev/null +++ b/fixtures/experimental-new-config/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.worker.json" } + ] +} diff --git a/fixtures/experimental-new-config/tsconfig.node.json b/fixtures/experimental-new-config/tsconfig.node.json new file mode 100644 index 0000000..78911ed --- /dev/null +++ b/fixtures/experimental-new-config/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/base.json"], + "include": ["test"] +} diff --git a/fixtures/experimental-new-config/tsconfig.worker.json b/fixtures/experimental-new-config/tsconfig.worker.json new file mode 100644 index 0000000..60c89ac --- /dev/null +++ b/fixtures/experimental-new-config/tsconfig.worker.json @@ -0,0 +1,13 @@ +{ + "extends": ["@cloudflare/workers-tsconfig/worker.json"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "types": ["@cloudflare/workers-types/experimental"] + }, + "include": [ + "src", + "cloudflare.config.ts", + "wrangler.config.ts", + "worker-configuration.d.ts" + ] +} diff --git a/fixtures/experimental-new-config/turbo.json b/fixtures/experimental-new-config/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/experimental-new-config/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/experimental-new-config/vitest.config.mts b/fixtures/experimental-new-config/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/experimental-new-config/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/experimental-new-config/worker-configuration.d.ts b/fixtures/experimental-new-config/worker-configuration.d.ts new file mode 100644 index 0000000..2e7a5f1 --- /dev/null +++ b/fixtures/experimental-new-config/worker-configuration.d.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +// Generated by @cloudflare/config +import type { InferEnv, InferDurableNamespaces, InferMainModule, UnwrapConfig } from "wrangler/experimental-config"; +import type Config from "./cloudflare.config"; + +type WorkerConfig = UnwrapConfig<typeof Config>; + +declare global { + namespace Cloudflare { + interface GlobalProps { + mainModule: InferMainModule<WorkerConfig>; + durableNamespaces: InferDurableNamespaces<WorkerConfig>; + } + interface Env extends InferEnv<WorkerConfig> {} + } + interface Env extends Cloudflare.Env {} +} diff --git a/fixtures/experimental-new-config/wrangler.config.ts b/fixtures/experimental-new-config/wrangler.config.ts new file mode 100644 index 0000000..92f77e9 --- /dev/null +++ b/fixtures/experimental-new-config/wrangler.config.ts @@ -0,0 +1,6 @@ +import { defineWranglerConfig } from "wrangler/experimental-config"; + +// Minimal empty form — exercises the optional case (no tooling overrides; +// defaults apply). Function form + non-empty configs are exercised by +// other unit tests in `packages/wrangler/src/__tests__/`. +export default defineWranglerConfig({}); diff --git a/fixtures/get-platform-proxy-remote-bindings/package.json b/fixtures/get-platform-proxy-remote-bindings/package.json new file mode 100644 index 0000000..6613796 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixture/get-platform-proxy-remote-bindings", + "private": true, + "description": "A test for the getPlatformProxy utility used with remote bindings", + "scripts": { + "test:e2e": "vitest run --reporter=default", + "type:tests": "tsc --noEmit -p tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "miniflare": "workspace:*", + "typescript": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/get-platform-proxy-remote-bindings/remote-worker.js b/fixtures/get-platform-proxy-remote-bindings/remote-worker.js new file mode 100644 index 0000000..a88e54f --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/remote-worker.js @@ -0,0 +1,7 @@ +export default { + fetch() { + return new Response( + "Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!" + ); + }, +}; diff --git a/fixtures/get-platform-proxy-remote-bindings/remote-worker.staging.js b/fixtures/get-platform-proxy-remote-bindings/remote-worker.staging.js new file mode 100644 index 0000000..94457a6 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/remote-worker.staging.js @@ -0,0 +1,7 @@ +export default { + fetch() { + return new Response( + "Hello from a remote Worker, defined for the staging environment, part of the getPlatformProxy remote bindings fixture!" + ); + }, +}; diff --git a/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts b/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts new file mode 100644 index 0000000..c0d8a50 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/tests/index.test.ts @@ -0,0 +1,360 @@ +import { execSync } from "child_process"; +import { randomUUID } from "crypto"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +/* eslint-disable workers-sdk/no-vitest-import-expect -- uses expect in helper functions and beforeAll */ +import { + afterAll, + assert, + beforeAll, + beforeEach, + describe, + expect, + test, + vi, +} from "vitest"; +/* eslint-enable workers-sdk/no-vitest-import-expect */ +import { getPlatformProxy } from "wrangler"; +import type { KVNamespace } from "@cloudflare/workers-types/experimental"; +import type { DispatchFetch, Response } from "miniflare"; + +type Fetcher = { fetch: DispatchFetch }; + +const workersDomain = + process.env.E2E_ACCOUNT_WORKERS_DEV_DOMAIN ?? + "devprod-testing7928.workers.dev"; +const auth = getAuthenticatedEnv(); +const execOptions = { + encoding: "utf8", + env: { ...process.env, ...auth }, +} as const; +const remoteWorkerName = `preserve-e2e-get-platform-proxy-remote`; +const remoteStagingWorkerName = `preserve-e2e-get-platform-proxy-remote-staging`; +const remoteKvName = `tmp-e2e-kv${Date.now()}-test-remote-bindings-${randomUUID().split("-")[0]}`; + +const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + +if (auth) { + describe("getPlatformProxy - remote bindings", { timeout: 50_000 }, () => { + let remoteKvId: string; + + beforeAll(async () => { + const deployedUrl = `https://preserve-e2e-get-platform-proxy-remote.${workersDomain}/`; + + try { + assert((await fetch(deployedUrl)).status !== 404); + } catch (e) { + execSync( + `pnpm wrangler deploy remote-worker.js --name ${remoteWorkerName} --compatibility-date 2025-06-19`, + execOptions + ); + await vi.waitFor( + async () => { + const response = await fetch(deployedUrl); + expect(response.status).toBe(200); + }, + { timeout: 5000, interval: 500 } + ); + } + + const stagingDeployedUrl = `https://preserve-e2e-get-platform-proxy-remote-staging.${workersDomain}/`; + try { + assert((await fetch(stagingDeployedUrl)).status !== 404); + } catch { + execSync( + `pnpm wrangler deploy remote-worker.staging.js --name ${remoteStagingWorkerName} --compatibility-date 2025-06-19`, + execOptions + ); + await vi.waitFor( + async () => { + const response = await fetch(stagingDeployedUrl); + expect(response.status).toBe(200); + }, + { timeout: 5000, interval: 500 } + ); + } + + const kvAddOut = execSync( + `pnpm wrangler kv namespace create ${remoteKvName}`, + execOptions + ); + + const createdKvRegexMatch = kvAddOut.match(/"id": "(?<id>[^"]*?)"/); + const maybeRemoteKvId = createdKvRegexMatch?.groups?.["id"]; + assert(maybeRemoteKvId, `Failed to create remote kv ${remoteKvName}`); + remoteKvId = maybeRemoteKvId; + + execSync( + `pnpm wrangler kv key put test-key remote-kv-value --namespace-id=${remoteKvId} --remote`, + execOptions + ); + + rmSync("./.tmp", { recursive: true, force: true }); + mkdirSync("./.tmp"); + }, 35_000); + + afterAll(() => { + try { + execSync( + `pnpm wrangler kv namespace delete --namespace-id=${remoteKvId}`, + execOptions + ); + } catch {} + + try { + rmSync("./.tmp", { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }); + } catch {} + }, 35_000); + + beforeEach(() => { + errorSpy.mockReset(); + }); + + describe("normal usage", () => { + beforeAll(async () => { + mkdirSync("./.tmp/normal-usage"); + + writeFileSync( + "./.tmp/normal-usage/wrangler.json", + JSON.stringify( + { + name: "get-platform-proxy-fixture-test", + compatibility_date: "2025-06-01", + services: [ + { + binding: "MY_WORKER", + service: remoteWorkerName, + remote: true, + }, + ], + kv_namespaces: [ + { + binding: "MY_KV", + id: remoteKvId, + remote: true, + }, + ], + env: { + staging: { + services: [ + { + binding: "MY_WORKER", + service: remoteStagingWorkerName, + remote: true, + }, + ], + kv_namespaces: [ + { + binding: "MY_KV", + id: remoteKvId, + remote: true, + }, + ], + }, + }, + }, + undefined, + 2 + ), + "utf8" + ); + }); + + test("getPlatformProxy works with remote bindings", async () => { + vi.stubEnv("CLOUDFLARE_ACCOUNT_ID", auth.CLOUDFLARE_ACCOUNT_ID); + vi.stubEnv("CLOUDFLARE_API_TOKEN", auth.CLOUDFLARE_API_TOKEN); + + const { env, dispose } = await getPlatformProxy<{ + MY_WORKER: Fetcher; + MY_KV: KVNamespace; + }>({ + configPath: "./.tmp/normal-usage/wrangler.json", + }); + + const response = await fetchFromWorker(env.MY_WORKER, "OK"); + const workerText = await response?.text(); + expect(workerText).toEqual( + "Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!" + ); + + const kvValue = await env.MY_KV.get("test-key"); + expect(kvValue).toEqual("remote-kv-value"); + + await dispose(); + }); + + test("getPlatformProxy works with remote bindings specified in an environment", async () => { + vi.stubEnv("CLOUDFLARE_ACCOUNT_ID", auth.CLOUDFLARE_ACCOUNT_ID); + vi.stubEnv("CLOUDFLARE_API_TOKEN", auth.CLOUDFLARE_API_TOKEN); + const { env, dispose } = await getPlatformProxy<{ + MY_WORKER: Fetcher; + MY_KV: KVNamespace; + }>({ + configPath: "./.tmp/normal-usage/wrangler.json", + environment: "staging", + }); + + const workerText = await ( + await env.MY_WORKER.fetch("http://example.com") + ).text(); + expect(workerText).toEqual( + "Hello from a remote Worker, defined for the staging environment, part of the getPlatformProxy remote bindings fixture!" + ); + + const kvValue = await env.MY_KV.get("test-key"); + expect(kvValue).toEqual("remote-kv-value"); + + await dispose(); + }); + }); + + describe("account id taken from the wrangler config", () => { + vi.stubEnv("CLOUDFLARE_ACCOUNT_ID", undefined); + vi.stubEnv("CLOUDFLARE_API_TOKEN", auth.CLOUDFLARE_API_TOKEN); + + test("usage with a wrangler config file with an invalid account id", async () => { + mkdirSync("./.tmp/config-with-invalid-account-id"); + + writeFileSync( + "./.tmp/config-with-invalid-account-id/wrangler.json", + JSON.stringify( + { + name: "get-platform-proxy-fixture-test", + account_id: "NOT a valid account id", + compatibility_date: "2025-06-01", + services: [ + { + binding: "MY_WORKER", + service: remoteWorkerName, + remote: true, + }, + ], + }, + undefined, + 2 + ), + "utf8" + ); + + await expect( + getPlatformProxy<{ + MY_WORKER: Fetcher; + }>({ + configPath: "./.tmp/config-with-invalid-account-id/wrangler.json", + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Failed to start the remote proxy session. Error reloading remote server: A request to the Cloudflare API (/accounts/NOT a valid account id/workers/subdomain/edge-preview) failed.]` + ); + + expect(errorSpy).toHaveBeenCalledOnce(); + expect( + `${errorSpy.mock.calls?.[0]?.[0]}` + // Windows gets a different marker for ✘, so let's normalize it here + // so that this test can be platform independent + .replaceAll("✘", "X") + ).toMatchInlineSnapshot(` + "X [ERROR] A request to the Cloudflare API (/accounts/NOT a valid account id/workers/subdomain/edge-preview) failed. + + " + `); + }); + + test("usage with a wrangler config file with a valid account id", async () => { + mkdirSync("./.tmp/config-with-no-account-id"); + + writeFileSync( + "./.tmp/config-with-no-account-id/wrangler.json", + JSON.stringify( + { + name: "get-platform-proxy-fixture-test", + account_id: auth.CLOUDFLARE_ACCOUNT_ID, + compatibility_date: "2025-06-01", + services: [ + { + binding: "MY_WORKER", + service: remoteWorkerName, + remote: true, + }, + ], + }, + undefined, + 2 + ), + "utf8" + ); + + const { env, dispose } = await getPlatformProxy<{ + MY_WORKER: Fetcher; + }>({ + configPath: "./.tmp/config-with-no-account-id/wrangler.json", + }); + + const response = await fetchFromWorker(env.MY_WORKER, "OK"); + const workerText = await response?.text(); + expect(workerText).toEqual( + "Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!" + ); + + await dispose(); + }); + }); + }); +} else { + test.skip("getPlatformProxy - remote bindings (no auth credentials)"); +} + +/** + * Tries to fetch from a worker multiple times until a response is returned which matches a specified + * statusText. Each fetch has a timeout signal making sure that it can't simply get stuck. + * + * This utility is used, instead of directly fetching from the Worker in order to prevent flakiness. + * + * @param worker The Worker to fetch from. + * @param expectedStatusText The response's expected statusText. + * @returns The successful Worker's response or null if no such response was obtained. + */ +async function fetchFromWorker( + worker: Fetcher, + expectedStatusText: string, + timeout = 30_000 +): Promise<Response | undefined> { + return vi.waitFor( + async () => { + try { + const response = await worker.fetch("http://example.com", { + signal: AbortSignal.timeout(5_000), + }); + expect(response.statusText).toEqual(expectedStatusText); + return response; + } catch {} + }, + { timeout, interval: 500 } + ); +} + +/** + * Gets an env object containing Cloudflare credentials or undefined if not authenticated. + * + * In the Github actions we convert the TEST_CLOUDFLARE_ACCOUNT_ID and TEST_CLOUDFLARE_API_TOKEN env variables. + * In local development we can rely on CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN env variables directly. + */ +function getAuthenticatedEnv() { + const CLOUDFLARE_ACCOUNT_ID = + process.env.TEST_CLOUDFLARE_ACCOUNT_ID || process.env.CLOUDFLARE_ACCOUNT_ID; + const CLOUDFLARE_API_TOKEN = + process.env.TEST_CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_API_TOKEN; + + if (CLOUDFLARE_ACCOUNT_ID && CLOUDFLARE_API_TOKEN) { + return { + CLOUDFLARE_API_TOKEN, + CLOUDFLARE_ACCOUNT_ID, + }; + } + console.warn( + "Skipping vitest-pool-workers remote bindings tests because the environment is not authenticated with Cloudflare." + ); +} diff --git a/fixtures/get-platform-proxy-remote-bindings/tests/remote-bindings-false.test.ts b/fixtures/get-platform-proxy-remote-bindings/tests/remote-bindings-false.test.ts new file mode 100644 index 0000000..a60dff2 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/tests/remote-bindings-false.test.ts @@ -0,0 +1,24 @@ +import { describe, test } from "vitest"; +import { getPlatformProxy } from "wrangler"; +import type { Ai } from "@cloudflare/workers-types/experimental"; + +describe("getPlatformProxy - remote bindings with remoteBindings: false", () => { + test("getPlatformProxy works with remote bindings", async ({ expect }) => { + const { env, dispose } = await getPlatformProxy<{ + AI: Ai; + }>({ + configPath: "./wrangler.remote-bindings-false.jsonc", + remoteBindings: false, + }); + + await expect( + env.AI.run("@cf/meta/llama-3.1-8b-instruct-fp8", { + messages: [], + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Binding AI needs to be run remotely]` + ); + + await dispose(); + }); +}); diff --git a/fixtures/get-platform-proxy-remote-bindings/tests/tsconfig.json b/fixtures/get-platform-proxy-remote-bindings/tests/tsconfig.json new file mode 100644 index 0000000..a8480e7 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["*.ts"] +} diff --git a/fixtures/get-platform-proxy-remote-bindings/turbo.json b/fixtures/get-platform-proxy-remote-bindings/turbo.json new file mode 100644 index 0000000..57926cd --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/turbo.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "test:e2e": { + "env": [ + "$TURBO_EXTENDS$", + "VITEST", + "MINIFLARE_WORKERD_PATH", + "WRANGLER", + "WRANGLER_IMPORT", + "MINIFLARE_IMPORT", + "TEST_CLOUDFLARE_ACCOUNT_ID", + "TEST_CLOUDFLARE_API_TOKEN", + "WRANGLER_E2E_TEST_FILE" + ] + } + } +} diff --git a/fixtures/get-platform-proxy-remote-bindings/wrangler.remote-bindings-false.jsonc b/fixtures/get-platform-proxy-remote-bindings/wrangler.remote-bindings-false.jsonc new file mode 100644 index 0000000..2bb0fec --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings/wrangler.remote-bindings-false.jsonc @@ -0,0 +1,8 @@ +{ + "name": "get-platform-proxy-remote-bindings-with-local-bindings-only-test", + "compatibility_date": "2025-05-07", + "ai": { + "binding": "AI", + "remote": true, + }, +} diff --git a/fixtures/get-platform-proxy/custom-toml/path/test.toml b/fixtures/get-platform-proxy/custom-toml/path/test.toml new file mode 100644 index 0000000..ef7778b --- /dev/null +++ b/fixtures/get-platform-proxy/custom-toml/path/test.toml @@ -0,0 +1,7 @@ +name = "get-bindings-proxy-fixture" +main = "src/index.ts" +compatibility_date = "2023-11-21" + +[vars] +MY_VAR = "my-var-value-from-a-custom-toml" +MY_JSON_VAR = { test = true, customToml = true } diff --git a/fixtures/get-platform-proxy/package.json b/fixtures/get-platform-proxy/package.json new file mode 100644 index 0000000..f254c9d --- /dev/null +++ b/fixtures/get-platform-proxy/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/get-platform-proxy", + "private": true, + "description": "A test for the getPlatformProxy utility", + "scripts": { + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc --noEmit -p tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@types/jest-image-snapshot": "^6.4.0", + "jest-image-snapshot": "^6.5.1", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/get-platform-proxy/public/test.txt b/fixtures/get-platform-proxy/public/test.txt new file mode 100644 index 0000000..2dd981b --- /dev/null +++ b/fixtures/get-platform-proxy/public/test.txt @@ -0,0 +1 @@ +this is a test text file! diff --git a/fixtures/get-platform-proxy/tests/__image_snapshots__/get-platform-proxy-env-test-ts-get-platform-proxy-env-correctly-obtains-functioning-image-bindings-2-snap.png b/fixtures/get-platform-proxy/tests/__image_snapshots__/get-platform-proxy-env-test-ts-get-platform-proxy-env-correctly-obtains-functioning-image-bindings-2-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..b53eecda609100b6fdcff3bfb6a8619f7f26e23a GIT binary patch literal 4092 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 G7ycg%CE}a_ literal 0 HcmV?d00001 diff --git a/fixtures/get-platform-proxy/tests/get-platform-proxy.caches.test.ts b/fixtures/get-platform-proxy/tests/get-platform-proxy.caches.test.ts new file mode 100644 index 0000000..c9aea3a --- /dev/null +++ b/fixtures/get-platform-proxy/tests/get-platform-proxy.caches.test.ts @@ -0,0 +1,71 @@ +import { Request, Response } from "undici"; +import { describe, it } from "vitest"; +import { getPlatformProxy } from "./shared"; + +describe("getPlatformProxy - caches", () => { + (["default", "named"] as const).forEach((cacheType) => + it(`correctly obtains a no-op ${cacheType} cache`, async ({ expect }) => { + const { caches, dispose } = await getPlatformProxy(); + try { + const cache = + cacheType === "default" + ? caches.default + : await caches.open("my-cache"); + + let match = await cache.match("http://0.0.0.0/test"); + expect(match).toBeUndefined(); + + const req = new Request("http://0.0.0.0/test"); + await cache.put(req, new Response("test")); + + const resp = await cache.match(req); + expect(resp).toBeUndefined(); + + const deleted = await cache.delete(req); + expect(deleted).toBe(false); + } finally { + await dispose(); + } + }) + ); + + it("should match the production runtime caches object", async ({ + expect, + }) => { + const { caches: platformProxyCaches, dispose } = await getPlatformProxy(); + const caches = platformProxyCaches as any; + try { + expect(Object.keys(caches)).toEqual(["default"]); + + expect(() => { + caches.has("my-cache"); + }).toThrow( + "Failed to execute 'has' on 'CacheStorage': the method is not implemented." + ); + + expect(() => { + caches.delete("my-cache"); + }).toThrow( + "Failed to execute 'delete' on 'CacheStorage': the method is not implemented." + ); + + expect(() => { + caches.keys(); + }).toThrow( + "Failed to execute 'keys' on 'CacheStorage': the method is not implemented." + ); + + expect(() => { + caches.match(new URL("https://localhost")); + }).toThrow( + "Failed to execute 'match' on 'CacheStorage': the method is not implemented." + ); + + expect(() => { + caches.nonExistentMethod(); + }).toThrow("caches.nonExistentMethod is not a function"); + } finally { + await dispose(); + } + }); +}); diff --git a/fixtures/get-platform-proxy/tests/get-platform-proxy.cf.test.ts b/fixtures/get-platform-proxy/tests/get-platform-proxy.cf.test.ts new file mode 100644 index 0000000..d117df7 --- /dev/null +++ b/fixtures/get-platform-proxy/tests/get-platform-proxy.cf.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "vitest"; +import { getPlatformProxy } from "./shared"; + +describe("getPlatformProxy - cf", () => { + it("should provide mock data", async ({ expect }) => { + const { cf, dispose } = await getPlatformProxy(); + try { + expect(cf).toMatchObject({ + colo: "DFW", + city: "Austin", + regionCode: "TX", + }); + } finally { + await dispose(); + } + }); + + it("should match the production runtime cf object", async ({ expect }) => { + const { cf, dispose } = await getPlatformProxy(); + try { + expect(cf.constructor.name).toBe("Object"); + + expect(() => { + cf.city = "test city"; + }).toThrow( + "Cannot assign to read only property 'city' of object '#<Object>'" + ); + expect(cf.city).not.toBe("test city"); + + expect(() => { + cf.newField = "test new field"; + }).toThrow("Cannot add property newField, object is not extensible"); + expect("newField" in cf).toBe(false); + + expect(cf.botManagement).toMatchObject({ + score: 99, + }); + expect(Object.isFrozen(cf.botManagement)).toBe(true); + } finally { + await dispose(); + } + }); +}); diff --git a/fixtures/get-platform-proxy/tests/get-platform-proxy.ctx.test.ts b/fixtures/get-platform-proxy/tests/get-platform-proxy.ctx.test.ts new file mode 100644 index 0000000..6e81128 --- /dev/null +++ b/fixtures/get-platform-proxy/tests/get-platform-proxy.ctx.test.ts @@ -0,0 +1,148 @@ +import { describe, it } from "vitest"; +import { getPlatformProxy } from "./shared"; + +describe("getPlatformProxy - ctx", () => { + it("should provide a no-op waitUntil method", async ({ expect }) => { + const { ctx, dispose } = await getPlatformProxy(); + try { + let value = 4; + ctx.waitUntil( + new Promise((resolve) => { + value++; + resolve(value); + }) + ); + expect(value).toBe(5); + } finally { + await dispose(); + } + }); + + it("should provide a no-op passThroughOnException method", async ({ + expect, + }) => { + const { ctx, dispose } = await getPlatformProxy(); + try { + expect(ctx.passThroughOnException()).toBe(undefined); + } finally { + await dispose(); + } + }); + + it("should match the production runtime ctx object", async ({ expect }) => { + const { ctx, dispose } = await getPlatformProxy(); + try { + expect(ctx.constructor.name).toBe("ExecutionContext"); + expect(typeof ctx.waitUntil).toBe("function"); + expect(typeof ctx.passThroughOnException).toBe("function"); + expect(ctx.props).toEqual({}); + + ctx.waitUntil = ((str: string) => `- ${str} -`) as any; + expect(ctx.waitUntil("waitUntil can be overridden" as any)).toBe( + "- waitUntil can be overridden -" + ); + + ctx.passThroughOnException = ((str: string) => `_ ${str} _`) as any; + expect( + (ctx.passThroughOnException as any)( + "passThroughOnException can be overridden" + ) + ).toBe("_ passThroughOnException can be overridden _"); + + (ctx as any).text = "the ExecutionContext can be extended"; + expect((ctx as any).text).toBe("the ExecutionContext can be extended"); + } finally { + await dispose(); + } + }); + + describe("detached methods should behave like workerd", () => { + it("destructured methods should throw illegal invocation errors", async ({ + expect, + }) => { + const { + ctx: { waitUntil, passThroughOnException }, + dispose, + } = await getPlatformProxy(); + try { + expect(() => { + waitUntil(new Promise(() => {})); + }).toThrow("Illegal invocation"); + + expect(() => { + passThroughOnException(); + }).toThrow("Illegal invocation"); + } finally { + await dispose(); + } + }); + + it("extracted methods should throw illegal invocation errors", async ({ + expect, + }) => { + const { ctx, dispose } = await getPlatformProxy(); + const waitUntil = ctx.waitUntil; + const passThroughOnException = ctx.passThroughOnException; + + try { + expect(() => { + waitUntil(new Promise(() => {})); + }).toThrow("Illegal invocation"); + + expect(() => { + passThroughOnException(); + }).toThrow("Illegal invocation"); + } finally { + await dispose(); + } + }); + + it("extracted methods which correctly bind this should not throw illegal invocation errors", async ({ + expect, + }) => { + const { ctx, dispose } = await getPlatformProxy(); + const waitUntil = ctx.waitUntil.bind(ctx); + const passThroughOnException = ctx.passThroughOnException; + + try { + expect(() => { + waitUntil(new Promise(() => {})); + }).not.toThrow("Illegal invocation"); + + expect(() => { + passThroughOnException.apply(ctx, []); + }).not.toThrow("Illegal invocation"); + + expect(() => { + passThroughOnException.call(ctx); + }).not.toThrow("Illegal invocation"); + } finally { + await dispose(); + } + }); + + it("extracted methods which incorrectly bind this should throw illegal invocation errors", async ({ + expect, + }) => { + const { ctx, dispose } = await getPlatformProxy(); + const waitUntil = ctx.waitUntil.bind({}); + const passThroughOnException = ctx.passThroughOnException; + + try { + expect(() => { + waitUntil(new Promise(() => {})); + }).toThrow("Illegal invocation"); + + expect(() => { + passThroughOnException.apply(5, []); + }).toThrow("Illegal invocation"); + + expect(() => { + passThroughOnException.call(new Boolean()); + }).toThrow("Illegal invocation"); + } finally { + await dispose(); + } + }); + }); +}); diff --git a/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts b/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts new file mode 100644 index 0000000..a6e02e0 --- /dev/null +++ b/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts @@ -0,0 +1,384 @@ +import path from "path"; +import { D1Database, R2Bucket } from "@cloudflare/workers-types"; +import { toMatchImageSnapshot } from "jest-image-snapshot"; +import { beforeEach, describe, it, MockInstance, vi } from "vitest"; +import { getPlatformProxy } from "./shared"; +import type { + Fetcher, + Hyperdrive, + ImagesBinding, + KVNamespace, + Workflow, +} from "@cloudflare/workers-types"; + +type Env = { + MY_VAR: string; + MY_VAR_A: string; + MY_JSON_VAR: Object; + MY_DEV_VAR: string; + MY_KV: KVNamespace; + MY_KV_PROD: KVNamespace; + MY_BUCKET: R2Bucket; + MY_D1: D1Database; + MY_HYPERDRIVE: Hyperdrive; + ASSETS: Fetcher; + IMAGES: ImagesBinding; + MY_WORKFLOW_INTERNAL: Workflow; + MY_WORKFLOW_EXTERNAL: Workflow; +}; + +const wranglerConfigFilePath = path.join(__dirname, "..", "wrangler.jsonc"); + +describe("getPlatformProxy - env", () => { + let warn = {} as MockInstance<typeof console.warn>; + + beforeEach(() => { + // Hide stdout messages from the test logs + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + warn.mockClear(); + }); + + describe("var bindings", () => { + it("correctly obtains var bindings from both wrangler config and .dev.vars", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { MY_VAR, MY_JSON_VAR, MY_DEV_VAR } = env; + expect(MY_VAR).toEqual("my-var-value"); + expect(MY_JSON_VAR).toEqual({ + test: true, + }); + expect(MY_DEV_VAR).toEqual("my-dev-var-value"); + } finally { + await dispose(); + } + }); + + it("correctly makes vars from .dev.vars override the ones in wrangler config", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { MY_VAR_A } = env; + expect(MY_VAR_A).not.toEqual("my-var-a"); // if this fails, the value was read from wrangler config – not .dev.vars + expect(MY_VAR_A).toEqual("my-dev-var-a"); + } finally { + await dispose(); + } + }); + + it("correctly makes vars from .dev.vars not override bindings of the same name from wrangler config", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { MY_KV } = env; + expect(MY_KV).not.toEqual("my-dev-kv"); + ["get", "delete", "list", "put", "getWithMetadata"].every( + (methodName) => + expect( + typeof (MY_KV as unknown as Record<string, unknown>)[methodName] + ).toBe("function") + ); + } finally { + await dispose(); + } + }); + + it("correctly reads a toml from a custom path alongside with its .dev.vars", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: path.join( + __dirname, + "..", + "custom-toml", + "path", + "test.toml" + ), + }); + try { + const { MY_VAR, MY_JSON_VAR, MY_DEV_VAR } = env; + expect(MY_VAR).toEqual("my-var-value-from-a-custom-toml"); + expect(MY_JSON_VAR).toEqual({ + test: true, + customToml: true, + }); + expect(MY_DEV_VAR).toEqual("my-dev-var-value-from-a-custom-location"); + } finally { + await dispose(); + } + }); + }); + + it("correctly reads a json config file", async ({ expect }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: path.join(__dirname, "..", "wrangler.json"), + }); + try { + const { MY_VAR, MY_JSON_VAR } = env; + expect(MY_VAR).toEqual("my-var-value-from-a-json-config-file"); + expect(MY_JSON_VAR).toEqual({ + test: true, + fromJson: true, + }); + } finally { + await dispose(); + } + }); + + it("correctly obtains functioning ASSETS bindings", async ({ expect }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + const res = await env.ASSETS.fetch("https://0.0.0.0/test.txt"); + const text = await res.text(); + expect(text).toEqual("this is a test text file!\n"); + await dispose(); + }); + + it("correctly obtains functioning KV bindings", async ({ expect }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + const { MY_KV } = env; + let numOfKeys = (await MY_KV.list()).keys.length; + expect(numOfKeys).toBe(0); + await MY_KV.put("my-key", "my-value"); + numOfKeys = (await MY_KV.list()).keys.length; + expect(numOfKeys).toBe(1); + const value = await MY_KV.get("my-key"); + expect(value).toBe("my-value"); + await dispose(); + }); + + it("correctly obtains functioning R2 bindings", async ({ expect }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { MY_BUCKET } = env; + let numOfObjects = (await MY_BUCKET.list()).objects.length; + expect(numOfObjects).toBe(0); + await MY_BUCKET.put("my-object", "my-value"); + numOfObjects = (await MY_BUCKET.list()).objects.length; + expect(numOfObjects).toBe(1); + const value = await MY_BUCKET.get("my-object"); + expect(await value?.text()).toBe("my-value"); + } finally { + await dispose(); + } + }); + + it("correctly obtains functioning D1 bindings", async ({ expect }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { MY_D1 } = env; + await MY_D1.exec( + `CREATE TABLE IF NOT EXISTS users ( id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL )` + ); + const stmt = MY_D1.prepare("insert into users (name) values (?1)"); + await MY_D1.batch([ + stmt.bind("userA"), + stmt.bind("userB"), + stmt.bind("userC"), + ]); + const { results } = await MY_D1.prepare( + "SELECT name FROM users LIMIT 5" + ).all(); + expect(results).toEqual([ + { name: "userA" }, + { name: "userB" }, + { name: "userC" }, + ]); + } finally { + await dispose(); + } + }); + + it("correctly obtains functioning Image bindings", async ({ expect }) => { + expect.extend({ toMatchImageSnapshot }); + + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { IMAGES } = env; + const streams = ( + await fetch("https://playground.devprod.cloudflare.dev/flares.png") + ).body!.tee(); + + // @ts-expect-error The stream types aren't matching up properly? + expect(await IMAGES.info(streams[0])).toMatchInlineSnapshot(` + { + "fileSize": 96549, + "format": "image/png", + "height": 1145, + "width": 2048, + } + `); + + // @ts-expect-error The stream types aren't matching up properly? + const response = await env.IMAGES.input(streams[1]) + .transform({ rotate: 90 }) + .transform({ width: 128, height: 100 }) + .transform({ blur: 20 }) + .output({ format: "image/png" }); + + expect( + Buffer.from(await response.response().arrayBuffer()) + ).toMatchImageSnapshot(); + } finally { + await dispose(); + } + }); + + // Important: the hyperdrive values are passthrough ones since the workerd specific hyperdrive values only make sense inside + // workerd itself and would simply not work in a node.js process + it("correctly obtains passthrough Hyperdrive bindings", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + const { MY_HYPERDRIVE } = env; + expect(MY_HYPERDRIVE.connectionString).toEqual( + "postgres://user:pass@127.0.0.1:1234/db" + ); + expect(MY_HYPERDRIVE.database).toEqual("db"); + expect(MY_HYPERDRIVE.host).toEqual("127.0.0.1"); + expect(MY_HYPERDRIVE.user).toEqual("user"); + expect(MY_HYPERDRIVE.password).toEqual("pass"); + expect(MY_HYPERDRIVE.port).toEqual(1234); + } finally { + await dispose(); + } + }); + + describe("DO warnings", () => { + it("warns about internal DOs and doesn't crash", async ({ expect }) => { + await getPlatformProxy<Env>({ + configPath: path.join(__dirname, "..", "wrangler_internal_do.jsonc"), + }); + expect(warn.mock.calls[0][0].replaceAll(/[\r\n]+/g, "\n")) + .toMatchInlineSnapshot(` + "▲ [WARNING]  You have defined bindings to the following internal Durable Objects: + - {"class_name":"MyDurableObject","name":"MY_DURABLE_OBJECT"} + These will not work in local development, but they should work in production. + + If you want to develop these locally, you can define your DO in a separate Worker, with a separate configuration file. + For detailed instructions, refer to the Durable Objects section here: https://developers.cloudflare.com/workers/wrangler/api#supported-bindings + " + `); + }); + + it("doesn't warn about external DOs and doesn't crash", async ({ + expect, + }) => { + await getPlatformProxy<Env>({ + configPath: path.join(__dirname, "..", "wrangler_external_do.jsonc"), + }); + expect(warn).not.toHaveBeenCalled(); + }); + + it("warns only about internal Workflows, not cross-script ones, and doesn't crash", async ({ + expect, + }) => { + await getPlatformProxy<Env>({ + configPath: path.join(__dirname, "..", "wrangler_workflow.jsonc"), + }); + // Only MY_WORKFLOW_INTERNAL (no script_name) is unsupported in + // getPlatformProxy(); MY_WORKFLOW_EXTERNAL (script_name set) is + // passed through to miniflare, which routes it via the dev-registry + // proxy to the named worker. + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][0].replaceAll(/[\r\n]+/g, "\n")) + .toMatchInlineSnapshot(` + "▲ [WARNING] You have defined bindings to the following Workflows without a script_name: + - {"binding":"MY_WORKFLOW_INTERNAL","name":"my-workflow-internal","class_name":"MyWorkflowInternal"} + These are not available in local development, so you will not be able to bind to them when testing locally, but they should work in production. + " + `); + }); + }); + + describe("with a target environment", () => { + it("should provide bindings targeting a specified environment and also inherit top-level ones", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + environment: "production", + }); + try { + expect(env.MY_VAR).not.toBe("my-var-value"); + expect(env.MY_VAR).toBe("my-PRODUCTION-var-value"); + expect(env.MY_JSON_VAR).toEqual({ test: true, production: true }); + + expect(env.MY_KV).toBeTruthy(); + expect(env.MY_KV_PROD).toBeTruthy(); + } finally { + await dispose(); + } + }); + + it("should not provide bindings targeting an environment when none was specified", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + }); + try { + expect(env.MY_VAR).not.toBe("my-PRODUCTION-var-value"); + expect(env.MY_VAR).toBe("my-var-value"); + expect(env.MY_JSON_VAR).toEqual({ test: true }); + + expect(env.MY_KV).toBeTruthy(); + expect(env.MY_KV_PROD).toBeFalsy(); + } finally { + await dispose(); + } + }); + + it("should provide secrets targeting a specified environment", async ({ + expect, + }) => { + const { env, dispose } = await getPlatformProxy<Env>({ + configPath: wranglerConfigFilePath, + environment: "production", + }); + try { + const { MY_DEV_VAR } = env; + expect(MY_DEV_VAR).not.toEqual("my-dev-var-value"); + expect(MY_DEV_VAR).toEqual("my-PRODUCTION-dev-var-value"); + } finally { + await dispose(); + } + }); + + it("should error if a non-existent environment is provided", async ({ + expect, + }) => { + await expect( + getPlatformProxy({ + configPath: wranglerConfigFilePath, + environment: "non-existent-environment", + }) + ).rejects.toThrow( + /No environment found in configuration with name "non-existent-environment"/ + ); + }); + }); +}); diff --git a/fixtures/get-platform-proxy/tests/shared.ts b/fixtures/get-platform-proxy/tests/shared.ts new file mode 100644 index 0000000..1115643 --- /dev/null +++ b/fixtures/get-platform-proxy/tests/shared.ts @@ -0,0 +1,13 @@ +import { getPlatformProxy as originalGetPlatformProxy } from "wrangler"; +import type { GetPlatformProxyOptions } from "wrangler"; + +// Here we wrap the actual original getPlatformProxy function and disable its persistance, this is to make sure +// that we don't implement any persistance during these tests (which would add unnecessary extra complexity) +export function getPlatformProxy<T>( + options: Omit<GetPlatformProxyOptions, "persist"> = {} +): ReturnType<typeof originalGetPlatformProxy<T>> { + return originalGetPlatformProxy({ + ...options, + persist: false, + }); +} diff --git a/fixtures/get-platform-proxy/tests/tsconfig.json b/fixtures/get-platform-proxy/tests/tsconfig.json new file mode 100644 index 0000000..a8480e7 --- /dev/null +++ b/fixtures/get-platform-proxy/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["*.ts"] +} diff --git a/fixtures/get-platform-proxy/vitest.config.mts b/fixtures/get-platform-proxy/vitest.config.mts new file mode 100644 index 0000000..48307cd --- /dev/null +++ b/fixtures/get-platform-proxy/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + reporters: ["default"], + testTimeout: 25_000, + hookTimeout: 25_000, + teardownTimeout: 25_000, + useAtomics: true, + }, +}); diff --git a/fixtures/get-platform-proxy/wrangler.json b/fixtures/get-platform-proxy/wrangler.json new file mode 100644 index 0000000..092b751 --- /dev/null +++ b/fixtures/get-platform-proxy/wrangler.json @@ -0,0 +1,11 @@ +{ + "name": "get-bindings-proxy-fixture", + "main": "src/index.ts", + "vars": { + "MY_VAR": "my-var-value-from-a-json-config-file", + "MY_JSON_VAR": { + "test": true, + "fromJson": true + } + } +} diff --git a/fixtures/get-platform-proxy/wrangler.jsonc b/fixtures/get-platform-proxy/wrangler.jsonc new file mode 100644 index 0000000..eb1e8d6 --- /dev/null +++ b/fixtures/get-platform-proxy/wrangler.jsonc @@ -0,0 +1,64 @@ +{ + "name": "get-bindings-proxy-fixture", + "main": "src/index.ts", + "compatibility_date": "2023-11-21", + "assets": { + "directory": "./public", + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "none", + }, + "vars": { + "MY_VAR": "my-var-value", + "MY_VAR_A": "my-var-a", + "MY_JSON_VAR": { + "test": true, + }, + }, + "images": { + "binding": "IMAGES", + }, + "kv_namespaces": [ + { + "binding": "MY_KV", + "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + }, + ], + "r2_buckets": [ + { + "binding": "MY_BUCKET", + "bucket_name": "my-bucket", + }, + ], + "hyperdrive": [ + { + "binding": "MY_HYPERDRIVE", + "id": "000000000000000000000000000000000", + "localConnectionString": "postgres://user:pass@127.0.0.1:1234/db", + }, + ], + "d1_databases": [ + { + "binding": "MY_D1", + "database_name": "test-db", + "database_id": "000000000-0000-0000-0000-000000000000", + }, + ], + "env": { + "production": { + "vars": { + "MY_VAR": "my-PRODUCTION-var-value", + "MY_JSON_VAR": { + "test": true, + "production": true, + }, + }, + "kv_namespaces": [ + { + "binding": "MY_KV_PROD", + "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + }, + ], + }, + }, +} diff --git a/fixtures/get-platform-proxy/wrangler_external_do.jsonc b/fixtures/get-platform-proxy/wrangler_external_do.jsonc new file mode 100644 index 0000000..597fd3b --- /dev/null +++ b/fixtures/get-platform-proxy/wrangler_external_do.jsonc @@ -0,0 +1,24 @@ +{ + "name": "external-do", + "main": "src/index.ts", + // external durable object = binding has script_name. This indicates that + // the DO is exported in a separate Worker called `do-worker`. For the + // purposes of testing, we don't need to set that up because + // getPlatformProxy would not be involved in running that Worker. + + "durable_objects": { + "bindings": [ + { + "class_name": "MyDurableObject", + "name": "MY_DURABLE_OBJECT", + "script_name": "do-worker", + }, + ], + }, + "migrations": [ + { + "new_sqlite_classes": ["MyDurableObject"], + "tag": "v1", + }, + ], +} diff --git a/fixtures/get-platform-proxy/wrangler_internal_do.jsonc b/fixtures/get-platform-proxy/wrangler_internal_do.jsonc new file mode 100644 index 0000000..f36328c --- /dev/null +++ b/fixtures/get-platform-proxy/wrangler_internal_do.jsonc @@ -0,0 +1,23 @@ +{ + "name": "internal-do", + "main": "src/index.ts", + // Internal durable object = the binding does not specify a script name. + // This implies the DO is exported alongside this worker in `index.ts`, + // which it isn't actually. However we don't care about this here because + // getPlatformProxy will discard all user code anyway. We are simply making + // sure the warning shows up. + "durable_objects": { + "bindings": [ + { + "class_name": "MyDurableObject", + "name": "MY_DURABLE_OBJECT", + }, + ], + }, + "migrations": [ + { + "new_sqlite_classes": ["MyDurableObject"], + "tag": "v1", + }, + ], +} diff --git a/fixtures/get-platform-proxy/wrangler_workflow.jsonc b/fixtures/get-platform-proxy/wrangler_workflow.jsonc new file mode 100644 index 0000000..d636acc --- /dev/null +++ b/fixtures/get-platform-proxy/wrangler_workflow.jsonc @@ -0,0 +1,22 @@ +{ + "name": "workflow-test", + "main": "src/index.ts", + // An internal workflow is indicated by the binding not specifying a script name. + // This implies the workflow is exported alongside this worker in `index.ts`, which it isn't actually. + // However we don't care about this here because `getPlatformProxy()` will discard all user code anyway. + // We are simply making sure the warning shows up. + "compatibility_date": "2023-11-21", + "workflows": [ + { + "binding": "MY_WORKFLOW_INTERNAL", + "name": "my-workflow-internal", + "class_name": "MyWorkflowInternal", + }, + { + "binding": "MY_WORKFLOW_EXTERNAL", + "name": "my-workflow-external", + "class_name": "MyWorkflowExternal", + "script_name": "OtherWorker", + }, + ], +} diff --git a/fixtures/import-npm/.gitignore b/fixtures/import-npm/.gitignore new file mode 100644 index 0000000..d8b83df --- /dev/null +++ b/fixtures/import-npm/.gitignore @@ -0,0 +1 @@ +package-lock.json diff --git a/fixtures/import-npm/README.md b/fixtures/import-npm/README.md new file mode 100644 index 0000000..2db0d93 --- /dev/null +++ b/fixtures/import-npm/README.md @@ -0,0 +1,3 @@ +# import-npm example + +This is an npm workspace within pnpm in order to test dependencies managed through npm can also be resolved by wrangler diff --git a/fixtures/import-npm/package.json b/fixtures/import-npm/package.json new file mode 100644 index 0000000..8755361 --- /dev/null +++ b/fixtures/import-npm/package.json @@ -0,0 +1,16 @@ +{ + "name": "@fixture/import-npm", + "private": true, + "description": "", + "author": "", + "workspaces": [ + "packages/*" + ], + "scripts": { + "_clean_install": "rm -rf node_modules && npm install --no-package-lock", + "check:type": "npm run check:type --workspaces", + "test:ci": "npm run test:ci --workspaces", + "test:watch": "npm run test:watch --workspaces", + "type:tests": "npm run type:tests --workspaces" + } +} diff --git a/fixtures/import-npm/packages/import-example/package.json b/fixtures/import-npm/packages/import-example/package.json new file mode 100644 index 0000000..4892ef0 --- /dev/null +++ b/fixtures/import-npm/packages/import-example/package.json @@ -0,0 +1,21 @@ +{ + "name": "@cloudflare/import-npm-fixture-import-example", + "private": true, + "description": "", + "author": "", + "main": "src/index.js", + "scripts": { + "check:type": "tsc", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "@fixture/import-wasm-static": "../../../../fixtures/import-wasm-static" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "../../../../packages/workers-tsconfig", + "undici": "^7.18.2", + "wrangler": "../../../../packages/wrangler" + } +} diff --git a/fixtures/import-npm/packages/import-example/src/index.js b/fixtures/import-npm/packages/import-example/src/index.js new file mode 100644 index 0000000..bf5533d --- /dev/null +++ b/fixtures/import-npm/packages/import-example/src/index.js @@ -0,0 +1,19 @@ +// this is from the `import-wasm-static` fixture defined above +// and setup inside package.json to mimic an npm package +import multiply from "@fixture/import-wasm-static/multiply.wasm"; +import otherMultiple from "@fixture/import-wasm-static/wasm/not-exported.wasm"; + +export default { + async fetch(request) { + // just instantiate and return something + // we're really just testing the imports at the top of this file + const multiplyModule = await WebAssembly.instantiate(multiply); + const otherModule = await WebAssembly.instantiate(otherMultiple); + + const results = [ + multiplyModule.exports.multiply(7, 3), + otherModule.exports.multiply(7, 3), + ]; + return new Response(results.join(", ")); + }, +}; diff --git a/fixtures/import-npm/packages/import-example/tests/index.test.ts b/fixtures/import-npm/packages/import-example/tests/index.test.ts new file mode 100644 index 0000000..2d4c185 --- /dev/null +++ b/fixtures/import-npm/packages/import-example/tests/index.test.ts @@ -0,0 +1,25 @@ +import { resolve } from "path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { createTestHarness } from "wrangler"; + +describe("wrangler correctly imports wasm files with npm resolution", () => { + const server = createTestHarness({ + root: resolve(__dirname, ".."), + workers: [{ configPath: "wrangler.jsonc" }], + }); + + beforeAll(async () => { + await server.listen(); + }); + + afterAll(async () => { + await server.close(); + }); + + // if the worker compiles, is running, and returns 21 (7 * 3) we can assume that the wasm module was imported correctly + it("responds", async ({ expect }) => { + const response = await server.fetch("/"); + const text = await response.text(); + expect(text).toBe("21, 21"); + }); +}); diff --git a/fixtures/import-npm/packages/import-example/tests/tsconfig.json b/fixtures/import-npm/packages/import-example/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/import-npm/packages/import-example/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/import-npm/packages/import-example/tsconfig.json b/fixtures/import-npm/packages/import-example/tsconfig.json new file mode 100644 index 0000000..65a5ca7 --- /dev/null +++ b/fixtures/import-npm/packages/import-example/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/import-npm/packages/import-example/vitest.config.mts b/fixtures/import-npm/packages/import-example/vitest.config.mts new file mode 100644 index 0000000..12e6a14 --- /dev/null +++ b/fixtures/import-npm/packages/import-example/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/import-npm/packages/import-example/wrangler.jsonc b/fixtures/import-npm/packages/import-example/wrangler.jsonc new file mode 100644 index 0000000..5b0f25d --- /dev/null +++ b/fixtures/import-npm/packages/import-example/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "import-example", + "compatibility_date": "2024-10-31", + "main": "src/index.js", +} diff --git a/fixtures/import-npm/turbo.json b/fixtures/import-npm/turbo.json new file mode 100644 index 0000000..ea3755d --- /dev/null +++ b/fixtures/import-npm/turbo.json @@ -0,0 +1,21 @@ +{ + "extends": ["//"], + "tasks": { + "_clean_install": { + "dependsOn": ["wrangler#build"], + "outputs": ["node_modules"] + }, + "check:type": { + "dependsOn": ["_clean_install"] + }, + "test:watch": { + "dependsOn": ["_clean_install"] + }, + "type:tests": { + "dependsOn": ["_clean_install"] + }, + "test:ci": { + "dependsOn": ["_clean_install"] + } + } +} diff --git a/fixtures/import-wasm-example/.gitignore b/fixtures/import-wasm-example/.gitignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/fixtures/import-wasm-example/.gitignore @@ -0,0 +1 @@ +dist diff --git a/fixtures/import-wasm-example/README.md b/fixtures/import-wasm-example/README.md new file mode 100644 index 0000000..565bd3c --- /dev/null +++ b/fixtures/import-wasm-example/README.md @@ -0,0 +1,4 @@ +# import-wasm-example + +`import-wasm-example` is a test fixture that imports a `wasm` file from `import-wasm-static`, testing npm module resolution with wrangler imports. +It also imports a file that isn't technically exported from `import-wasm-static`, testing more traditional node module resolution. diff --git a/fixtures/import-wasm-example/package.json b/fixtures/import-wasm-example/package.json new file mode 100644 index 0000000..1c3023c --- /dev/null +++ b/fixtures/import-wasm-example/package.json @@ -0,0 +1,23 @@ +{ + "name": "@fixture/import-wasm", + "private": true, + "description": "", + "author": "", + "main": "src/index.js", + "scripts": { + "check:type": "tsc", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "@fixture/import-wasm-static": "workspace:^" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/import-wasm-example/src/index.js b/fixtures/import-wasm-example/src/index.js new file mode 100644 index 0000000..bf5533d --- /dev/null +++ b/fixtures/import-wasm-example/src/index.js @@ -0,0 +1,19 @@ +// this is from the `import-wasm-static` fixture defined above +// and setup inside package.json to mimic an npm package +import multiply from "@fixture/import-wasm-static/multiply.wasm"; +import otherMultiple from "@fixture/import-wasm-static/wasm/not-exported.wasm"; + +export default { + async fetch(request) { + // just instantiate and return something + // we're really just testing the imports at the top of this file + const multiplyModule = await WebAssembly.instantiate(multiply); + const otherModule = await WebAssembly.instantiate(otherMultiple); + + const results = [ + multiplyModule.exports.multiply(7, 3), + otherModule.exports.multiply(7, 3), + ]; + return new Response(results.join(", ")); + }, +}; diff --git a/fixtures/import-wasm-example/tests/index.test.ts b/fixtures/import-wasm-example/tests/index.test.ts new file mode 100644 index 0000000..2d4c185 --- /dev/null +++ b/fixtures/import-wasm-example/tests/index.test.ts @@ -0,0 +1,25 @@ +import { resolve } from "path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { createTestHarness } from "wrangler"; + +describe("wrangler correctly imports wasm files with npm resolution", () => { + const server = createTestHarness({ + root: resolve(__dirname, ".."), + workers: [{ configPath: "wrangler.jsonc" }], + }); + + beforeAll(async () => { + await server.listen(); + }); + + afterAll(async () => { + await server.close(); + }); + + // if the worker compiles, is running, and returns 21 (7 * 3) we can assume that the wasm module was imported correctly + it("responds", async ({ expect }) => { + const response = await server.fetch("/"); + const text = await response.text(); + expect(text).toBe("21, 21"); + }); +}); diff --git a/fixtures/import-wasm-example/tests/tsconfig.json b/fixtures/import-wasm-example/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/import-wasm-example/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/import-wasm-example/tsconfig.json b/fixtures/import-wasm-example/tsconfig.json new file mode 100644 index 0000000..65a5ca7 --- /dev/null +++ b/fixtures/import-wasm-example/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/import-wasm-example/vitest.config.mts b/fixtures/import-wasm-example/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/import-wasm-example/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/import-wasm-example/wrangler.jsonc b/fixtures/import-wasm-example/wrangler.jsonc new file mode 100644 index 0000000..745c4cb --- /dev/null +++ b/fixtures/import-wasm-example/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "import-wasm-example", + "compatibility_date": "2023-10-02", + "main": "src/index.js", +} diff --git a/fixtures/import-wasm-static/README.md b/fixtures/import-wasm-static/README.md new file mode 100644 index 0000000..71f461e --- /dev/null +++ b/fixtures/import-wasm-static/README.md @@ -0,0 +1,5 @@ +# import-wasm-static + +`import-wasm-static` is a fixture that simply exports a `wasm` file via `package.json` exports to be used and imported in other fixtures, to test npm module resolution. + +It also provides a `not-exported.wasm` file that is not exported via `package.json` exports, to test node module resolution. diff --git a/fixtures/import-wasm-static/package.json b/fixtures/import-wasm-static/package.json new file mode 100644 index 0000000..92315ab --- /dev/null +++ b/fixtures/import-wasm-static/package.json @@ -0,0 +1,8 @@ +{ + "name": "@fixture/import-wasm-static", + "private": true, + "sideEffects": false, + "exports": { + "./multiply.wasm": "./wasm/multiply.wasm" + } +} diff --git a/fixtures/import-wasm-static/wasm/multiply.wasm b/fixtures/import-wasm-static/wasm/multiply.wasm new file mode 100644 index 0000000000000000000000000000000000000000..0fbf9272721fa9c854cd555878d6f6b10e474195 GIT binary patch literal 46 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2Pu=V9c?EzK#(EXb*3VBq3pWM@!dP+-jA1^~GP B2cZA} literal 0 HcmV?d00001 diff --git a/fixtures/import-wasm-static/wasm/multiply.wat b/fixtures/import-wasm-static/wasm/multiply.wat new file mode 100644 index 0000000..064fbe7 --- /dev/null +++ b/fixtures/import-wasm-static/wasm/multiply.wat @@ -0,0 +1,7 @@ +(module + (func $multiply (param $p1 i32) (param $p2 i32) (result i32) + local.get $p1 + local.get $p2 + i32.mul) + (export "multiply" (func $multiply)) +) diff --git a/fixtures/import-wasm-static/wasm/not-exported.wasm b/fixtures/import-wasm-static/wasm/not-exported.wasm new file mode 100644 index 0000000000000000000000000000000000000000..0fbf9272721fa9c854cd555878d6f6b10e474195 GIT binary patch literal 46 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2Pu=V9c?EzK#(EXb*3VBq3pWM@!dP+-jA1^~GP B2cZA} literal 0 HcmV?d00001 diff --git a/fixtures/import-wasm-static/wasm/not-exported.wat b/fixtures/import-wasm-static/wasm/not-exported.wat new file mode 100644 index 0000000..064fbe7 --- /dev/null +++ b/fixtures/import-wasm-static/wasm/not-exported.wat @@ -0,0 +1,7 @@ +(module + (func $multiply (param $p1 i32) (param $p2 i32) (result i32) + local.get $p1 + local.get $p2 + i32.mul) + (export "multiply" (func $multiply)) +) diff --git a/fixtures/interactive-dev-tests/README.md b/fixtures/interactive-dev-tests/README.md new file mode 100644 index 0000000..a52158a --- /dev/null +++ b/fixtures/interactive-dev-tests/README.md @@ -0,0 +1,3 @@ +# `interactive-dev-tests` + +This package contains tests for interactive `wrangler dev` sessions running in a pseudo-TTY. These have slightly different behaviour to non-interactive sessions tested by other fixtures. diff --git a/fixtures/interactive-dev-tests/container-app/Dockerfile b/fixtures/interactive-dev-tests/container-app/Dockerfile new file mode 100644 index 0000000..eaeb93f --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app", "version": "1.0.0", "dependencies": {"ws": "^8.0.0"}}' > package.json +RUN npm install + +COPY ./container/simple-node-app.js app.js +EXPOSE 8080 diff --git a/fixtures/interactive-dev-tests/container-app/DockerfileWithLongSleep b/fixtures/interactive-dev-tests/container-app/DockerfileWithLongSleep new file mode 100644 index 0000000..c2fe7c6 --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/DockerfileWithLongSleep @@ -0,0 +1,6 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app + +RUN echo 'This (no-op) build takes forever...' +RUN sleep 50000 diff --git a/fixtures/interactive-dev-tests/container-app/container/simple-node-app.js b/fixtures/interactive-dev-tests/container-app/container/simple-node-app.js new file mode 100644 index 0000000..fd5e60e --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/container/simple-node-app.js @@ -0,0 +1,49 @@ +const { createServer } = require("http"); + +const webSocketEnabled = process.env.WS_ENABLED === "true"; + +// Create HTTP server +const server = createServer(function (req, res) { + if (req.url === "/ws") { + // WebSocket upgrade will be handled by the WebSocket server + return; + } + + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("Hello World! Have an env var! " + process.env.MESSAGE); + res.end(); +}); + +// Check if WebSocket functionality is enabled +if (webSocketEnabled) { + const WebSocket = require("ws"); + + // Create WebSocket server + const wss = new WebSocket.Server({ + server: server, + path: "/ws", + }); + + wss.on("connection", function connection(ws) { + console.log("WebSocket connection established"); + + ws.on("message", function message(data) { + console.log("Received:", data.toString()); + // Echo the message back with prefix + ws.send("Echo: " + data.toString()); + }); + + ws.on("close", function close() { + console.log("WebSocket connection closed"); + }); + + ws.on("error", console.error); + }); +} + +server.listen(8080, function () { + console.log("Server listening on port 8080"); + if (webSocketEnabled) { + console.log("WebSocket support enabled"); + } +}); diff --git a/fixtures/interactive-dev-tests/container-app/src/index.ts b/fixtures/interactive-dev-tests/container-app/src/index.ts new file mode 100644 index 0000000..9b42281 --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/src/index.ts @@ -0,0 +1,67 @@ +import { DurableObject } from "cloudflare:workers"; + +export class FixtureTestContainer extends DurableObject<Env> { + container: globalThis.Container; + monitor?: Promise<unknown>; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.container = ctx.container; + } + + async fetch(req: Request) { + const path = new URL(req.url).pathname; + switch (path) { + case "/status": + return new Response(JSON.stringify(this.container.running)); + + case "/destroy": + if (!this.container.running) { + throw new Error("Container is not running."); + } + await this.container.destroy(); + return new Response(JSON.stringify(this.container.running)); + + case "/start": + this.container.start({ + entrypoint: ["node", "app.js"], + env: { MESSAGE: "I'm an env var!" }, + enableInternet: false, + }); + // this doesn't instantly start, so we will need to poll /fetch + return new Response("Container create request sent..."); + + case "/fetch": + const res = await this.container + .getTcpPort(8080) + // actual request doesn't matter + .fetch("http://foo/bar/baz", { method: "POST", body: "hello" }); + return new Response(await res.text()); + + case "/destroy-with-monitor": + const monitor = this.container.monitor(); + await this.container.destroy(); + await monitor; + return new Response("Container destroyed with monitor."); + + default: + return new Response("Hi from Container DO"); + } + } +} + +export default { + async fetch(request, env): Promise<Response> { + const url = new URL(request.url); + if (url.pathname === "/second") { + // This is a second Durable Object that can be used to test multiple DOs + const id = env.CONTAINER.idFromName("second-container"); + const stub = env.CONTAINER.get(id); + const query = url.searchParams.get("req"); + return stub.fetch("http://example.com/" + query); + } + const id = env.CONTAINER.idFromName("container"); + const stub = env.CONTAINER.get(id); + return stub.fetch(request); + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/interactive-dev-tests/container-app/src/tsconfig.json b/fixtures/interactive-dev-tests/container-app/src/tsconfig.json new file mode 100644 index 0000000..e4a18bc --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/interactive-dev-tests/container-app/src/worker-configuration.d.ts b/fixtures/interactive-dev-tests/container-app/src/worker-configuration.d.ts new file mode 100644 index 0000000..fd1bb2f --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/src/worker-configuration.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./container-app/src/worker-configuration.d.ts -c ./container-app/wrangler.jsonc --no-include-runtime` (hash: 3e9f96a73efdcd8aacad1235e0761dab) +interface __BaseEnv_Env { + CONTAINER: DurableObjectNamespace<import("./index").FixtureTestContainer>; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "FixtureTestContainer"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/interactive-dev-tests/container-app/wrangler.jsonc b/fixtures/interactive-dev-tests/container-app/wrangler.jsonc new file mode 100644 index 0000000..3a2d79b --- /dev/null +++ b/fixtures/interactive-dev-tests/container-app/wrangler.jsonc @@ -0,0 +1,27 @@ +{ + "name": "container-app", + "main": "src/index.ts", + "compatibility_date": "2025-04-03", + "containers": [ + { + "image": "./Dockerfile", + "class_name": "FixtureTestContainer", + "name": "container", + "max_instances": 2, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "FixtureTestContainer", + "name": "CONTAINER", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["FixtureTestContainer"], + }, + ], +} diff --git a/fixtures/interactive-dev-tests/index.js b/fixtures/interactive-dev-tests/index.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/fixtures/interactive-dev-tests/index.js @@ -0,0 +1 @@ +export default {}; diff --git a/fixtures/interactive-dev-tests/multi-containers-app/DockerfileA b/fixtures/interactive-dev-tests/multi-containers-app/DockerfileA new file mode 100644 index 0000000..47b7031 --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-containers-app/DockerfileA @@ -0,0 +1,20 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app-a", "version": "1.0.0"}' > package.json +RUN npm install + +RUN sleep 1 + +RUN echo 'const { createServer } = require("http");\ +\ +const server = createServer(function (req, res) {\ + res.writeHead(200, { "Content-Type": "text/plain" });\ + res.write("Hello from Container A");\ + res.end();\ +});\ +\ +server.listen(8080);\ +' > app.js + +EXPOSE 8080 diff --git a/fixtures/interactive-dev-tests/multi-containers-app/DockerfileB b/fixtures/interactive-dev-tests/multi-containers-app/DockerfileB new file mode 100644 index 0000000..f6aa17b --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-containers-app/DockerfileB @@ -0,0 +1,20 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app-b", "version": "1.0.0"}' > package.json +RUN npm install + +RUN sleep 1 + +RUN echo 'const { createServer } = require("http");\ +\ +const server = createServer(function (req, res) {\ + res.writeHead(200, { "Content-Type": "text/plain" });\ + res.write("Hello from Container B");\ + res.end();\ +});\ +\ +server.listen(8080);\ +' > app.js + +EXPOSE 8080 diff --git a/fixtures/interactive-dev-tests/multi-containers-app/src/index.ts b/fixtures/interactive-dev-tests/multi-containers-app/src/index.ts new file mode 100644 index 0000000..262872a --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-containers-app/src/index.ts @@ -0,0 +1,49 @@ +import { DurableObject } from "cloudflare:workers"; + +class FixtureTestContainerBase extends DurableObject<Env> { + container: globalThis.Container; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.container = ctx.container; + } + + async fetch(req: Request) { + if (!this.container.running) { + this.container.start({ + entrypoint: ["node", "app.js"], + enableInternet: false, + }); + // On the first request we simply start the container and return, + // on the following requests the container can actually be accessed. + // Note that we do this this way because container.start is not awaitable + // meaning that we can't simply wait here for the container to be ready + return new Response("Container started"); + } + return this.container + .getTcpPort(8080) + .fetch("http://foo/bar/baz", { method: "POST", body: "hello" }); + } +} + +export class FixtureTestContainerA extends FixtureTestContainerBase {} +export class FixtureTestContainerB extends FixtureTestContainerBase {} + +export default { + async fetch(request, env): Promise<Response> { + const getContainerText = async ( + container: "CONTAINER_A" | "CONTAINER_B" + ) => { + const id = env[container].idFromName("container"); + const stub = env[container].get(id); + return await (await stub.fetch(request)).text(); + }; + const containerAText = await getContainerText("CONTAINER_A"); + const containerBText = await getContainerText("CONTAINER_B"); + return new Response( + `Response from A: "${containerAText}"` + + " " + + `Response from B: "${containerBText}"` + ); + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/interactive-dev-tests/multi-containers-app/src/tsconfig.json b/fixtures/interactive-dev-tests/multi-containers-app/src/tsconfig.json new file mode 100644 index 0000000..e4a18bc --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-containers-app/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/interactive-dev-tests/multi-containers-app/src/worker-configuration.d.ts b/fixtures/interactive-dev-tests/multi-containers-app/src/worker-configuration.d.ts new file mode 100644 index 0000000..dfc01f3 --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-containers-app/src/worker-configuration.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./multi-containers-app/src/worker-configuration.d.ts -c ./multi-containers-app/wrangler.jsonc --no-include-runtime` (hash: bbb41fc3f9d6ad4abf178ab91cb66515) +interface __BaseEnv_Env { + CONTAINER_A: DurableObjectNamespace<import("./index").FixtureTestContainerA>; + CONTAINER_B: DurableObjectNamespace<import("./index").FixtureTestContainerB>; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "FixtureTestContainerA" | "FixtureTestContainerB"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/interactive-dev-tests/multi-containers-app/wrangler.jsonc b/fixtures/interactive-dev-tests/multi-containers-app/wrangler.jsonc new file mode 100644 index 0000000..e198197 --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-containers-app/wrangler.jsonc @@ -0,0 +1,41 @@ +{ + "name": "multi-containers-app", + "main": "src/index.ts", + "compatibility_date": "2025-04-03", + "containers": [ + { + "image": "./DockerfileA", + "class_name": "FixtureTestContainerA", + "name": "containerA", + "max_instances": 2, + }, + { + "image": "./DockerfileB", + "class_name": "FixtureTestContainerB", + "name": "containerB", + "max_instances": 2, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "FixtureTestContainerA", + "name": "CONTAINER_A", + }, + { + "class_name": "FixtureTestContainerB", + "name": "CONTAINER_B", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["FixtureTestContainerA"], + }, + { + "tag": "v1", + "new_sqlite_classes": ["FixtureTestContainerB"], + }, + ], +} diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/tsconfig.json b/fixtures/interactive-dev-tests/multi-workers-containers-app/tsconfig.json new file mode 100644 index 0000000..cee17af --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/Dockerfile b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/Dockerfile new file mode 100644 index 0000000..80908ad --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app", "version": "1.0.0"}' > package.json +RUN npm install + +RUN echo 'const { createServer } = require("http");\ +\ +const server = createServer(function (req, res) {\ + res.writeHead(200, { "Content-Type": "text/plain" });\ + res.write("Hello from Container A");\ + res.end();\ +});\ +\ +server.listen(8080);\ +' > app.js + +EXPOSE 8080 diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/index.ts b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/index.ts new file mode 100644 index 0000000..cf8177b --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/index.ts @@ -0,0 +1,38 @@ +import { DurableObject } from "cloudflare:workers"; + +export class FixtureTestContainerA extends DurableObject<Env> { + container: globalThis.Container; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.container = ctx.container; + } + + async fetch(req: Request) { + if (!this.container.running) { + this.container.start({ + entrypoint: ["node", "app.js"], + enableInternet: false, + }); + // On the first request we simply start the container and return, + // on the following requests the container can actually be accessed. + // Note that we do this this way because container.start is not awaitable + // meaning that we can't simply wait here for the container to be ready + return new Response("Container started"); + } + return this.container + .getTcpPort(8080) + .fetch("http://foo/bar/baz", { method: "POST", body: "hello" }); + } +} + +export default { + async fetch(request, env): Promise<Response> { + const id = env.CONTAINER_A.idFromName("container"); + const stub = env.CONTAINER_A.get(id); + return Response.json({ + containerAText: await (await stub.fetch(request)).text(), + containerBText: await (await env.WORKER_B.fetch(request)).text(), + }); + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/worker-configuration.d.ts b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/worker-configuration.d.ts new file mode 100644 index 0000000..63993be --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/worker-configuration.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./multi-workers-containers-app/workerA/worker-configuration.d.ts -c ./multi-workers-containers-app/workerA/wrangler.jsonc --no-include-runtime` (hash: e414ffcd6be2e270f12de677a3fccaaf) +interface __BaseEnv_Env { + CONTAINER_A: DurableObjectNamespace<import("./index").FixtureTestContainerA>; + WORKER_B: Fetcher /* worker-b */; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "FixtureTestContainerA"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/wrangler.jsonc b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/wrangler.jsonc new file mode 100644 index 0000000..0ea4daa --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerA/wrangler.jsonc @@ -0,0 +1,28 @@ +{ + "name": "worker-a", + "main": "index.ts", + "compatibility_date": "2025-04-03", + "services": [{ "binding": "WORKER_B", "service": "worker-b" }], + "containers": [ + { + "image": "./Dockerfile", + "class_name": "FixtureTestContainerA", + "name": "container", + "max_instances": 2, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "FixtureTestContainerA", + "name": "CONTAINER_A", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["FixtureTestContainerA"], + }, + ], +} diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/Dockerfile b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/Dockerfile new file mode 100644 index 0000000..3e06448 --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app", "version": "1.0.0"}' > package.json +RUN npm install + +RUN echo 'const { createServer } = require("http");\ +\ +const server = createServer(function (req, res) {\ + res.writeHead(200, { "Content-Type": "text/plain" });\ + res.write("Hello from Container B");\ + res.end();\ +});\ +\ +server.listen(8080);\ +' > app.js + +EXPOSE 8080 diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/index.ts b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/index.ts new file mode 100644 index 0000000..9e300cd --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/index.ts @@ -0,0 +1,35 @@ +import { DurableObject } from "cloudflare:workers"; + +export class FixtureTestContainerB extends DurableObject<Env> { + container: globalThis.Container; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.container = ctx.container; + } + + async fetch(req: Request) { + if (!this.container.running) { + this.container.start({ + entrypoint: ["node", "app.js"], + enableInternet: false, + }); + // On the first request we simply start the container and return, + // on the following requests the container can actually be accessed. + // Note that we do this this way because container.start is not awaitable + // meaning that we can't simply wait here for the container to be ready + return new Response("Container started"); + } + return this.container + .getTcpPort(8080) + .fetch("http://foo/bar/baz", { method: "POST", body: "hello" }); + } +} + +export default { + async fetch(request, env): Promise<Response> { + const id = env.CONTAINER_B.idFromName("container"); + const stub = env.CONTAINER_B.get(id); + return stub.fetch(request); + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/worker-configuration.d.ts b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/worker-configuration.d.ts new file mode 100644 index 0000000..c97ba41 --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/worker-configuration.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./multi-workers-containers-app/workerB/worker-configuration.d.ts -c ./multi-workers-containers-app/workerB/wrangler.jsonc --no-include-runtime` (hash: ffbf775a9ee9c3d83577a68a6b563542) +interface __BaseEnv_Env { + CONTAINER_B: DurableObjectNamespace<import("./index").FixtureTestContainerB>; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "FixtureTestContainerB"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/wrangler.jsonc b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/wrangler.jsonc new file mode 100644 index 0000000..8af9d53 --- /dev/null +++ b/fixtures/interactive-dev-tests/multi-workers-containers-app/workerB/wrangler.jsonc @@ -0,0 +1,27 @@ +{ + "name": "worker-b", + "main": "index.ts", + "compatibility_date": "2025-04-03", + "containers": [ + { + "image": "./Dockerfile", + "class_name": "FixtureTestContainerB", + "name": "container", + "max_instances": 2, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "FixtureTestContainerB", + "name": "CONTAINER_B", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_classes": ["FixtureTestContainerB"], + }, + ], +} diff --git a/fixtures/interactive-dev-tests/package.json b/fixtures/interactive-dev-tests/package.json new file mode 100644 index 0000000..104aed1 --- /dev/null +++ b/fixtures/interactive-dev-tests/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/interactive-dev", + "private": true, + "scripts": { + "cf-typegen": "pnpm run \"/^typegen:.*/\"", + "test:ci": "vitest run", + "test:watch": "vitest", + "typegen:container-app": "wrangler types ./container-app/src/worker-configuration.d.ts -c ./container-app/wrangler.jsonc --no-include-runtime", + "typegen:multi-containers-app": "wrangler types ./multi-containers-app/src/worker-configuration.d.ts -c ./multi-containers-app/wrangler.jsonc --no-include-runtime", + "typegen:multi-workers-containers-app": "wrangler types ./multi-workers-containers-app/workerA/worker-configuration.d.ts -c ./multi-workers-containers-app/workerA/wrangler.jsonc --no-include-runtime && wrangler types ./multi-workers-containers-app/workerB/worker-configuration.d.ts -c ./multi-workers-containers-app/workerB/wrangler.jsonc --no-include-runtime" + }, + "devDependencies": { + "@fixture/shared": "workspace:*", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + }, + "optionalDependencies": { + "@cdktf/node-pty-prebuilt-multiarch": "0.10.2" + } +} diff --git a/fixtures/interactive-dev-tests/public/index.html b/fixtures/interactive-dev-tests/public/index.html new file mode 100644 index 0000000..68fea10 --- /dev/null +++ b/fixtures/interactive-dev-tests/public/index.html @@ -0,0 +1 @@ +<p>body</p> diff --git a/fixtures/interactive-dev-tests/src/index.mjs b/fixtures/interactive-dev-tests/src/index.mjs new file mode 100644 index 0000000..0ef27d4 --- /dev/null +++ b/fixtures/interactive-dev-tests/src/index.mjs @@ -0,0 +1,5 @@ +export default { + async fetch() { + return new Response("body"); + }, +}; diff --git a/fixtures/interactive-dev-tests/src/startup-error.ts b/fixtures/interactive-dev-tests/src/startup-error.ts new file mode 100644 index 0000000..5837167 --- /dev/null +++ b/fixtures/interactive-dev-tests/src/startup-error.ts @@ -0,0 +1,5 @@ +export default { + async fetch(): Promise<Response { + return new Response("body"); + }, +}; diff --git a/fixtures/interactive-dev-tests/src/worker-a.mjs b/fixtures/interactive-dev-tests/src/worker-a.mjs new file mode 100644 index 0000000..6b48cb7 --- /dev/null +++ b/fixtures/interactive-dev-tests/src/worker-a.mjs @@ -0,0 +1,7 @@ +export default { + async fetch(req, env) { + return new Response( + "hello from a & " + (await env.WORKER.fetch(req).then((r) => r.text())) + ); + }, +}; diff --git a/fixtures/interactive-dev-tests/src/worker-b.mjs b/fixtures/interactive-dev-tests/src/worker-b.mjs new file mode 100644 index 0000000..5d190a0 --- /dev/null +++ b/fixtures/interactive-dev-tests/src/worker-b.mjs @@ -0,0 +1,5 @@ +export default { + async fetch() { + return new Response("hello from b"); + }, +}; diff --git a/fixtures/interactive-dev-tests/tests/index.test.ts b/fixtures/interactive-dev-tests/tests/index.test.ts new file mode 100644 index 0000000..a066e3a --- /dev/null +++ b/fixtures/interactive-dev-tests/tests/index.test.ts @@ -0,0 +1,943 @@ +import childProcess, { execSync } from "node:child_process"; +import fs from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import rl from "node:readline"; +import stream from "node:stream"; +import { stripVTControlCharacters } from "node:util"; +import { removeDir } from "@fixture/shared/src/fs-helpers"; +import { fetch } from "undici"; +/* eslint-disable workers-sdk/no-vitest-import-expect -- complex test with .each patterns */ +import { + afterAll, + afterEach, + assert, + describe as baseDescribe, + beforeAll, + expect, + it, + vi, +} from "vitest"; +/* eslint-enable workers-sdk/no-vitest-import-expect */ +import { wranglerEntryPath } from "../../shared/src/run-wrangler-long-lived"; +import type pty from "@cdktf/node-pty-prebuilt-multiarch"; + +// These tests are failing with `Error: read EPIPE` on Windows in CI. There's still value running them on macOS and Linux. +if (process.platform === "win32") { + baseDescribe("interactive dev session tests", () => { + it.skip("skipped on Windows", () => {}); + }); +} else { + // Windows doesn't have a built-in way to get the CWD of a process by its ID. + // This functionality is provided by the Windows Driver Kit which is installed + // on GitHub actions Windows runners. + const tlistPath = + "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\tlist.exe"; + let windowsProcessCwdSupported = true; + if (!fs.existsSync(tlistPath)) { + windowsProcessCwdSupported = false; + const message = [ + "=".repeat(80), + "Unable to find Windows Driver Kit, skipping zombie process tests... :(", + "=".repeat(80), + ].join("\n"); + console.error(message); + } + + const pkgRoot = path.resolve(__dirname, ".."); + const ptyOptions: pty.IPtyForkOptions = { + name: "xterm-color", + cols: 80, + rows: 30, + cwd: pkgRoot, + env: process.env as Record<string, string>, + }; + + // Check `node-pty` installed and working correctly, skipping tests if not + let nodePtySupported = true; + try { + const pty = await import("@cdktf/node-pty-prebuilt-multiarch"); + const ptyProcess = pty.spawn( + process.execPath, + ["-p", "'ran node'"], + ptyOptions + ); + let output = ""; + ptyProcess.onData((data) => (output += data)); + const code = await new Promise<number>((resolve) => + ptyProcess.onExit(({ exitCode }) => resolve(exitCode)) + ); + assert.strictEqual(code, 0); + assert(output.includes("ran node")); + } catch (e) { + nodePtySupported = false; + const message = [ + "=".repeat(80), + "`node-pty` unsupported, skipping interactive dev session tests... :(", + "", + "Ensure its dependencies (https://github.com/microsoft/node-pty#dependencies)", + "are installed, then re-run `pnpm install` in the repository root.", + "", + "On Windows, make sure you have `Desktop development with C++`, `Windows SDK`,", + "`MSVC VS C++ build tools`, and `MSVC VS C++ Spectre-mitigated libs` Visual", + "Studio components installed.", + "", + e instanceof Error ? e.stack : String(e), + "=".repeat(80), + ].join("\n"); + console.error(message); + } + const describe = baseDescribe.runIf(nodePtySupported); + + interface PtyProcess { + pty: pty.IPty; + stdout: string; + exitCode: number | null; + exitPromise: Promise<number>; + url: string; + } + const processes: PtyProcess[] = []; + afterEach(() => { + for (const p of processes.splice(0)) { + // If the process didn't exit cleanly, log its output for debugging + if (p.exitCode !== 0) console.log(p.stdout); + // If the process hasn't exited yet, kill it + if (p.exitCode === null) { + // `node-pty` throws if signal passed on Windows + if (process.platform === "win32") p.pty.kill(); + else p.pty.kill("SIGKILL"); + } + } + }); + + const readyRegexp = /Ready on (http:\/\/[a-z0-9.]+:[0-9]+)/; + async function startWranglerDev( + args: string[], + skipWaitingForReady = false, + env?: Record<string, string> + ) { + const stdoutStream = new stream.PassThrough(); + const stdoutInterface = rl.createInterface(stdoutStream); + + let exitResolve: ((code: number) => void) | undefined; + const exitPromise = new Promise<number>( + (resolve) => (exitResolve = resolve) + ); + + const ptyOptionsWithEnv = { + ...ptyOptions, + env: env ?? (process.env as Record<string, string>), + } satisfies pty.IPtyForkOptions; + + const pty = await import("@cdktf/node-pty-prebuilt-multiarch"); + const ptyProcess = pty.spawn( + process.execPath, + [ + wranglerEntryPath, + ...args, + "--ip=127.0.0.1", + "--port=0", + "--inspector-port=0", + ], + ptyOptionsWithEnv + ); + const result: PtyProcess = { + pty: ptyProcess, + stdout: "", + exitCode: null, + exitPromise, + url: "", + }; + processes.push(result); + ptyProcess.onData((data) => { + result.stdout += data; + stdoutStream.write(data); + }); + ptyProcess.onExit(({ exitCode }) => { + result.exitCode = exitCode; + exitResolve?.(exitCode); + stdoutStream.end(); + }); + + if (!skipWaitingForReady) { + let readyMatch: RegExpMatchArray | null = null; + for await (const line of stdoutInterface) { + if ( + (readyMatch = readyRegexp.exec(stripVTControlCharacters(line))) !== + null + ) + break; + } + assert(readyMatch !== null, "Expected ready message"); + result.url = readyMatch[1]; + } + return result; + } + + interface Process { + pid: string; + cmd: string; + } + function getProcesses(): Process[] { + if (process.platform === "win32") { + return childProcess + .execSync("tasklist /fo csv", { encoding: "utf8" }) + .trim() + .split("\r\n") + .slice(1) + .map((line) => { + const [cmd, pid] = line.replaceAll('"', "").split(","); + return { pid, cmd }; + }); + } else { + return childProcess + .execSync("ps -e | awk '{print $1,$4}'", { encoding: "utf8" }) + .trim() + .split("\n") + .map((line) => { + const [pid, cmd] = line.split(" "); + return { pid, cmd }; + }); + } + } + function getProcessCwd(pid: string | number) { + if (process.platform === "win32") { + if (windowsProcessCwdSupported) { + return ( + childProcess + .spawnSync(tlistPath, [String(pid)], { encoding: "utf8" }) + .stdout.match(/^\s*CWD:\s*(.+)\\$/m)?.[1] ?? "" + ); + } else { + return ""; + } + } else { + return childProcess + .execSync(`lsof -p ${pid} | awk '$4=="cwd" {print $9}'`, { + encoding: "utf8", + }) + .trim(); + } + } + function getStartedWorkerdProcesses(): Process[] { + return getProcesses().filter( + ({ cmd, pid }) => + cmd.includes("workerd") && getProcessCwd(pid) === pkgRoot + ); + } + + const devScripts = [ + { args: ["dev"], expectedBody: "body" }, + { args: ["pages", "dev", "public"], expectedBody: "<p>body</p>" }, + ]; + const exitKeys = [ + { name: "CTRL-C", key: "\x03" }, + { name: "x", key: "x" }, + ]; + + describe.each(devScripts)("wrangler $args", ({ args, expectedBody }) => { + it.each(exitKeys)("cleanly exits with $name", async ({ key }) => { + const beginProcesses = getStartedWorkerdProcesses(); + + const wrangler = await startWranglerDev(args); + const duringProcesses = getStartedWorkerdProcesses(); + + // Check dev server working correctly + const res = await fetch(wrangler.url); + expect((await res.text()).trim()).toBe(expectedBody); + + // Check key cleanly exits dev server + wrangler.pty.write(key); + expect(await wrangler.exitPromise).toBe(0); + const endProcesses = getStartedWorkerdProcesses(); + + // Check no hanging workerd processes + if (process.platform !== "win32" || windowsProcessCwdSupported) { + expect(beginProcesses.length).toBe(endProcesses.length); + expect(duringProcesses.length).toBeGreaterThan(beginProcesses.length); + } + }); + describe("--show-interactive-dev-session", () => { + it("should show hotkeys when interactive", async () => { + const wrangler = await startWranglerDev(args); + wrangler.pty.kill(); + expect(wrangler.stdout).toContain("open a browser"); + expect(wrangler.stdout).toContain("open devtools"); + expect(wrangler.stdout).toContain("clear console"); + expect(wrangler.stdout).toContain("to exit"); + expect(wrangler.stdout).toContain("open local explorer"); + expect(wrangler.stdout).not.toContain("rebuild container"); + }); + it("should not show hotkeys with --show-interactive-dev-session=false", async () => { + const wrangler = await startWranglerDev([ + ...args, + "--show-interactive-dev-session=false", + ]); + wrangler.pty.kill(); + expect(wrangler.stdout).not.toContain("open a browser"); + expect(wrangler.stdout).not.toContain("open devtools"); + expect(wrangler.stdout).not.toContain("clear console"); + expect(wrangler.stdout).not.toContain("to exit"); + expect(wrangler.stdout).not.toContain("rebuild container"); + expect(wrangler.stdout).not.toContain("open local explorer"); + }); + }); + }); + + it.each(exitKeys)("multiworker cleanly exits with $name", async ({ key }) => { + const beginProcesses = getStartedWorkerdProcesses(); + + const wrangler = await startWranglerDev([ + "dev", + "-c", + "wrangler.a.jsonc", + "-c", + "wrangler.b.jsonc", + ]); + const duringProcesses = getStartedWorkerdProcesses(); + + // Check dev server working correctly + const res = await fetch(wrangler.url); + expect((await res.text()).trim()).toBe("hello from a & hello from b"); + + // Check key cleanly exits dev server + wrangler.pty.write(key); + expect(await wrangler.exitPromise).toBe(0); + const endProcesses = getStartedWorkerdProcesses(); + + // Check no hanging workerd processes + if (process.platform !== "win32" || windowsProcessCwdSupported) { + expect(beginProcesses.length).toBe(endProcesses.length); + expect(duringProcesses.length).toBeGreaterThan(beginProcesses.length); + } + }); + + const isCINonLinux = + process.platform !== "linux" && process.env.CI === "true"; + + function isDockerRunning() { + try { + execSync("docker ps", { stdio: "ignore" }); + return true; + } catch (e) { + return false; + } + } + + /** Indicates whether the test is being run locally (not in CI) AND docker is currently not running on the system */ + const isLocalWithoutDockerRunning = + process.env.CI !== "true" && !isDockerRunning(); + + if (isLocalWithoutDockerRunning) { + console.warn( + "The tests are running locally but there is no docker instance running on the system, skipping containers tests" + ); + } + + baseDescribe.skipIf( + isCINonLinux || + // If the tests are being run locally and docker is not running we just skip this test + isLocalWithoutDockerRunning + )("containers", () => { + // it seems like if we spam the container too often, it freezes up and crashes + const WAITFOR_OPTIONS = { timeout: 2000, interval: 500 }; + baseDescribe("container dev", { retry: 0, timeout: 90000 }, () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = fs.mkdtempSync(path.join(tmpdir(), "wrangler-container-")); + fs.cpSync( + path.resolve(__dirname, "../", "container-app"), + path.join(tmpDir), + { + recursive: true, + } + ); + + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + + afterEach(async () => { + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + afterAll(() => { + removeDir(tmpDir, { fireAndForget: true }); + }); + + it("should print rebuild containers hotkey", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "wrangler.jsonc"), + ]); + wrangler.pty.kill(); + expect(wrangler.stdout).toContain("rebuild container"); + }); + + it("should rebuild a container when the hotkey is pressed", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "wrangler.jsonc"), + ]); + await fetch(wrangler.url + "/start"); + + // wait container to be ready + await vi.waitFor(async () => { + const status = await fetch(wrangler.url + "/status", { + signal: AbortSignal.timeout(500), + }); + expect(await status.json()).toBe(true); + }, WAITFOR_OPTIONS); + + await vi.waitFor(async () => { + const res = await fetch(wrangler.url + "/fetch", { + // Sometimes this fetch can hang if the container is not ready + // The default timeout is longer than the timeout on the `waitFor()` which results in the test failing. + // So abort this request sooner to allow it to retry. + signal: AbortSignal.timeout(500), + }); + + expect(await res.text()).toBe( + "Hello World! Have an env var! I'm an env var!" + ); + }, WAITFOR_OPTIONS); + const output = wrangler.stdout; + // Extract the Docker image name from the output + const imageNameMatch = output.match( + /cloudflare-dev\/fixturetestcontainer:[a-f0-9]+/ + ); + expect(imageNameMatch).not.toBe(null); + const imageName = imageNameMatch![0]; + expect( + JSON.parse( + execSync( + `docker image inspect ${imageName} --format "{{ json .RepoTags }}"`, + { encoding: "utf8" } + ) + ).length + ).toBeGreaterThanOrEqual(1); + + fs.writeFileSync( + path.join(tmpDir, "container", "simple-node-app.js"), + `const { createServer } = require("http"); + + const server = createServer(function (req, res) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("Blah! " + process.env.MESSAGE); + res.end(); + }); + + server.listen(8080, function () { + console.log("Server listening on port 8080"); + });`, + "utf-8" + ); + + // Clear the captured stdout so we can match on log output + // produced AFTER the rebuild hotkey is pressed. + wrangler.stdout = ""; + wrangler.pty.write("r"); + + // Wait for the rebuild and workerd reload to actually + // finish before asserting on `/status`. Without this, + // the old workerd (which still reports the container as + // running even after `docker rm`) can satisfy /status + // requests during the rebuild window, and requests can + // hang while miniflare is reloading — both cause flakes + // on slow CI. + await vi.waitFor( + () => { + expect(wrangler.stdout).toMatch(/Local server updated and ready/); + }, + { timeout: 30_000, interval: 500 } + ); + + await vi.waitFor(async () => { + const status = await fetch(wrangler.url + "/status", { + signal: AbortSignal.timeout(500), + }); + expect(await status.json()).toBe(false); + }, WAITFOR_OPTIONS); + + await fetch(wrangler.url + "/start"); + await vi.waitFor(async () => { + const status = await fetch(wrangler.url + "/status", { + signal: AbortSignal.timeout(500), + }); + expect(await status.json()).toBe(true); + }, WAITFOR_OPTIONS); + await vi.waitFor(async () => { + const res = await fetch(wrangler.url + "/fetch", { + signal: AbortSignal.timeout(500), + }); + expect(await res.text()).toBe("Blah! I'm an env var!"); + }, WAITFOR_OPTIONS); + + // Verify that the old image tag has been deleted after rebuild + expect(() => { + execSync(`docker image inspect ${imageName}`, { encoding: "utf8" }); + }).toThrow(); + + wrangler.pty.kill(); + }); + + it("should clean up any containers that were started", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "wrangler.jsonc"), + ]); + await fetch(wrangler.url + "/start"); + + // wait container to be ready + await vi.waitFor(async () => { + const ids = getContainerIds(); + expect(ids.length).toBe(1); + }, WAITFOR_OPTIONS); + + // ctrl + c + wrangler.pty.write("\x03"); + await new Promise<void>((resolve) => { + wrangler.pty.onExit(() => resolve()); + }); + + // `docker rm -f` of the running container after the SIGINT + // teardown can take several seconds on a busy CI runner, so + // allow more than the default 5s `vi.waitFor` timeout. + await vi.waitFor( + () => { + expect(getContainerIds()).toHaveLength(0); + }, + { timeout: 10_000, interval: 500 } + ); + }); + }); + + baseDescribe( + "container dev where image build takes a long time", + { retry: 0, timeout: 90000 }, + () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = fs.mkdtempSync( + path.join(tmpdir(), "wrangler-container-sleep-") + ); + fs.cpSync( + path.resolve(__dirname, "../", "container-app"), + path.join(tmpDir), + { + recursive: true, + } + ); + const tmpDockerFilePath = path.join(tmpDir, "Dockerfile"); + fs.rmSync(tmpDockerFilePath); + fs.renameSync( + path.join(tmpDir, "DockerfileWithLongSleep"), + tmpDockerFilePath + ); + + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + + afterEach(async () => { + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + + afterAll(() => { + removeDir(tmpDir, { fireAndForget: true }); + }); + + it("should allow quitting while the image is building", async () => { + const wrangler = await startWranglerDev( + ["dev", "-c", path.join(tmpDir, "wrangler.jsonc")], + true + ); + + // Use a generous timeout to accommodate slow CI runners — + // 10s was occasionally tight when the docker daemon was + // under contention from other parallel fixtures. + const waitForOptions = { timeout: 30_000, interval: 500 }; + + // wait for long sleep instruction to start + await vi.waitFor(async () => { + expect(wrangler.stdout).toContain("RUN sleep 50000"); + }, waitForOptions); + + wrangler.pty.write("q"); + + await vi.waitFor(async () => { + expect(wrangler.stdout).toMatch(/CANCELED \[.*?\] RUN sleep 50000/); + }, waitForOptions); + }); + + it("should rebuilding while the image is building", async () => { + const wrangler = await startWranglerDev( + ["dev", "-c", path.join(tmpDir, "wrangler.jsonc")], + true + ); + + // We assert on wrangler's own deterministic log line, which + // is emitted synchronously when wrangler picks up a new + // `containerBuildId` and starts the rebuild flow. We + // intentionally do NOT match "This (no-op) build takes + // forever..." or buildkit's `CANCELED` line — both come + // from docker buildkit's TTY output and are unreliable: + // the first is redrawn an unpredictable number of times + // per build, and the second is only emitted when buildkit + // has time to flush its cancellation status before the + // docker CLI is killed by the abort. Relying on either of + // those made earlier versions of this test flaky on slow + // CI (see + // https://github.com/cloudflare/workers-sdk/actions/runs/24924588002). + const PREPARING_RE = /⎔ Preparing container image\(s\)/; + const SLEEP_RE = /RUN sleep 50000/; + + const waitForOptions = { timeout: 30_000, interval: 500 }; + + // 1. Wait for the initial build to reach the long sleep + // instruction — this is the "while the image is building" + // state that the test name refers to. + await vi.waitFor(() => { + expect(wrangler.stdout).toMatch(SLEEP_RE); + }, waitForOptions); + + // 2. First `r`: should trigger another rebuild while the + // initial build is still in its long-sleep phase. Clear + // the captured stdout so the assertion below only sees + // output produced after the press. + wrangler.stdout = ""; + wrangler.pty.write("r"); + + await vi.waitFor(() => { + expect(wrangler.stdout).toMatch(PREPARING_RE); + }, waitForOptions); + + // 3. Wait for the second build to also reach the long sleep + // instruction so the next `r` press is genuinely "while + // the image is building". + await vi.waitFor(() => { + expect(wrangler.stdout).toMatch(SLEEP_RE); + }, waitForOptions); + + // 4. Second `r`: should again trigger another rebuild. + // Clear stdout again so the assertion only sees the new + // Preparing log. + wrangler.stdout = ""; + wrangler.pty.write("r"); + + await vi.waitFor(() => { + expect(wrangler.stdout).toMatch(PREPARING_RE); + }, waitForOptions); + + wrangler.pty.kill(); + }); + } + ); + + baseDescribe("multi-containers dev", { retry: 0, timeout: 50000 }, () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = fs.mkdtempSync( + path.join(tmpdir(), "wrangler-multi-containers-") + ); + fs.cpSync( + path.resolve(__dirname, "../", "multi-containers-app"), + path.join(tmpDir), + { + recursive: true, + } + ); + + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + + afterEach(async () => { + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + afterAll(() => { + removeDir(tmpDir, { fireAndForget: true }); + }); + + it("should print build logs for all the containers", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "wrangler.jsonc"), + ]); + await vi.waitFor( + () => { + expect(wrangler.stdout).toContain('"name": "simple-node-app-a"'); + expect(wrangler.stdout).toContain('"name": "simple-node-app-b"'); + }, + { timeout: 10_000 } + ); + wrangler.pty.kill(); + }); + + it("should rebuild all the containers when the hotkey is pressed", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "wrangler.jsonc"), + ]); + + await vi.waitFor( + async () => { + const text = await ( + await fetch(wrangler.url, { signal: AbortSignal.timeout(3_000) }) + ).text(); + expect(text).toBe( + 'Response from A: "Hello from Container A" Response from B: "Hello from Container B"' + ); + }, + { timeout: 30_000, interval: 1000 } + ); + + const tmpDockerfileAPath = path.join(tmpDir, "DockerfileA"); + const dockerFileAContent = fs.readFileSync(tmpDockerfileAPath, "utf8"); + fs.writeFileSync( + tmpDockerfileAPath, + dockerFileAContent.replace( + '"Hello from Container A"', + '"Hello World from Container A"' + ), + "utf-8" + ); + + const tmpDockerfileBPath = path.join(tmpDir, "DockerfileB"); + const dockerFileBContent = fs.readFileSync(tmpDockerfileBPath, "utf8"); + fs.writeFileSync( + tmpDockerfileBPath, + dockerFileBContent.replace( + '"Hello from Container B"', + '"Hello from the B Container"' + ), + "utf-8" + ); + + wrangler.pty.write("r"); + + await vi.waitFor( + async () => { + const text = await ( + await fetch(wrangler.url, { signal: AbortSignal.timeout(3_000) }) + ).text(); + expect(text).toBe( + 'Response from A: "Hello World from Container A" Response from B: "Hello from the B Container"' + ); + }, + { timeout: 30_000, interval: 1000 } + ); + + fs.writeFileSync(tmpDockerfileAPath, dockerFileAContent, "utf-8"); + fs.writeFileSync(tmpDockerfileBPath, dockerFileBContent, "utf-8"); + + wrangler.pty.kill(); + }); + + it("should clean up any containers that were started", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "wrangler.jsonc"), + ]); + // wait container to be ready + await vi.waitFor( + async () => { + const text = await ( + await fetch(wrangler.url, { signal: AbortSignal.timeout(3_000) }) + ).text(); + expect(text).toBe( + 'Response from A: "Hello from Container A" Response from B: "Hello from Container B"' + ); + }, + { timeout: 30_000, interval: 1000 } + ); + const ids = getContainerIds(); + expect(ids.length).toBe(2); + + // ctrl + c + wrangler.pty.write("\x03"); + await new Promise<void>((resolve) => { + wrangler.pty.onExit(() => resolve()); + }); + // `docker rm -f` of multiple running containers after the + // SIGINT teardown can take several seconds on a busy CI + // runner, so allow more than the default 5s `vi.waitFor` + // timeout. + await vi.waitFor( + () => { + const remainingIds = getContainerIds(); + expect(remainingIds).toHaveLength(0); + }, + { timeout: 10_000, interval: 500 } + ); + }); + }); + + baseDescribe( + "multi-workers containers dev", + { retry: 0, timeout: 50000 }, + () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = fs.mkdtempSync( + path.join(tmpdir(), "wrangler-multi-workers-containers-") + ); + fs.cpSync( + path.resolve(__dirname, "../", "multi-workers-containers-app"), + path.join(tmpDir), + { + recursive: true, + } + ); + + const ids = getContainerIds(); + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + + afterEach(async () => { + const ids = getContainerIds(); + + if (ids.length > 0) { + execSync("docker rm -f " + ids.join(" "), { + encoding: "utf8", + }); + } + }); + afterAll(() => { + removeDir(tmpDir, { fireAndForget: true }); + }); + + it("should be able to interact with both workers, rebuild the containers with the hotkey and all containers should be cleaned up at the end", async () => { + const wrangler = await startWranglerDev([ + "dev", + "-c", + path.join(tmpDir, "workerA/wrangler.jsonc"), + "-c", + path.join(tmpDir, "workerB/wrangler.jsonc"), + ]); + + await vi.waitFor( + async () => { + const json = await ( + await fetch(wrangler.url, { + signal: AbortSignal.timeout(5_000), + }) + ).json(); + expect(json).toEqual({ + containerAText: "Hello from Container A", + containerBText: "Hello from Container B", + }); + }, + { timeout: 10_000, interval: 1000 } + ); + + const tmpDockerfileAPath = path.join(tmpDir, "workerA/Dockerfile"); + const dockerFileAContent = fs.readFileSync( + tmpDockerfileAPath, + "utf8" + ); + fs.writeFileSync( + tmpDockerfileAPath, + dockerFileAContent.replace( + '"Hello from Container A"', + '"Hello World from Container A"' + ), + "utf-8" + ); + + const tmpDockerfileBPath = path.join(tmpDir, "workerB/Dockerfile"); + const dockerFileBContent = fs.readFileSync( + tmpDockerfileBPath, + "utf8" + ); + fs.writeFileSync( + tmpDockerfileBPath, + dockerFileBContent.replace( + '"Hello from Container B"', + '"Hello from the B Container"' + ), + "utf-8" + ); + + wrangler.pty.write("r"); + + await vi.waitFor( + async () => { + const json = await ( + await fetch(wrangler.url, { + signal: AbortSignal.timeout(5_000), + }) + ).json(); + expect(json).toEqual({ + containerAText: "Hello World from Container A", + containerBText: "Hello from the B Container", + }); + }, + { timeout: 30_000, interval: 1000 } + ); + + fs.writeFileSync(tmpDockerfileAPath, dockerFileAContent, "utf-8"); + fs.writeFileSync(tmpDockerfileBPath, dockerFileBContent, "utf-8"); + + wrangler.pty.kill("SIGINT"); + }); + } + ); + + /** gets any containers that were created by running this fixture */ + const getContainerIds = () => { + // note the -a to include stopped containers + const allContainers = execSync(`docker ps -a --format json`) + .toString() + .split("\n") + .filter((line) => line.trim()); + if (allContainers.length === 0) { + return []; + } + const jsonOutput = allContainers.map((line) => JSON.parse(line)); + + const matches = jsonOutput.map((container) => { + if (container.Image.includes("cloudflare-dev/fixturetestcontainer")) { + return container.ID; + } + }); + return matches.filter(Boolean); + }; + }); +} diff --git a/fixtures/interactive-dev-tests/tsconfig.json b/fixtures/interactive-dev-tests/tsconfig.json new file mode 100644 index 0000000..3241aae --- /dev/null +++ b/fixtures/interactive-dev-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "target": "esnext", + "module": "esnext", + "strict": true, + "lib": ["esnext"] + } +} diff --git a/fixtures/interactive-dev-tests/vitest.config.mts b/fixtures/interactive-dev-tests/vitest.config.mts new file mode 100644 index 0000000..d88b387 --- /dev/null +++ b/fixtures/interactive-dev-tests/vitest.config.mts @@ -0,0 +1,12 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + // `node-pty` doesn't work inside worker threads + pool: "forks", + }, + }) +); diff --git a/fixtures/interactive-dev-tests/wrangler.a.jsonc b/fixtures/interactive-dev-tests/wrangler.a.jsonc new file mode 100644 index 0000000..33a9bfc --- /dev/null +++ b/fixtures/interactive-dev-tests/wrangler.a.jsonc @@ -0,0 +1,11 @@ +{ + "name": "a", + "main": "src/worker-a.mjs", + "compatibility_date": "2023-12-01", + "services": [ + { + "binding": "WORKER", + "service": "b", + }, + ], +} diff --git a/fixtures/interactive-dev-tests/wrangler.b.jsonc b/fixtures/interactive-dev-tests/wrangler.b.jsonc new file mode 100644 index 0000000..d2454c1 --- /dev/null +++ b/fixtures/interactive-dev-tests/wrangler.b.jsonc @@ -0,0 +1,5 @@ +{ + "name": "b", + "main": "src/worker-b.mjs", + "compatibility_date": "2023-12-01", +} diff --git a/fixtures/interactive-dev-tests/wrangler.jsonc b/fixtures/interactive-dev-tests/wrangler.jsonc new file mode 100644 index 0000000..b75d756 --- /dev/null +++ b/fixtures/interactive-dev-tests/wrangler.jsonc @@ -0,0 +1,4 @@ +{ + "main": "src/index.mjs", + "compatibility_date": "2023-12-01", +} diff --git a/fixtures/isomorphic-random-example/README.md b/fixtures/isomorphic-random-example/README.md new file mode 100644 index 0000000..0522830 --- /dev/null +++ b/fixtures/isomorphic-random-example/README.md @@ -0,0 +1,7 @@ +# Isomorphic Package Example + +This package implements an isomorphic library that generates cryptographically-strong pseudorandom numbers. +What this package does isn't really important here, the key part is the [`package.json`](./package.json)'s `exports` field. +Conditional exports provide a way to load a different file depending on where the module is being imported from. +By default, Wrangler will try to look for a [**`workerd`** key](https://runtime-keys.proposal.wintercg.org/#workerd). +This allows you as a library developer to implement different behaviour for different JavaScript runtimes. diff --git a/fixtures/isomorphic-random-example/package.json b/fixtures/isomorphic-random-example/package.json new file mode 100644 index 0000000..78a3d27 --- /dev/null +++ b/fixtures/isomorphic-random-example/package.json @@ -0,0 +1,11 @@ +{ + "name": "@fixture/isomorphic-random", + "private": true, + "description": "Isomorphic secure-random library, demonstrating `exports` use with Workers", + "exports": { + "other": "./src/other.js", + "node": "./src/node.js", + "workerd": "./src/workerd.mjs", + "default": "./src/default.js" + } +} diff --git a/fixtures/isomorphic-random-example/src/default.js b/fixtures/isomorphic-random-example/src/default.js new file mode 100644 index 0000000..71c6176 --- /dev/null +++ b/fixtures/isomorphic-random-example/src/default.js @@ -0,0 +1,4 @@ +// This entry point should only be used if no other build condition match. +export function randomBytes(length) { + return new Uint8Array([8, 9, 10, 11, 12, 13]); +} diff --git a/fixtures/isomorphic-random-example/src/node.js b/fixtures/isomorphic-random-example/src/node.js new file mode 100644 index 0000000..eb4c8af --- /dev/null +++ b/fixtures/isomorphic-random-example/src/node.js @@ -0,0 +1,5 @@ +const crypto = require("node:crypto"); + +module.exports.randomBytes = function (length) { + return new Uint8Array(crypto.randomBytes(length)); +}; diff --git a/fixtures/isomorphic-random-example/src/other.js b/fixtures/isomorphic-random-example/src/other.js new file mode 100644 index 0000000..860543a --- /dev/null +++ b/fixtures/isomorphic-random-example/src/other.js @@ -0,0 +1,4 @@ +// This entry point should only be used if the build condition contains `other`. +export function randomBytes(length) { + return new Uint8Array([1, 2, 3, 4, 5, 6]); +} diff --git a/fixtures/isomorphic-random-example/src/workerd.mjs b/fixtures/isomorphic-random-example/src/workerd.mjs new file mode 100644 index 0000000..36cb99d --- /dev/null +++ b/fixtures/isomorphic-random-example/src/workerd.mjs @@ -0,0 +1,3 @@ +export function randomBytes(length) { + return crypto.getRandomValues(new Uint8Array(length)); +} diff --git a/fixtures/legacy-site-app/package.json b/fixtures/legacy-site-app/package.json new file mode 100644 index 0000000..ae9c8d7 --- /dev/null +++ b/fixtures/legacy-site-app/package.json @@ -0,0 +1,9 @@ +{ + "name": "@fixture/sites-legacy", + "private": true, + "description": "", + "keywords": [], + "license": "ISC", + "author": "", + "main": "index.js" +} diff --git a/fixtures/legacy-site-app/public/404.html b/fixtures/legacy-site-app/public/404.html new file mode 100644 index 0000000..5ac9921 --- /dev/null +++ b/fixtures/legacy-site-app/public/404.html @@ -0,0 +1,50 @@ +<!doctype html> +<html> + <head> + <link rel="icon" type="image/x-icon" href="favicon.ico" /> + <link + href="https://fonts.googleapis.com/css?family=Pacifico&display=swap" + rel="stylesheet" + /> + <link + href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" + rel="stylesheet" + /> + <style> + h1 { + font-family: Pacifico, sans-serif; + font-size: 4em; + color: #3eb5f1; + margin: 0; + } + + h2 { + font-weight: 300; + font-family: sans-serif; + } + + .centered { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + } + + #ferris { + width: 75%; + } + </style> + </head> + <body> + <div class="centered"> + <h1>404 Not Found</h1> + <h2>Oh dang! We couldn't find that page.</h2> + <img + id="ferris" + alt="a sad crab is unable to unable to lasso a paper airplane. 404 not found." + src="./img/404-wrangler-ferris.gif" + /> + </div> + </body> +</html> diff --git a/fixtures/legacy-site-app/public/favicon.ico b/fixtures/legacy-site-app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cc6c23bd37bb4fa58f810c5f9e36041d96b23462 GIT binary patch literal 1150 zcmXw&dr(wW9LJA6klCG0Y<YxQ!eEMm8By+-ugse9&;DthX{JA#`oj&KL>`fdtcD^W zZ;*%(!UqcotRg<b0ydhAMjL2wbjB2INe$S$yLb2AyX)Tb?ROVCcRuIbd(QcN&hMN% zhp`3pL`5;G6WFfj7>i|$B@@&|J#+okFh*l9>gNc5&sPx{583mU`;2{d&-7lcYdrHJ zV6g+cdI9tI3XjTu1YOoNblCzTvxM=;eZu<4eJ-{zQwSS$UpSd=@030w<3Vp+juRO@ zL9RJ=p9em-bEK6D+>qyj;VU=vxdL=KLJy%QSXAB<rq7&WhU|aLy1h<m;qHgnR_)^Z zuL4<@0L}ApdyvOl1uQObV^O|a^GJp~x7m;<u$Y50EVgjQxOo2)#|t@>XQmS|xl??P zJ@7Pt0LZ-pWRqv}2_KI><c87e345XrxE+Ksx^RXqJt|n@4$X#)2mZyP+_1y>X@^vI zWC%*ZH6Z^7?JS=1#1apWEAk-but)QRjD;RcQk6F;q1-dJti+9%zMe&N0Xe>WTv<9C ziZ1^JLq$V?^(WvUd3Ll&e918{;)}hAEAk@tuxD&#y;!xP#;Yb)dhtrB8}UW6lQD(U zsUO)SRzHfJ<dBOUz>#4(kINyxrds67%0w(J347EvjXqa$gNRq_L{KiET#i=)tk@A^ z%{O7F8^u7yuR!StaO`K`gR=@xs1&ihLd47DdF`a%mD=RP>Ti5lRZpH;@%ZvG7poan znQa1wrkfb3{~f5l4pfro!!Cs<)%uWB<3nPVh_p7T>h)H?y0+PmlyAw?AWkNq5L5PC z4YH0~Ft?6FZu$dgya5~?QMswb&yySdSk>UCoPFw>XQZU`)6y7uu&%|A)bD(tJymqx z=F#1EFnD$XX!{dr9Roh;5Ad}oC1|8H`LVjuKW4JYY`smkywxs;(oadCT#(xAcb1KY zYWx0za_$b$c^jy?q4F&@nZI#brkrK0Yn4Lev25s)8DS(G<U;xx30vAF-gZk3UU>lY z-2?1*>Hl8~a8suYQ-=ati{{A=%GtQ7M`3TDBf1qcA!%b-!vWRt2>8(n4Ezl=PpJI; z3ku(44`5@Df(>1YL>@D>;rm1TXM{fOl+?SuL&Ev{D!)Drj64Pgr|2Fzg8ZF}0c`6J zV(WzfHroR|w5EPz_gs98RbVUYM7E^dYswo8jQ>WlTKwn?V7nXQJNgxDzZ?wn?xo;3 mxlH6@Tl)fR@>#})7R+I#Y&5j;3=uJp_O2D{Jpa$vV*dkKub%Ay literal 0 HcmV?d00001 diff --git a/fixtures/legacy-site-app/public/img/200-wrangler-ferris.gif b/fixtures/legacy-site-app/public/img/200-wrangler-ferris.gif new file mode 100644 index 0000000000000000000000000000000000000000..8853751fac7b39559f140971700360fe249fcdc5 GIT binary patch literal 45598 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 Os#2HA)TVYVAOJf?`|0KY literal 0 HcmV?d00001 diff --git a/fixtures/legacy-site-app/public/img/404-wrangler-ferris.gif b/fixtures/legacy-site-app/public/img/404-wrangler-ferris.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ac1479fcf6f4ded732b8b8ea7bfd9f98b5ca3e2 GIT binary patch literal 31606 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 F06Ss&*g60J literal 0 HcmV?d00001 diff --git a/fixtures/legacy-site-app/public/index.html b/fixtures/legacy-site-app/public/index.html new file mode 100644 index 0000000..102985f --- /dev/null +++ b/fixtures/legacy-site-app/public/index.html @@ -0,0 +1,50 @@ +<!doctype html> +<html> + <head> + <link rel="icon" type="image/x-icon" href="favicon.ico" /> + <link + href="https://fonts.googleapis.com/css?family=Pacifico&display=swap" + rel="stylesheet" + /> + <link + href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" + rel="stylesheet" + /> + <style> + h1 { + font-family: Pacifico, sans-serif; + font-size: 4em; + color: #3eb5f1; + margin: 0; + } + + h2 { + font-weight: 300; + font-family: sans-serif; + } + + .centered { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + } + + #ferris { + width: 75%; + } + </style> + </head> + <body> + <div class="centered"> + <h1>200 Success</h1> + <h2>Hello World! Welcome to your Workers Site.</h2> + <img + id="ferris" + alt="a happy crab is wearing a cowboy hat and holding a lasso. 200 success." + src="./img/200-wrangler-ferris.gif" + /> + </div> + </body> +</html> diff --git a/fixtures/legacy-site-app/workers-site/.gitignore b/fixtures/legacy-site-app/workers-site/.gitignore new file mode 100644 index 0000000..7915249 --- /dev/null +++ b/fixtures/legacy-site-app/workers-site/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +worker diff --git a/fixtures/legacy-site-app/workers-site/index.js b/fixtures/legacy-site-app/workers-site/index.js new file mode 100644 index 0000000..48e8be8 --- /dev/null +++ b/fixtures/legacy-site-app/workers-site/index.js @@ -0,0 +1,87 @@ +import { + getAssetFromKV, + mapRequestToAsset, +} from "@cloudflare/kv-asset-handler"; + +/** + * The DEBUG flag will do two things that help during development: + * 1. we will skip caching on the edge, which makes it easier to + * debug. + * 2. we will return an error message on exception in your Response rather + * than the default 404.html page. + */ +const DEBUG = false; + +addEventListener("fetch", (event) => { + event.respondWith(handleEvent(event)); +}); + +async function handleEvent(event) { + let options = {}; + + /** + * You can add custom logic to how we fetch your assets + * by configuring the function `mapRequestToAsset` + */ + // options.mapRequestToAsset = handlePrefix(/^\/docs/) + + try { + if (DEBUG) { + // customize caching + options.cacheControl = { + bypassCache: true, + }; + } + + const page = await getAssetFromKV(event, options); + + // allow headers to be altered + const response = new Response(page.body, page); + + response.headers.set("X-XSS-Protection", "1; mode=block"); + response.headers.set("X-Content-Type-Options", "nosniff"); + response.headers.set("X-Frame-Options", "DENY"); + response.headers.set("Referrer-Policy", "unsafe-url"); + response.headers.set("Feature-Policy", "none"); + + return response; + } catch (e) { + // if an error is thrown try to serve the asset at 404.html + if (!DEBUG) { + try { + let notFoundResponse = await getAssetFromKV(event, { + mapRequestToAsset: (req) => + new Request(`${new URL(req.url).origin}/404.html`, req), + }); + + return new Response(notFoundResponse.body, { + ...notFoundResponse, + status: 404, + }); + } catch (e) {} + } + + return new Response(e.message || e.toString(), { status: 500 }); + } +} + +/** + * Here's one example of how to modify a request to + * remove a specific prefix, in this case `/docs` from + * the url. This can be useful if you are deploying to a + * route on a zone, or if you only want your static content + * to exist at a specific path. + */ +function handlePrefix(prefix) { + return (request) => { + // compute the default (e.g. / -> index.html) + let defaultAssetKey = mapRequestToAsset(request); + let url = new URL(defaultAssetKey.url); + + // strip the prefix from the path for lookup + url.pathname = url.pathname.replace(prefix, "/"); + + // inherit all other props from the default request + return new Request(url.toString(), defaultAssetKey); + }; +} diff --git a/fixtures/legacy-site-app/workers-site/package.json b/fixtures/legacy-site-app/workers-site/package.json new file mode 100644 index 0000000..cf4244c --- /dev/null +++ b/fixtures/legacy-site-app/workers-site/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "description": "A template for kick starting a Cloudflare Workers project", + "license": "MIT", + "main": "index.js", + "dependencies": { + "@cloudflare/kv-asset-handler": "workspace:*" + } +} diff --git a/fixtures/legacy-site-app/wrangler.jsonc b/fixtures/legacy-site-app/wrangler.jsonc new file mode 100644 index 0000000..d29209f --- /dev/null +++ b/fixtures/legacy-site-app/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "account_id": "", + "name": "legacy-site-app", + "type": "webpack", + "workers_dev": true, + "site": { + "bucket": "./public", + }, + "compatibility_date": "2022-04-24", +} diff --git a/fixtures/miniflare-node-test/package.json b/fixtures/miniflare-node-test/package.json new file mode 100644 index 0000000..93e7de9 --- /dev/null +++ b/fixtures/miniflare-node-test/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/miniflare-node", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "scripts": { + "test:ci": "node --test" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@types/is-even": "^1.0.2", + "is-even": "^1.0.0", + "miniflare": "workspace:*", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/miniflare-node-test/src/index-build.test.js b/fixtures/miniflare-node-test/src/index-build.test.js new file mode 100644 index 0000000..a9985b5 --- /dev/null +++ b/fixtures/miniflare-node-test/src/index-build.test.js @@ -0,0 +1,41 @@ +import assert from "node:assert"; +import { spawnSync } from "node:child_process"; +import test, { after, before, describe } from "node:test"; +import { Miniflare } from "miniflare"; + +before(() => { + spawnSync("npx wrangler build -c wrangler-build.json", { + shell: true, + stdio: "pipe", + }); +}); + +describe("worker build", () => { + /** + * @type {Miniflare} + */ + let worker; + + before(async () => { + worker = new Miniflare({ + modules: [ + { + type: "ESModule", + path: "dist/index.js", + }, + ], + }); + await worker.ready; + }); + + test("even", async () => { + assert.strictEqual( + await (await worker.dispatchFetch("http://example.com?number=2")).text(), + "even" + ); + }); + + after(async () => { + await worker.dispose(); + }); +}); diff --git a/fixtures/miniflare-node-test/src/index-with-bindings.test.js b/fixtures/miniflare-node-test/src/index-with-bindings.test.js new file mode 100644 index 0000000..6287e9a --- /dev/null +++ b/fixtures/miniflare-node-test/src/index-with-bindings.test.js @@ -0,0 +1,51 @@ +import assert from "node:assert"; +import test, { after, before, describe } from "node:test"; +import { Miniflare } from "miniflare"; + +describe("worker", () => { + /** + * @type {Miniflare} + */ + let worker; + + before(async () => { + worker = new Miniflare({ + modules: [ + { + type: "ESModule", + path: "src/index.js", + }, + ], + bindings: { + FOO: "Hello Bindings", + }, + kvNamespaces: ["KV"], + }); + await worker.ready; + }); + + test("hello world", async () => { + assert.strictEqual( + await (await worker.dispatchFetch("http://example.com")).text(), + "Hello World" + ); + }); + + test("text binding", async () => { + const bindings = await worker.getBindings(); + assert.strictEqual(bindings.FOO, "Hello Bindings"); + }); + + test("kv binding", async () => { + /** + * @type {{KV: import("@cloudflare/workers-types/experimental").KVNamespace, FOO: string}} + */ + const bindings = await worker.getBindings(); + await bindings.KV.put("key", "value"); + assert.strictEqual(await bindings.KV.get("key"), "value"); + }); + + after(async () => { + await worker.dispose(); + }); +}); diff --git a/fixtures/miniflare-node-test/src/index-with-imports.js b/fixtures/miniflare-node-test/src/index-with-imports.js new file mode 100644 index 0000000..1c2052b --- /dev/null +++ b/fixtures/miniflare-node-test/src/index-with-imports.js @@ -0,0 +1,12 @@ +import { sayHello } from "./say-hello.js"; + +export default { + /** + * + * @param {Request} request + * @returns + */ + async fetch(request) { + return new Response(sayHello(request.url)); + }, +}; diff --git a/fixtures/miniflare-node-test/src/index-with-imports.test.js b/fixtures/miniflare-node-test/src/index-with-imports.test.js new file mode 100644 index 0000000..1a070ad --- /dev/null +++ b/fixtures/miniflare-node-test/src/index-with-imports.test.js @@ -0,0 +1,34 @@ +import assert from "node:assert"; +import test, { after, before, describe } from "node:test"; +import { Miniflare } from "miniflare"; + +describe("worker", () => { + /** + * @type {Miniflare} + */ + let worker; + + before(async () => { + worker = new Miniflare({ + scriptPath: "src/index-with-imports.js", + modules: true, + modulesRules: [{ type: "ESModule", include: ["**/*.js"] }], + }); + try { + await worker.ready; + } catch (e) { + console.error(e); + } + }); + + test("hello world", async () => { + assert.strictEqual( + await (await worker.dispatchFetch("http://example.com/path")).text(), + "Hello from http://example.com/path" + ); + }); + + after(async () => { + await worker.dispose(); + }); +}); diff --git a/fixtures/miniflare-node-test/src/index.js b/fixtures/miniflare-node-test/src/index.js new file mode 100644 index 0000000..2bd0131 --- /dev/null +++ b/fixtures/miniflare-node-test/src/index.js @@ -0,0 +1,10 @@ +export default { + /** + * + * @param {Request} request + * @returns + */ + async fetch(request) { + return new Response(`Hello World`); + }, +}; diff --git a/fixtures/miniflare-node-test/src/index.test.js b/fixtures/miniflare-node-test/src/index.test.js new file mode 100644 index 0000000..874f120 --- /dev/null +++ b/fixtures/miniflare-node-test/src/index.test.js @@ -0,0 +1,33 @@ +import assert from "node:assert"; +import test, { after, before, describe } from "node:test"; +import { Miniflare } from "miniflare"; + +describe("worker", () => { + /** + * @type {Miniflare} + */ + let worker; + + before(async () => { + worker = new Miniflare({ + modules: [ + { + type: "ESModule", + path: "src/index.js", + }, + ], + }); + await worker.ready; + }); + + test("hello world", async () => { + assert.strictEqual( + await (await worker.dispatchFetch("http://example.com")).text(), + "Hello World" + ); + }); + + after(async () => { + await worker.dispose(); + }); +}); diff --git a/fixtures/miniflare-node-test/src/index.ts b/fixtures/miniflare-node-test/src/index.ts new file mode 100644 index 0000000..2f3566a --- /dev/null +++ b/fixtures/miniflare-node-test/src/index.ts @@ -0,0 +1,10 @@ +import isEven from "is-even"; + +export default { + async fetch(request): Promise<Response> { + const url = new URL(request.url); + return new Response( + isEven(Number(url.searchParams.get("number") ?? "0")) ? "even" : "odd" + ); + }, +} satisfies ExportedHandler; diff --git a/fixtures/miniflare-node-test/src/say-hello.js b/fixtures/miniflare-node-test/src/say-hello.js new file mode 100644 index 0000000..79451b8 --- /dev/null +++ b/fixtures/miniflare-node-test/src/say-hello.js @@ -0,0 +1,8 @@ +/** + * + * @param {string} url + * @returns + */ +export function sayHello(url) { + return `Hello from ${url}`; +} diff --git a/fixtures/miniflare-node-test/tsconfig.json b/fixtures/miniflare-node-test/tsconfig.json new file mode 100644 index 0000000..76e6c38 --- /dev/null +++ b/fixtures/miniflare-node-test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types"], + "checkJs": true + }, + "include": ["**/*.js", "**/*.ts"] +} diff --git a/fixtures/miniflare-node-test/wrangler-build.json b/fixtures/miniflare-node-test/wrangler-build.json new file mode 100644 index 0000000..46ed31d --- /dev/null +++ b/fixtures/miniflare-node-test/wrangler-build.json @@ -0,0 +1,5 @@ +{ + "name": "build-miniflare", + "main": "src/index.ts", + "compatibility_date": "2025-01-16" +} diff --git a/fixtures/miniflare-node-test/wrangler.jsonc b/fixtures/miniflare-node-test/wrangler.jsonc new file mode 100644 index 0000000..74fcb98 --- /dev/null +++ b/fixtures/miniflare-node-test/wrangler.jsonc @@ -0,0 +1,8 @@ +{ + "name": "miniflare-node-test", + "main": "dist/out.js", + "compatibility_date": "2022-12-08", + "build": { + "command": "npx esbuild src/index.js --outfile=dist/out.js", + }, +} diff --git a/fixtures/multi-worker/package.json b/fixtures/multi-worker/package.json new file mode 100644 index 0000000..bdaffe6 --- /dev/null +++ b/fixtures/multi-worker/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/multi-worker", + "private": true, + "scripts": { + "cf-typegen": "pnpm run \"/^typegen:.*/\"", + "deploy": "wrangler deploy", + "start": "wrangler dev -c workers/sentry/wrangler.jsonc -c workers/default/wrangler.jsonc", + "test:ci": "vitest run", + "test:watch": "vitest", + "typegen:default": "wrangler types ./workers/default/worker-configuration.d.ts -c ./workers/default/wrangler.jsonc --no-include-runtime", + "typegen:sentry": "wrangler types ./workers/sentry/worker-configuration.d.ts -c ./workers/sentry/wrangler.jsonc --no-include-runtime" + }, + "dependencies": { + "@sentry/cloudflare": "^10" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/multi-worker/tests/multi-worker.test.ts b/fixtures/multi-worker/tests/multi-worker.test.ts new file mode 100644 index 0000000..f2a8223 --- /dev/null +++ b/fixtures/multi-worker/tests/multi-worker.test.ts @@ -0,0 +1,21 @@ +import * as path from "node:path"; +import { describe, it, vi } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Multi Worker", () => { + it("can start a multi worker application with Sentry", async ({ expect }) => { + const { ip, port, stop } = await runWranglerDev( + path.resolve(__dirname, ".."), + ["-c=workers/sentry/wrangler.jsonc", "-c=workers/default/wrangler.jsonc"] + ); + try { + await vi.waitFor(async () => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toBe(`Hello World!`); + }); + } finally { + await stop(); + } + }); +}); diff --git a/fixtures/multi-worker/vitest.config.mts b/fixtures/multi-worker/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/multi-worker/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/multi-worker/workers/default/src/index.ts b/fixtures/multi-worker/workers/default/src/index.ts new file mode 100644 index 0000000..e1286c9 --- /dev/null +++ b/fixtures/multi-worker/workers/default/src/index.ts @@ -0,0 +1,6 @@ +export default { + async fetch(request, env, ctx): Promise<Response> { + console.log("Hello World!"); + return new Response("Hello World!"); + }, +} satisfies ExportedHandler<Env>; diff --git a/fixtures/multi-worker/workers/default/tsconfig.json b/fixtures/multi-worker/workers/default/tsconfig.json new file mode 100644 index 0000000..52b1f79 --- /dev/null +++ b/fixtures/multi-worker/workers/default/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2024", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2024"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "es2022", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true, + "types": ["@cloudflare/workers-types"] + }, + "exclude": ["test"], + "include": ["worker-configuration.d.ts", "src/**/*.ts"] +} diff --git a/fixtures/multi-worker/workers/default/worker-configuration.d.ts b/fixtures/multi-worker/workers/default/worker-configuration.d.ts new file mode 100644 index 0000000..4ddaf76 --- /dev/null +++ b/fixtures/multi-worker/workers/default/worker-configuration.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./workers/default/worker-configuration.d.ts -c ./workers/default/wrangler.jsonc --no-include-runtime` (hash: 3d3fdd145760295159fc1eac24135779) +interface __BaseEnv_Env {} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/multi-worker/workers/default/wrangler.jsonc b/fixtures/multi-worker/workers/default/wrangler.jsonc new file mode 100644 index 0000000..e45361f --- /dev/null +++ b/fixtures/multi-worker/workers/default/wrangler.jsonc @@ -0,0 +1,14 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "b", + "main": "src/index.ts", + "compatibility_date": "2026-01-01", + "compatibility_flags": ["nodejs_als"], + "observability": { + "enabled": true, + }, +} diff --git a/fixtures/multi-worker/workers/sentry/src/index.ts b/fixtures/multi-worker/workers/sentry/src/index.ts new file mode 100644 index 0000000..c37a481 --- /dev/null +++ b/fixtures/multi-worker/workers/sentry/src/index.ts @@ -0,0 +1,13 @@ +import * as Sentry from "@sentry/cloudflare"; + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: "https://foo@sentry.com/123456", + }), + { + async fetch(request, env, ctx): Promise<Response> { + console.log("Hello World!"); + return new Response("Hello World!"); + }, + } satisfies ExportedHandler<Env> +); diff --git a/fixtures/multi-worker/workers/sentry/tsconfig.json b/fixtures/multi-worker/workers/sentry/tsconfig.json new file mode 100644 index 0000000..52b1f79 --- /dev/null +++ b/fixtures/multi-worker/workers/sentry/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2024", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2024"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "es2022", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true, + "types": ["@cloudflare/workers-types"] + }, + "exclude": ["test"], + "include": ["worker-configuration.d.ts", "src/**/*.ts"] +} diff --git a/fixtures/multi-worker/workers/sentry/worker-configuration.d.ts b/fixtures/multi-worker/workers/sentry/worker-configuration.d.ts new file mode 100644 index 0000000..d753bc9 --- /dev/null +++ b/fixtures/multi-worker/workers/sentry/worker-configuration.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./workers/sentry/worker-configuration.d.ts -c ./workers/sentry/wrangler.jsonc --no-include-runtime` (hash: 3d3fdd145760295159fc1eac24135779) +interface __BaseEnv_Env {} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/multi-worker/workers/sentry/wrangler.jsonc b/fixtures/multi-worker/workers/sentry/wrangler.jsonc new file mode 100644 index 0000000..ac537c6 --- /dev/null +++ b/fixtures/multi-worker/workers/sentry/wrangler.jsonc @@ -0,0 +1,14 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "a", + "main": "src/index.ts", + "compatibility_date": "2026-01-01", + "compatibility_flags": ["nodejs_als"], + "observability": { + "enabled": true, + }, +} diff --git a/fixtures/no-bundle-import/CHANGELOG.md b/fixtures/no-bundle-import/CHANGELOG.md new file mode 100644 index 0000000..258111a --- /dev/null +++ b/fixtures/no-bundle-import/CHANGELOG.md @@ -0,0 +1,77 @@ +# no-bundle-import + +## 0.0.1 + +### Patch Changes + +- [#2769](https://github.com/cloudflare/workers-sdk/pull/2769) [`0a779904`](https://github.com/cloudflare/workers-sdk/commit/0a77990457652af36c60c52bf9c38c3a69945de4) Thanks [@penalosa](https://github.com/penalosa)! - feature: Support modules with `--no-bundle` + + When the `--no-bundle` flag is set, Wrangler now has support for uploading additional modules alongside the entrypoint. This will allow modules to be imported at runtime on Cloudflare's Edge. This respects Wrangler's [module rules](https://developers.cloudflare.com/workers/wrangler/configuration/#bundling) configuration, which means that only imports of non-JS modules will trigger an upload by default. For instance, the following code will now work with `--no-bundle` (assuming the `example.wasm` file exists at the correct path): + + ```js + // index.js + import wasm from './example.wasm' + + export default { + async fetch() { + await WebAssembly.instantiate(wasm, ...) + ... + } + } + ``` + + For JS modules, it's necessary to specify an additional [module rule](https://developers.cloudflare.com/workers/wrangler/configuration/#bundling) (or rules) in your `wrangler.toml` to configure your modules as ES modules or Common JS modules. For instance, to upload additional JavaScript files as ES modules, add the following module rule to your `wrangler.toml`, which tells Wrangler that all `**/*.js` files are ES modules. + + ```toml + rules = [ + { type = "ESModule", globs = ["**/*.js"]}, + ] + ``` + + If you have Common JS modules, you'd configure Wrangler with a CommonJS rule (the following rule tells Wrangler that all `.cjs` files are Common JS modules): + + ```toml + rules = [ + { type = "CommonJS", globs = ["**/*.cjs"]}, + ] + ``` + + In most projects, adding a single rule will be sufficient. However, for advanced usecases where you're mixing ES modules and Common JS modules, you'll need to use multiple rule definitions. For instance, the following set of rules will match all `.mjs` files as ES modules, all `.cjs` files as Common JS modules, and the `nested/say-hello.js` file as Common JS. + + ```toml + rules = [ + { type = "CommonJS", globs = ["nested/say-hello.js", "**/*.cjs"]}, + { type = "ESModule", globs = ["**/*.mjs"]} + ] + ``` + + If multiple rules overlap, Wrangler will log a warning about the duplicate rules, and will discard additional rules that matches a module. For example, the following rule configuration classifies `dep.js` as both a Common JS module and an ES module: + + ```toml + rules = [ + { type = "CommonJS", globs = ["dep.js"]}, + { type = "ESModule", globs = ["dep.js"]} + ] + ``` + + Wrangler will treat `dep.js` as a Common JS module, since that was the first rule that matched, and will log the following warning: + + ``` + ▲ [WARNING] Ignoring duplicate module: dep.js (esm) + ``` + + This also adds a new configuration option to `wrangler.toml`: `base_dir`. Defaulting to the directory of your Worker's main entrypoint, this tells Wrangler where your additional modules are located, and determines the module paths against which your module rule globs are matched. + + For instance, given the following directory structure: + + ``` + - wrangler.toml + - src/ + - index.html + - vendor/ + - dependency.js + - js/ + - index.js + ``` + + If your `wrangler.toml` had `main = "src/js/index.js"`, you would need to set `base_dir = "src"` in order to be able to import `src/vendor/dependency.js` and `src/index.html` from `src/js/index.js`. diff --git a/fixtures/no-bundle-import/README.md b/fixtures/no-bundle-import/README.md new file mode 100644 index 0000000..bad6c1a --- /dev/null +++ b/fixtures/no-bundle-import/README.md @@ -0,0 +1,3 @@ +# `no-bundle-import` + +This worker exercises the module collection system when `--no-bundle` is specified. It demonstrates dynamic import and static import, as well as module rules whcih treat different JS files as CommonJS or ESModule diff --git a/fixtures/no-bundle-import/package.json b/fixtures/no-bundle-import/package.json new file mode 100644 index 0000000..246a5b5 --- /dev/null +++ b/fixtures/no-bundle-import/package.json @@ -0,0 +1,15 @@ +{ + "name": "@fixture/no-bundle-import", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "get-port": "^7.0.0", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/no-bundle-import/src/data.bin b/fixtures/no-bundle-import/src/data.bin new file mode 100644 index 0000000000000000000000000000000000000000..2ba219bec224c94416e6484e38cc23247b2ceaba GIT binary patch literal 4 LcmZQzWa0t<02KfZ literal 0 HcmV?d00001 diff --git a/fixtures/no-bundle-import/src/data.txt b/fixtures/no-bundle-import/src/data.txt new file mode 100644 index 0000000..1aa4b5d --- /dev/null +++ b/fixtures/no-bundle-import/src/data.txt @@ -0,0 +1 @@ +TEST DATA \ No newline at end of file diff --git a/fixtures/no-bundle-import/src/dynamic.cjs b/fixtures/no-bundle-import/src/dynamic.cjs new file mode 100644 index 0000000..6cfc3dd --- /dev/null +++ b/fixtures/no-bundle-import/src/dynamic.cjs @@ -0,0 +1 @@ +module.exports = "cjs-string"; diff --git a/fixtures/no-bundle-import/src/dynamic.js b/fixtures/no-bundle-import/src/dynamic.js new file mode 100644 index 0000000..6c40343 --- /dev/null +++ b/fixtures/no-bundle-import/src/dynamic.js @@ -0,0 +1 @@ +export default "dynamic"; diff --git a/fixtures/no-bundle-import/src/index.js b/fixtures/no-bundle-import/src/index.js new file mode 100644 index 0000000..5557de3 --- /dev/null +++ b/fixtures/no-bundle-import/src/index.js @@ -0,0 +1,74 @@ +import binData from "./data.bin"; +import text from "./data.txt"; +import { johnSmith } from "./nested/index.js"; +import nestedWasm from "./nested/simple.wasm"; +import cjs from "./say-hello.cjs"; +import { sayHello } from "./say-hello.js"; +import WASM from "./simple.wasm"; + +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + if (url.pathname === "/dynamic") { + return new Response(`${(await import("./dynamic.js")).default}`); + } + if (url.pathname === "/wasm") { + return new Response( + await new Promise(async (resolve) => { + const moduleImport = { + imports: { + imported_func(arg) { + resolve(arg); + }, + }, + }; + const module1 = await WebAssembly.instantiate(WASM, moduleImport); + module1.exports.exported_func(); + }) + ); + } + if (url.pathname === "/wasm-nested") { + return new Response( + await new Promise(async (resolve) => { + const moduleImport = { + imports: { + imported_func(arg) { + resolve("nested" + arg); + }, + }, + }; + const m = await WebAssembly.instantiate(nestedWasm, moduleImport); + m.exports.exported_func(); + }) + ); + } + if (url.pathname === "/wasm-dynamic") { + return new Response( + `${await (await import("./nested/index.js")).loadWasm()}` + ); + } + + if (url.pathname.startsWith("/lang")) { + const language = url.pathname.split("/lang/")[1]; + return new Response( + `${JSON.parse((await import(`./lang/${language}`)).default).hello}` + ); + } + + if (url.pathname === "/txt") { + return new Response(text); + } + if (url.pathname === "/bin") { + return new Response(binData); + } + if (url.pathname === "/cjs") { + return new Response( + `CJS: ${cjs.sayHello("Jane Smith")} and ${johnSmith}` + ); + } + if (url.pathname === "/cjs-loop") { + return new Response(`CJS: ${cjs.loop}`); + } + return new Response(`${sayHello("Jane Smith")} and ${johnSmith}`); + }, +}; diff --git a/fixtures/no-bundle-import/src/index.test.ts b/fixtures/no-bundle-import/src/index.test.ts new file mode 100644 index 0000000..d1b6e8d --- /dev/null +++ b/fixtures/no-bundle-import/src/index.test.ts @@ -0,0 +1,88 @@ +import path from "path"; +import { afterAll, beforeAll, describe, test } from "vitest"; +import { createTestHarness } from "wrangler"; + +describe("Worker", () => { + const server = createTestHarness({ + root: path.resolve(__dirname, ".."), + workers: [{ configPath: "wrangler.jsonc" }], + }); + + beforeAll(async () => { + await server.listen(); + }, 30_000); + + afterAll(() => server.close()); + + test("module traversal results in correct response", async ({ expect }) => { + const resp = await server.fetch("/"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot( + `"Hello Jane Smith and Hello John Smith"` + ); + }); + + test("module traversal results in correct response for CommonJS", async ({ + expect, + }) => { + const resp = await server.fetch("/cjs"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot( + `"CJS: Hello Jane Smith and Hello John Smith"` + ); + }); + + test("correct response for CommonJS which imports ESM", async ({ + expect, + }) => { + const resp = await server.fetch("/cjs-loop"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot('"CJS: cjs-string"'); + }); + + test("support for dynamic imports", async ({ expect }) => { + const resp = await server.fetch("/dynamic"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot(`"dynamic"`); + }); + + test("basic wasm support", async ({ expect }) => { + const resp = await server.fetch("/wasm"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot('"42"'); + }); + + test("resolves wasm import paths relative to root", async ({ expect }) => { + const resp = await server.fetch("/wasm-nested"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot('"nested42"'); + }); + + test("wasm can be imported from a dynamic import", async ({ expect }) => { + const resp = await server.fetch("/wasm-dynamic"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot('"sibling42subdirectory42"'); + }); + + test("text data can be imported", async ({ expect }) => { + const resp = await server.fetch("/txt"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot('"TEST DATA"'); + }); + + test("binary data can be imported", async ({ expect }) => { + const resp = await server.fetch("/bin"); + const bin = await resp.arrayBuffer(); + const expected = new Uint8Array(new ArrayBuffer(4)); + expected.set([0, 1, 2, 10]); + expect(new Uint8Array(bin)).toEqual(expected); + }); + + test("actual dynamic import (that cannot be inlined by an esbuild run)", async ({ + expect, + }) => { + const resp = await server.fetch("/lang/fr.json"); + const text = await resp.text(); + expect(text).toMatchInlineSnapshot('"Bonjour"'); + }); +}); diff --git a/fixtures/no-bundle-import/src/lang/en.json b/fixtures/no-bundle-import/src/lang/en.json new file mode 100644 index 0000000..7961a72 --- /dev/null +++ b/fixtures/no-bundle-import/src/lang/en.json @@ -0,0 +1,3 @@ +{ + "hello": "Hello" +} diff --git a/fixtures/no-bundle-import/src/lang/fr.json b/fixtures/no-bundle-import/src/lang/fr.json new file mode 100644 index 0000000..8574d7a --- /dev/null +++ b/fixtures/no-bundle-import/src/lang/fr.json @@ -0,0 +1,3 @@ +{ + "hello": "Bonjour" +} diff --git a/fixtures/no-bundle-import/src/nested/index.js b/fixtures/no-bundle-import/src/nested/index.js new file mode 100644 index 0000000..238137f --- /dev/null +++ b/fixtures/no-bundle-import/src/nested/index.js @@ -0,0 +1,36 @@ +import { sayHello } from "../say-hello.js"; +import subWasm from "../simple.wasm"; +import cjs from "./say-hello.js"; +import sibWasm from "./simple.wasm"; + +export const johnSmith = + sayHello("John Smith") === cjs.sayHello("John Smith") + ? sayHello("John Smith") + : false; + +export async function loadWasm() { + const sibling = await new Promise(async (resolve) => { + const moduleImport = { + imports: { + imported_func(arg) { + resolve("sibling" + arg); + }, + }, + }; + const m = await WebAssembly.instantiate(sibWasm, moduleImport); + m.exports.exported_func(); + }); + + const subdirectory = await new Promise(async (resolve) => { + const moduleImport = { + imports: { + imported_func(arg) { + resolve("subdirectory" + arg); + }, + }, + }; + const m = await WebAssembly.instantiate(subWasm, moduleImport); + m.exports.exported_func(); + }); + return sibling + subdirectory; +} diff --git a/fixtures/no-bundle-import/src/nested/say-hello.js b/fixtures/no-bundle-import/src/nested/say-hello.js new file mode 100644 index 0000000..3dc2add --- /dev/null +++ b/fixtures/no-bundle-import/src/nested/say-hello.js @@ -0,0 +1 @@ +module.exports.sayHello = (name) => `Hello ${name}`; diff --git a/fixtures/no-bundle-import/src/nested/simple.wasm b/fixtures/no-bundle-import/src/nested/simple.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a2e9ad6acbc70204dba743dd5064f0afab5a6e38 GIT binary patch literal 78 zcmZQbEY4+QU|?Y6U`k-DXGmaRV3K5H&&(~zFDfbKh0v)f@oA-b$qWq4OpJ`|f{eVW U6(DJtFe4WSBO8OGmH-1c0JS<1egFUf literal 0 HcmV?d00001 diff --git a/fixtures/no-bundle-import/src/say-hello.cjs b/fixtures/no-bundle-import/src/say-hello.cjs new file mode 100644 index 0000000..bb97cd1 --- /dev/null +++ b/fixtures/no-bundle-import/src/say-hello.cjs @@ -0,0 +1,3 @@ +module.exports.sayHello = (name) => `Hello ${name}`; + +module.exports.loop = require("./dynamic.cjs"); diff --git a/fixtures/no-bundle-import/src/say-hello.js b/fixtures/no-bundle-import/src/say-hello.js new file mode 100644 index 0000000..722d080 --- /dev/null +++ b/fixtures/no-bundle-import/src/say-hello.js @@ -0,0 +1 @@ +export const sayHello = (name) => `Hello ${name}`; diff --git a/fixtures/no-bundle-import/src/simple.wasm b/fixtures/no-bundle-import/src/simple.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a2e9ad6acbc70204dba743dd5064f0afab5a6e38 GIT binary patch literal 78 zcmZQbEY4+QU|?Y6U`k-DXGmaRV3K5H&&(~zFDfbKh0v)f@oA-b$qWq4OpJ`|f{eVW U6(DJtFe4WSBO8OGmH-1c0JS<1egFUf literal 0 HcmV?d00001 diff --git a/fixtures/no-bundle-import/vitest.config.mts b/fixtures/no-bundle-import/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/no-bundle-import/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/no-bundle-import/wrangler.jsonc b/fixtures/no-bundle-import/wrangler.jsonc new file mode 100644 index 0000000..0dcfa3e --- /dev/null +++ b/fixtures/no-bundle-import/wrangler.jsonc @@ -0,0 +1,21 @@ +{ + "name": "no-bundle-import", + "main": "src/index.js", + "compatibility_date": "2023-02-20", + "no_bundle": true, + "rules": [ + { + "type": "CommonJS", + "globs": ["nested/say-hello.js", "**/*.cjs"], + }, + { + "type": "ESModule", + "globs": ["**/*.js"], + }, + { + "type": "Text", + "globs": ["**/*.json"], + "fallthrough": true, + }, + ], +} diff --git a/fixtures/node-app-pages/.gitignore b/fixtures/node-app-pages/.gitignore new file mode 100644 index 0000000..79451db --- /dev/null +++ b/fixtures/node-app-pages/.gitignore @@ -0,0 +1 @@ +cdn-cgi/ diff --git a/fixtures/node-app-pages/functions/stripe.ts b/fixtures/node-app-pages/functions/stripe.ts new file mode 100644 index 0000000..34e9703 --- /dev/null +++ b/fixtures/node-app-pages/functions/stripe.ts @@ -0,0 +1,12 @@ +import path from "path"; +import Stripe from "stripe"; + +export const onRequest = () => { + // make sure path actually works + return new Response( + JSON.stringify({ + PATH: path.join("path/to", "some-file"), + STRIPE_OBJECT: Stripe.toString(), + }) + ); +}; diff --git a/fixtures/node-app-pages/package.json b/fixtures/node-app-pages/package.json new file mode 100644 index 0000000..0172e5a --- /dev/null +++ b/fixtures/node-app-pages/package.json @@ -0,0 +1,24 @@ +{ + "name": "@fixture/pages-node-app", + "private": true, + "sideEffects": false, + "main": "dist/worker.js", + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 12345", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "stripe": "^9.1.0" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/node-app-pages/public/index.html b/fixtures/node-app-pages/public/index.html new file mode 100644 index 0000000..1520bf7 --- /dev/null +++ b/fixtures/node-app-pages/public/index.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> + <body> + <h1>Hello, world!</h1> + </body> +</html> diff --git a/fixtures/node-app-pages/tests/index.test.ts b/fixtures/node-app-pages/tests/index.test.ts new file mode 100644 index 0000000..7d273f2 --- /dev/null +++ b/fixtures/node-app-pages/tests/index.test.ts @@ -0,0 +1,30 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Dev", () => { + it("should work with `nodejs_compat` when running code requiring polyfills", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + [ + "--port=0", + "--inspector-port=0", + "--compatibility-flags=nodejs_compat", + "--compatibility-date=2024-11-01", + ] + ); + try { + const response = await fetch(`http://${ip}:${port}/stripe`); + + await expect(response.text()).resolves.toContain( + `"PATH":"path/to/some-file","STRIPE_OBJECT"` + ); + } finally { + await stop(); + } + }); +}); diff --git a/fixtures/node-app-pages/tests/tsconfig.json b/fixtures/node-app-pages/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/node-app-pages/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/node-app-pages/tsconfig.json b/fixtures/node-app-pages/tsconfig.json new file mode 100644 index 0000000..059b256 --- /dev/null +++ b/fixtures/node-app-pages/tsconfig.json @@ -0,0 +1,13 @@ +{ + "include": ["functions"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types/experimental"], + "moduleResolution": "node", + "skipLibCheck": true, + "esModuleInterop": true, + "noEmit": true + } +} diff --git a/fixtures/node-app-pages/vitest.config.mts b/fixtures/node-app-pages/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/node-app-pages/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/node-env/package.json b/fixtures/node-env/package.json new file mode 100644 index 0000000..85c3b1d --- /dev/null +++ b/fixtures/node-env/package.json @@ -0,0 +1,25 @@ +{ + "name": "@fixture/node-env", + "private": true, + "scripts": { + "check:type": "tsc", + "dev": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@types/node": "catalog:default", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.2.0", + "miniflare": "workspace:*", + "typescript": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/node-env/src/index.ts b/fixtures/node-env/src/index.ts new file mode 100644 index 0000000..437bfbc --- /dev/null +++ b/fixtures/node-env/src/index.ts @@ -0,0 +1,20 @@ +import React from "react"; +import { renderToString } from "react-dom/server"; + +export default { + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === "/ssr") { + const content = renderToString( + React.createElement("h1", null, "Hello world") + ); + + return new Response(content); + } + + return new Response( + `The value of process.env.NODE_ENV is "${process.env.NODE_ENV}"` + ); + }, +} satisfies ExportedHandler; diff --git a/fixtures/node-env/tests/node-env.test.ts b/fixtures/node-env/tests/node-env.test.ts new file mode 100644 index 0000000..c7f53dc --- /dev/null +++ b/fixtures/node-env/tests/node-env.test.ts @@ -0,0 +1,137 @@ +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { Miniflare } from "miniflare"; +import { describe, it, vi } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("`process.env.NODE_ENV` replacement in development", () => { + it("replaces `process.env.NODE_ENV` with `development` if it is `undefined`", async ({ + expect, + }) => { + vi.stubEnv("NODE_ENV", undefined); + + const { ip, port, stop } = await runWranglerDev( + path.resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + ); + + await vi.waitFor(async () => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toBe(`The value of process.env.NODE_ENV is "development"`); + }); + + await stop(); + + vi.unstubAllEnvs(); + }); + + it("replaces `process.env.NODE_ENV` with the given value if it is set", async ({ + expect, + }) => { + vi.stubEnv("NODE_ENV", "some-value"); + + const { ip, port, stop } = await runWranglerDev( + path.resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + ); + + await vi.waitFor(async () => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toBe(`The value of process.env.NODE_ENV is "some-value"`); + }); + + await stop(); + + vi.unstubAllEnvs(); + }); +}); + +describe("`process.env.NODE_ENV` replacement in production", () => { + const url = "http://localhost"; + + it("replaces `process.env.NODE_ENV` with `production` if it is `undefined`", async ({ + expect, + }) => { + vi.stubEnv("NODE_ENV", undefined); + + spawnSync("npx wrangler build", { + shell: true, + stdio: "pipe", + }); + + const miniflare = new Miniflare({ + modules: [ + { + type: "ESModule", + path: "./dist/index.js", + }, + ], + }); + + await miniflare.ready; + + await vi.waitFor(async () => { + const response = await miniflare.dispatchFetch(url); + const text = await response.text(); + expect(text).toBe(`The value of process.env.NODE_ENV is "production"`); + }); + + await miniflare.dispose(); + + vi.unstubAllEnvs(); + }); + + it("replaces `process.env.NODE_ENV` with the given value if it is set", async ({ + expect, + }) => { + vi.stubEnv("NODE_ENV", "some-value"); + + spawnSync("npx wrangler build", { + shell: true, + stdio: "pipe", + }); + + const miniflare = new Miniflare({ + modules: [ + { + type: "ESModule", + path: "./dist/index.js", + }, + ], + }); + + await miniflare.ready; + + await vi.waitFor(async () => { + const response = await miniflare.dispatchFetch(url); + const text = await response.text(); + expect(text).toBe(`The value of process.env.NODE_ENV is "some-value"`); + }); + + await miniflare.dispose(); + + vi.unstubAllEnvs(); + }); + + it("tree shakes React when `process.env.NODE_ENV` is `production`", ({ + expect, + }) => { + vi.stubEnv("NODE_ENV", undefined); + + spawnSync("npx wrangler build", { + shell: true, + stdio: "pipe", + }); + + const outputJs = fs.readFileSync("./dist/index.js", "utf8"); + + expect(outputJs).not.toContain("react-dom.development.js"); + // the React development code links to the facebook/react repo + expect(outputJs).not.toContain("https://github.com/facebook/react"); + + vi.unstubAllEnvs(); + }); +}); diff --git a/fixtures/node-env/tsconfig.json b/fixtures/node-env/tsconfig.json new file mode 100644 index 0000000..caa95db --- /dev/null +++ b/fixtures/node-env/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental", "node"] + }, + "include": ["src", "tests"] +} diff --git a/fixtures/node-env/vitest.config.mts b/fixtures/node-env/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/node-env/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/node-env/wrangler.jsonc b/fixtures/node-env/wrangler.jsonc new file mode 100644 index 0000000..e2dd3d1 --- /dev/null +++ b/fixtures/node-env/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "node-env", + "compatibility_date": "2025-10-01", + "main": "./src/index.ts", +} diff --git a/fixtures/nodejs-als-app/package.json b/fixtures/nodejs-als-app/package.json new file mode 100644 index 0000000..be70296 --- /dev/null +++ b/fixtures/nodejs-als-app/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixture/nodejs-als", + "private": true, + "scripts": { + "build": "wrangler deploy --dry-run --outdir=./dist", + "dev": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/nodejs-als-app/src/index.ts b/fixtures/nodejs-als-app/src/index.ts new file mode 100644 index 0000000..5ab5b1b --- /dev/null +++ b/fixtures/nodejs-als-app/src/index.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const asyncLocalStorage = new AsyncLocalStorage<number>(); + +export default { + async fetch(req: Request, env: unknown, ctx: ExecutionContext) { + if (new URL(req.url).pathname !== "/") { + return new Response("No Found", { status: 404 }); + } + + const results = await Promise.all([ + asyncLocalStorage.run(1, async () => getValue()), + asyncLocalStorage.run(2, async () => getValue()), + asyncLocalStorage.run(3, async () => getValue()), + ]); + + return new Response(`Working ${JSON.stringify(results)}`); + }, +}; + +async function getValue() { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return asyncLocalStorage.getStore(); +} diff --git a/fixtures/nodejs-als-app/tests/index.test.ts b/fixtures/nodejs-als-app/tests/index.test.ts new file mode 100644 index 0000000..a320020 --- /dev/null +++ b/fixtures/nodejs-als-app/tests/index.test.ts @@ -0,0 +1,25 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("nodejs als", () => { + it("should work with node:async_hooks without full nodejs compat", async ({ + expect, + }) => { + const { ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, "../src"), + ["--port=0", "--inspector-port=0"] + ); + try { + // There should be no warning about async_hooks + expect(getOutput()).not.toContain("node:async_hooks"); + + const response = await fetch(`http://${ip}:${port}/`); + const body = await response.text(); + expect(body).toMatchInlineSnapshot(`"Working [1,2,3]"`); + } finally { + await stop(); + } + }); +}); diff --git a/fixtures/nodejs-als-app/tests/tsconfig.json b/fixtures/nodejs-als-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/nodejs-als-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/nodejs-als-app/tsconfig.json b/fixtures/nodejs-als-app/tsconfig.json new file mode 100644 index 0000000..3ffb885 --- /dev/null +++ b/fixtures/nodejs-als-app/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "strict": true, + "isolatedModules": true, + "noEmit": true, + "types": ["@cloudflare/workers-types/experimental", "node"], + "allowJs": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} diff --git a/fixtures/nodejs-als-app/turbo.json b/fixtures/nodejs-als-app/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/nodejs-als-app/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/nodejs-als-app/vitest.config.mts b/fixtures/nodejs-als-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/nodejs-als-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/nodejs-als-app/wrangler.jsonc b/fixtures/nodejs-als-app/wrangler.jsonc new file mode 100644 index 0000000..ff506b3 --- /dev/null +++ b/fixtures/nodejs-als-app/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "nodejs-als-app", + "main": "src/index.ts", + "compatibility_date": "2024-08-03", + "compatibility_flags": ["nodejs_als"], +} diff --git a/fixtures/nodejs-hybrid-app/.env b/fixtures/nodejs-hybrid-app/.env new file mode 100644 index 0000000..d81f8ff --- /dev/null +++ b/fixtures/nodejs-hybrid-app/.env @@ -0,0 +1 @@ +DEV_VAR_FROM_DOT_ENV="dev-var-from-dot-env" \ No newline at end of file diff --git a/fixtures/nodejs-hybrid-app/.gitignore b/fixtures/nodejs-hybrid-app/.gitignore new file mode 100644 index 0000000..1e18f27 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/.gitignore @@ -0,0 +1 @@ +!.env \ No newline at end of file diff --git a/fixtures/nodejs-hybrid-app/package.json b/fixtures/nodejs-hybrid-app/package.json new file mode 100644 index 0000000..04bd408 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "@fixture/nodejs-hybrid", + "private": true, + "scripts": { + "build": "wrangler deploy --dry-run --outdir=./dist", + "dev": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@types/debug": "4.1.12", + "@types/pg": "^8.11.2", + "debug": "4.4.1", + "pg": "8.16.3", + "pg-cloudflare": "^1.2.7", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/nodejs-hybrid-app/src/dep.cjs b/fixtures/nodejs-hybrid-app/src/dep.cjs new file mode 100644 index 0000000..e1a3189 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/src/dep.cjs @@ -0,0 +1,4 @@ +const Stream = require("stream"); + +const s = new Stream(); +module.exports.s = s; diff --git a/fixtures/nodejs-hybrid-app/src/index.ts b/fixtures/nodejs-hybrid-app/src/index.ts new file mode 100644 index 0000000..a15e789 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/src/index.ts @@ -0,0 +1,354 @@ +import nodeCrypto, { getRandomValues, webcrypto } from "crypto"; +// node:assert/strict is currently an unenv alias to node:assert +// this is not very common, but happens and we need to support it +import assert from "node:assert/strict"; +import { Stream } from "node:stream"; +import { Context } from "vm"; +import { Client } from "pg"; +import { s } from "./dep.cjs"; + +testBasicNodejsProperties(); + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise<Response> { + const url = new URL(request.url); + + switch (url.pathname) { + case "/test-random": + return testGetRandomValues(); + case "/test-process": + return testProcessBehavior(); + case "/env": + return Response.json(env); + case "/process-env": + return Response.json(process.env); + case "/query": + return testPostgresLibrary(env, ctx); + case "/test-x509-certificate": + return testX509Certificate(); + case "/test-require-alias": + return testRequireUnenvAliasedPackages(); + case "/test-immediate": + return await testImmediate(); + case "/test-tls": + return await testTls(); + case "/test-crypto": + return await testCrypto(); + case "/test-sqlite": + return await testSqlite(); + case "/test-http": + return await testHttp(); + case "/test-debug-import": + return await testDebugImport(); + case "/test-debug-require": + return await testDebugRequire(); + } + + return new Response( + `<a href="query">Postgres query</a> +<a href="test-process">Test process global</a> +<a href="test-random">Test getRandomValues()</a> +<a href="test-x509-certificate">Test X509Certificate</a> +<a href="test-require-alias">Test require unenv aliased packages</a> +<a href="test-immediate">Test setImmediate</a> +<a href="test-tls">node:tls</a> +<a href="test-crypto">node:crypto</a> +<a href="test-sqlite">node:sqlite</a> +<a href="test-http">node:http</a> +<a href="test-debug-import">debug (import)</a> +<a href="test-debug-require">debug (require)</a> +`, + { headers: { "Content-Type": "text/html; charset=utf-8" } } + ); + }, +}; + +async function testImmediate() { + try { + await new Promise((resolve, reject) => { + // Give it a whole second otherwise it times-out + setTimeout(reject, 1000); + + // This setImmediate should never trigger the reject if the clearImmediate is working + const id = setImmediate(reject); + // clearImmediate should cancel reject callback + clearImmediate(id); + // This setImmediate should trigger the resolve callback + setImmediate(resolve); + }); + + return new Response("OK"); + } catch (e) { + return new Response(`NOT OK: ${e}`); + } +} + +function testRequireUnenvAliasedPackages() { + const fetch = require("cross-fetch"); + const supportsDefaultExports = typeof fetch === "function"; + const supportsNamedExports = typeof fetch.Headers === "function"; + return new Response( + supportsDefaultExports && supportsNamedExports ? `"OK!"` : `"KO!"` + ); +} + +function testX509Certificate() { + try { + new nodeCrypto.X509Certificate(`-----BEGIN CERTIFICATE----- +MIICZjCCAc+gAwIBAgIUOsv8Y+x40C+gdNuu40N50KpGUhEwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjAwOTA4MTNaFw0yNTA5 +MjAwOTA4MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwgZ8wDQYJKoZIhvcNAQEB +BQADgY0AMIGJAoGBALpJn3dUrNmZhZV02RbjZKTd5j3hpgTncF4lG4Y3sQA18k0l +7pt6xpZuXYSFH7v2zTAxYy+uYyYwX2NZur48dZc76FSzIeuQdoTCkT0NacwFRTR5 +fEEqPvvB85ozYuyk8Bl3vSsonivOH3WftEDp9mjkHROQzS4wAZbIj7Cp+is/AgMB +AAGjUzBRMB0GA1UdDgQWBBSzFJSiPAw2tJOg8oUXrFBdqWI6zDAfBgNVHSMEGDAW +gBSzFJSiPAw2tJOg8oUXrFBdqWI6zDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4GBACbto0+Ds40F7faRFFMwg5nPyh7gsiX+ZK3FYcrO3oxh5ejfzwow +DKOOje4Ncaw0rIkVpxacPyjg+wANuK2Nv/Z4CVAD3mneE4gwgRdn38q8IYN9AtSv +GzEf4UxiLBbUB6WRBgyVyquGfUMlKl/tnm4q0yeYQloYKSoHpGeHVJuN +-----END CERTIFICATE-----`); + return new Response(`"OK!"`); + } catch { + return new Response(`"KO!"`); + } +} + +function testGetRandomValues() { + assert.strictEqual(nodeCrypto.getRandomValues, getRandomValues); + + return Response.json([ + crypto.getRandomValues(new Uint8Array(6)).toString(), // global + webcrypto.getRandomValues(new Uint8Array(6)).toString(), // webcrypto + nodeCrypto.getRandomValues(new Uint8Array(6)).toString(), // namespace import + getRandomValues(new Uint8Array(6)).toString(), // named import + ]); +} + +function testBasicNodejsProperties() { + assert(s instanceof Stream, "expected s to be an instance of Stream"); + + const buffer1 = Buffer.of(1); + assert.strictEqual(buffer1.toJSON().data[0], 1); + + const buffer2 = global.Buffer.of(1); + assert.strictEqual(buffer2.toJSON().data[0], 1); + + const buffer3 = globalThis.Buffer.of(1); + assert.strictEqual(buffer3.toJSON().data[0], 1); + + assert.notEqual(performance, undefined); + assert.strictEqual(global.performance, performance); + assert.strictEqual(globalThis.performance, performance); + + assert.notEqual(Performance, undefined); + assert.strictEqual(global.Performance, Performance); + assert.strictEqual(globalThis.Performance, Performance); + + assert.strictEqual(typeof performance.measure, "function"); + assert.strictEqual(typeof performance.clearMarks, "function"); +} + +function testProcessBehavior() { + assert.strictEqual(typeof process.version, "string"); + assert.strictEqual(typeof process.versions.node, "string"); + + const originalProcess = process; + try { + assert.notEqual(process, undefined); + assert.strictEqual(globalThis.process, process); + assert.strictEqual(global.process, process); + + const fakeProcess1 = {} as typeof process; + process = fakeProcess1; + assert.strictEqual(process, fakeProcess1); + assert.strictEqual(global.process, fakeProcess1); + assert.strictEqual(globalThis.process, fakeProcess1); + + const fakeProcess2 = {} as typeof process; + global.process = fakeProcess2; + assert.strictEqual(process, fakeProcess2); + assert.strictEqual(global.process, fakeProcess2); + assert.strictEqual(globalThis.process, fakeProcess2); + + const fakeProcess3 = {} as typeof process; + globalThis.process = fakeProcess3; + assert.strictEqual(process, fakeProcess3); + assert.strictEqual(global.process, fakeProcess3); + assert.strictEqual(globalThis.process, fakeProcess3); + + const fakeProcess4 = {} as typeof process; + globalThis["process"] = fakeProcess4; + assert.strictEqual(process, fakeProcess4); + assert.strictEqual(global.process, fakeProcess4); + assert.strictEqual(globalThis.process, fakeProcess4); + } catch (e) { + if (e instanceof Error) { + return new Response(`${e.stack}`, { status: 500 }); + } else { + throw e; + } + } finally { + process = originalProcess; + } + + return new Response("OK!"); +} + +async function testPostgresLibrary(env: Env, ctx: Context) { + const client = new Client({ + user: env.DB_USERNAME, + password: env.DB_PASSWORD, + host: env.DB_HOSTNAME, + port: Number(env.DB_PORT), + database: env.DB_NAME, + }); + await client.connect(); + const result = await client.query( + `SELECT * FROM rnacen.rnc_database LIMIT 5` + ); + // Return the first row as JSON + const resp = new Response(JSON.stringify(result.rows[0]), { + headers: { "Content-Type": "application/json" }, + }); + + // Clean up the client + ctx.waitUntil(client.end()); + return resp; +} + +async function testTls() { + const tls = await import("node:tls"); + + assert.strictEqual(typeof tls.connect, "function"); + assert.strictEqual(typeof tls.TLSSocket, "function"); + assert.strictEqual(typeof tls.checkServerIdentity, "function"); + assert.strictEqual( + tls.checkServerIdentity("a.com", { subject: { CN: "a.com" } }), + undefined + ); + assert.strictEqual(typeof tls.SecureContext, "function"); + assert.strictEqual(typeof tls.createSecureContext, "function"); + assert.strictEqual( + tls.createSecureContext({}) instanceof tls.SecureContext, + true + ); + + assert.strictEqual(typeof tls.convertALPNProtocols, "function"); + + return new Response("OK"); +} + +async function testCrypto() { + const crypto = await import("node:crypto"); + + const test = { name: "aes-128-cbc", size: 16, iv: 16 }; + + const key = crypto.createSecretKey(Buffer.alloc(test.size)); + const iv = Buffer.alloc(test.iv); + + const cipher = crypto.createCipheriv(test.name, key, iv); + const decipher = crypto.createDecipheriv(test.name, key, iv); + + let data = ""; + data += decipher.update(cipher.update("Hello World", "utf8")); + data += decipher.update(cipher.final()); + data += decipher.final(); + assert.strictEqual(data, "Hello World"); + + assert.strictEqual(crypto.constants.DH_UNABLE_TO_CHECK_GENERATOR, 4); + assert.strictEqual(crypto.constants.RSA_PSS_SALTLEN_DIGEST, -1); + assert.strictEqual( + crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, + 262144 + ); + assert.strictEqual(crypto.constants.SSL_OP_NO_TICKET, 16384); + + return new Response("OK"); +} + +async function testSqlite() { + const sqlite = await import("node:sqlite"); + + assert.strictEqual(typeof sqlite.DatabaseSync, "function"); + + return new Response("OK"); +} + +async function testHttp() { + const http = await import("node:http"); + + const agent = new http.Agent(); + assert.strictEqual(typeof agent.options, "object"); + + return new Response("OK"); +} + +async function testDebugImport() { + const debug = (await import("debug")).default; + const capturedLogs: string[] = []; + + // Override debug.log to capture output for verification + debug.log = (...args: string[]) => { + capturedLogs.push(args.join(" ")); + }; + + // Test different namespaces based on DEBUG env var: "example:*,test" + const testNamespace = debug("test"); // Should log (matches "test") + const exampleNamespace = debug("example"); // Should NOT log (doesn't match "example:*") + const exampleFooNamespace = debug("example:foo"); // Should log (matches "example:*") + + testNamespace("Test import message 1"); + exampleNamespace("Example import message (should not appear)"); + exampleFooNamespace("Example foo import message"); + + if (testNamespace.enabled) { + testNamespace("Test import enabled message"); + } + + // Strip timestamps from captured logs, keeping namespace and message + // Format: "2025-08-14T20:09:49.769Z test Test import message 1" + const logsWithoutTimestamp = capturedLogs.map((log) => { + const parts = log.split(" "); + return parts.slice(1).join(" "); // Remove timestamp, keep namespace + message + }); + + return Response.json(logsWithoutTimestamp); +} + +async function testDebugRequire() { + const debug = require("debug"); + const capturedLogs: string[] = []; + + // Override debug.log to capture output for verification + debug.log = (...args: string[]) => { + capturedLogs.push(args.join(" ")); + }; + + // Test different namespaces based on DEBUG env var: "example:*,test" + const testNamespace = debug("test"); // Should log (matches "test") + const exampleNamespace = debug("example"); // Should NOT log (doesn't match "example:*") + const exampleFooNamespace = debug("example:foo"); // Should log (matches "example:*") + + testNamespace("Test require message 1"); + exampleNamespace("Example require message (should not appear)"); + exampleFooNamespace("Example foo require message"); + + if (testNamespace.enabled) { + testNamespace("Test require enabled message"); + } + + // Strip timestamps from captured logs, keeping namespace and message + // Format: "2025-08-14T20:09:49.769Z test Test require message 1" + const logsWithoutTimestamp = capturedLogs.map((log) => { + const parts = log.split(" "); + return parts.slice(1).join(" "); // Remove timestamp, keep namespace + message + }); + + return Response.json(logsWithoutTimestamp); +} diff --git a/fixtures/nodejs-hybrid-app/tests/index.test.ts b/fixtures/nodejs-hybrid-app/tests/index.test.ts new file mode 100644 index 0000000..f5d4448 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/tests/index.test.ts @@ -0,0 +1,140 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it, test } from "vitest"; +import { createMockPostgresServer } from "../../shared/src/mock-postgres-server"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("nodejs compat", () => { + let wrangler: Awaited<ReturnType<typeof runWranglerDev>>; + let mockPg: Awaited<ReturnType<typeof createMockPostgresServer>>; + + beforeAll(async () => { + // Start a local mock Postgres server that returns canned results + mockPg = await createMockPostgresServer({ + rows: [{ id: "1", name: "test-row" }], + }); + + wrangler = await runWranglerDev(resolve(__dirname, "../src"), [ + "--port=0", + "--inspector-port=0", + `--var`, + `DB_PORT:${mockPg.port}`, + ]); + }); + + afterAll(async () => { + await wrangler.stop(); + await mockPg.stop(); + }); + + it("should work when running code requiring polyfills", async ({ + expect, + }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-process`); + const body = await response.text(); + expect(body).toMatchInlineSnapshot(`"OK!"`); + }); + + it("should be able to call `getRandomValues()` bound to any object", async ({ + expect, + }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-random`); + const body = await response.json(); + expect(body).toEqual([ + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + ]); + }); + + test("crypto.X509Certificate is implemented", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-x509-certificate`); + await expect(response.text()).resolves.toBe(`"OK!"`); + }); + + test("import unenv aliased packages", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-require-alias`); + await expect(response.text()).resolves.toBe(`"OK!"`); + }); + + test("set/clearImmediate", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-immediate`); + await expect(response.text()).resolves.toBe("OK"); + }); + + test("node:tls", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-tls`); + await expect(response.text()).resolves.toBe("OK"); + }); + + test("node:crypto", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-crypto`); + await expect(response.text()).resolves.toBe("OK"); + }); + + test("node:sqlite", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-sqlite`); + await expect(response.text()).resolves.toBe("OK"); + }); + + test("node:http", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-http`); + await expect(response.text()).resolves.toBe("OK"); + }); + + test("debug import", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-debug-import`); + await expect(response.json()).resolves.toEqual([ + "test Test import message 1", + "example:foo Example foo import message", + "test Test import enabled message", + ]); + }); + + test("debug require", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/test-debug-require`); + await expect(response.json()).resolves.toEqual([ + "test Test require message 1", + "example:foo Example foo require message", + "test Test require enabled message", + ]); + }); + + test("process.env contains vars", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/process-env`); + await expect(response.json()).resolves.toMatchObject({ + DB_HOSTNAME: "127.0.0.1", + DEV_VAR_FROM_DOT_ENV: "dev-var-from-dot-env", + }); + }); + + test("env contains vars", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/env`); + await expect(response.json()).resolves.toMatchObject({ + DB_HOSTNAME: "127.0.0.1", + DEV_VAR_FROM_DOT_ENV: "dev-var-from-dot-env", + }); + }); + + test("Postgres", async ({ expect }) => { + const { ip, port } = wrangler; + const response = await fetch(`http://${ip}:${port}/query`); + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toMatchObject({ id: "1" }); + }); +}); diff --git a/fixtures/nodejs-hybrid-app/tests/tsconfig.json b/fixtures/nodejs-hybrid-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/nodejs-hybrid-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/nodejs-hybrid-app/tsconfig.json b/fixtures/nodejs-hybrid-app/tsconfig.json new file mode 100644 index 0000000..c36bd33 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "strict": true, + "isolatedModules": true, + "noEmit": true, + "types": [ + "@cloudflare/workers-types/experimental", + "./worker-configuration.d.ts" + ], + "allowJs": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} diff --git a/fixtures/nodejs-hybrid-app/turbo.json b/fixtures/nodejs-hybrid-app/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/nodejs-hybrid-app/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/nodejs-hybrid-app/vitest.config.mts b/fixtures/nodejs-hybrid-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/nodejs-hybrid-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/nodejs-hybrid-app/worker-configuration.d.ts b/fixtures/nodejs-hybrid-app/worker-configuration.d.ts new file mode 100644 index 0000000..f7c75b2 --- /dev/null +++ b/fixtures/nodejs-hybrid-app/worker-configuration.d.ts @@ -0,0 +1,9 @@ +// Generated by Wrangler by running `wrangler types` + +interface Env { + DB_HOSTNAME: string; + DB_PORT: string; + DB_NAME: string; + DB_USERNAME: string; + DB_PASSWORD: string; +} diff --git a/fixtures/nodejs-hybrid-app/wrangler.jsonc b/fixtures/nodejs-hybrid-app/wrangler.jsonc new file mode 100644 index 0000000..6bbdccf --- /dev/null +++ b/fixtures/nodejs-hybrid-app/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "name": "nodejs-hybrid-app", + "main": "src/index.ts", + // Setting compat date after 2024/09/23 means we don't need to use `nodejs_compat_v2` + // Setting compat date after 2025/04/01 means we don't need to use `nodejs_compat_populate_process_env` + "compatibility_date": "2025-07-01", + "compatibility_flags": ["nodejs_compat"], + // Default DB connection values point at 127.0.0.1. + // Tests override DB_PORT at runtime to point at a local mock Postgres server. + "vars": { + "DB_HOSTNAME": "127.0.0.1", + "DB_PORT": "5432", + "DB_NAME": "testdb", + "DB_USERNAME": "testuser", + "DB_PASSWORD": "testpassword", + "DEBUG": "example:*,test", + }, +} diff --git a/fixtures/pages-dev-proxy-with-script/_worker.js b/fixtures/pages-dev-proxy-with-script/_worker.js new file mode 100644 index 0000000..3fb22f4 --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/_worker.js @@ -0,0 +1,5 @@ +export default { + fetch() { + return new Response("hello from _worker.js"); + }, +}; diff --git a/fixtures/pages-dev-proxy-with-script/custom/script/path/index.js b/fixtures/pages-dev-proxy-with-script/custom/script/path/index.js new file mode 100644 index 0000000..a74d25b --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/custom/script/path/index.js @@ -0,0 +1,5 @@ +export default { + fetch() { + return new Response("hello from custom/script/path/index.js"); + }, +}; diff --git a/fixtures/pages-dev-proxy-with-script/package.json b/fixtures/pages-dev-proxy-with-script/package.json new file mode 100644 index 0000000..5ff2dad --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/pages-dev-proxy", + "private": true, + "sideEffects": false, + "scripts": { + "dev": "wrangler pages dev public", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-dev-proxy-with-script/tests/index.test.ts b/fixtures/pages-dev-proxy-with-script/tests/index.test.ts new file mode 100644 index 0000000..628cddb --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/tests/index.test.ts @@ -0,0 +1,75 @@ +import { ChildProcess, fork } from "node:child_process"; +import path from "node:path"; +import { setTimeout } from "node:timers/promises"; +import { fetch, Response } from "undici"; +import { describe, it, onTestFinished } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages dev with proxy and a script file", () => { + it("should handle requests using a script from the default _worker.js path", async ({ + expect, + }) => { + const process = await startWranglerPagesDevProxy(); + const combinedResponse = await waitUntilReady( + `http://127.0.0.1:${process.port}/` + ); + const respText = await combinedResponse.text(); + expect(respText).toMatchInlineSnapshot('"hello from _worker.js"'); + expect( + process + .getOutput() + .includes( + "Specifying a `-- <command>` or `--proxy` is deprecated and will be removed in a future version of Wrangler." + ) + ).toBeTruthy(); + + expect( + process + .getOutput() + .includes( + "On Node.js 17+, wrangler will default to fetching only the IPv6 address. Please ensure that the process listening on the port specified via `--proxy` is configured for IPv6." + ) + ).toBeTruthy(); + }); + + it("should handle requests using a script from a custom script path", async ({ + expect, + }) => { + const process = await startWranglerPagesDevProxy([ + "--script-path=custom/script/path/index.js", + ]); + const combinedResponse = await waitUntilReady( + `http://127.0.0.1:${process.port}/` + ); + const respText = await combinedResponse.text(); + expect(respText).toMatchInlineSnapshot( + '"hello from custom/script/path/index.js"' + ); + }); +}); + +async function startWranglerPagesDevProxy(extraArgs: string[] = []) { + const process = await runWranglerPagesDev( + path.resolve(__dirname, ".."), + undefined, + ["--port=0", "--proxy=9999", ...extraArgs] + ); + + onTestFinished(process.stop); + + return process; +} + +async function waitUntilReady(url: string): Promise<Response> { + let response: Response | undefined = undefined; + + while (response === undefined) { + await setTimeout(500); + + try { + response = await fetch(url); + } catch (e) {} + } + + return response as Response; +} diff --git a/fixtures/pages-dev-proxy-with-script/tests/tsconfig.json b/fixtures/pages-dev-proxy-with-script/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-dev-proxy-with-script/tsconfig.json b/fixtures/pages-dev-proxy-with-script/tsconfig.json new file mode 100644 index 0000000..570d2e9 --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/tsconfig.json @@ -0,0 +1,12 @@ +{ + "include": ["index.d.ts"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/fixtures/pages-dev-proxy-with-script/vitest.config.mts b/fixtures/pages-dev-proxy-with-script/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-dev-proxy-with-script/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-functions-app/.gitignore b/fixtures/pages-functions-app/.gitignore new file mode 100644 index 0000000..79451db --- /dev/null +++ b/fixtures/pages-functions-app/.gitignore @@ -0,0 +1 @@ +cdn-cgi/ diff --git a/fixtures/pages-functions-app/functions/_middleware.ts b/fixtures/pages-functions-app/functions/_middleware.ts new file mode 100644 index 0000000..f860d7e --- /dev/null +++ b/fixtures/pages-functions-app/functions/_middleware.ts @@ -0,0 +1,5 @@ +export const onRequest = async ({ next }) => { + const response = await next(); + response.headers.set("x-custom", "header value"); + return response; +}; diff --git a/fixtures/pages-functions-app/functions/blog/[slug].ts b/fixtures/pages-functions-app/functions/blog/[slug].ts new file mode 100644 index 0000000..97f0175 --- /dev/null +++ b/fixtures/pages-functions-app/functions/blog/[slug].ts @@ -0,0 +1,6 @@ +export const onRequestGet: PagesFunction<unknown, "slug"> = ({ params }) => { + const { slug } = params; + return new Response(`<h1>A blog with a slug: ${slug}</h1>`, { + headers: { "content-type": "text/html" }, + }); +}; diff --git a/fixtures/pages-functions-app/functions/date.ts b/fixtures/pages-functions-app/functions/date.ts new file mode 100644 index 0000000..b964214 --- /dev/null +++ b/fixtures/pages-functions-app/functions/date.ts @@ -0,0 +1,3 @@ +export const onRequest = () => { + return new Response(new Date().toISOString()); +}; diff --git a/fixtures/pages-functions-app/functions/import-html.ts b/fixtures/pages-functions-app/functions/import-html.ts new file mode 100644 index 0000000..b37bf9e --- /dev/null +++ b/fixtures/pages-functions-app/functions/import-html.ts @@ -0,0 +1,5 @@ +import html from "../static-assets/index.html"; + +export const onRequestGet = () => { + return new Response(html, { headers: { "Content-Type": "text/html" } }); +}; diff --git a/fixtures/pages-functions-app/functions/intercept.ts b/fixtures/pages-functions-app/functions/intercept.ts new file mode 100644 index 0000000..87fb5fa --- /dev/null +++ b/fixtures/pages-functions-app/functions/intercept.ts @@ -0,0 +1,10 @@ +export const onRequest: PagesFunction = async ({ next }) => { + const response = await next(); + return new Response(response.body, { + status: response.status, + headers: { + ...Object.fromEntries(response.headers.entries()), + "x-set-from-functions": "true", + }, + }); +}; diff --git a/fixtures/pages-functions-app/functions/middleware-data/additional-data.ts b/fixtures/pages-functions-app/functions/middleware-data/additional-data.ts new file mode 100644 index 0000000..24b40eb --- /dev/null +++ b/fixtures/pages-functions-app/functions/middleware-data/additional-data.ts @@ -0,0 +1,9 @@ +export const onRequest = [ + async (context) => { + context.data.foo = "bar"; + return await context.next(); + }, + async (context) => { + return Response.json(context.data); + }, +]; diff --git a/fixtures/pages-functions-app/functions/middleware-data/bad-data.ts b/fixtures/pages-functions-app/functions/middleware-data/bad-data.ts new file mode 100644 index 0000000..6c04c3e --- /dev/null +++ b/fixtures/pages-functions-app/functions/middleware-data/bad-data.ts @@ -0,0 +1,4 @@ +export const onRequest = async (context) => { + context.data = "foo-bar"; + return await context.next(); +}; diff --git a/fixtures/pages-functions-app/functions/middleware-data/merge-data.ts b/fixtures/pages-functions-app/functions/middleware-data/merge-data.ts new file mode 100644 index 0000000..694bf2c --- /dev/null +++ b/fixtures/pages-functions-app/functions/middleware-data/merge-data.ts @@ -0,0 +1,13 @@ +export const onRequest = [ + async (context) => { + context.data = { foo: "bar" }; + return await context.next(); + }, + async (context) => { + context.data = { bar: "baz" }; + return await context.next(); + }, + async (context) => { + return Response.json(context.data); + }, +]; diff --git a/fixtures/pages-functions-app/functions/middleware-data/mutate-data.ts b/fixtures/pages-functions-app/functions/middleware-data/mutate-data.ts new file mode 100644 index 0000000..26b34b1 --- /dev/null +++ b/fixtures/pages-functions-app/functions/middleware-data/mutate-data.ts @@ -0,0 +1,13 @@ +export const onRequest = [ + async (context) => { + context.data = { foo: "bar" }; + return await context.next(); + }, + async (context) => { + context.data.bar = "baz"; + return await context.next(); + }, + async (context) => { + return Response.json(context.data); + }, +]; diff --git a/fixtures/pages-functions-app/functions/mounted-plugin/_middleware.ts b/fixtures/pages-functions-app/functions/mounted-plugin/_middleware.ts new file mode 100644 index 0000000..a0409d4 --- /dev/null +++ b/fixtures/pages-functions-app/functions/mounted-plugin/_middleware.ts @@ -0,0 +1,5 @@ +import examplePlugin from "@fixture/pages-plugin"; + +export const onRequest = examplePlugin({ + footerText: "Set from a Plugin!", +}); diff --git a/fixtures/pages-functions-app/functions/mounted-with-param/[p]/plugin/_middleware.ts b/fixtures/pages-functions-app/functions/mounted-with-param/[p]/plugin/_middleware.ts new file mode 100644 index 0000000..df50a1f --- /dev/null +++ b/fixtures/pages-functions-app/functions/mounted-with-param/[p]/plugin/_middleware.ts @@ -0,0 +1,3 @@ +import examplePlugin from "@fixture/pages-plugin"; + +export const onRequest = examplePlugin({ footerText: "Set from a Plugin!" }); diff --git a/fixtures/pages-functions-app/functions/next.ts b/fixtures/pages-functions-app/functions/next.ts new file mode 100644 index 0000000..b1d02a0 --- /dev/null +++ b/fixtures/pages-functions-app/functions/next.ts @@ -0,0 +1 @@ +export const onRequest = ({ next }) => next("/some-asset.html"); diff --git a/fixtures/pages-functions-app/functions/passThroughOnException/_middleware.ts b/fixtures/pages-functions-app/functions/passThroughOnException/_middleware.ts new file mode 100644 index 0000000..e0cb094 --- /dev/null +++ b/fixtures/pages-functions-app/functions/passThroughOnException/_middleware.ts @@ -0,0 +1,5 @@ +export const onRequest = ({ passThroughOnException, next }) => { + passThroughOnException(); + + return next(); +}; diff --git a/fixtures/pages-functions-app/functions/passThroughOnException/nested.ts b/fixtures/pages-functions-app/functions/passThroughOnException/nested.ts new file mode 100644 index 0000000..5ba5e42 --- /dev/null +++ b/fixtures/pages-functions-app/functions/passThroughOnException/nested.ts @@ -0,0 +1,4 @@ +export const onRequest = ({ passThroughOnException }) => { + // @ts-expect-error expecting ReferenceError + x; +}; diff --git a/fixtures/pages-functions-app/functions/passThroughOnExceptionClosed.ts b/fixtures/pages-functions-app/functions/passThroughOnExceptionClosed.ts new file mode 100644 index 0000000..5ba5e42 --- /dev/null +++ b/fixtures/pages-functions-app/functions/passThroughOnExceptionClosed.ts @@ -0,0 +1,4 @@ +export const onRequest = ({ passThroughOnException }) => { + // @ts-expect-error expecting ReferenceError + x; +}; diff --git a/fixtures/pages-functions-app/functions/passThroughOnExceptionOpen.ts b/fixtures/pages-functions-app/functions/passThroughOnExceptionOpen.ts new file mode 100644 index 0000000..32076dd --- /dev/null +++ b/fixtures/pages-functions-app/functions/passThroughOnExceptionOpen.ts @@ -0,0 +1,5 @@ +export const onRequest = ({ passThroughOnException }) => { + passThroughOnException(); + // @ts-expect-error expecting ReferenceError + x; +}; diff --git a/fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/_middleware.ts b/fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/_middleware.ts new file mode 100644 index 0000000..a507dbd --- /dev/null +++ b/fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/_middleware.ts @@ -0,0 +1,13 @@ +export const onRequest = async ({ request, passThroughOnException, next }) => { + passThroughOnException(); + + try { + return await next(); + } catch (e) { + if (new URL(request.url).searchParams.has("catch")) { + return new Response(`Manually caught error: ${e}`); + } + + throw e; + } +}; diff --git a/fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/nested.ts b/fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/nested.ts new file mode 100644 index 0000000..5ba5e42 --- /dev/null +++ b/fixtures/pages-functions-app/functions/passThroughOnExceptionWithCapture/nested.ts @@ -0,0 +1,4 @@ +export const onRequest = ({ passThroughOnException }) => { + // @ts-expect-error expecting ReferenceError + x; +}; diff --git a/fixtures/pages-functions-app/functions/r2/create.ts b/fixtures/pages-functions-app/functions/r2/create.ts new file mode 100644 index 0000000..e659e43 --- /dev/null +++ b/fixtures/pages-functions-app/functions/r2/create.ts @@ -0,0 +1,8 @@ +type Env = { + bucket: R2Bucket; +}; + +export const onRequestPut: PagesFunction<Env> = async ({ env }) => { + const object = await env.bucket.put("test", "Hello world!"); + return new Response(JSON.stringify(object)); +}; diff --git a/fixtures/pages-functions-app/functions/r2/get.ts b/fixtures/pages-functions-app/functions/r2/get.ts new file mode 100644 index 0000000..f461931 --- /dev/null +++ b/fixtures/pages-functions-app/functions/r2/get.ts @@ -0,0 +1,8 @@ +type Env = { + bucket: R2Bucket; +}; + +export const onRequestGet: PagesFunction<Env> = async ({ env }) => { + const object = await env.bucket.get("test"); + return new Response(JSON.stringify(object)); +}; diff --git a/fixtures/pages-functions-app/functions/regex_chars/my-file.ts b/fixtures/pages-functions-app/functions/regex_chars/my-file.ts new file mode 100644 index 0000000..e28f548 --- /dev/null +++ b/fixtures/pages-functions-app/functions/regex_chars/my-file.ts @@ -0,0 +1 @@ +export const onRequestGet = () => new Response("My file with regex chars"); diff --git a/fixtures/pages-functions-app/functions/static/_middleware.ts b/fixtures/pages-functions-app/functions/static/_middleware.ts new file mode 100644 index 0000000..9d2758d --- /dev/null +++ b/fixtures/pages-functions-app/functions/static/_middleware.ts @@ -0,0 +1 @@ +export { onRequest } from "assets:../../static-assets"; diff --git a/fixtures/pages-functions-app/functions/variables.ts b/fixtures/pages-functions-app/functions/variables.ts new file mode 100644 index 0000000..20f9f90 --- /dev/null +++ b/fixtures/pages-functions-app/functions/variables.ts @@ -0,0 +1,5 @@ +export const onRequest = ({ env }) => { + return new Response(JSON.stringify(env), { + headers: { "Content-Type": "application/json" }, + }); +}; diff --git a/fixtures/pages-functions-app/index.d.ts b/fixtures/pages-functions-app/index.d.ts new file mode 100644 index 0000000..6d78d73 --- /dev/null +++ b/fixtures/pages-functions-app/index.d.ts @@ -0,0 +1,4 @@ +declare module "*.html" { + const content: string; + export default content; +} diff --git a/fixtures/pages-functions-app/package.json b/fixtures/pages-functions-app/package.json new file mode 100644 index 0000000..06d87c8 --- /dev/null +++ b/fixtures/pages-functions-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "@fixture/pages-functions", + "private": true, + "sideEffects": false, + "main": "dist/worker.js", + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --binding=NAME=VALUE --binding=OTHER_NAME=THING=WITH=EQUALS --r2=bucket --port 8789", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "is-odd": "^3.0.1" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@fixture/pages-plugin": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-functions-app/public/[id].js b/fixtures/pages-functions-app/public/[id].js new file mode 100644 index 0000000..d29038f --- /dev/null +++ b/fixtures/pages-functions-app/public/[id].js @@ -0,0 +1 @@ +// test script diff --git a/fixtures/pages-functions-app/public/_headers b/fixtures/pages-functions-app/public/_headers new file mode 100644 index 0000000..afd4be0 --- /dev/null +++ b/fixtures/pages-functions-app/public/_headers @@ -0,0 +1,6 @@ +/* + A-Header: Some-Value + +/header-test + ! A-Header + A-Header: New-Value \ No newline at end of file diff --git a/fixtures/pages-functions-app/public/_redirects b/fixtures/pages-functions-app/public/_redirects new file mode 100644 index 0000000..dedb379 --- /dev/null +++ b/fixtures/pages-functions-app/public/_redirects @@ -0,0 +1,2 @@ +/redirect /me +/users/:id /users/[id] 200 \ No newline at end of file diff --git a/fixtures/pages-functions-app/public/a.b.html b/fixtures/pages-functions-app/public/a.b.html new file mode 100644 index 0000000..bc3deb9 --- /dev/null +++ b/fixtures/pages-functions-app/public/a.b.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> + <body> + <h1>Hello, a.b!</h1> + </body> +</html> diff --git a/fixtures/pages-functions-app/public/here.html b/fixtures/pages-functions-app/public/here.html new file mode 100644 index 0000000..ae9fd9c --- /dev/null +++ b/fixtures/pages-functions-app/public/here.html @@ -0,0 +1 @@ +<h1>Successful!</h1> diff --git a/fixtures/pages-functions-app/public/index.html b/fixtures/pages-functions-app/public/index.html new file mode 100644 index 0000000..1520bf7 --- /dev/null +++ b/fixtures/pages-functions-app/public/index.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> + <body> + <h1>Hello, world!</h1> + </body> +</html> diff --git a/fixtures/pages-functions-app/public/some-asset.html b/fixtures/pages-functions-app/public/some-asset.html new file mode 100644 index 0000000..2d03483 --- /dev/null +++ b/fixtures/pages-functions-app/public/some-asset.html @@ -0,0 +1,6 @@ +<!doctype html> +<html> + <body> + <h1>An asset</h1> + </body> +</html> diff --git a/fixtures/pages-functions-app/public/users/[id].html b/fixtures/pages-functions-app/public/users/[id].html new file mode 100644 index 0000000..f846c03 --- /dev/null +++ b/fixtures/pages-functions-app/public/users/[id].html @@ -0,0 +1,6 @@ +<!doctype html> +<html> + <body> + <h1>Hello, /users/[id]!</h1> + </body> +</html> diff --git a/fixtures/pages-functions-app/static-assets/index.html b/fixtures/pages-functions-app/static-assets/index.html new file mode 100644 index 0000000..0956211 --- /dev/null +++ b/fixtures/pages-functions-app/static-assets/index.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> + <head> + <title>Static asset + + +

Hello from an imported static asset!

+ + diff --git a/fixtures/pages-functions-app/tests/index.test.ts b/fixtures/pages-functions-app/tests/index.test.ts new file mode 100644 index 0000000..c722b3e --- /dev/null +++ b/fixtures/pages-functions-app/tests/index.test.ts @@ -0,0 +1,358 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Functions", () => { + let ip: string, + port: number, + stop: (() => Promise) | undefined, + getOutput: () => string; + + beforeAll(async () => { + ({ ip, port, stop, getOutput } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + [ + "--binding=NAME=VALUE", + "--binding=OTHER_NAME=THING=WITH=EQUALS", + "--r2=bucket", + "--port=0", + "--inspector-port=0", + ] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("renders static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + expect(response.headers.get("x-custom")).toBe("header value"); + const text = await response.text(); + expect(text).toContain("Hello, world!"); + }); + + it("renders pages with . characters", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/a.b`); + expect(response.headers.get("x-custom")).toBe("header value"); + const text = await response.text(); + expect(text).toContain("Hello, a.b!"); + }); + + it("parses URL encoded requests", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/[id].js`); + const text = await response.text(); + expect(text).toContain("// test script"); + }); + + it("parses URLs with regex chars", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/regex_chars/my-file`); + const text = await response.text(); + expect(text).toEqual("My file with regex chars"); + }); + + it("passes environment variables", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/variables`); + const env = await response.json(); + // Use objectContaining to allow for additional CF_PAGES_* variables + expect(env).toEqual( + expect.objectContaining({ + ASSETS: {}, + bucket: {}, + NAME: "VALUE", + OTHER_NAME: "THING=WITH=EQUALS", + VAR_1: "var #1 value", + VAR_3: "var #3 value", + VAR_MULTI_LINE_1: "A: line 1\nline 2", + VAR_MULTI_LINE_2: "B: line 1\nline 2", + EMPTY: "", + UNQUOTED: "unquoted value", // Note that whitespace is trimmed + }) + ); + }); + + it("intercepts static requests with next()", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/intercept`); + const text = await response.text(); + expect(text).toContain("Hello, world!"); + expect(response.headers.get("x-set-from-functions")).toBe("true"); + }); + + it("can make SSR responses", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/date`); + const text = await response.text(); + expect(text).toMatch(/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d/); + }); + + it("can use parameters", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/blog/hello-world`); + const text = await response.text(); + expect(text).toContain("

A blog with a slug: hello-world

"); + }); + + it("can override the incoming request with next() parameters", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/next`); + const text = await response.text(); + expect(text).toContain("

An asset

"); + }); + + describe("can mount a plugin", () => { + it("should mount Middleware", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/some-page` + ); + const text = await response.text(); + expect(text).toContain("
Set from a Plugin!
"); + }); + + it("should return a status code", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/status` + ); + const text = await response.text(); + expect(text).toMatchInlineSnapshot( + `"This should return a 502 status code"` + ); + expect(response.status).toBe(502); + }); + + it("should work with peer externals", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/mounted-plugin/ext`); + const text = await response.text(); + expect(text).toMatchInlineSnapshot(`"42 is even"`); + expect(response.status).toBe(200); + }); + + it("should mount a Plugin even if in a parameterized route", async ({ + expect, + }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-with-param/p123/plugin/status` + ); + const text = await response.text(); + expect(text).toMatchInlineSnapshot( + `"This should return a 502 status code"` + ); + expect(response.status).toBe(502); + }); + + it("should work for nested folders", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/api/v1/instance` + ); + const text = await response.text(); + expect(text).toMatchInlineSnapshot(`"Response from a nested folder"`); + }); + + it("should mount Fixed page", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/mounted-plugin/fixed`); + const text = await response.text(); + expect(text).toContain("I'm a fixed response"); + }); + + it("should support proxying through to next(request)", async ({ + expect, + }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/proxy-me-somewhere-else` + ); + const text = await response.text(); + expect(text).toContain("Successful!"); + }); + }); + + describe("can import static assets", () => { + it("should render a static asset", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/static`); + const text = await response.text(); + expect(text).toContain("

Hello from an imported static asset!

"); + }); + + it("should render from a Plugin", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/static` + ); + const text = await response.text(); + expect(text).toContain( + "

Hello from a static asset brought from a Plugin!

" + ); + }); + + it("should render static/foo", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/static/foo` + ); + const text = await response.text(); + expect(text).toContain("

foo

"); + }); + + it("should render static/dir/bar", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/mounted-plugin/static/dir/bar` + ); + const text = await response.text(); + expect(text).toContain("

bar

"); + }); + + it("supports importing .html from a function", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/import-html`); + expect(response.headers.get("x-custom")).toBe("header value"); + const text = await response.text(); + expect(text).toContain("

Hello from an imported static asset!

"); + }); + }); + + describe("it supports R2", () => { + it("should allow creates", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/r2/create`, { + method: "PUT", + }); + const object = (await response.json()) as { + key: string; + version: string; + }; + expect(object.key).toEqual("test"); + + const getResponse = await fetch(`http://${ip}:${port}/r2/get`); + const getObject = (await getResponse.json()) as { + key: string; + version: string; + }; + expect(getObject.key).toEqual("test"); + expect(getObject.version).toEqual(object.version); + }); + }); + + describe("redirects", () => { + it("still attaches redirects correctly", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/redirect`, { + redirect: "manual", + }); + expect(response.status).toEqual(302); + expect(response.headers.get("Location")).toEqual("/me"); + }); + + it("should support proxying (200) redirects", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/users/123`, { + redirect: "manual", + }); + const text = await response.text(); + expect(response.status).toEqual(200); + expect(text).toContain("Hello, /users/[id]!"); + }); + }); + + describe("headers", () => { + it("still attaches headers correctly", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + + expect(response.headers.get("A-Header")).toEqual("Some-Value"); + }); + + it("can unset and set together", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/header-test`); + + expect(response.headers.get("A-Header")).toEqual("New-Value"); + }); + }); + + describe("passThroughOnException", () => { + it("works on a single handler", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/passThroughOnExceptionOpen` + ); + + expect(response.status).toEqual(200); + expect(await response.text()).toContain("Hello, world!"); + }); + + it("defaults closed", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/passThroughOnExceptionClosed` + ); + + expect(response.status).toEqual(500); + expect(await response.text()).not.toContain("Hello, world!"); + }); + + it("works for nested handlers", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/passThroughOnException/nested` + ); + + expect(response.status).toEqual(200); + expect(await response.text()).toContain("Hello, world!"); + }); + + it("allows errors to still be manually caught in middleware", async ({ + expect, + }) => { + let response = await fetch( + `http://${ip}:${port}/passThroughOnExceptionWithCapture/nested` + ); + + expect(response.status).toEqual(200); + expect(await response.text()).toContain("Hello, world!"); + + response = await fetch( + `http://${ip}:${port}/passThroughOnExceptionWithCapture/nested?catch` + ); + + expect(response.status).toEqual(200); + expect(await response.text()).toMatchInlineSnapshot( + `"Manually caught error: ReferenceError: x is not defined"` + ); + }); + }); + + describe.concurrent("middleware data", () => { + it("allows middleware to set data", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/middleware-data/additional-data` + ); + + expect(response.status).toEqual(200); + const data = await response.json(); + expect(data).toEqual({ + foo: "bar", + }); + }); + + it("allows middleware to mutate data", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/middleware-data/mutate-data` + ); + + expect(response.status).toEqual(200); + const data = await response.json(); + expect(data).toEqual({ + foo: "bar", + bar: "baz", + }); + }); + + it("allows middleware to be overridden and not merged", async ({ + expect, + }) => { + const response = await fetch( + `http://${ip}:${port}/middleware-data/merge-data` + ); + const data = await response.json(); + expect(data).toEqual({ + bar: "baz", + }); + }); + + it("middleware throws when set to non-object", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/middleware-data/bad-data` + ); + + expect(response.status).toEqual(500); + }); + }); +}); diff --git a/fixtures/pages-functions-app/tests/tsconfig.json b/fixtures/pages-functions-app/tests/tsconfig.json new file mode 100644 index 0000000..f60c0bf --- /dev/null +++ b/fixtures/pages-functions-app/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-functions-app/tsconfig.json b/fixtures/pages-functions-app/tsconfig.json new file mode 100644 index 0000000..f71b527 --- /dev/null +++ b/fixtures/pages-functions-app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "include": ["index.d.ts", "functions"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/fixtures/pages-functions-app/vitest.config.mts b/fixtures/pages-functions-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-functions-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-functions-unenv-alias/functions/[[path]].ts b/fixtures/pages-functions-unenv-alias/functions/[[path]].ts new file mode 100644 index 0000000..b39ea9a --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/functions/[[path]].ts @@ -0,0 +1,10 @@ +const fetch = require("cross-fetch"); + +export const onRequest = () => { + const supportsDefaultExports = typeof fetch === "function"; + const supportsNamedExports = typeof fetch.Headers === "function"; + + return new Response( + supportsDefaultExports && supportsNamedExports ? "OK!" : "KO!" + ); +}; diff --git a/fixtures/pages-functions-unenv-alias/package.json b/fixtures/pages-functions-unenv-alias/package.json new file mode 100644 index 0000000..0df7fdd --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/pages-functions-unenv", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev:functions-app": "wrangler pages dev --port 8792", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-functions-unenv-alias/tests/index.test.ts b/fixtures/pages-functions-unenv-alias/tests/index.test.ts new file mode 100644 index 0000000..ee22c01 --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/tests/index.test.ts @@ -0,0 +1,20 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { describe, it, onTestFinished } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages functions with unenv aliased packages", () => { + it("should run dev server when requiring an unenv aliased package", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./functions", + ["--port=0", "--inspector-port=0"] + ); + onTestFinished(stop); + const response = await fetch(`http://${ip}:${port}/`); + const body = await response.text(); + expect(body).toEqual(`OK!`); + }); +}); diff --git a/fixtures/pages-functions-unenv-alias/tests/tsconfig.json b/fixtures/pages-functions-unenv-alias/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-functions-unenv-alias/tsconfig.json b/fixtures/pages-functions-unenv-alias/tsconfig.json new file mode 100644 index 0000000..a729102 --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node", "@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true, + "checkJs": true + }, + "include": [ + "apps/workerjs-directory/_worker.js/index.js", + "apps/workerjs-file/_worker.js", + "functions/[[path]].ts", + "tests" + ] +} diff --git a/fixtures/pages-functions-unenv-alias/vitest.config.mts b/fixtures/pages-functions-unenv-alias/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-functions-unenv-alias/wrangler.jsonc b/fixtures/pages-functions-unenv-alias/wrangler.jsonc new file mode 100644 index 0000000..7bfd15a --- /dev/null +++ b/fixtures/pages-functions-unenv-alias/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "pages-functions-unenv-alias", + "compatibility_date": "2024-12-30", + "compatibility_flags": ["nodejs_compat"], +} diff --git a/fixtures/pages-functions-wasm-app/README.md b/fixtures/pages-functions-wasm-app/README.md new file mode 100644 index 0000000..3598404 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/README.md @@ -0,0 +1,41 @@ +# ⚡️ pages-functions-wasm-app + +`pages-functions-wasm-app` is a test fixture that sets up a ⚡️Pages project with [Functions](https://developers.cloudflare.com/pages/platform/functions) and [`wasm` module imports](https://blog.cloudflare.com/workers-javascript-modules/) + +## Dev + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-wasm-app + +# start dev server +npm run dev +``` + +## Publish + +> Please note that in order to deploy this project to `.pages.dev` you need to have a [Cloudflare account](https://dash.cloudflare.com/login) + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-wasm-app + +# Deploy the directory of static assets as a Pages deployment +npm run publish +``` + +If deployment was successful, follow the URL refrenced in the success message in your terminal + +``` +✨ Deployment complete! Take a peek over at https:/..pages.dev +``` + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-wasm-app + +# Run tests +npm run test +``` diff --git a/fixtures/pages-functions-wasm-app/external-modules/add.wasm b/fixtures/pages-functions-wasm-app/external-modules/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 diff --git a/fixtures/pages-functions-wasm-app/external-modules/add.wat b/fixtures/pages-functions-wasm-app/external-modules/add.wat new file mode 100644 index 0000000..00c20e7 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/external-modules/add.wat @@ -0,0 +1,7 @@ +(module + (func $add (param $p1 i32) (param $p2 i32) (result i32) + local.get $p1 + local.get $p2 + i32.add) + (export "add" (func $add)) +) diff --git a/fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.html b/fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.html new file mode 100644 index 0000000..9ee3bcd --- /dev/null +++ b/fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.html @@ -0,0 +1,5 @@ + + + [.html]: The meaning of life is 21 + + diff --git a/fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.txt b/fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.txt new file mode 100644 index 0000000..ee67156 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/external-modules/meaning-of-life.txt @@ -0,0 +1 @@ +[.txt]: The meaning of life is 21 \ No newline at end of file diff --git a/fixtures/pages-functions-wasm-app/functions/meaning-of-life-html.js b/fixtures/pages-functions-wasm-app/functions/meaning-of-life-html.js new file mode 100644 index 0000000..08cdf63 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/functions/meaning-of-life-html.js @@ -0,0 +1,7 @@ +import html from "./../external-modules/meaning-of-life.html"; + +export async function onRequest() { + return new Response(html, { + headers: { "Content-Type": "text/html" }, + }); +} diff --git a/fixtures/pages-functions-wasm-app/functions/meaning-of-life-text.js b/fixtures/pages-functions-wasm-app/functions/meaning-of-life-text.js new file mode 100644 index 0000000..9d9d777 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/functions/meaning-of-life-text.js @@ -0,0 +1,7 @@ +import text from "./../external-modules/meaning-of-life.txt"; + +export async function onRequest() { + return new Response(text, { + headers: { "Content-Type": "text/plain" }, + }); +} diff --git a/fixtures/pages-functions-wasm-app/functions/meaning-of-life-wasm.js b/fixtures/pages-functions-wasm-app/functions/meaning-of-life-wasm.js new file mode 100644 index 0000000..9a32cc6 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/functions/meaning-of-life-wasm.js @@ -0,0 +1,8 @@ +import add from "../external-modules/add.wasm"; + +export async function onRequest() { + const addModule = await WebAssembly.instantiate(add); + return new Response( + `[.wasm]: The meaning of life is ${addModule.exports.add(20, 1)}` + ); +} diff --git a/fixtures/pages-functions-wasm-app/package.json b/fixtures/pages-functions-wasm-app/package.json new file mode 100644 index 0000000..5579118 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/pages-functions-wasm", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 8776", + "publish": "wrangler pages deploy public", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-functions-wasm-app/public/index.html b/fixtures/pages-functions-wasm-app/public/index.html new file mode 100644 index 0000000..f57ce3d --- /dev/null +++ b/fixtures/pages-functions-wasm-app/public/index.html @@ -0,0 +1,6 @@ + + + +

Hello from pages-functions-wasm-app!

+ + diff --git a/fixtures/pages-functions-wasm-app/tests/index.test.ts b/fixtures/pages-functions-wasm-app/tests/index.test.ts new file mode 100644 index 0000000..a76ad1f --- /dev/null +++ b/fixtures/pages-functions-wasm-app/tests/index.test.ts @@ -0,0 +1,50 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Functions with wasm module imports", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should render static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}`); + const text = await response.text(); + expect(text).toContain("Hello from pages-functions-wasm-app!"); + }); + + it("should resolve wasm module imports and render /meaning-of-life-wasm", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/meaning-of-life-wasm`); + const text = await response.text(); + expect(text).toEqual("[.wasm]: The meaning of life is 21"); + }); + + it("should resolve text module imports and render /meaning-of-life-wasm-text", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/meaning-of-life-text`); + const text = await response.text(); + expect(text).toEqual("[.txt]: The meaning of life is 21"); + }); + + it("should resolve html module imports and render /meaning-of-life-wasm-html", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/meaning-of-life-html`); + const text = await response.text(); + expect(text).toContain(`[.html]: The meaning of life is 21`); + }); +}); diff --git a/fixtures/pages-functions-wasm-app/tests/tsconfig.json b/fixtures/pages-functions-wasm-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-functions-wasm-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-functions-wasm-app/tsconfig.json b/fixtures/pages-functions-wasm-app/tsconfig.json new file mode 100644 index 0000000..7e797b5 --- /dev/null +++ b/fixtures/pages-functions-wasm-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-functions-wasm-app/vitest.config.mts b/fixtures/pages-functions-wasm-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-functions-wasm-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-functions-with-config-file-app/README.md b/fixtures/pages-functions-with-config-file-app/README.md new file mode 100644 index 0000000..baa8018 --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/README.md @@ -0,0 +1,29 @@ +# ⚡️ pages-functions-with-config-file-app + +`pages-functions-with-config-file-app` is a test fixture that sets up a ⚡️Pages⚡️ Functions project, with a `wrangler.toml` [configuration file](hhttps://developers.cloudflare.com/workers/wrangler/configuration). The purpose of this fixture is to demonstrate that `wrangler pages dev` can take configuration from a `wrangler.toml` file. + +## Local dev + +To test this fixture run `wrangler pages dev` in the fixture folder: + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-with-config-file-app + +# Start local dev server +npx wrangler pages dev +``` + +Once the local dev server was started, you should see the configuration specified in the `wrangler.toml` at the root of the fixture folder, affect the generated Worker. + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-with-config-file-app + +# Run tests +npm run test +``` + +You can still override what is in the wrangler.toml by adding command line args: wrangler pages dev --binding=KEY:VALUE diff --git a/fixtures/pages-functions-with-config-file-app/functions/celebrate.ts b/fixtures/pages-functions-with-config-file-app/functions/celebrate.ts new file mode 100644 index 0000000..1542fa6 --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/functions/celebrate.ts @@ -0,0 +1,11 @@ +export async function onRequest(context) { + return new Response( + `[/celebrate]:\n` + + `🎵 🎵 🎵\n` + + `You can turn this world around\n` + + `And bring back all of those happy days\n` + + `Put your troubles down\n` + + `It's time to ${context.env.VAR2}\n` + + `🎵 🎵 🎵` + ); +} diff --git a/fixtures/pages-functions-with-config-file-app/functions/holiday.ts b/fixtures/pages-functions-with-config-file-app/functions/holiday.ts new file mode 100644 index 0000000..a9e146b --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/functions/holiday.ts @@ -0,0 +1,11 @@ +export async function onRequest(context) { + return new Response( + `[/holiday]:\n` + + `🎵 🎵 🎵\n` + + `If we took a ${context.env.VAR1}\n` + + `Took some time to ${context.env.VAR2}\n` + + `Just one day out of life\n` + + `It would be, it would be so nice\n` + + `🎵 🎵 🎵` + ); +} diff --git a/fixtures/pages-functions-with-config-file-app/package.json b/fixtures/pages-functions-with-config-file-app/package.json new file mode 100644 index 0000000..464111e --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/pages-functions-config", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-functions-with-config-file-app/public/_routes.json b/fixtures/pages-functions-with-config-file-app/public/_routes.json new file mode 100644 index 0000000..2b71aa6 --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/public/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/celebrate"], + "exclude": ["/holiday"] +} diff --git a/fixtures/pages-functions-with-config-file-app/public/index.html b/fixtures/pages-functions-with-config-file-app/public/index.html new file mode 100644 index 0000000..f7b327e --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/public/index.html @@ -0,0 +1,6 @@ + + + +

Celebrate! Pages now supports 'wrangler.toml' 🎉

+ + diff --git a/fixtures/pages-functions-with-config-file-app/tests/index.test.ts b/fixtures/pages-functions-with-config-file-app/tests/index.test.ts new file mode 100644 index 0000000..327c4bb --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/tests/index.test.ts @@ -0,0 +1,48 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Functions with wrangler.toml", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + undefined, + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should render static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}`); + const text = await response.text(); + expect(text).toContain("Celebrate! Pages now supports 'wrangler.toml' 🎉"); + }); + + it("should correctly apply the routing rules provided in the custom _routes.json file", async ({ + expect, + }) => { + // matches `/celebrate` include rule + let response = await fetch(`http://${ip}:${port}/celebrate`); + let text = await response.text(); + expect(text).toEqual( + `[/celebrate]:\n` + + `🎵 🎵 🎵\n` + + `You can turn this world around\n` + + `And bring back all of those happy days\n` + + `Put your troubles down\n` + + `It's time to celebrate\n` + + `🎵 🎵 🎵` + ); + + // matches `/holiday` exclude rule + response = await fetch(`http://${ip}:${port}/holiday`); + text = await response.text(); + expect(text).toContain("Celebrate! Pages now supports 'wrangler.toml' 🎉"); + }); +}); diff --git a/fixtures/pages-functions-with-config-file-app/tests/tsconfig.json b/fixtures/pages-functions-with-config-file-app/tests/tsconfig.json new file mode 100644 index 0000000..f60c0bf --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-functions-with-config-file-app/tsconfig.json b/fixtures/pages-functions-with-config-file-app/tsconfig.json new file mode 100644 index 0000000..375ad3b --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["functions"] +} diff --git a/fixtures/pages-functions-with-config-file-app/vitest.config.mts b/fixtures/pages-functions-with-config-file-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-functions-with-config-file-app/wrangler.jsonc b/fixtures/pages-functions-with-config-file-app/wrangler.jsonc new file mode 100644 index 0000000..4a5b963 --- /dev/null +++ b/fixtures/pages-functions-with-config-file-app/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "pages_build_output_dir": "./public", + "name": "pages-functions-with-config-file-app", + "compatibility_date": "2024-01-01", + "vars": { + "VAR1": "holiday", + "VAR2": "celebrate", + }, +} diff --git a/fixtures/pages-functions-with-routes-app/CHANGELOG.md b/fixtures/pages-functions-with-routes-app/CHANGELOG.md new file mode 100644 index 0000000..d1b5a88 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/CHANGELOG.md @@ -0,0 +1,18 @@ +# pages-functions-with-routes-app + +## 0.0.1 + +### Patch Changes + +- [#2065](https://github.com/cloudflare/workers-sdk/pull/2065) [`14c44588`](https://github.com/cloudflare/workers-sdk/commit/14c44588c9d22e9c9f2ad2740df57809d0cbcfbc) Thanks [@CarmenPopoviciu](https://github.com/CarmenPopoviciu)! - fix(pages): `wrangler pages dev` matches routing rules in `_routes.json` too loosely + + Currently, the logic by which we transform routing rules in `_routes.json` to + regular expressions, so we can perform `pathname` matching & routing when we + run `wrangler pages dev`, is too permissive, and leads to serving incorrect + assets for certain url paths. + + For example, a routing rule such as `/foo` will incorrectly match pathname + `/bar/foo`. Similarly, pathname `/foo` will be incorrectly matched by the + `/` routing rule. + This commit fixes our routing rule to pathname matching logic and brings + `wrangler pages dev` on par with routing in deployed Pages projects. diff --git a/fixtures/pages-functions-with-routes-app/README.md b/fixtures/pages-functions-with-routes-app/README.md new file mode 100644 index 0000000..c15778e --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/README.md @@ -0,0 +1,31 @@ +# ⚡️ pages-functions-with-routes-app + +`pages-functions-with-routes-app` is a test fixture that sets up a ⚡️Pages project with [Functions](https://developers.cloudflare.com/pages/platform/functions) and a custom `_routes.json` file + +## Publish + +> Please note that in order to deploy this project to `.pages.dev` you need to have a [Cloudflare account](https://dash.cloudflare.com/login) + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-with-routes-app + +# Deploy the directory of static assets as a Pages deployment +npx wrangler pages deploy public +``` + +If deployment was successful, follow the URL refrenced in the success message in your terminal + +``` +✨ Deployment complete! Take a peek over at https:/..pages.dev +``` + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-functions-with-routes-app + +# Run tests +npm run test +``` diff --git a/fixtures/pages-functions-with-routes-app/functions/bye.ts b/fixtures/pages-functions-with-routes-app/functions/bye.ts new file mode 100644 index 0000000..7b0ffeb --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/bye.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("[/functions/bye]: Adieu 🙁"); +} diff --git a/fixtures/pages-functions-with-routes-app/functions/date.ts b/fixtures/pages-functions-with-routes-app/functions/date.ts new file mode 100644 index 0000000..c680a55 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/date.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response(`[/functions/date]: ${new Date().toISOString()}`); +} diff --git a/fixtures/pages-functions-with-routes-app/functions/greeting/bye.ts b/fixtures/pages-functions-with-routes-app/functions/greeting/bye.ts new file mode 100644 index 0000000..ae9d368 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/greeting/bye.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("[/functions/greeting/bye]: A plus tard alligator 👋"); +} diff --git a/fixtures/pages-functions-with-routes-app/functions/greeting/hello.ts b/fixtures/pages-functions-with-routes-app/functions/greeting/hello.ts new file mode 100644 index 0000000..d47bc04 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/greeting/hello.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("[/functions/greeting/hello]: Bonjour le monde!"); +} diff --git a/fixtures/pages-functions-with-routes-app/functions/greeting/index.ts b/fixtures/pages-functions-with-routes-app/functions/greeting/index.ts new file mode 100644 index 0000000..e3c1489 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/greeting/index.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("[/functions/greeting/index]: Bonjour alligator!"); +} diff --git a/fixtures/pages-functions-with-routes-app/functions/greetings.ts b/fixtures/pages-functions-with-routes-app/functions/greetings.ts new file mode 100644 index 0000000..ab53815 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/greetings.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("[/functions/greetings]: Bonjour à tous!"); +} diff --git a/fixtures/pages-functions-with-routes-app/functions/index.ts b/fixtures/pages-functions-with-routes-app/functions/index.ts new file mode 100644 index 0000000..f3afeda --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/functions/index.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("ROOT"); +} diff --git a/fixtures/pages-functions-with-routes-app/package.json b/fixtures/pages-functions-with-routes-app/package.json new file mode 100644 index 0000000..a0fb0cc --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/pages-functions-routes", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 8776", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-functions-with-routes-app/public/_routes.json b/fixtures/pages-functions-with-routes-app/public/_routes.json new file mode 100644 index 0000000..255dd80 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/public/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/", "/greeting/*", "/greeting*", "/date"], + "exclude": ["/bye*", "/date", "/*.*"] +} diff --git a/fixtures/pages-functions-with-routes-app/public/greeting/test.json b/fixtures/pages-functions-with-routes-app/public/greeting/test.json new file mode 100644 index 0000000..30164b1 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/public/greeting/test.json @@ -0,0 +1,3 @@ +{ + "value": 99 +} diff --git a/fixtures/pages-functions-with-routes-app/public/index.html b/fixtures/pages-functions-with-routes-app/public/index.html new file mode 100644 index 0000000..6d1288b --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/public/index.html @@ -0,0 +1,8 @@ + + + +

+ Bienvenue sur notre projet ✨ pages-functions-with-routes-app! +

+ + diff --git a/fixtures/pages-functions-with-routes-app/tests/index.test.ts b/fixtures/pages-functions-with-routes-app/tests/index.test.ts new file mode 100644 index 0000000..0324f67 --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/tests/index.test.ts @@ -0,0 +1,76 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Functions with custom _routes.json", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should render static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/undefined-route`); + const text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-functions-with-routes-app!" + ); + }); + + it("should correctly apply the routing rules provided in the custom _routes.json file", async ({ + expect, + }) => { + // matches / include rule + let response = await fetch(`http://${ip}:${port}`); + let text = await response.text(); + expect(text).toEqual("ROOT"); + + // matches /greeting/* include rule + response = await fetch(`http://${ip}:${port}/greeting`); + text = await response.text(); + expect(text).toEqual("[/functions/greeting/index]: Bonjour alligator!"); + + // matches /greeting/* include rule + response = await fetch(`http://${ip}:${port}/greeting/hello`); + text = await response.text(); + expect(text).toEqual("[/functions/greeting/hello]: Bonjour le monde!"); + + // matches /greeting/* include rule + response = await fetch(`http://${ip}:${port}/greeting/bye`); + text = await response.text(); + expect(text).toEqual("[/functions/greeting/bye]: A plus tard alligator 👋"); + + // matches both include|exclude /date rules, but exclude has priority + response = await fetch(`http://${ip}:${port}/date`); + text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-functions-with-routes-app!" + ); + + // matches /bye* exclude rule + response = await fetch(`http://${ip}:${port}/bye`); + text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-functions-with-routes-app!" + ); + + // matches /greeting* include rule + response = await fetch(`http://${ip}:${port}/greetings`); + text = await response.text(); + expect(text).toEqual("[/functions/greetings]: Bonjour à tous!"); + + // matches /*.* exclude rule + response = await fetch(`http://${ip}:${port}/greeting/test.json`); + const json = await response.json(); + expect(json).toEqual({ value: 99 }); + }); +}); diff --git a/fixtures/pages-functions-with-routes-app/tests/tsconfig.json b/fixtures/pages-functions-with-routes-app/tests/tsconfig.json new file mode 100644 index 0000000..f60c0bf --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-functions-with-routes-app/tsconfig.json b/fixtures/pages-functions-with-routes-app/tsconfig.json new file mode 100644 index 0000000..375ad3b --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["functions"] +} diff --git a/fixtures/pages-functions-with-routes-app/vitest.config.mts b/fixtures/pages-functions-with-routes-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-functions-with-routes-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-nodejs-v2-compat/apps/functions/README.md b/fixtures/pages-nodejs-v2-compat/apps/functions/README.md new file mode 100644 index 0000000..ac0815e --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/apps/functions/README.md @@ -0,0 +1,5 @@ +# Page Functions fixture + +This directory is just here to avoid picking up any other \_worker.js files in the other app. +The actual Pages Functions handlers are in the `functions` directory at the root of the fixture. +They have to be there because Pages likes `wrangler pages dev` to be run from the root of the fixture and it always looks for a `functions` directory in that root. diff --git a/fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/README.md b/fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/README.md new file mode 100644 index 0000000..a3348ed --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/README.md @@ -0,0 +1 @@ +# \_worker.js directory fixture diff --git a/fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/_worker.js/index.js b/fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/_worker.js/index.js new file mode 100644 index 0000000..4dea348 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/apps/workerjs-directory/_worker.js/index.js @@ -0,0 +1,12 @@ +process.env.foo = "bar"; + +// Check that we don't get a build time error when assigning to globalThis.process. +globalThis.process = process; + +export default { + fetch() { + return new Response( + `_worker.js directory, process: ${Object.keys(process).sort()}` + ); + }, +}; diff --git a/fixtures/pages-nodejs-v2-compat/apps/workerjs-file/README.md b/fixtures/pages-nodejs-v2-compat/apps/workerjs-file/README.md new file mode 100644 index 0000000..f38aec3 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/apps/workerjs-file/README.md @@ -0,0 +1 @@ +# \_worker.js file fixture diff --git a/fixtures/pages-nodejs-v2-compat/apps/workerjs-file/_worker.js b/fixtures/pages-nodejs-v2-compat/apps/workerjs-file/_worker.js new file mode 100644 index 0000000..370cef8 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/apps/workerjs-file/_worker.js @@ -0,0 +1,12 @@ +process.env.foo = "bar"; + +// Check that we don't get a build time error when assigning to globalThis.process. +globalThis.process = process; + +export default { + fetch() { + return new Response( + `_worker.js file, process: ${Object.keys(process).sort()}` + ); + }, +}; diff --git a/fixtures/pages-nodejs-v2-compat/functions/[[path]].ts b/fixtures/pages-nodejs-v2-compat/functions/[[path]].ts new file mode 100644 index 0000000..a6f6c28 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/functions/[[path]].ts @@ -0,0 +1,10 @@ +process.env.foo = "bar"; + +// Check that we don't get a build time error when assigning to globalThis.process. +globalThis.process = process; + +export const onRequest = () => { + return new Response( + `Pages functions, process: ${Object.keys(process).sort()}` + ); +}; diff --git a/fixtures/pages-nodejs-v2-compat/package.json b/fixtures/pages-nodejs-v2-compat/package.json new file mode 100644 index 0000000..08c4474 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/package.json @@ -0,0 +1,22 @@ +{ + "name": "@fixture/pages-nodejs-v2", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev:functions-app": "wrangler pages dev ./apps/functions --port 8792", + "dev:workerjs-directory": "wrangler pages dev ./apps/workerjs-directory --port 8792", + "dev:workerjs-file": "wrangler pages dev ./apps/workerjs-file --port 8792", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-nodejs-v2-compat/tests/index.test.ts b/fixtures/pages-nodejs-v2-compat/tests/index.test.ts new file mode 100644 index 0000000..43e8829 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/tests/index.test.ts @@ -0,0 +1,54 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { describe, it, onTestFinished } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages with Node.js compat v2", () => { + describe("with _worker.js file", () => { + it("should polyfill `process`", async ({ expect }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./apps/workerjs-file", + ["--port=0", "--inspector-port=0"] + ); + onTestFinished(stop); + const response = await fetch(`http://${ip}:${port}/`); + const body = await response.text(); + expect(body).toMatchInlineSnapshot( + `"_worker.js file, process: _channel,_debugEnd,_debugProcess,_disconnect,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_handleQueue,_kill,_linkedBinding,_maxListeners,_pendingMessage,_preload_modules,_rawDebug,_send,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,channel,chdir,config,connected,constrainedMemory,cpuUsage,cwd,debugPort,disconnect,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,finalization,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,mainModule,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,permission,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,send,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,throwDeprecation,title,traceDeprecation,umask,uptime,version,versions"` + ); + }); + }); + + describe("with _worker.js directory", () => { + it("should polyfill `process`", async ({ expect }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./apps/workerjs-directory", + ["--port=0", "--inspector-port=0"] + ); + onTestFinished(stop); + const response = await fetch(`http://${ip}:${port}/`); + const body = await response.text(); + expect(body).toMatchInlineSnapshot( + `"_worker.js directory, process: _channel,_debugEnd,_debugProcess,_disconnect,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_handleQueue,_kill,_linkedBinding,_maxListeners,_pendingMessage,_preload_modules,_rawDebug,_send,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,channel,chdir,config,connected,constrainedMemory,cpuUsage,cwd,debugPort,disconnect,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,finalization,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,mainModule,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,permission,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,send,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,throwDeprecation,title,traceDeprecation,umask,uptime,version,versions"` + ); + }); + }); + + describe("with Pages functions", () => { + it("should polyfill `process`", async ({ expect }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./apps/functions", + ["--port=0", "--inspector-port=0"] + ); + onTestFinished(stop); + const response = await fetch(`http://${ip}:${port}/`); + const body = await response.text(); + expect(body).toMatchInlineSnapshot( + `"Pages functions, process: _channel,_debugEnd,_debugProcess,_disconnect,_events,_eventsCount,_exiting,_fatalException,_getActiveHandles,_getActiveRequests,_handleQueue,_kill,_linkedBinding,_maxListeners,_pendingMessage,_preload_modules,_rawDebug,_send,_startProfilerIdleNotifier,_stopProfilerIdleNotifier,_tickCallback,abort,addListener,allowedNodeEnvironmentFlags,arch,argv,argv0,assert,availableMemory,binding,channel,chdir,config,connected,constrainedMemory,cpuUsage,cwd,debugPort,disconnect,dlopen,domain,emit,emitWarning,env,eventNames,execArgv,execPath,exit,exitCode,features,finalization,getActiveResourcesInfo,getBuiltinModule,getMaxListeners,getegid,geteuid,getgid,getgroups,getuid,hasUncaughtExceptionCaptureCallback,hrtime,initgroups,kill,listenerCount,listeners,loadEnvFile,mainModule,memoryUsage,moduleLoadList,nextTick,off,on,once,openStdin,permission,pid,platform,ppid,prependListener,prependOnceListener,rawListeners,reallyExit,release,removeAllListeners,removeListener,report,resourceUsage,send,setMaxListeners,setSourceMapsEnabled,setUncaughtExceptionCaptureCallback,setegid,seteuid,setgid,setgroups,setuid,sourceMapsEnabled,stderr,stdin,stdout,throwDeprecation,title,traceDeprecation,umask,uptime,version,versions"` + ); + }); + }); +}); diff --git a/fixtures/pages-nodejs-v2-compat/tests/tsconfig.json b/fixtures/pages-nodejs-v2-compat/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-nodejs-v2-compat/tsconfig.json b/fixtures/pages-nodejs-v2-compat/tsconfig.json new file mode 100644 index 0000000..a729102 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node", "@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true, + "checkJs": true + }, + "include": [ + "apps/workerjs-directory/_worker.js/index.js", + "apps/workerjs-file/_worker.js", + "functions/[[path]].ts", + "tests" + ] +} diff --git a/fixtures/pages-nodejs-v2-compat/vitest.config.mts b/fixtures/pages-nodejs-v2-compat/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-nodejs-v2-compat/wrangler.jsonc b/fixtures/pages-nodejs-v2-compat/wrangler.jsonc new file mode 100644 index 0000000..f14c814 --- /dev/null +++ b/fixtures/pages-nodejs-v2-compat/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "pages-nodejs-compat", + "compatibility_date": "2024-08-20", + "compatibility_flags": ["nodejs_compat_v2"], +} diff --git a/fixtures/pages-plugin-example/.gitignore b/fixtures/pages-plugin-example/.gitignore new file mode 100644 index 0000000..f720180 --- /dev/null +++ b/fixtures/pages-plugin-example/.gitignore @@ -0,0 +1 @@ +/index.js \ No newline at end of file diff --git a/fixtures/pages-plugin-example/functions/_middleware.ts b/fixtures/pages-plugin-example/functions/_middleware.ts new file mode 100644 index 0000000..74d0cba --- /dev/null +++ b/fixtures/pages-plugin-example/functions/_middleware.ts @@ -0,0 +1,32 @@ +import type { PluginArgs } from ".."; + +type ExamplePagesPluginFunction< + Env = unknown, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Params extends string = any, + Data extends Record = Record, +> = PagesPluginFunction; + +class BodyHandler { + footerText: string; + + constructor({ footerText }) { + this.footerText = footerText; + } + + element(element) { + // Don't actually set HTML like this! + element.append(`
${this.footerText}
`, { html: true }); + } +} + +export const onRequest: ExamplePagesPluginFunction = async ({ + next, + pluginArgs, +}) => { + const response = await next(); + + return new HTMLRewriter() + .on("body", new BodyHandler({ footerText: pluginArgs.footerText })) + .transform(response); +}; diff --git a/fixtures/pages-plugin-example/functions/api/v1/instance.ts b/fixtures/pages-plugin-example/functions/api/v1/instance.ts new file mode 100644 index 0000000..53ff35a --- /dev/null +++ b/fixtures/pages-plugin-example/functions/api/v1/instance.ts @@ -0,0 +1 @@ +export const onRequest = () => new Response("Response from a nested folder"); diff --git a/fixtures/pages-plugin-example/functions/ext.ts b/fixtures/pages-plugin-example/functions/ext.ts new file mode 100644 index 0000000..550f2ad --- /dev/null +++ b/fixtures/pages-plugin-example/functions/ext.ts @@ -0,0 +1,5 @@ +import isOdd from "is-odd"; + +export const onRequest: PagesFunction = () => { + return new Response(`42 is ${isOdd(42) ? "odd" : "even"}`); +}; diff --git a/fixtures/pages-plugin-example/functions/fixed.ts b/fixtures/pages-plugin-example/functions/fixed.ts new file mode 100644 index 0000000..59fb7a0 --- /dev/null +++ b/fixtures/pages-plugin-example/functions/fixed.ts @@ -0,0 +1,3 @@ +export const onRequest: PagesFunction = () => { + return new Response("I'm a fixed response"); +}; diff --git a/fixtures/pages-plugin-example/functions/proxy-me-somewhere-else.ts b/fixtures/pages-plugin-example/functions/proxy-me-somewhere-else.ts new file mode 100644 index 0000000..b999910 --- /dev/null +++ b/fixtures/pages-plugin-example/functions/proxy-me-somewhere-else.ts @@ -0,0 +1 @@ +export const onRequest = ({ next }) => next("/here"); diff --git a/fixtures/pages-plugin-example/functions/static/_middleware.ts b/fixtures/pages-plugin-example/functions/static/_middleware.ts new file mode 100644 index 0000000..63d4986 --- /dev/null +++ b/fixtures/pages-plugin-example/functions/static/_middleware.ts @@ -0,0 +1 @@ +export { onRequest } from "assets:../../public"; diff --git a/fixtures/pages-plugin-example/functions/status.ts b/fixtures/pages-plugin-example/functions/status.ts new file mode 100644 index 0000000..bb64261 --- /dev/null +++ b/fixtures/pages-plugin-example/functions/status.ts @@ -0,0 +1,3 @@ +export const onRequest = () => { + return new Response("This should return a 502 status code", { status: 502 }); +}; diff --git a/fixtures/pages-plugin-example/index.d.ts b/fixtures/pages-plugin-example/index.d.ts new file mode 100644 index 0000000..a357a2d --- /dev/null +++ b/fixtures/pages-plugin-example/index.d.ts @@ -0,0 +1,5 @@ +export type PluginArgs = { + footerText: string; +}; + +export default function (args: PluginArgs): PagesFunction; diff --git a/fixtures/pages-plugin-example/package.json b/fixtures/pages-plugin-example/package.json new file mode 100644 index 0000000..57c78a9 --- /dev/null +++ b/fixtures/pages-plugin-example/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/pages-plugin", + "private": true, + "files": [ + "dist/", + "index.d.ts", + "tsconfig.json", + "public/" + ], + "main": "dist/index.js", + "types": "index.d.ts", + "scripts": { + "build": "wrangler pages functions build --plugin --outdir=dist --external=is-odd", + "check:type": "tsc" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "is-odd": "^3.0.1", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-plugin-example/public/dir/bar.html b/fixtures/pages-plugin-example/public/dir/bar.html new file mode 100644 index 0000000..792ce70 --- /dev/null +++ b/fixtures/pages-plugin-example/public/dir/bar.html @@ -0,0 +1,9 @@ + + + + Static asset from a Plugin! + + +

bar

+ + diff --git a/fixtures/pages-plugin-example/public/foo.html b/fixtures/pages-plugin-example/public/foo.html new file mode 100644 index 0000000..1beda88 --- /dev/null +++ b/fixtures/pages-plugin-example/public/foo.html @@ -0,0 +1,9 @@ + + + + Static asset from a Plugin! + + +

foo

+ + diff --git a/fixtures/pages-plugin-example/public/index.html b/fixtures/pages-plugin-example/public/index.html new file mode 100644 index 0000000..832789b --- /dev/null +++ b/fixtures/pages-plugin-example/public/index.html @@ -0,0 +1,9 @@ + + + + Static asset from a Plugin! + + +

Hello from a static asset brought from a Plugin!

+ + diff --git a/fixtures/pages-plugin-example/tsconfig.json b/fixtures/pages-plugin-example/tsconfig.json new file mode 100644 index 0000000..b8cb913 --- /dev/null +++ b/fixtures/pages-plugin-example/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["functions", "index.d.ts"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + } +} diff --git a/fixtures/pages-plugin-example/turbo.json b/fixtures/pages-plugin-example/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/pages-plugin-example/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/pages-plugin-mounted-on-root-app/.gitignore b/fixtures/pages-plugin-mounted-on-root-app/.gitignore new file mode 100644 index 0000000..79451db --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/.gitignore @@ -0,0 +1 @@ +cdn-cgi/ diff --git a/fixtures/pages-plugin-mounted-on-root-app/functions/_middleware.ts b/fixtures/pages-plugin-mounted-on-root-app/functions/_middleware.ts new file mode 100644 index 0000000..df50a1f --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/functions/_middleware.ts @@ -0,0 +1,3 @@ +import examplePlugin from "@fixture/pages-plugin"; + +export const onRequest = examplePlugin({ footerText: "Set from a Plugin!" }); diff --git a/fixtures/pages-plugin-mounted-on-root-app/index.d.ts b/fixtures/pages-plugin-mounted-on-root-app/index.d.ts new file mode 100644 index 0000000..6d78d73 --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/index.d.ts @@ -0,0 +1,4 @@ +declare module "*.html" { + const content: string; + export default content; +} diff --git a/fixtures/pages-plugin-mounted-on-root-app/package.json b/fixtures/pages-plugin-mounted-on-root-app/package.json new file mode 100644 index 0000000..01c9385 --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "@fixture/pages-plugin-root", + "private": true, + "sideEffects": false, + "main": "dist/worker.js", + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 8793", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "is-odd": "^3.0.1" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@fixture/pages-plugin": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-plugin-mounted-on-root-app/public/index.html b/fixtures/pages-plugin-mounted-on-root-app/public/index.html new file mode 100644 index 0000000..1520bf7 --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/public/index.html @@ -0,0 +1,6 @@ + + + +

Hello, world!

+ + diff --git a/fixtures/pages-plugin-mounted-on-root-app/tests/index.test.ts b/fixtures/pages-plugin-mounted-on-root-app/tests/index.test.ts new file mode 100644 index 0000000..4d56d3c --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/tests/index.test.ts @@ -0,0 +1,28 @@ +import * as path from "path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Functions", () => { + let ip: string; + let port: number; + let stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + path.resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("mounts a plugin correctly at root", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/api/v1/instance`); + const text = await response.text(); + expect(text).toMatchInlineSnapshot(`"Response from a nested folder"`); + }); +}); diff --git a/fixtures/pages-plugin-mounted-on-root-app/tests/tsconfig.json b/fixtures/pages-plugin-mounted-on-root-app/tests/tsconfig.json new file mode 100644 index 0000000..f60c0bf --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-plugin-mounted-on-root-app/tsconfig.json b/fixtures/pages-plugin-mounted-on-root-app/tsconfig.json new file mode 100644 index 0000000..b7ef12c --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["index.d.ts", "functions"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + } +} diff --git a/fixtures/pages-plugin-mounted-on-root-app/vitest.config.mts b/fixtures/pages-plugin-mounted-on-root-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-plugin-mounted-on-root-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-proxy-app/package.json b/fixtures/pages-proxy-app/package.json new file mode 100644 index 0000000..6cb2fbe --- /dev/null +++ b/fixtures/pages-proxy-app/package.json @@ -0,0 +1,23 @@ +{ + "name": "@fixture/pages-proxy", + "private": true, + "sideEffects": false, + "main": "server/index.js", + "scripts": { + "build": "esbuild --bundle --platform=node server/index.ts --outfile=dist/index.js", + "check:type": "tsc", + "dev": "wrangler pages dev --compatibility-date=2024-01-17 --port 8790 --proxy 8791 -- pnpm run server", + "server": "node dist/index.js", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "miniflare": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-proxy-app/server/index.ts b/fixtures/pages-proxy-app/server/index.ts new file mode 100644 index 0000000..e6efbc2 --- /dev/null +++ b/fixtures/pages-proxy-app/server/index.ts @@ -0,0 +1,10 @@ +import { createServer } from "http"; + +const server = createServer(); + +server.on("request", (req, res) => { + res.write("Host:" + req.headers.host); + res.end(); +}); + +server.listen(8791); diff --git a/fixtures/pages-proxy-app/tests/index.test.ts b/fixtures/pages-proxy-app/tests/index.test.ts new file mode 100644 index 0000000..8333c75 --- /dev/null +++ b/fixtures/pages-proxy-app/tests/index.test.ts @@ -0,0 +1,34 @@ +import { fork } from "node:child_process"; +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; +import type { ChildProcess } from "node:child_process"; + +describe("pages-proxy-app", async () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + let devServer: ChildProcess; + + beforeAll(async () => { + devServer = fork(resolve(__dirname, "../dist/index.js"), { + stdio: "ignore", + }); + + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + undefined, + ["--port=0", "--inspector-port=0", "--proxy=8791"] + )); + }); + + afterAll(async () => { + await stop?.(); + devServer.kill(); + }); + + it("receives the correct Host header", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toContain(`Host:${ip}:${port}`); + }); +}); diff --git a/fixtures/pages-proxy-app/tests/tsconfig.json b/fixtures/pages-proxy-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-proxy-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-proxy-app/tsconfig.json b/fixtures/pages-proxy-app/tsconfig.json new file mode 100644 index 0000000..6d77a1e --- /dev/null +++ b/fixtures/pages-proxy-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "include": ["server"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "esModuleInterop": true, + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/fixtures/pages-proxy-app/turbo.json b/fixtures/pages-proxy-app/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/pages-proxy-app/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/pages-proxy-app/vitest.config.mts b/fixtures/pages-proxy-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-proxy-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-redirected-config/.gitignore b/fixtures/pages-redirected-config/.gitignore new file mode 100644 index 0000000..2cf1da4 --- /dev/null +++ b/fixtures/pages-redirected-config/.gitignore @@ -0,0 +1,2 @@ +dist +build \ No newline at end of file diff --git a/fixtures/pages-redirected-config/package.json b/fixtures/pages-redirected-config/package.json new file mode 100644 index 0000000..aace93d --- /dev/null +++ b/fixtures/pages-redirected-config/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/config-redirected-pages", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "build": "node -r esbuild-register tools/build.ts", + "check:type": "tsc", + "dev": "pnpm run build && wrangler pages dev", + "test:ci": "pnpm run build && vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-redirected-config/src/index.js b/fixtures/pages-redirected-config/src/index.js new file mode 100644 index 0000000..e1ce8e6 --- /dev/null +++ b/fixtures/pages-redirected-config/src/index.js @@ -0,0 +1,5 @@ +export default { + async fetch(request, env) { + return new Response("Generated: " + env.generated ?? false); + }, +}; diff --git a/fixtures/pages-redirected-config/tests/index.test.ts b/fixtures/pages-redirected-config/tests/index.test.ts new file mode 100644 index 0000000..b3849ed --- /dev/null +++ b/fixtures/pages-redirected-config/tests/index.test.ts @@ -0,0 +1,52 @@ +import { rmSync, writeFileSync } from "fs"; +import path, { resolve } from "path"; +import { fetch } from "undici"; +import { describe, it, onTestFinished } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +const basePath = resolve(__dirname, ".."); + +describe("wrangler pages dev", () => { + it("uses the generated config if there is no wrangler.toml", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerPagesDev(basePath, undefined, [ + "--port=0", + "--inspector-port=0", + ]); + onTestFinished(async () => await stop?.()); + + // Note that the local protocol defaults to http + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(`"Generated: true"`); + }); + + it("uses the generated config instead of a user wrangler.toml", async ({ + expect, + }) => { + writeFileSync( + path.join(basePath, "wrangler.toml"), + [ + `name = "redirected-config-worker"`, + `compatibility_date = "2024-12-01"`, + `pages_build_output_dir = "public"`, + ].join("\n") + ); + const { ip, port, stop } = await runWranglerPagesDev(basePath, undefined, [ + "--port=0", + "--inspector-port=0", + ]); + onTestFinished(async () => { + rmSync(path.join(basePath, "wrangler.toml"), { force: true }); + await stop?.(); + }); + + // Note that the local protocol defaults to http + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(`"Generated: true"`); + }); +}); diff --git a/fixtures/pages-redirected-config/tools/build.ts b/fixtures/pages-redirected-config/tools/build.ts new file mode 100644 index 0000000..01594c4 --- /dev/null +++ b/fixtures/pages-redirected-config/tools/build.ts @@ -0,0 +1,24 @@ +import { copyFileSync, mkdirSync, rmSync, writeFileSync } from "fs"; + +// Create a pseudo build directory +rmSync("build", { recursive: true, force: true }); +mkdirSync("build"); +const config = { + name: "redirected-config-worker", + compatibility_date: "2024-12-01", + pages_build_output_dir: "./public", + vars: { generated: true }, +}; +writeFileSync("build/wrangler.json", JSON.stringify(config, undefined, 2)); + +mkdirSync("build/public"); +copyFileSync("src/index.js", "build/public/_worker.js"); + +// Create the redirect file +rmSync(".wrangler/deploy", { recursive: true, force: true }); +mkdirSync(".wrangler/deploy", { recursive: true }); +const redirect = { configPath: "../../build/wrangler.json" }; +writeFileSync( + ".wrangler/deploy/config.json", + JSON.stringify(redirect, undefined, 2) +); diff --git a/fixtures/pages-redirected-config/tsconfig.json b/fixtures/pages-redirected-config/tsconfig.json new file mode 100644 index 0000000..7480e11 --- /dev/null +++ b/fixtures/pages-redirected-config/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-redirected-config/turbo.json b/fixtures/pages-redirected-config/turbo.json new file mode 100644 index 0000000..3394ff5 --- /dev/null +++ b/fixtures/pages-redirected-config/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["build/**"] + } + } +} diff --git a/fixtures/pages-redirected-config/vitest.config.mts b/fixtures/pages-redirected-config/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-redirected-config/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-simple-assets/index.d.ts b/fixtures/pages-simple-assets/index.d.ts new file mode 100644 index 0000000..6d78d73 --- /dev/null +++ b/fixtures/pages-simple-assets/index.d.ts @@ -0,0 +1,4 @@ +declare module "*.html" { + const content: string; + export default content; +} diff --git a/fixtures/pages-simple-assets/package.json b/fixtures/pages-simple-assets/package.json new file mode 100644 index 0000000..4638795 --- /dev/null +++ b/fixtures/pages-simple-assets/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/pages-simple-assets", + "private": true, + "sideEffects": false, + "main": "dist/worker.js", + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-simple-assets/public/index.html b/fixtures/pages-simple-assets/public/index.html new file mode 100644 index 0000000..9f735a6 --- /dev/null +++ b/fixtures/pages-simple-assets/public/index.html @@ -0,0 +1 @@ +

Hello, world!

diff --git a/fixtures/pages-simple-assets/tests/index.test.ts b/fixtures/pages-simple-assets/tests/index.test.ts new file mode 100644 index 0000000..6fb327b --- /dev/null +++ b/fixtures/pages-simple-assets/tests/index.test.ts @@ -0,0 +1,65 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Functions", async () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("renders static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toContain("Hello, world!"); + }); + + it("doesn't escape out of the build output directory", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/..%2fpackage.json`); + let text = await response.text(); + expect(text).toContain("Hello, world!"); + + response = await fetch( + `http://${ip}:${port}/other-path%2f..%2f..%2fpackage.json` + ); + text = await response.text(); + expect(text).toContain("Hello, world!"); + }); + + it("doesn't redirect to protocol-less URLs", async ({ expect }) => { + { + const response = await fetch( + `http://${ip}:${port}/%2Fwww.example.com/index/`, + { redirect: "manual" } + ); + expect(response.status).toEqual(308); + expect(response.headers.get("Location")).toEqual("/www.example.com/"); + } + { + const response = await fetch( + `http://${ip}:${port}/%5Cwww.example.com/index/`, + { redirect: "manual" } + ); + expect(response.status).toEqual(308); + expect(response.headers.get("Location")).toEqual("/www.example.com/"); + } + { + const response = await fetch( + `http://${ip}:${port}/%09/www.example.com/index/`, + { redirect: "manual" } + ); + expect(response.status).toEqual(308); + expect(response.headers.get("Location")).toEqual("/www.example.com/"); + } + }); +}); diff --git a/fixtures/pages-simple-assets/tests/tsconfig.json b/fixtures/pages-simple-assets/tests/tsconfig.json new file mode 100644 index 0000000..a6373c6 --- /dev/null +++ b/fixtures/pages-simple-assets/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-simple-assets/tsconfig.json b/fixtures/pages-simple-assets/tsconfig.json new file mode 100644 index 0000000..570d2e9 --- /dev/null +++ b/fixtures/pages-simple-assets/tsconfig.json @@ -0,0 +1,12 @@ +{ + "include": ["index.d.ts"], + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/fixtures/pages-simple-assets/vitest.config.mts b/fixtures/pages-simple-assets/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-simple-assets/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-workerjs-and-functions-app/CHANGELOG.md b/fixtures/pages-workerjs-and-functions-app/CHANGELOG.md new file mode 100644 index 0000000..c8aec36 --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/CHANGELOG.md @@ -0,0 +1,10 @@ +# pages-workerjs-and-functions-app + +## 0.0.1 + +### Patch Changes + +- [#1950](https://github.com/cloudflare/workers-sdk/pull/1950) [`daf73fbe`](https://github.com/cloudflare/workers-sdk/commit/daf73fbe03b55631383cdc86a05eac12d2775875) Thanks [@CarmenPopoviciu](https://github.com/CarmenPopoviciu)! - `wrangler pages dev` should prioritize `_worker.js` + + When using a `_worker.js` file, the entire `/functions` directory should be ignored – this includes its routing and middleware characteristics. Currently `wrangler pages dev` does the reverse, by prioritizing + `/functions` over `_worker.js`. These changes fix the current behaviour. diff --git a/fixtures/pages-workerjs-and-functions-app/README.md b/fixtures/pages-workerjs-and-functions-app/README.md new file mode 100644 index 0000000..ba3b95c --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/README.md @@ -0,0 +1,32 @@ +# ⚡️ pages-workerjs-and-functions-app + +`pages-workerjs-and-functions-app` is a test fixture that sets up an [Advanced Mode](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode) ⚡️Pages project that also includes a `/functions` directory. This fixture is meant to test that for such projects, the single worker script provided by users will +always take precedence over the `functions` directory. + +## Publish + +> Please note that in order to deploy this project to `.pages.dev` you need to have a [Cloudflare account](https://dash.cloudflare.com/login) + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-and-functions-app + +# Deploy the directory of static assets as a Pages deployment +npx wrangler pages deploy public +``` + +If deployment was successful, follow the URL refrenced in the success message in your terminal + +``` +✨ Deployment complete! Take a peek over at https:/..pages.dev +``` + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-and-functions-app + +# Run tests +npm run test +``` diff --git a/fixtures/pages-workerjs-and-functions-app/functions/date.ts b/fixtures/pages-workerjs-and-functions-app/functions/date.ts new file mode 100644 index 0000000..0cd8f26 --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/functions/date.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response(new Date().toISOString()); +} diff --git a/fixtures/pages-workerjs-and-functions-app/functions/greeting/hello.ts b/fixtures/pages-workerjs-and-functions-app/functions/greeting/hello.ts new file mode 100644 index 0000000..d69cb1d --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/functions/greeting/hello.ts @@ -0,0 +1,3 @@ +export async function onRequest() { + return new Response("Hello world!"); +} diff --git a/fixtures/pages-workerjs-and-functions-app/package.json b/fixtures/pages-workerjs-and-functions-app/package.json new file mode 100644 index 0000000..01c96d4 --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/pages-workerjs-functions", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 8955", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-workerjs-and-functions-app/public/_worker.js b/fixtures/pages-workerjs-and-functions-app/public/_worker.js new file mode 100644 index 0000000..cef8349 --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/public/_worker.js @@ -0,0 +1,24 @@ +export default { + async fetch(request, env) { + const url = new URL(request.url); + if (url.pathname.startsWith("/greeting/hello")) { + return new Response("Bonjour le monde!"); + } + + if (url.pathname.startsWith("/greeting/goodbye")) { + return new Response("A plus tard alligator 👋"); + } + + if (url.pathname.startsWith("/party")) { + return new Response("Oops! Tous les alligators sont allés à la fête 🎉"); + } + + if (url.pathname.startsWith("/date")) { + return new Response( + "Yesterday is history, tomorrow is a mystery, but today is a gift. That’s why it is called the present." + ); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/fixtures/pages-workerjs-and-functions-app/public/index.html b/fixtures/pages-workerjs-and-functions-app/public/index.html new file mode 100644 index 0000000..a4c96fc --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/public/index.html @@ -0,0 +1,8 @@ + + + +

+ Bienvenue sur notre projet ✨ pages-workerjs-and-functions-app! +

+ + diff --git a/fixtures/pages-workerjs-and-functions-app/tests/index.test.ts b/fixtures/pages-workerjs-and-functions-app/tests/index.test.ts new file mode 100644 index 0000000..d72ad67 --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/tests/index.test.ts @@ -0,0 +1,50 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages project with `_worker.js` and `/functions` directory", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("renders static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-workerjs-and-functions-app!" + ); + }); + + it("runs our _worker.js and ignores the functions directory", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/greeting/hello`); + let text = await response.text(); + expect(text).toEqual("Bonjour le monde!"); + + response = await fetch(`http://${ip}:${port}/greeting/goodbye`); + text = await response.text(); + expect(text).toEqual("A plus tard alligator 👋"); + + response = await fetch(`http://${ip}:${port}/date`); + text = await response.text(); + expect(text).toEqual( + "Yesterday is history, tomorrow is a mystery, but today is a gift. That’s why it is called the present." + ); + + response = await fetch(`http://${ip}:${port}/party`); + text = await response.text(); + expect(text).toEqual("Oops! Tous les alligators sont allés à la fête 🎉"); + }); +}); diff --git a/fixtures/pages-workerjs-and-functions-app/tests/tsconfig.json b/fixtures/pages-workerjs-and-functions-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-workerjs-and-functions-app/tsconfig.json b/fixtures/pages-workerjs-and-functions-app/tsconfig.json new file mode 100644 index 0000000..0ee136f --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["functions"] +} diff --git a/fixtures/pages-workerjs-and-functions-app/vitest.config.mts b/fixtures/pages-workerjs-and-functions-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-workerjs-and-functions-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-workerjs-app/.env b/fixtures/pages-workerjs-app/.env new file mode 100644 index 0000000..1566bb1 --- /dev/null +++ b/fixtures/pages-workerjs-app/.env @@ -0,0 +1 @@ +FOO=bar \ No newline at end of file diff --git a/fixtures/pages-workerjs-app/.gitignore b/fixtures/pages-workerjs-app/.gitignore new file mode 100644 index 0000000..1e18f27 --- /dev/null +++ b/fixtures/pages-workerjs-app/.gitignore @@ -0,0 +1 @@ +!.env \ No newline at end of file diff --git a/fixtures/pages-workerjs-app/package.json b/fixtures/pages-workerjs-app/package.json new file mode 100644 index 0000000..80df3f5 --- /dev/null +++ b/fixtures/pages-workerjs-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/pages-workerjs", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev ./workerjs-test --port 8792 --compatibility-date=2025-07-15", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-workerjs-app/tests/index.test.ts b/fixtures/pages-workerjs-app/tests/index.test.ts new file mode 100644 index 0000000..d1a21b7 --- /dev/null +++ b/fixtures/pages-workerjs-app/tests/index.test.ts @@ -0,0 +1,201 @@ +import { execSync } from "node:child_process"; +import { rename } from "node:fs/promises"; +import path, { resolve } from "node:path"; +import { setTimeout } from "node:timers/promises"; +import { fetch } from "undici"; +import { describe, it, vi } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages _worker.js", () => { + it("should throw an error when the _worker.js file imports something and --bundle is false", ({ + expect, + }) => { + expect(() => + execSync("pnpm run dev -- --bundle=false", { + cwd: path.resolve(__dirname, ".."), + stdio: "ignore", + }) + ).toThrow(); + }); + + it("should throw an error when the _worker.js file imports something and --no-bundle is true", ({ + expect, + }) => { + expect(() => + execSync("pnpm run dev -- --no-bundle", { + cwd: path.resolve(__dirname, ".."), + stdio: "ignore", + }) + ).toThrow(); + }); + + it("should not throw an error when the _worker.js file imports something if --no-bundle is false", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./workerjs-test", + [ + "--no-bundle=false", + "--port=0", + "--inspector-port=0", + "--compatibility-date=2025-07-15", + ] + ); + try { + await expect( + fetch(`http://${ip}:${port}/`).then((resp) => resp.text()) + ).resolves.toContain("test"); + } finally { + await stop(); + } + }); + + it("should not throw an error when the _worker.js file imports something if --bundle is true", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./workerjs-test", + [ + "--bundle", + "--port=0", + "--inspector-port=0", + "--compatibility-date=2025-07-15", + ] + ); + try { + await expect( + fetch(`http://${ip}:${port}/`).then((resp) => resp.text()) + ).resolves.toContain("test"); + } finally { + await stop(); + } + }); + + it("should not error if the worker.js file is removed while watching", async ({ + expect, + }) => { + const basePath = resolve(__dirname, ".."); + const { ip, port, getOutput, clearOutput, stop } = + await runWranglerPagesDev(resolve(__dirname, ".."), "./workerjs-test", [ + "--port=0", + "--inspector-port=0", + "--compatibility-date=2025-07-15", + ]); + await vi.waitFor( + () => { + expect(getOutput()).toContain("Ready on"); + }, + { + timeout: 5_000, + } + ); + await setTimeout(200); + try { + clearOutput(); + await tryRename( + basePath, + "workerjs-test/_worker.js", + "workerjs-test/XXX_worker.js" + ); + await setTimeout(200); + // Expect no output since the deletion of the worker should be ignored + expect(getOutput()).toBe(""); + await tryRename( + basePath, + "workerjs-test/XXX_worker.js", + "workerjs-test/_worker.js" + ); + await setTimeout(200); + // Expect replacing the worker to now trigger a success build. + expect(getOutput()).toContain("Compiled Worker successfully"); + } finally { + await stop(); + await tryRename( + basePath, + "workerjs-test/XXX_worker.js", + "workerjs-test/_worker.js" + ); + } + }); + + it("should not error if the _routes.json file is removed while watching", async ({ + expect, + }) => { + const basePath = resolve(__dirname, ".."); + const { ip, port, getOutput, clearOutput, stop } = + await runWranglerPagesDev(resolve(__dirname, ".."), "./workerjs-test", [ + "--port=0", + "--inspector-port=0", + ]); + await vi.waitFor( + () => { + expect(getOutput()).toContain("Ready on"); + }, + { + timeout: 5_000, + } + ); + await setTimeout(200); + try { + clearOutput(); + await tryRename( + basePath, + "workerjs-test/_routes.json", + "workerjs-test/XXX_routes.json" + ); + await setTimeout(200); + // Expect no output since the deletion of the routes file should be ignored + expect(getOutput()).toBe(""); + await tryRename( + basePath, + "workerjs-test/XXX_routes.json", + "workerjs-test/_routes.json" + ); + await setTimeout(200); + // Expect replacing the routes file to trigger a build, although + // the routes build does not provide any output feedback to compare against, + // so we just check that nothing else is being printed. + expect(getOutput()).toBe(""); + } finally { + await stop(); + await tryRename( + basePath, + "workerjs-test/XXX_routes.json", + "workerjs-test/_routes.json" + ); + } + }); + + // Serendipitously, this .env reading also works for `wrangler pages dev`. + it("should read local dev vars from the .env file", async ({ expect }) => { + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "./workerjs-test", + ["--port=0", "--inspector-port=0", "--compatibility-date=2025-07-15"] + ); + try { + const response = await fetch(`http://${ip}:${port}/env`); + const env = (await response.json()) as { FOO: string }; + expect(env.FOO).toBe("bar"); + } finally { + await stop(); + } + }); + + async function tryRename( + basePath: string, + from: string, + to: string + ): Promise { + try { + await rename(resolve(basePath, from), resolve(basePath, to)); + } catch (e) { + // Do nothing if the file was not found + if ((e as any).code !== "ENOENT") { + throw e; + } + } + } +}); diff --git a/fixtures/pages-workerjs-app/tests/tsconfig.json b/fixtures/pages-workerjs-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-workerjs-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-workerjs-app/tsconfig.json b/fixtures/pages-workerjs-app/tsconfig.json new file mode 100644 index 0000000..7e797b5 --- /dev/null +++ b/fixtures/pages-workerjs-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-workerjs-app/vitest.config.mts b/fixtures/pages-workerjs-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-workerjs-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-workerjs-app/workerjs-test/_routes.json b/fixtures/pages-workerjs-app/workerjs-test/_routes.json new file mode 100644 index 0000000..1cf7931 --- /dev/null +++ b/fixtures/pages-workerjs-app/workerjs-test/_routes.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "description": "", + "include": ["/*"], + "exclude": [] +} diff --git a/fixtures/pages-workerjs-app/workerjs-test/_worker.js b/fixtures/pages-workerjs-app/workerjs-test/_worker.js new file mode 100644 index 0000000..c517450 --- /dev/null +++ b/fixtures/pages-workerjs-app/workerjs-test/_worker.js @@ -0,0 +1,11 @@ +import text from "./other-script"; + +export default { + async fetch(request, env) { + if (request.url.endsWith("/env")) { + return Response.json(env); + } else { + return new Response(text); + } + }, +}; diff --git a/fixtures/pages-workerjs-app/workerjs-test/other-script.js b/fixtures/pages-workerjs-app/workerjs-test/other-script.js new file mode 100644 index 0000000..58c5715 --- /dev/null +++ b/fixtures/pages-workerjs-app/workerjs-test/other-script.js @@ -0,0 +1 @@ +export default "test"; diff --git a/fixtures/pages-workerjs-directory/CHANGELOG.md b/fixtures/pages-workerjs-directory/CHANGELOG.md new file mode 100644 index 0000000..7120a78 --- /dev/null +++ b/fixtures/pages-workerjs-directory/CHANGELOG.md @@ -0,0 +1,7 @@ +# pages-workerjs-directory + +## 0.0.1 + +### Patch Changes + +- [#3595](https://github.com/cloudflare/workers-sdk/pull/3595) [`c302bec6`](https://github.com/cloudflare/workers-sdk/commit/c302bec639c0eec10d07d6b950c0a2d3e16eab1e) Thanks [@geelen](https://github.com/geelen)! - Removing the D1 shim from the build process, in preparation for the Open Beta. D1 can now be used with --no-bundle enabled. diff --git a/fixtures/pages-workerjs-directory/package.json b/fixtures/pages-workerjs-directory/package.json new file mode 100644 index 0000000..90c6f0e --- /dev/null +++ b/fixtures/pages-workerjs-directory/package.json @@ -0,0 +1,18 @@ +{ + "name": "@fixture/pages-workerjs-directory", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --d1=D1 --d1=PUT=elsewhere --kv KV --kv KV_REF=other_kv --r2 r2bucket --r2=R2_REF=other-r2 --port 8794", + "test:ci": "vitest run", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/add.wasm b/fixtures/pages-workerjs-directory/public/_worker.js/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/index.js b/fixtures/pages-workerjs-directory/public/_worker.js/index.js new file mode 100644 index 0000000..ba1a06d --- /dev/null +++ b/fixtures/pages-workerjs-directory/public/_worker.js/index.js @@ -0,0 +1,59 @@ +import add from "./add.wasm"; +import staticJsMod from "./static.js"; +import staticMjsMod from "./static.mjs"; + +export default { + async fetch(request, env) { + const { pathname } = new URL(request.url); + + if (pathname === "/wasm") { + const addModule = await WebAssembly.instantiate(add); + return new Response(addModule.exports.add(1, 2).toString()); + } + + if (pathname === "/static-js") { + return new Response(`static import text (via js): '${staticJsMod}'`); + } + + if (pathname === "/static-mjs") { + return new Response(`static import text (via mjs): '${staticMjsMod}'`); + } + + if (pathname === "/d1") { + const stmt1 = env.D1.prepare("SELECT 1"); + const values1 = await stmt1.first(); + + const stmt = env.PUT.prepare("SELECT 1"); + const values = await stmt.first(); + + if (JSON.stringify(values1) === JSON.stringify(values)) { + return new Response(JSON.stringify(values)); + } + + return new Response("couldn't select 1"); + } + + if (pathname === "/kv") { + await env.KV.put("key", "value"); + + await env.KV_REF.put("key", "value"); + + return new Response("saved"); + } + + if (pathname === "/r2") { + await env.r2bucket.put("key", "value"); + + await env.R2_REF.put("key", "value"); + + return new Response("saved"); + } + + if (pathname !== "/") { + const file = "." + pathname; + return new Response((await import(file)).default); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/other-other-script.mjs b/fixtures/pages-workerjs-directory/public/_worker.js/other-other-script.mjs new file mode 100644 index 0000000..23cb267 --- /dev/null +++ b/fixtures/pages-workerjs-directory/public/_worker.js/other-other-script.mjs @@ -0,0 +1 @@ +export default "other-other-script-test"; diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/other-script.js b/fixtures/pages-workerjs-directory/public/_worker.js/other-script.js new file mode 100644 index 0000000..720d88d --- /dev/null +++ b/fixtures/pages-workerjs-directory/public/_worker.js/other-script.js @@ -0,0 +1 @@ +export default "other-script-test"; diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/static.js b/fixtures/pages-workerjs-directory/public/_worker.js/static.js new file mode 100644 index 0000000..c4f3aa8 --- /dev/null +++ b/fixtures/pages-workerjs-directory/public/_worker.js/static.js @@ -0,0 +1 @@ +export default "js static"; diff --git a/fixtures/pages-workerjs-directory/public/_worker.js/static.mjs b/fixtures/pages-workerjs-directory/public/_worker.js/static.mjs new file mode 100644 index 0000000..b0e9b76 --- /dev/null +++ b/fixtures/pages-workerjs-directory/public/_worker.js/static.mjs @@ -0,0 +1 @@ +export default "mjs static"; diff --git a/fixtures/pages-workerjs-directory/public/index.html b/fixtures/pages-workerjs-directory/public/index.html new file mode 100644 index 0000000..9f735a6 --- /dev/null +++ b/fixtures/pages-workerjs-directory/public/index.html @@ -0,0 +1 @@ +

Hello, world!

diff --git a/fixtures/pages-workerjs-directory/tests/index.test.ts b/fixtures/pages-workerjs-directory/tests/index.test.ts new file mode 100644 index 0000000..6875eec --- /dev/null +++ b/fixtures/pages-workerjs-directory/tests/index.test.ts @@ -0,0 +1,92 @@ +import { execSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path, { join, resolve } from "node:path"; +import { fetch } from "undici"; +import { describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages _worker.js/ directory", () => { + it("should support non-bundling with 'dev'", async ({ expect }) => { + const tmpDir = realpathSync( + mkdtempSync(join(tmpdir(), "worker-directory-tests")) + ); + + const { ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + [ + "--port=0", + "--inspector-port=0", + `--persist-to=${tmpDir}`, + "--d1=D1", + "--d1=PUT=elsewhere", + "--kv=KV", + "--kv=KV_REF=other_kv", + "--r2=r2bucket", + "--r2=R2_REF=other-r2", + ] + ); + try { + await expect( + fetch(`http://${ip}:${port}/`).then((resp) => resp.text()) + ).resolves.toContain("Hello, world!"); + await expect( + fetch(`http://${ip}:${port}/wasm`).then((resp) => resp.text()) + ).resolves.toContain("3"); + await expect( + fetch(`http://${ip}:${port}/static-js`).then((resp) => resp.text()) + ).resolves.toEqual("static import text (via js): 'js static'"); + await expect( + fetch(`http://${ip}:${port}/static-mjs`).then((resp) => resp.text()) + ).resolves.toEqual("static import text (via mjs): 'mjs static'"); + await expect( + fetch(`http://${ip}:${port}/other-script.js`).then((resp) => + resp.text() + ) + ).resolves.toContain("other-script-test"); + await expect( + fetch(`http://${ip}:${port}/other-other-script.mjs`).then((resp) => + resp.text() + ) + ).resolves.toContain("other-other-script-test"); + await expect( + fetch(`http://${ip}:${port}/d1`).then((resp) => resp.text()) + ).resolves.toContain('{"1":1}'); + await expect( + fetch(`http://${ip}:${port}/kv`).then((resp) => resp.text()) + ).resolves.toContain("saved"); + await expect( + fetch(`http://${ip}:${port}/r2`).then((resp) => resp.text()) + ).resolves.toContain("saved"); + } finally { + await stop(); + } + + expect(existsSync(join(tmpDir, "./v3/d1"))).toBeTruthy(); + expect(existsSync(join(tmpDir, "./v3/kv"))).toBeTruthy(); + expect(existsSync(join(tmpDir, "./v3/r2"))).toBeTruthy(); + }); + + it("should bundle", ({ expect }) => { + const tempDir = realpathSync( + mkdtempSync(join(tmpdir(), "worker-bundle-tests")) + ); + const file = join(tempDir, "_worker.bundle"); + + execSync( + `npx wrangler pages functions build --build-output-directory public --outfile ${file} --bindings="{\\"d1_databases\\":{\\"D1\\":{}}}"`, + { + cwd: path.resolve(__dirname, ".."), + } + ); + + const contents = readFileSync(file, "utf-8"); + + expect(contents).not.toContain("D1_ERROR"); // No more D1 shim in the bundle! + expect(contents).toContain('"other-script-test"'); + expect(contents).toContain('"other-other-script-test"'); + expect(contents).toContain('import staticJsMod from "./static.js";'); + expect(contents).toContain('import staticMjsMod from "./static.mjs";'); + }); +}); diff --git a/fixtures/pages-workerjs-directory/tests/tsconfig.json b/fixtures/pages-workerjs-directory/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-workerjs-directory/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-workerjs-directory/tsconfig.json b/fixtures/pages-workerjs-directory/tsconfig.json new file mode 100644 index 0000000..7e797b5 --- /dev/null +++ b/fixtures/pages-workerjs-directory/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-workerjs-directory/vitest.config.mts b/fixtures/pages-workerjs-directory/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-workerjs-directory/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-workerjs-wasm-app/README.md b/fixtures/pages-workerjs-wasm-app/README.md new file mode 100644 index 0000000..8242262 --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/README.md @@ -0,0 +1,41 @@ +# ⚡️ pages-workerjs-wasm-app + +`pages-workerjs-wasm-app` is a test fixture that sets up an [Advanced Mode](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode) ⚡️Pages project with [`wasm` module imports](https://blog.cloudflare.com/workers-javascript-modules/) + +## Dev + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-wasm-app + +# start dev server +npm run dev +``` + +## Publish + +> Please note that in order to deploy this project to `.pages.dev` you need to have a [Cloudflare account](https://dash.cloudflare.com/login) + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-wasm-app + +# Deploy the directory of static assets as a Pages deployment +npm run publish +``` + +If deployment was successful, follow the URL refrenced in the success message in your terminal + +``` +✨ Deployment complete! Take a peek over at https:/..pages.dev +``` + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-wasm-app + +# Run tests +npm run test +``` diff --git a/fixtures/pages-workerjs-wasm-app/external-modules/meaning-of-life.html b/fixtures/pages-workerjs-wasm-app/external-modules/meaning-of-life.html new file mode 100644 index 0000000..9ee3bcd --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/external-modules/meaning-of-life.html @@ -0,0 +1,5 @@ + + + [.html]: The meaning of life is 21 + + diff --git a/fixtures/pages-workerjs-wasm-app/package.json b/fixtures/pages-workerjs-wasm-app/package.json new file mode 100644 index 0000000..eda1aba --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/pages-workerjs-wasm", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 8776", + "publish": "wrangler pages deploy public", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-workerjs-wasm-app/public/_worker.js b/fixtures/pages-workerjs-wasm-app/public/_worker.js new file mode 100644 index 0000000..d4ef241 --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/public/_worker.js @@ -0,0 +1,28 @@ +import html from "./../external-modules/meaning-of-life.html"; +import multiply from "./../wasm/multiply.wasm"; + +export default { + async fetch(request, env) { + const url = new URL(request.url); + const multiplyModule = await WebAssembly.instantiate(multiply); + + if (url.pathname === "/meaning-of-life-wasm") { + return new Response( + `[.wasm]: The meaning of life is ${multiplyModule.exports.multiply( + 7, + 3 + )}` + ); + } + + if (url.pathname === "/meaning-of-life-html") { + return new Response(html, { + headers: { + "Content-Type": "text/html", + }, + }); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/fixtures/pages-workerjs-wasm-app/public/index.html b/fixtures/pages-workerjs-wasm-app/public/index.html new file mode 100644 index 0000000..67ae272 --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/public/index.html @@ -0,0 +1,6 @@ + + + +

Hello from pages-workerjs-wasm-app!

+ + diff --git a/fixtures/pages-workerjs-wasm-app/tests/index.test.ts b/fixtures/pages-workerjs-wasm-app/tests/index.test.ts new file mode 100644 index 0000000..e19119b --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/tests/index.test.ts @@ -0,0 +1,42 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Advanced Mode with wasm module imports", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should render static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}`); + const text = await response.text(); + expect(text).toContain("Hello from pages-workerjs-wasm-app!"); + }); + + it("should resolve wasm module imports and correctly render /meaning-of-life", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/meaning-of-life-wasm`); + const text = await response.text(); + expect(text).toEqual("[.wasm]: The meaning of life is 21"); + }); + + it("should resolve text module imports and correctly render /meaning-of-life-html", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/meaning-of-life-html`); + const text = await response.text(); + expect(text).toContain(`[.html]: The meaning of life is 21`); + }); +}); diff --git a/fixtures/pages-workerjs-wasm-app/tests/tsconfig.json b/fixtures/pages-workerjs-wasm-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-workerjs-wasm-app/tsconfig.json b/fixtures/pages-workerjs-wasm-app/tsconfig.json new file mode 100644 index 0000000..7e797b5 --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-workerjs-wasm-app/vitest.config.mts b/fixtures/pages-workerjs-wasm-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-workerjs-wasm-app/wasm/multiply.wasm b/fixtures/pages-workerjs-wasm-app/wasm/multiply.wasm new file mode 100644 index 0000000000000000000000000000000000000000..0fbf9272721fa9c854cd555878d6f6b10e474195 GIT binary patch literal 46 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2Pu=V9c?EzK#(EXb*3VBq3pWM@!dP+-jA1^~GP B2cZA} literal 0 HcmV?d00001 diff --git a/fixtures/pages-workerjs-wasm-app/wasm/multiply.wat b/fixtures/pages-workerjs-wasm-app/wasm/multiply.wat new file mode 100644 index 0000000..064fbe7 --- /dev/null +++ b/fixtures/pages-workerjs-wasm-app/wasm/multiply.wat @@ -0,0 +1,7 @@ +(module + (func $multiply (param $p1 i32) (param $p2 i32) (result i32) + local.get $p1 + local.get $p2 + i32.mul) + (export "multiply" (func $multiply)) +) diff --git a/fixtures/pages-workerjs-with-config-file-app/README.md b/fixtures/pages-workerjs-with-config-file-app/README.md new file mode 100644 index 0000000..5a5623b --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/README.md @@ -0,0 +1,27 @@ +# ⚡️ pages-workerjs-with-config-file-app + +`pages-workerjs-with-config-file-app` is a test fixture that sets up an [Advanced Mode](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode) ⚡️Pages⚡️ project with a `wrangler.toml` [configuration file](hhttps://developers.cloudflare.com/workers/wrangler/configuration). he purpose of this fixture is to demonstrate that `wrangler pages dev` can take configuration from a `wrangler.toml` file. + +## Local dev + +To test this fixture run `wrangler pages dev` in the fixture folder: + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-with-config-file-app + +# Start local dev server +npx wrangler pages dev +``` + +Once the local dev server was started, you should see the configuration specified in the `wrangler.toml` at the root of the fixture folder, affect the generated Worker. + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-with-config-file-app + +# Run tests +npm run test +``` diff --git a/fixtures/pages-workerjs-with-config-file-app/package.json b/fixtures/pages-workerjs-with-config-file-app/package.json new file mode 100644 index 0000000..3a817fb --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/pages-workerjs-config", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-workerjs-with-config-file-app/public/_routes.json b/fixtures/pages-workerjs-with-config-file-app/public/_routes.json new file mode 100644 index 0000000..8443f0e --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/public/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/holiday", "/celebrate", "/version_metadata"], + "exclude": [] +} diff --git a/fixtures/pages-workerjs-with-config-file-app/public/_worker.js b/fixtures/pages-workerjs-with-config-file-app/public/_worker.js new file mode 100644 index 0000000..b015185 --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/public/_worker.js @@ -0,0 +1,41 @@ +export default { + async fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname === "/version_metadata") { + return Response.json(env.METADATA); + } + + if (url.pathname === "/holiday") { + return new Response( + `[/holiday]:\n` + + `🎶 🎶 🎶\n` + + `If we took a ${env.VAR2}\n` + + `Took some time to ${env.VAR1}\n` + + `Just one day out of life\n` + + `It would be, it would be so nice 🎉\n` + + `🎶 🎶 🎶` + ); + } + + if (url.pathname === "/celebrate") { + return new Response( + `[/celebrate]:\n` + + `🎶 🎶 🎶\n` + + `Everybody spread the word\n` + + `We're gonna have a ${env.VAR3}\n` + + `All across the world\n` + + `In every nation\n` + + `🎶 🎶 🎶` + ); + } + + if (url.pathname === "/oh-yeah") { + return new Response( + `[/oh-yeah]: 🎶 🎶 🎶 ${env.VAR2} (ooh yeah, ooh yeah)🎶 🎶 🎶` + ); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/fixtures/pages-workerjs-with-config-file-app/public/index.html b/fixtures/pages-workerjs-with-config-file-app/public/index.html new file mode 100644 index 0000000..3e44b06 --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/public/index.html @@ -0,0 +1,6 @@ + + + +

🪩 Holiday! Celebrate! Pages supports 'wrangler.toml' 🪩

+ + diff --git a/fixtures/pages-workerjs-with-config-file-app/tests/index.test.ts b/fixtures/pages-workerjs-with-config-file-app/tests/index.test.ts new file mode 100644 index 0000000..35bdc34 --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/tests/index.test.ts @@ -0,0 +1,74 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Advanced Mode with wrangler.toml", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + undefined, + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should render static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toContain( + "🪩 Holiday! Celebrate! Pages supports 'wrangler.toml' 🪩" + ); + }); + + it("should run our _worker.js, and correctly apply the routing rules provided in the custom _routes.json file", async ({ + expect, + }) => { + // matches `/holiday` include rule + let response = await fetch(`http://${ip}:${port}/holiday`); + let text = await response.text(); + expect(text).toEqual( + `[/holiday]:\n` + + `🎶 🎶 🎶\n` + + `If we took a holiday\n` + + `Took some time to celebrate\n` + + `Just one day out of life\n` + + `It would be, it would be so nice 🎉\n` + + `🎶 🎶 🎶` + ); + + // matches `/celebrate` include rule + response = await fetch(`http://${ip}:${port}/celebrate`); + text = await response.text(); + expect(text).toEqual( + `[/celebrate]:\n` + + `🎶 🎶 🎶\n` + + `Everybody spread the word\n` + + `We're gonna have a celebration\n` + + `All across the world\n` + + `In every nation\n` + + `🎶 🎶 🎶` + ); + + // matches `/oh-yeah` exclude rule + response = await fetch(`http://${ip}:${port}/oh-yeah`); + text = await response.text(); + expect(text).toContain( + "🪩 Holiday! Celebrate! Pages supports 'wrangler.toml' 🪩" + ); + }); + + it("has version_metadata binding", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/version_metadata`); + + await expect(response.json()).resolves.toMatchObject({ + id: expect.any(String), + tag: expect.any(String), + }); + }); +}); diff --git a/fixtures/pages-workerjs-with-config-file-app/tests/tsconfig.json b/fixtures/pages-workerjs-with-config-file-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-workerjs-with-config-file-app/tsconfig.json b/fixtures/pages-workerjs-with-config-file-app/tsconfig.json new file mode 100644 index 0000000..7e797b5 --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-workerjs-with-config-file-app/vitest.config.mts b/fixtures/pages-workerjs-with-config-file-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/pages-workerjs-with-config-file-app/wrangler.jsonc b/fixtures/pages-workerjs-with-config-file-app/wrangler.jsonc new file mode 100644 index 0000000..d9c1789 --- /dev/null +++ b/fixtures/pages-workerjs-with-config-file-app/wrangler.jsonc @@ -0,0 +1,14 @@ +{ + "pages_build_output_dir": "./public", + "name": "pages-with-config-file-app", + "compatibility_date": "2024-01-01", + "compatibility_flags": [], + "vars": { + "VAR1": "celebrate", + "VAR2": "holiday", + "VAR3": "celebration", + }, + "version_metadata": { + "binding": "METADATA", + }, +} diff --git a/fixtures/pages-workerjs-with-routes-app/CHANGELOG.md b/fixtures/pages-workerjs-with-routes-app/CHANGELOG.md new file mode 100644 index 0000000..e04ac02 --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/CHANGELOG.md @@ -0,0 +1,18 @@ +# pages-workerjs-with-routes-app + +## 0.0.1 + +### Patch Changes + +- [#2065](https://github.com/cloudflare/workers-sdk/pull/2065) [`14c44588`](https://github.com/cloudflare/workers-sdk/commit/14c44588c9d22e9c9f2ad2740df57809d0cbcfbc) Thanks [@CarmenPopoviciu](https://github.com/CarmenPopoviciu)! - fix(pages): `wrangler pages dev` matches routing rules in `_routes.json` too loosely + + Currently, the logic by which we transform routing rules in `_routes.json` to + regular expressions, so we can perform `pathname` matching & routing when we + run `wrangler pages dev`, is too permissive, and leads to serving incorrect + assets for certain url paths. + + For example, a routing rule such as `/foo` will incorrectly match pathname + `/bar/foo`. Similarly, pathname `/foo` will be incorrectly matched by the + `/` routing rule. + This commit fixes our routing rule to pathname matching logic and brings + `wrangler pages dev` on par with routing in deployed Pages projects. diff --git a/fixtures/pages-workerjs-with-routes-app/README.md b/fixtures/pages-workerjs-with-routes-app/README.md new file mode 100644 index 0000000..eec745b --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/README.md @@ -0,0 +1,31 @@ +# ⚡️ pages-workerjs-with-routes-app + +`pages-workerjs-with-routes-app` is a test fixture that sets up an [Advanced Mode](https://developers.cloudflare.com/pages/platform/functions/#advanced-mode) ⚡️Pages project with a custom `_routes.json` file + +## Publish + +> Please note that in order to deploy this project to `.pages.dev` you need to have a [Cloudflare account](https://dash.cloudflare.com/login) + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-with-routes-app + +# Deploy the directory of static assets as a Pages deployment +npx wrangler pages deploy public +``` + +If deployment was successful, follow the URL refrenced in the success message in your terminal + +``` +✨ Deployment complete! Take a peek over at https:/..pages.dev +``` + +## Run tests + +```bash +# cd into the test fixture folder +cd fixtures/pages-workerjs-with-routes-app + +# Run tests +npm run test +``` diff --git a/fixtures/pages-workerjs-with-routes-app/package.json b/fixtures/pages-workerjs-with-routes-app/package.json new file mode 100644 index 0000000..7948f71 --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/pages-workerjs-routes", + "private": true, + "sideEffects": false, + "scripts": { + "check:type": "tsc", + "dev": "wrangler pages dev public --port 8751", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/pages-workerjs-with-routes-app/public/_routes.json b/fixtures/pages-workerjs-with-routes-app/public/_routes.json new file mode 100644 index 0000000..8ae2128 --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/public/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/greeting/*", "/date", "/party*"], + "exclude": ["/", "/party"] +} diff --git a/fixtures/pages-workerjs-with-routes-app/public/_worker.js b/fixtures/pages-workerjs-with-routes-app/public/_worker.js new file mode 100644 index 0000000..dcb2692 --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/public/_worker.js @@ -0,0 +1,41 @@ +export default { + async fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname === "/") { + return new Response("ROOT"); + } + + if (url.pathname === "/party") { + return new Response( + "[/party]: Oops! Tous les alligators sont allés à la fête 🎉" + ); + } + + if (url.pathname === "/party-disco") { + return new Response("[/party-disco]: Tout le monde à la discothèque 🪩"); + } + + if (url.pathname === "/date") { + return new Response(`[/date]: ${new Date().toISOString()}`); + } + + if (url.pathname === "/greeting") { + return new Response("[/greeting]: Bonjour à tous!"); + } + + if (url.pathname === "/greeting/hello") { + return new Response("[/greeting/hello]: Bonjour le monde!"); + } + + if (url.pathname === "/greeting/bye") { + return new Response("[/greeting/bye]: A plus tard alligator 👋"); + } + + if (url.pathname === "/greetings") { + return new Response("[/greetings]: Bonjour alligators!"); + } + + return env.ASSETS.fetch(request); + }, +}; diff --git a/fixtures/pages-workerjs-with-routes-app/public/index.html b/fixtures/pages-workerjs-with-routes-app/public/index.html new file mode 100644 index 0000000..260248d --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/public/index.html @@ -0,0 +1,6 @@ + + + +

Bienvenue sur notre projet ✨ pages-workerjs-with-routes-app!

+ + diff --git a/fixtures/pages-workerjs-with-routes-app/tests/index.test.ts b/fixtures/pages-workerjs-with-routes-app/tests/index.test.ts new file mode 100644 index 0000000..a537e52 --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/tests/index.test.ts @@ -0,0 +1,70 @@ +import { resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerPagesDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Pages Advanced Mode with custom _routes.json", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerPagesDev( + resolve(__dirname, ".."), + "public", + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("renders static pages", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-workerjs-with-routes-app!" + ); + }); + + it("runs our _worker.js", async ({ expect }) => { + // matches /greeting/* include rule + let response = await fetch(`http://${ip}:${port}/greeting/hello`); + let text = await response.text(); + expect(text).toEqual("[/greeting/hello]: Bonjour le monde!"); + + // matches /greeting/* include rule + response = await fetch(`http://${ip}:${port}/greeting/bye`); + text = await response.text(); + expect(text).toEqual("[/greeting/bye]: A plus tard alligator 👋"); + + // matches /date include rule + response = await fetch(`http://${ip}:${port}/date`); + text = await response.text(); + expect(text).toMatch(/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d/); + + // matches both /party* include and /party exclude rules, but exclude + // has priority + response = await fetch(`http://${ip}:${port}/party`); + text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-workerjs-with-routes-app!" + ); + + // matches /party* include rule + response = await fetch(`http://${ip}:${port}/party-disco`); + text = await response.text(); + expect(text).toEqual("[/party-disco]: Tout le monde à la discothèque 🪩"); + + // matches /greeting/* include rule + response = await fetch(`http://${ip}:${port}/greeting`); + text = await response.text(); + expect(text).toEqual("[/greeting]: Bonjour à tous!"); + + // matches no rule + response = await fetch(`http://${ip}:${port}/greetings`); + text = await response.text(); + expect(text).toContain( + "Bienvenue sur notre projet ✨ pages-workerjs-with-routes-app!" + ); + }); +}); diff --git a/fixtures/pages-workerjs-with-routes-app/tests/tsconfig.json b/fixtures/pages-workerjs-with-routes-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/pages-workerjs-with-routes-app/tsconfig.json b/fixtures/pages-workerjs-with-routes-app/tsconfig.json new file mode 100644 index 0000000..7e797b5 --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["tests"] +} diff --git a/fixtures/pages-workerjs-with-routes-app/vitest.config.mts b/fixtures/pages-workerjs-with-routes-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/pages-workerjs-with-routes-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/python-worker/requirements.txt b/fixtures/python-worker/requirements.txt new file mode 100644 index 0000000..6b9ac58 --- /dev/null +++ b/fixtures/python-worker/requirements.txt @@ -0,0 +1 @@ +bcrypt==4.0.1 \ No newline at end of file diff --git a/fixtures/python-worker/src/arith.py b/fixtures/python-worker/src/arith.py new file mode 100644 index 0000000..0bcf862 --- /dev/null +++ b/fixtures/python-worker/src/arith.py @@ -0,0 +1,2 @@ +def mul(a,b): + return a*b diff --git a/fixtures/python-worker/src/index.py b/fixtures/python-worker/src/index.py new file mode 100644 index 0000000..2dd26a2 --- /dev/null +++ b/fixtures/python-worker/src/index.py @@ -0,0 +1,8 @@ +from js import Response +from other import add +from arith import mul +import bcrypt +def on_fetch(request): + password = b"super secret password" + hashed = bcrypt.hashpw(password, bcrypt.gensalt(14)) + return Response.new(f"Hi world {add(1,2)} {mul(2,3)} {hashed}") diff --git a/fixtures/python-worker/src/other.py b/fixtures/python-worker/src/other.py new file mode 100644 index 0000000..2a99cdf --- /dev/null +++ b/fixtures/python-worker/src/other.py @@ -0,0 +1,2 @@ +def add(a, b): + return a + b diff --git a/fixtures/python-worker/wrangler.jsonc b/fixtures/python-worker/wrangler.jsonc new file mode 100644 index 0000000..301a630 --- /dev/null +++ b/fixtures/python-worker/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "dep-python-worker", + "main": "src/index.py", + "compatibility_flags": ["python_workers"], + "compatibility_date": "2024-01-29", +} diff --git a/fixtures/ratelimit-app/.gitignore b/fixtures/ratelimit-app/.gitignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/fixtures/ratelimit-app/.gitignore @@ -0,0 +1 @@ +dist diff --git a/fixtures/ratelimit-app/package.json b/fixtures/ratelimit-app/package.json new file mode 100644 index 0000000..2ff53e9 --- /dev/null +++ b/fixtures/ratelimit-app/package.json @@ -0,0 +1,21 @@ +{ + "name": "@fixture/worker-ratelimit", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "check:type": "tsc", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/ratelimit-app/src/index.js b/fixtures/ratelimit-app/src/index.js new file mode 100644 index 0000000..6bdcfe1 --- /dev/null +++ b/fixtures/ratelimit-app/src/index.js @@ -0,0 +1,27 @@ +console.log("startup log"); + +export default { + async fetch(request, env) { + console.log("request log"); + + const { pathname } = new URL(request.url); + + if (pathname.startsWith("/unsafe")) { + const { success } = await env.UNSAFE_RATE_LIMITER.limit({ + key: pathname, + }); + if (!success) { + return new Response("unsafe: Slow down", { status: 429 }); + } + return new Response("unsafe: Success"); + } + const { success } = await env.RATE_LIMITER.limit({ key: pathname }); + if (!success) { + return new Response(`Slow down`, { + status: 429, + }); + } + + return new Response("Success"); + }, +}; diff --git a/fixtures/ratelimit-app/tests/index.test.ts b/fixtures/ratelimit-app/tests/index.test.ts new file mode 100644 index 0000000..6a67353 --- /dev/null +++ b/fixtures/ratelimit-app/tests/index.test.ts @@ -0,0 +1,51 @@ +import { resolve } from "path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { createTestHarness } from "wrangler"; + +const basePath = resolve(__dirname, ".."); +const server = createTestHarness({ + root: basePath, + workers: [{ configPath: "wrangler.jsonc" }], +}); + +describe("Rate limiting bindings", () => { + beforeAll(async () => { + await server.listen(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("ratelimit binding is defined ", async ({ expect }) => { + let response = await server.fetch("/"); + let content = await response.text(); + expect(content).toEqual("Success"); + + response = await server.fetch("/"); + content = await response.text(); + expect(content).toEqual("Success"); + + response = await server.fetch("/"); + content = await response.text(); + expect(content).toEqual("Success"); + + response = await server.fetch("/"); + content = await response.text(); + expect(content).toEqual("Slow down"); + }); + + it("ratelimit unsafe binding is defined ", async ({ expect }) => { + let response = await server.fetch("/unsafe"); + let content = await response.text(); + expect(content).toEqual("unsafe: Success"); + + response = await server.fetch("/unsafe"); + content = await response.text(); + expect(content).toEqual("unsafe: Success"); + + response = await server.fetch("/unsafe"); + content = await response.text(); + expect(content).toEqual("unsafe: Slow down"); + }); +}); diff --git a/fixtures/ratelimit-app/tests/tsconfig.json b/fixtures/ratelimit-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/ratelimit-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/ratelimit-app/tsconfig.json b/fixtures/ratelimit-app/tsconfig.json new file mode 100644 index 0000000..655d600 --- /dev/null +++ b/fixtures/ratelimit-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "esnext", + "esModuleInterop": true, + "module": "preserve", + "lib": ["esnext"], + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/ratelimit-app/vitest.config.mts b/fixtures/ratelimit-app/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/ratelimit-app/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/ratelimit-app/wrangler.jsonc b/fixtures/ratelimit-app/wrangler.jsonc new file mode 100644 index 0000000..bbfb5bf --- /dev/null +++ b/fixtures/ratelimit-app/wrangler.jsonc @@ -0,0 +1,28 @@ +{ + "name": "ratelimit-app", + "compatibility_date": "2024-02-23", + "main": "src/index.js", + "ratelimits": [ + { + "name": "RATE_LIMITER", + "namespace_id": "2001", + "simple": { + "limit": 3, + "period": 60, + }, + }, + ], + "unsafe": { + "bindings": [ + { + "name": "UNSAFE_RATE_LIMITER", + "type": "ratelimit", + "namespace_id": "1001", + "simple": { + "limit": 2, + "period": 60, + }, + }, + ], + }, +} diff --git a/fixtures/redirected-config-worker-with-environments/.gitignore b/fixtures/redirected-config-worker-with-environments/.gitignore new file mode 100644 index 0000000..1b823a2 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/.gitignore @@ -0,0 +1,2 @@ +dist +build diff --git a/fixtures/redirected-config-worker-with-environments/package.json b/fixtures/redirected-config-worker-with-environments/package.json new file mode 100644 index 0000000..62825b6 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/config-redirected-envs", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "build": "node -r esbuild-register tools/build.ts", + "check:type": "tsc", + "dev": "pnpm run build && wrangler dev", + "test:ci": "pnpm run build && vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/redirected-config-worker-with-environments/src/index.js b/fixtures/redirected-config-worker-with-environments/src/index.js new file mode 100644 index 0000000..e1ce8e6 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/src/index.js @@ -0,0 +1,5 @@ +export default { + async fetch(request, env) { + return new Response("Generated: " + env.generated ?? false); + }, +}; diff --git a/fixtures/redirected-config-worker-with-environments/tests/index.test.ts b/fixtures/redirected-config-worker-with-environments/tests/index.test.ts new file mode 100644 index 0000000..dfb7988 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/tests/index.test.ts @@ -0,0 +1,28 @@ +import { resolve } from "path"; +import { describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +const basePath = resolve(__dirname, ".."); + +describe("'wrangler dev' errors when a redirected config includes environments", () => { + it("A clear error message is presented which includes the offending environments and prompting the user to contact the tool's author", async ({ + expect, + }) => { + let error: string | undefined; + try { + await runWranglerDev(basePath, ["--port=0", "--inspector-port=0"]); + } catch (e) { + if (typeof e === "string") { + error = e; + } + } + expect(error).toBeTruthy(); + expect(error).toMatch( + /Redirected configurations cannot include environments but the following have been found:/ + ); + expect(error).toContain("staging"); + expect(error).toContain("testing"); + + expect(error).toContain("Report this issue to the tool's author"); + }); +}); diff --git a/fixtures/redirected-config-worker-with-environments/tools/build.ts b/fixtures/redirected-config-worker-with-environments/tools/build.ts new file mode 100644 index 0000000..fbfb509 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/tools/build.ts @@ -0,0 +1,36 @@ +import { copyFileSync, mkdirSync, rmSync, writeFileSync } from "fs"; + +// Create a pseudo build directory +rmSync("build", { recursive: true, force: true }); +mkdirSync("build"); +const config = { + name: "redirected-config-worker", + compatibility_date: "2024-12-01", + main: "index.js", + vars: { generated: true, environmentName: "top-level" }, + env: { + staging: { + vars: { + generated: true, + environmentName: "staging", + }, + }, + testing: { + vars: { + generated: true, + environmentName: "testing", + }, + }, + }, +}; +writeFileSync("build/wrangler.json", JSON.stringify(config, undefined, 2)); +copyFileSync("src/index.js", "build/index.js"); + +// Create the redirect file +rmSync(".wrangler/deploy", { recursive: true, force: true }); +mkdirSync(".wrangler/deploy", { recursive: true }); +const redirect = { configPath: "../../build/wrangler.json" }; +writeFileSync( + ".wrangler/deploy/config.json", + JSON.stringify(redirect, undefined, 2) +); diff --git a/fixtures/redirected-config-worker-with-environments/tsconfig.json b/fixtures/redirected-config-worker-with-environments/tsconfig.json new file mode 100644 index 0000000..7480e11 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/redirected-config-worker-with-environments/turbo.json b/fixtures/redirected-config-worker-with-environments/turbo.json new file mode 100644 index 0000000..3394ff5 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["build/**"] + } + } +} diff --git a/fixtures/redirected-config-worker-with-environments/vitest.config.mts b/fixtures/redirected-config-worker-with-environments/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/redirected-config-worker-with-environments/wrangler.jsonc b/fixtures/redirected-config-worker-with-environments/wrangler.jsonc new file mode 100644 index 0000000..7e57473 --- /dev/null +++ b/fixtures/redirected-config-worker-with-environments/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "redirected-config-worker", + "compatibility_date": "2024-12-01", + "main": "src/index.js", +} diff --git a/fixtures/redirected-config-worker/.gitignore b/fixtures/redirected-config-worker/.gitignore new file mode 100644 index 0000000..2cf1da4 --- /dev/null +++ b/fixtures/redirected-config-worker/.gitignore @@ -0,0 +1,2 @@ +dist +build \ No newline at end of file diff --git a/fixtures/redirected-config-worker/package.json b/fixtures/redirected-config-worker/package.json new file mode 100644 index 0000000..10d1e9f --- /dev/null +++ b/fixtures/redirected-config-worker/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/config-redirected", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "build": "node -r esbuild-register tools/build.ts", + "check:type": "tsc", + "dev": "pnpm run build && wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/redirected-config-worker/src/index.js b/fixtures/redirected-config-worker/src/index.js new file mode 100644 index 0000000..3031001 --- /dev/null +++ b/fixtures/redirected-config-worker/src/index.js @@ -0,0 +1,5 @@ +export default { + async fetch(request, env) { + return new Response("Generated: " + (env.generated ?? false)); + }, +}; diff --git a/fixtures/redirected-config-worker/tests/index.test.ts b/fixtures/redirected-config-worker/tests/index.test.ts new file mode 100644 index 0000000..7b3a78b --- /dev/null +++ b/fixtures/redirected-config-worker/tests/index.test.ts @@ -0,0 +1,140 @@ +import { execSync, spawnSync } from "child_process"; +import { resolve } from "path"; +import { fetch } from "undici"; +import { describe, it, onTestFinished } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +const basePath = resolve(__dirname, ".."); + +describe("'wrangler dev', when reading redirected config,", () => { + it("uses the generated config", async ({ expect }) => { + build("prod"); + const { ip, port, stop } = await runWranglerDev(basePath, [ + "--port=0", + "--inspector-port=0", + ]); + onTestFinished(async () => await stop?.()); + + // Note that the local protocol defaults to http + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(`"Generated: prod"`); + }); + + it("works when specifying the same environment via CLI arg to the one used in build", async ({ + expect, + }) => { + build("production"); + + const { ip, port, stop } = await runWranglerDev(basePath, [ + "--port=0", + "--inspector-port=0", + "--env=production", + ]); + onTestFinished(async () => await stop?.()); + + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(`"Generated: production"`); + }); + + it("errors when specifying a different environment via CLI arg to the one used in build", async ({ + expect, + }) => { + build("production"); + + const error = await runWranglerDev(basePath, [ + "--port=0", + "--inspector-port=0", + "--env=staging", + ]).then( + // it is doesn't error then stop the process + ({ stop }) => stop(), + (e) => e + ); + + expect(error).toMatch( + 'You have specified the environment "staging" via the `--env/-e` CLI argument.' + ); + expect(error).toMatch( + 'This does not match the target environment "production" that was used when building the application.' + ); + expect(error).toMatch( + 'Perhaps you need to re-run the custom build of the project with "staging" as the selected environment?' + ); + }); + + it("errors when specifying a different environment via CLOUDFLARE_ENV to the one used in build", async ({ + expect, + }) => { + build("production"); + + let error = ""; + try { + await runWranglerDev(basePath, ["--port=0", "--inspector-port=0"], { + CLOUDFLARE_ENV: "staging", + }); + } catch (e) { + error = e as string; + } + + expect(error).toMatch( + 'You have specified the environment "staging" via the CLOUDFLARE_ENV environment variable.' + ); + expect(error).toMatch( + 'This does not match the target environment "production" that was used when building the application.' + ); + expect(error).toMatch( + 'Perhaps you need to re-run the custom build of the project with "staging" as the selected environment?' + ); + }); + + it("uses a custom config from command line rather than generated config", async ({ + expect, + }) => { + const { ip, port, stop } = await runWranglerDev(basePath, [ + "-c=wrangler.jsonc", + "--port=0", + "--inspector-port=0", + ]); + onTestFinished(async () => await stop?.()); + + // Note that the local protocol defaults to http + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(`"Generated: false"`); + }); +}); + +describe("'wrangler deploy', when reading redirected config,", () => { + it("uses the generated config", ({ expect }) => { + build("prod"); + const output = spawnSync("pnpm", ["wrangler", "deploy", "--dry-run"], { + cwd: basePath, + stdio: "pipe", + shell: true, + encoding: "utf-8", + }); + expect(output.stdout).toContain(`Using redirected Wrangler configuration.`); + expect(output.stdout.replace(/\\/g, "/")).toContain( + ` - Configuration being used: "build/wrangler.json"` + ); + expect(output.stdout).toContain( + ` - Original user's configuration: "wrangler.jsonc"` + ); + expect(output.stdout.replace(/\\/g, "/")).toContain( + ` - Deploy configuration file: ".wrangler/deploy/config.json"` + ); + expect(output.stderr).toMatchInlineSnapshot(`""`); + }); +}); + +function build(env: string) { + execSync("node -r esbuild-register tools/build.ts", { + cwd: basePath, + env: { ...process.env, CLOUDFLARE_ENV: env }, + }); +} diff --git a/fixtures/redirected-config-worker/tools/build.ts b/fixtures/redirected-config-worker/tools/build.ts new file mode 100644 index 0000000..70f69cd --- /dev/null +++ b/fixtures/redirected-config-worker/tools/build.ts @@ -0,0 +1,24 @@ +import { copyFileSync, mkdirSync, rmSync, writeFileSync } from "fs"; + +// Create a pseudo build directory +rmSync("build", { recursive: true, force: true }); +mkdirSync("build"); +const config = { + name: "redirected-config-worker", + compatibility_date: "2024-12-01", + main: "index.js", + definedEnvironments: ["prod", "staging"], + targetEnvironment: process.env.CLOUDFLARE_ENV, + vars: { generated: process.env.CLOUDFLARE_ENV ?? "none" }, +}; +writeFileSync("build/wrangler.json", JSON.stringify(config, undefined, 2)); +copyFileSync("src/index.js", "build/index.js"); + +// Create the redirect file +rmSync(".wrangler/deploy", { recursive: true, force: true }); +mkdirSync(".wrangler/deploy", { recursive: true }); +const redirect = { configPath: "../../build/wrangler.json" }; +writeFileSync( + ".wrangler/deploy/config.json", + JSON.stringify(redirect, undefined, 2) +); diff --git a/fixtures/redirected-config-worker/tsconfig.json b/fixtures/redirected-config-worker/tsconfig.json new file mode 100644 index 0000000..7480e11 --- /dev/null +++ b/fixtures/redirected-config-worker/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/redirected-config-worker/turbo.json b/fixtures/redirected-config-worker/turbo.json new file mode 100644 index 0000000..3394ff5 --- /dev/null +++ b/fixtures/redirected-config-worker/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["build/**"] + } + } +} diff --git a/fixtures/redirected-config-worker/vitest.config.mts b/fixtures/redirected-config-worker/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/redirected-config-worker/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/redirected-config-worker/wrangler.jsonc b/fixtures/redirected-config-worker/wrangler.jsonc new file mode 100644 index 0000000..f63df02 --- /dev/null +++ b/fixtures/redirected-config-worker/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "name": "redirected-config-worker", + "compatibility_date": "2024-12-01", + "main": "src/index.js", + "env": { + "staging": { + "vars": { + "generated": "none", + }, + }, + "prod": { + "vars": { + "generated": "none", + }, + }, + }, +} diff --git a/fixtures/routing-app/functions/[default].js b/fixtures/routing-app/functions/[default].js new file mode 100644 index 0000000..671e2ed --- /dev/null +++ b/fixtures/routing-app/functions/[default].js @@ -0,0 +1,13 @@ +export async function onRequest(context) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/functions/_middleware.js b/fixtures/routing-app/functions/_middleware.js new file mode 100644 index 0000000..f68d175 --- /dev/null +++ b/fixtures/routing-app/functions/_middleware.js @@ -0,0 +1,15 @@ +const errorHandler = async ({ next }) => { + try { + return await next(); + } catch (err) { + return new Response(`${err.message}\n${err.stack}`, { status: 500 }); + } +}; + +const hello = async ({ next }) => { + const response = await next(); + response.headers.set("X-Hello", "Hello from functions Middleware!"); + return response; +}; + +export const onRequest = [errorHandler, hello]; diff --git a/fixtures/routing-app/functions/bar/[stuff].js b/fixtures/routing-app/functions/bar/[stuff].js new file mode 100644 index 0000000..671e2ed --- /dev/null +++ b/fixtures/routing-app/functions/bar/[stuff].js @@ -0,0 +1,13 @@ +export async function onRequest(context) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/functions/bar/baz/[[all]].js b/fixtures/routing-app/functions/bar/baz/[[all]].js new file mode 100644 index 0000000..671e2ed --- /dev/null +++ b/fixtures/routing-app/functions/bar/baz/[[all]].js @@ -0,0 +1,13 @@ +export async function onRequest(context) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/functions/bar/foo.js b/fixtures/routing-app/functions/bar/foo.js new file mode 100644 index 0000000..671e2ed --- /dev/null +++ b/fixtures/routing-app/functions/bar/foo.js @@ -0,0 +1,13 @@ +export async function onRequest(context) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/functions/blah.js b/fixtures/routing-app/functions/blah.js new file mode 100644 index 0000000..671e2ed --- /dev/null +++ b/fixtures/routing-app/functions/blah.js @@ -0,0 +1,13 @@ +export async function onRequest(context) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/functions/foo.js b/fixtures/routing-app/functions/foo.js new file mode 100644 index 0000000..671e2ed --- /dev/null +++ b/fixtures/routing-app/functions/foo.js @@ -0,0 +1,13 @@ +export async function onRequest(context) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/functions/index.ts b/fixtures/routing-app/functions/index.ts new file mode 100644 index 0000000..91121fe --- /dev/null +++ b/fixtures/routing-app/functions/index.ts @@ -0,0 +1,15 @@ +export async function onRequest( + context: EventContext> +) { + // Contents of context object + const { + request, // same as existing Worker API + env, // same as existing Worker API + params, // if filename includes [id] or [[path]] + waitUntil, // same as ctx.waitUntil in existing Worker API + next, // used for middleware or to fetch assets + data, // arbitrary space for passing data between middlewares + } = context; + + return new Response("Hello, world!"); +} diff --git a/fixtures/routing-app/package.json b/fixtures/routing-app/package.json new file mode 100644 index 0000000..79cdadc --- /dev/null +++ b/fixtures/routing-app/package.json @@ -0,0 +1,11 @@ +{ + "name": "@fixture/routing-basic", + "private": true, + "description": "routing-app for testing", + "scripts": { + "check:type": "tsc" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default" + } +} diff --git a/fixtures/routing-app/public/_routes.json b/fixtures/routing-app/public/_routes.json new file mode 100644 index 0000000..5d15c52 --- /dev/null +++ b/fixtures/routing-app/public/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/blah/"], + "exclude": ["/abc"] +} diff --git a/fixtures/routing-app/public/index.html b/fixtures/routing-app/public/index.html new file mode 100644 index 0000000..8c9051f --- /dev/null +++ b/fixtures/routing-app/public/index.html @@ -0,0 +1 @@ +

Hello world! 1

diff --git a/fixtures/routing-app/tsconfig.json b/fixtures/routing-app/tsconfig.json new file mode 100644 index 0000000..f8adbec --- /dev/null +++ b/fixtures/routing-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/rules-app/md.d.ts b/fixtures/rules-app/md.d.ts new file mode 100644 index 0000000..a47bd62 --- /dev/null +++ b/fixtures/rules-app/md.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const content: string; + export default content; +} diff --git a/fixtures/rules-app/package.json b/fixtures/rules-app/package.json new file mode 100644 index 0000000..654d1c2 --- /dev/null +++ b/fixtures/rules-app/package.json @@ -0,0 +1,14 @@ +{ + "name": "@fixture/rules-basic", + "private": true, + "description": "", + "keywords": [], + "license": "ISC", + "author": "", + "scripts": { + "check:type": "tsc" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default" + } +} diff --git a/fixtures/rules-app/src/content.md b/fixtures/rules-app/src/content.md new file mode 100644 index 0000000..2ef267e --- /dev/null +++ b/fixtures/rules-app/src/content.md @@ -0,0 +1 @@ +some content diff --git a/fixtures/rules-app/src/index.ts b/fixtures/rules-app/src/index.ts new file mode 100644 index 0000000..faddd04 --- /dev/null +++ b/fixtures/rules-app/src/index.ts @@ -0,0 +1,17 @@ +/** + * Welcome to Cloudflare Workers! This is your first worker. + * + * - Run `wrangler dev src/index.ts` in your terminal to start a development server + * - Open a browser tab at http://localhost:8787/ to see your worker in action + * - Run `wrangler deploy src/index.ts --name my-worker` to deploy your worker + * + * Learn more at https://developers.cloudflare.com/workers/ + */ + +import content from "./content.md"; + +export default { + async fetch(request: Request): Promise { + return new Response(`${request.url}: ${content}`); + }, +}; diff --git a/fixtures/rules-app/tsconfig.json b/fixtures/rules-app/tsconfig.json new file mode 100644 index 0000000..f8adbec --- /dev/null +++ b/fixtures/rules-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/rules-app/wrangler.jsonc b/fixtures/rules-app/wrangler.jsonc new file mode 100644 index 0000000..85f7388 --- /dev/null +++ b/fixtures/rules-app/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "rules-app", + "compatibility_date": "2022-02-28", + "main": "src/index.ts", + "rules": [ + { + "type": "Text", + "globs": ["**/*.md"], + "fallthrough": true, + }, + ], +} diff --git a/fixtures/secrets-store/package.json b/fixtures/secrets-store/package.json new file mode 100644 index 0000000..e84623e --- /dev/null +++ b/fixtures/secrets-store/package.json @@ -0,0 +1,12 @@ +{ + "name": "@fixture/secrets-store", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/secrets-store/src/index.ts b/fixtures/secrets-store/src/index.ts new file mode 100644 index 0000000..180d90c --- /dev/null +++ b/fixtures/secrets-store/src/index.ts @@ -0,0 +1,23 @@ +export interface Env { + SECRET: any; +} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + try { + const value = await env.SECRET.get(); + return new Response(value); + } catch (e) { + return new Response( + e instanceof Error ? e.message : "Something went wrong", + { + status: 404, + } + ); + } + }, +}; diff --git a/fixtures/secrets-store/tsconfig.json b/fixtures/secrets-store/tsconfig.json new file mode 100644 index 0000000..2431bac --- /dev/null +++ b/fixtures/secrets-store/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "types": ["@cloudflare/workers-types/experimental"], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/secrets-store/wrangler.jsonc b/fixtures/secrets-store/wrangler.jsonc new file mode 100644 index 0000000..b014452 --- /dev/null +++ b/fixtures/secrets-store/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "compatibility_date": "2025-01-01", + "main": "src/index.ts", + "name": "secret-store", + "secrets_store_secrets": [ + { + "binding": "SECRET", + "secret_name": "secret_name", + "store_id": "a3fb907f446e4d94bd946d7ea6365b1c", + }, + ], +} diff --git a/fixtures/shared/package.json b/fixtures/shared/package.json new file mode 100644 index 0000000..d1e3d80 --- /dev/null +++ b/fixtures/shared/package.json @@ -0,0 +1,9 @@ +{ + "name": "@fixture/shared", + "private": true, + "description": "Shared fixtures for testing", + "devDependencies": { + "@cloudflare/workers-utils": "workspace:*", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/shared/src/fs-helpers.ts b/fixtures/shared/src/fs-helpers.ts new file mode 100644 index 0000000..e23ce71 --- /dev/null +++ b/fixtures/shared/src/fs-helpers.ts @@ -0,0 +1,3 @@ +// Re-exported so that fixtures can import from `@fixture/shared/src/fs-helpers` +// without each fixture needing a direct dependency on `@cloudflare/workers-utils`. +export { removeDir, removeDirSync } from "@cloudflare/workers-utils"; diff --git a/fixtures/shared/src/mock-console.ts b/fixtures/shared/src/mock-console.ts new file mode 100644 index 0000000..85283e4 --- /dev/null +++ b/fixtures/shared/src/mock-console.ts @@ -0,0 +1,60 @@ +import * as util from "node:util"; +import { afterEach, beforeEach, vi } from "vitest"; +import type { MockInstance } from "vitest"; + +/** + * We use this module to mock console methods, and optionally + * assert on the values they're called with in our tests. + */ + +let debugSpy: MockInstance, + logSpy: MockInstance, + infoSpy: MockInstance, + errorSpy: MockInstance, + warnSpy: MockInstance; + +const std = { + get debug() { + return normalizeOutput(debugSpy); + }, + get out() { + return normalizeOutput(logSpy); + }, + get info() { + return normalizeOutput(infoSpy); + }, + get err() { + return normalizeOutput(errorSpy); + }, + get warn() { + return normalizeOutput(warnSpy); + }, +}; + +function normalizeOutput(spy: MockInstance, join = "\n"): string { + return captureCalls(spy, join); +} + +function captureCalls(spy: MockInstance, join = "\n"): string { + return spy.mock.calls + .map((args: unknown[]) => util.format("%s", ...args)) + .join(join); +} + +export function mockConsoleMethods() { + beforeEach(() => { + debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}); + logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + infoSpy = vi.spyOn(console, "info").mockImplementation(() => {}); + errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + afterEach(() => { + debugSpy.mockRestore(); + logSpy.mockRestore(); + infoSpy.mockRestore(); + errorSpy.mockRestore(); + warnSpy.mockRestore(); + }); + return std; +} diff --git a/fixtures/shared/src/mock-postgres-server.ts b/fixtures/shared/src/mock-postgres-server.ts new file mode 100644 index 0000000..511ac19 --- /dev/null +++ b/fixtures/shared/src/mock-postgres-server.ts @@ -0,0 +1,212 @@ +import assert from "node:assert"; +import events from "node:events"; +import net from "node:net"; +import util from "node:util"; + +/** + * The 8-byte Postgres SSLRequest packet. + * Length=8, Code=80877103 (0x04D2162F). + */ +export const POSTGRES_SSL_REQUEST_PACKET = Buffer.from([ + 0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x2f, +]); + +// ── Postgres wire protocol helper functions ────────────────────────────────── + +/** Build a single Postgres backend message: type (1 byte) + int32 length + payload. */ +function pgMsg(type: string, payload: Buffer): Buffer { + const len = 4 + payload.length; // length field includes itself but not the type byte + const buf = Buffer.alloc(1 + 4 + payload.length); + buf.write(type, 0, 1, "ascii"); + buf.writeInt32BE(len, 1); + payload.copy(buf, 5); + return buf; +} + +/** AuthenticationOk — type 'R', auth status 0. */ +function authenticationOk(): Buffer { + const payload = Buffer.alloc(4); + payload.writeInt32BE(0, 0); // auth type 0 = OK + return pgMsg("R", payload); +} + +/** ParameterStatus — type 'S', name + value (both null-terminated). */ +function parameterStatus(name: string, value: string): Buffer { + const payload = Buffer.from(`${name}\0${value}\0`, "utf8"); + return pgMsg("S", payload); +} + +/** BackendKeyData — type 'K', process ID + secret key. */ +function backendKeyData(pid: number, key: number): Buffer { + const payload = Buffer.alloc(8); + payload.writeInt32BE(pid, 0); + payload.writeInt32BE(key, 4); + return pgMsg("K", payload); +} + +/** ReadyForQuery — type 'Z', transaction status indicator. */ +function readyForQuery(status: "I" | "T" | "E" = "I"): Buffer { + return pgMsg("Z", Buffer.from(status, "ascii")); +} + +/** RowDescription — type 'T', field definitions for one or more columns. */ +function rowDescription(columns: string[]): Buffer { + // int16 field count, then for each field: + // name (null-terminated), table OID (int32), column attr (int16), + // type OID (int32), type size (int16), type modifier (int32), format code (int16) + const parts: Buffer[] = []; + const fieldCount = Buffer.alloc(2); + fieldCount.writeInt16BE(columns.length, 0); + parts.push(fieldCount); + + for (const col of columns) { + const name = Buffer.from(`${col}\0`, "utf8"); + const meta = Buffer.alloc(18); + meta.writeInt32BE(0, 0); // table OID + meta.writeInt16BE(0, 4); // column attribute number + meta.writeInt32BE(25, 6); // type OID: 25 = text + meta.writeInt16BE(-1, 10); // type size: -1 = variable length + meta.writeInt32BE(-1, 12); // type modifier + meta.writeInt16BE(0, 16); // format code: 0 = text + parts.push(name, meta); + } + + return pgMsg("T", Buffer.concat(parts)); +} + +/** DataRow — type 'D', column values as text. */ +function dataRow(values: string[]): Buffer { + const parts: Buffer[] = []; + const colCount = Buffer.alloc(2); + colCount.writeInt16BE(values.length, 0); + parts.push(colCount); + + for (const val of values) { + const valBuf = Buffer.from(val, "utf8"); + const lenBuf = Buffer.alloc(4); + lenBuf.writeInt32BE(valBuf.length, 0); + parts.push(lenBuf, valBuf); + } + + return pgMsg("D", Buffer.concat(parts)); +} + +/** CommandComplete — type 'C', command tag (null-terminated). */ +function commandComplete(tag: string): Buffer { + return pgMsg("C", Buffer.from(`${tag}\0`, "utf8")); +} + +// ── Mock server ────────────────────────────────────────────────────────────── + +export interface MockPgServerOptions { + /** Canned rows to return for any query. Defaults to [{ id: "1" }]. */ + rows?: Record[]; +} + +/** + * Creates a minimal mock Postgres server that speaks enough of the wire protocol + * for the `pg` (node-postgres) library to successfully connect, authenticate, + * and execute simple queries. + * + * Handles: SSLRequest, StartupMessage, SimpleQuery (Q), Terminate (X). + * Returns canned result rows for any query. + */ +export async function createMockPostgresServer( + options?: MockPgServerOptions +): Promise<{ server: net.Server; port: number; stop: () => Promise }> { + const rows = options?.rows ?? [{ id: "1" }]; + + const server = net.createServer((socket) => { + let startupHandled = false; + + socket.on("data", (chunk: Buffer) => { + // 1. SSLRequest — respond with 'N' (no SSL) + if (POSTGRES_SSL_REQUEST_PACKET.equals(chunk)) { + socket.write("N"); + return; + } + + // 2. StartupMessage — no type byte prefix; starts with int32 length + int32 version (196608 = 3.0) + if (!startupHandled) { + // Validate it looks like a StartupMessage (version 3.0) + if (chunk.length >= 8) { + const version = chunk.readInt32BE(4); + if (version === 196608) { + startupHandled = true; + socket.write( + Buffer.concat([ + authenticationOk(), + parameterStatus("server_version", "15.0"), + parameterStatus("client_encoding", "UTF8"), + backendKeyData(1, 1), + readyForQuery("I"), + ]) + ); + return; + } + } + } + + // 3. Parse typed messages (first byte = type, next 4 bytes = length) + let offset = 0; + while (offset < chunk.length) { + const typeByte = chunk[offset]; + if (typeByte === undefined || offset + 5 > chunk.length) { + break; + } + const msgType = String.fromCharCode(typeByte); + const msgLen = chunk.readInt32BE(offset + 1); + // Total message size = 1 (type) + msgLen + const msgEnd = offset + 1 + msgLen; + + switch (msgType) { + case "Q": { + // SimpleQuery — return canned rows + const firstRow = rows[0]; + const columns = firstRow ? Object.keys(firstRow) : []; + const parts: Buffer[] = [rowDescription(columns)]; + + for (const row of rows) { + parts.push(dataRow(columns.map((col) => row[col] ?? ""))); + } + + parts.push(commandComplete(`SELECT ${rows.length}`)); + parts.push(readyForQuery("I")); + socket.write(Buffer.concat(parts)); + break; + } + case "X": { + // Terminate + socket.end(); + return; + } + default: { + // Unknown message — ignore + break; + } + } + + offset = msgEnd; + } + }); + + // Prevent socket errors (e.g. ECONNRESET from abrupt client disconnects) + // from becoming uncaught exceptions — Node.js EventEmitters throw if an + // 'error' event fires with no listener attached. + socket.on("error", () => {}); + }); + + const listeningPromise = events.once(server, "listening"); + server.listen(0, "127.0.0.1"); + await listeningPromise; + + const address = server.address(); + assert(typeof address === "object" && address !== null); + const port = address.port; + + const stop = async () => { + await util.promisify(server.close.bind(server))(); + }; + + return { server, port, stop }; +} diff --git a/fixtures/shared/src/run-wrangler-long-lived.ts b/fixtures/shared/src/run-wrangler-long-lived.ts new file mode 100644 index 0000000..8e4c8e1 --- /dev/null +++ b/fixtures/shared/src/run-wrangler-long-lived.ts @@ -0,0 +1,296 @@ +import assert from "node:assert"; +import { fork } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { createConnection } from "node:net"; +import os from "node:os"; +import path from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import treeKill from "tree-kill"; + +export const wranglerEntryPath = path.resolve( + __dirname, + "../../../packages/wrangler/bin/wrangler.js" +); + +/** + * Returns a per-process temporary directory for the dev registry. + * + * When multiple fixture tests run in parallel (turbo `--concurrency=2`), each + * fixture's vitest process gets its own isolated registry directory. This + * prevents cross-fixture leakage — e.g. the local-explorer in one fixture + * picking up workers registered by a completely unrelated fixture. + * + * Within a single fixture process, all `runWranglerDev` / `runWranglerPagesDev` + * calls share the same registry so multi-worker setups (service bindings, + * durable objects across workers) continue to work. + * + * See `packages/miniflare/src/shared/DEV_REGISTRY.md` for registry architecture. + */ +let isolatedRegistryPath: string | undefined; +function getIsolatedRegistryPath(): string { + if (isolatedRegistryPath === undefined) { + isolatedRegistryPath = mkdtempSync( + path.join(os.tmpdir(), "wrangler-fixture-registry-") + ); + process.on("exit", () => { + try { + rmSync(isolatedRegistryPath!, { recursive: true, force: true }); + } catch { + // Best-effort cleanup; the OS will reap tmp dirs eventually. + } + }); + } + return isolatedRegistryPath; +} + +/** + * Runs the command `wrangler pages dev` in a child process. + * + * Returns an object that gives you access to: + * + * - `ip` and `port` of the http-server hosting the pages project + * - `stop()` function that will close down the server. + */ +export async function runWranglerPagesDev( + cwd: string, + publicPath: string | undefined, + options: string[], + env?: NodeJS.ProcessEnv +) { + if (publicPath) { + return runLongLivedWrangler( + ["pages", "dev", publicPath, "--ip=127.0.0.1", ...options], + cwd, + env + ); + } else { + return runLongLivedWrangler( + ["pages", "dev", "--ip=127.0.0.1", ...options], + cwd, + env + ); + } +} + +/** + * Runs the command `wrangler dev` in a child process. + * + * Returns an object that gives you access to: + * + * - `ip` and `port` of the http-server hosting the pages project + * - `stop()` function that will close down the server. + */ +export async function runWranglerDev( + cwd: string, + options: string[], + env?: NodeJS.ProcessEnv +) { + return runLongLivedWrangler(["dev", "--ip=127.0.0.1", ...options], cwd, env); +} + +async function runLongLivedWrangler( + command: string[], + cwd: string, + env?: NodeJS.ProcessEnv +) { + let settledReadyPromise = false; + let resolveReadyPromise: (value: { ip: string; port: number }) => void; + let rejectReadyPromise: (reason: unknown) => void; + let processExited = false; + let stopping = false; + + const ready = new Promise<{ ip: string; port: number }>((resolve, reject) => { + resolveReadyPromise = resolve; + rejectReadyPromise = reject; + }); + + // Ensure each fixture process uses an isolated dev registry unless the + // caller (or the environment) has already specified one explicitly. + const registryEnv: NodeJS.ProcessEnv = {}; + if (!env?.WRANGLER_REGISTRY_PATH && !process.env.WRANGLER_REGISTRY_PATH) { + registryEnv.WRANGLER_REGISTRY_PATH = getIsolatedRegistryPath(); + } + if (!env?.MINIFLARE_REGISTRY_PATH && !process.env.MINIFLARE_REGISTRY_PATH) { + registryEnv.MINIFLARE_REGISTRY_PATH = getIsolatedRegistryPath(); + } + + const wranglerProcess = fork(wranglerEntryPath, command, { + stdio: [/*stdin*/ "ignore", /*stdout*/ "pipe", /*stderr*/ "pipe", "ipc"], + cwd, + env: { ...process.env, ...registryEnv, ...env, PWD: cwd }, + }).on("message", (message) => { + if (settledReadyPromise) { + return; + } + settledReadyPromise = true; + clearTimeout(timeoutHandle); + resolveReadyPromise(JSON.parse(message.toString())); + }); + + const chunks: Buffer[] = []; + wranglerProcess.stdout?.on("data", (chunk) => { + if (process.env.WRANGLER_LOG === "debug") { + console.log(`[${command}]`, chunk.toString()); + } + chunks.push(chunk); + }); + wranglerProcess.stderr?.on("data", (chunk) => { + if (process.env.WRANGLER_LOG === "debug") { + console.log(`[${command}]`, chunk.toString()); + } + chunks.push(chunk); + }); + const getOutput = () => Buffer.concat(chunks).toString(); + const clearOutput = () => (chunks.length = 0); + + wranglerProcess.once("exit", (exitCode, signal) => { + processExited = true; + if (!settledReadyPromise) { + settledReadyPromise = true; + rejectReadyPromise( + `Wrangler exited with error code: ${exitCode}\nOutput: ${getOutput()}` + ); + return; + } + if (stopping) { + // Exit was triggered by `stop()` (normal test teardown). No diagnostic + // needed — the tests are already done. + return; + } + // The process exited *after* we sent back a ready signal — any pending + // tests will now see ECONNREFUSED. Dump the captured output so CI logs + // contain the diagnostic info needed to understand what happened. + const separator = "=".repeat(80); + console.error( + [ + `Wrangler process exited unexpectedly after startup (code=${exitCode}, signal=${signal})`, + `Command: ${command.join(" ")}`, + separator, + getOutput(), + separator, + ].join("\n") + ); + }); + + const timeoutHandle = setTimeout(() => { + if (settledReadyPromise) { + return; + } + settledReadyPromise = true; + const separator = "=".repeat(80); + const message = [ + "Timed out starting long-lived Wrangler:", + separator, + getOutput(), + separator, + ].join("\n"); + rejectReadyPromise(new Error(message)); + }, 50_000); + + async function stop() { + stopping = true; + return new Promise((resolve) => { + if (processExited) { + // Already dead — nothing to kill. Avoid noisy Windows taskkill errors. + resolve(); + return; + } + assert( + wranglerProcess.pid, + `Command "${command.join(" ")}" had no process id` + ); + treeKill(wranglerProcess.pid, (e) => { + if (e) { + console.error( + "Failed to kill command: " + command.join(" "), + wranglerProcess.pid, + e + ); + } + // fallthrough to resolve() because either the process is already dead + // or don't have permission to kill it or some other reason? + // either way, there is nothing we can do and we don't want to fail the test because of this + resolve(); + }); + }); + } + + const { ip, port } = await ready; + + // Close the race between Wrangler's IPC "ready" message and the TCP socket + // actually accepting connections. Without this, tests can run immediately + // after `beforeAll` returns and hit ECONNREFUSED — especially under CI load + // when fixtures run in parallel (see ci-flake label for prior art). If the + // server never starts listening, surface a clear error with captured output + // instead of letting every test in the suite fail with ECONNREFUSED. + await waitForServerListening(ip, port, getOutput, () => processExited, stop); + + return { ip, port, stop, getOutput, clearOutput }; +} + +/** + * Polls `ip:port` until it accepts a TCP connection, giving up after + * `totalTimeoutMs`. Throws with a diagnostic message (including the captured + * Wrangler output) if the server never starts listening or the process exits. + */ +async function waitForServerListening( + ip: string, + port: number, + getOutput: () => string, + hasExited: () => boolean, + stop: () => Promise, + totalTimeoutMs = 10_000 +) { + const deadline = Date.now() + totalTimeoutMs; + let lastError: unknown; + while (Date.now() < deadline) { + if (hasExited()) { + break; + } + try { + await tryConnect(ip, port); + return; + } catch (e) { + lastError = e; + await delay(100); + } + } + await stop(); + const separator = "=".repeat(80); + throw new Error( + [ + `Wrangler reported ready on ${ip}:${port} but the server is not accepting connections (last error: ${lastError instanceof Error ? lastError.message : String(lastError)})`, + separator, + getOutput(), + separator, + ].join("\n") + ); +} + +function tryConnect(ip: string, port: number, timeoutMs = 500): Promise { + return new Promise((resolve, reject) => { + const socket = createConnection({ host: ip, port }); + const onError = (err: Error) => { + cleanup(); + reject(err); + }; + const onConnect = () => { + cleanup(); + resolve(); + }; + const onTimeout = () => { + cleanup(); + reject(new Error("TCP connect timed out")); + }; + const cleanup = () => { + socket.removeListener("error", onError); + socket.removeListener("connect", onConnect); + socket.removeListener("timeout", onTimeout); + socket.destroy(); + }; + socket.setTimeout(timeoutMs); + socket.once("error", onError); + socket.once("connect", onConnect); + socket.once("timeout", onTimeout); + }); +} diff --git a/fixtures/sites-app/package.json b/fixtures/sites-app/package.json new file mode 100644 index 0000000..42227f2 --- /dev/null +++ b/fixtures/sites-app/package.json @@ -0,0 +1,12 @@ +{ + "name": "@fixture/sites-basic", + "private": true, + "description": "", + "keywords": [], + "license": "ISC", + "author": "", + "main": "index.js", + "devDependencies": { + "@cloudflare/kv-asset-handler": "workspace:*" + } +} diff --git a/fixtures/sites-app/public/404.html b/fixtures/sites-app/public/404.html new file mode 100644 index 0000000..5ac9921 --- /dev/null +++ b/fixtures/sites-app/public/404.html @@ -0,0 +1,50 @@ + + + + + + + + + +
+

404 Not Found

+

Oh dang! We couldn't find that page.

+ a sad crab is unable to unable to lasso a paper airplane. 404 not found. +
+ + diff --git a/fixtures/sites-app/public/favicon.ico b/fixtures/sites-app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cc6c23bd37bb4fa58f810c5f9e36041d96b23462 GIT binary patch literal 1150 zcmXw&dr(wW9LJA6klCG0Y`fdtcD^W zZ;*%(!UqcotRgi|$B@@&|J#+okFh*l9>gNc5&sPx{583mU`;2{d&-7lcYdrHJ zV6g+cdI9tI3XjTu1YOoNblCzTvxM=;eZu<4eJ-{zQwSS$UpSd=@030w<3Vp+juRO@ zL9RJ=p9em-bEK6D+>qyj;VU=vxdL=KLJy%QSXABXQmS|xl??P zJ@7Pt0LZ-pWRqv}2_KI>X@^vI zWC%*ZH6Z^7?JS=1#1apWEAk-but)QRjD;RcQk6F;q1-dJti+9%zMe&N0Xe>WTv<9C ziZ1^JLq$V?^(WvUd3Ll&e918{;)}hAEAk@tuxD&#y;!xP#;Yb)dhtrB8}UW6lQD(U zsUO)SRzHfJ#4(kINyxrds67%0w(J347EvjXqa$gNRq_L{KiET#i=)tk@A^ z%{O7F8^u7yuR!StaO`K`gR=@xs1&ihLd47DdF`a%mD=RP>Ti5lRZpH;@%ZvG7poan znQa1wrkfb3{~f5l4pfro!!Cs<)%uWB<3nPVh_p7T>h)H?y0+PmlyAw?AWkNq5L5PC z4YH0~Ft?6FZu$dgya5~?QMswb&yySdSk>UCoPFw>XQZU`)6y7uu&%|A)bD(tJymqx z=F#1EFnD$XX!{dr9Roh;5Ad}oC1|8H`LVjuKW4JYY`smkywxs;(oadCT#(xAcb1KY zYWx0za_$b$c^jy?q4F&@nZI#brkrK0Yn4Lev25s)8DS(GlY z-2?1*>Hl8~a8suYQ-=ati{{A=%GtQ7M`3TDBf1qcA!%b-!vWRt2>8(n4Ezl=PpJI; z3ku(44`5@Df(>1YL>@D>;rm1TXM{fOl+?SuL&Ev{D!)Drj64Pgr|2Fzg8ZF}0c`6J zV(WzfHroR|w5EPz_gs98RbVUYM7E^dYswo8jQ>WlTKwn?V7nXQJNgxDzZ?wn?xo;3 mxlH6@Tl)fR@>#})7R+I#Y&5j;3=uJp_O2D{Jpa$vV*dkKub%Ay literal 0 HcmV?d00001 diff --git a/fixtures/sites-app/public/img/200-wrangler-ferris.gif b/fixtures/sites-app/public/img/200-wrangler-ferris.gif new file mode 100644 index 0000000000000000000000000000000000000000..8853751fac7b39559f140971700360fe249fcdc5 GIT binary patch literal 45598 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 Os#2HA)TVYVAOJf?`|0KY literal 0 HcmV?d00001 diff --git a/fixtures/sites-app/public/img/404-wrangler-ferris.gif b/fixtures/sites-app/public/img/404-wrangler-ferris.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ac1479fcf6f4ded732b8b8ea7bfd9f98b5ca3e2 GIT binary patch literal 31606 zcmV)5K*_&HNk%w1VWkA10{8y_=l1=o-ue5+toWL9@@{3aOD6sF@A7SF^?xe;_VkC* z_S&hK^n5h^-OurAa{7;8`MIcvhKEYJ@^QqHZz2G1nyve?jl5t$le2aG(!cu5xqHm@ z)z8ZNyq``^PUb}b^ZNd_fhD)2lk|Qr^m#w>Z(#CrR`Ym6@@``EdOY-paP^Bz_Lfz-H2|!Xi}Q9! z!ER92kv#HoTHEpb^L0)2hC1?aUi5r6^K?=4h=r!9W|U_}nVF&YnoG~@`|X7(^LIt} zSO@iiBVxn!_okZgN&!f->66y?W~Rx@h;fRw$n}6I{pQ=1E&#}OGWx)f_G%LNtB0w? z(5H-f{q5_Y*R}8O@AG(T*g*i{+0x^@uh~--{_o{gy6oqOF;}V7{p8~Kpj-Ug*gO*e z`_s$vaag)qD)_E`_^4{FE&zg|xhNag{g}Q>khJdr9T4KBB{MNql zY;5_ym*RaW{pZ*5X>|5t4fB40=X7WHrgC`3?2yUj#@Xci(b48m47;CH`LvOQ&GPeg zOZ(5R_^+e#a#-_rN{7tuYr^jKkV5l$Kl#7B^n74>z>DjZbmxRV{NKp(Z(sMIh4q3U ziDpdpi7@qj9+=hhKDF_sD*)e&qybur&sx$zfgKp&D-pp7bT)^_R;Kucto!Rf= z?fCKDk9Fhh@Q={&>1i;;s%PVzU~WS&<9AGp!Qtfe>f7e(q*O5UfKa+b5VCVx@#*L6 z^Y`~m0RR90A^8LW00930EC2ui0Hp+=0ssjA0RIUbNU)&6g9sBUT*$DY!-o(fN}Ncs zqQ#3CGiuz(v7^V2AVZ2ANwTELlPFWFT*({Vj%brcUw(Z-vbL-yCySMM(z=I1PPQ1ABYHsj|o4k0kB$EK#Y3QNcshB38TRI6SoP3gi=%kcFm*=323Uh`rfVv4KpEg!1 z>Zru+sAZ;1D$r)9W~`EFHl)V?Dl2YCDv2tees=2VHknR2>#xAFHU*Ej^2wzHy1EHw zreX%`?6aULS}db%ChI1ona&CAw%k${BMEy>%clb9SnDP$QKHIXx9qlySToArh)fve&QdGpXj2bJf_ zI*(iK%LUI{%(_KKEj3N>%9}K!jxr51z5&xb_19pZ1n||Fa;f#BXF!Z_*lf4$k;7!) zTJ71&vRtU!c;_7v$8q2Pnk(HRvs|*?gcokmut@i9Y}SCI%W|lOM~-*SilcglBY*?i zs?I`7?)lW;CSLitt7iTtuR8ns`RX=TJ^HkNn;z)ZUbAld!boz{@#~BpEBm$YT&?Zx z#PbU`@5RpjJ1?xneRuKDKYOOF$QP>s$jkp6_rB40FRRK-?p}SLJug2A>Q_VV{rZSf zKEB)oFAwMjeHU{v`}mW2^!wsUEdQehBb(~-M?fOM>tX!!8}|Ok3rC1gXSFM!1WlB? z2PUv(<@=um6i~V0P4I&vI^G4Rr91|@X$-rtAj_Zz!WL?1dMX@Y#Fl14H{EM=`f{NT zFN8Q528?kvJl*U6w2`nK8nHp{^I@^f7Q{|fY;^ZKq7(}hIwc0oe@%3g1cJxA!A
uC&F**JbTh&`zBsW=}7+H;y7-XcbjFG_Saq^Vo85<=_ zC`w*PfNSsbA}VJ|o+*$ZhVQ#xD|L0g1gejgz#LCDz%{}+X0VkKpkD&-7|do0GfU&! z)Gm|t2red&h0e^Tc+P}CvMq9&Zj#vu)u_#Key3JsDmKJ2?J$V7k5pvI;2ptdjQbogoUQ3V{{AEHX8lG$f;Ftq7hC2rt zMT&a#oDS{OBkHKc0ep>v zVNI)Ww#vYQ@{yTpjq7m6D!)fGm6C*%t6qu2R@wbR4c1KLBlk+!;LufeXsAF1)bNQ! zYO=79?ac<{_Y1{ZRuT1h<5eTeS=)F)ewGbw1r(tMea5o0sMSsUGWQ43x>gOML!Lxa zOWWB0eAaYkaII@W@sA|E53aQh?rTzhgY1L(pd|>p_T7PK%hH5p_Rorj!X#nD_Z}Cq``Z#X^w|XnFV$V#-m-Z zNv3dLB0KppJ5E+^Nvz~2TUj?o<^_V)fZZ#588=Y|l!ayd-o>B0f8~ZGzl^T`p~60gPRnfTtqv1G$iQM7@GX( zN?!&98I%B}EzRl3M0%8+4)tZcfIUr%`qY&vhMNS8>Q*~u3TJS&tY=N@3l-xLH?Fm= z_kw9R;dQj` z*-gcOO0U)Z)qqP|(F7w1!~tb)rW~9X*LHZbL7;#s4BT1-S*-#Jq5y#R#K;Bz*Eq7< zB!MZE4rGr=xvp5K>TV-N-@4+r%4@CENlV}nUI4(q4?+S3NFe1nUv$NT0)hZwFXKB0 zHd~9mbcC3axEn}{7gUT zBuSGOo43dmdUp7ofZgPrKeWdKFgUY|K7(>^3311edRdblP-E!(MkWp@sNdwUv9GUH^SY6F21^A$6zlZW9(wL8tIooCMV<09VU{v}SYWyb>0B}q? z#}e`5c+Yo$e|A_S0d}Sq6Coyc5-4iCFa{b`4Ctp3A2&ZTL0t5=fv4sLw-kI6!F}zv zd?68H-iLyfW_RsY63-@fB+zm%v0pGqgA&#NLf8N&K!ijn0384UNeBT8zy(hDgb)yg zQW%9AZ~|6%g;vN06u~CtH*lB75;6#c1U7_}&;e#h3ImXa1MmRJa0JN!25=aM!7zt( zSOZT`19tcXs-Ox_@C2q{3Ro}6*X1BjT2h=>J@_ydmrhynwE0w`b$Q+S1d z@DD>Ge}JNWDxrS5@`GZCRYF*X9S{dQFaS$H1gfZtjnD`)@QScF1If^aZWxDhNQZNn zhke+GrjQ0)pa_59CWbhShp32&$cR|rh%xX3J+O?-*aOhm14b~7M(_iiU@f3Z%V2Ms`qJFo+(m;~sU1VnHQ$FPd-NCd4Ii?DbE^jM3w2nM*Ai@Ugss*o(h zpbErTjK&xP$moa$X^_zfjnqhuT%apv5D6=TiXp8w647#|FXrL=j5Rd|C zj0Jg+2$_%zX#|B(2tVm2XfO;C8I@8HkyE(>D^LhofC8vs0)n8C7cqhg5RxJ(PYFN^ zCfNx)a0M!P1!}pHDhUK|8J91ajxsru@aT#*iIZ=5k2~3ueW(fz*(r)3lvFT~i`a-p z>4*s_jY(+)!|)5dzy((+m6U0fmU)?0Ui`kexPzaF_2~to9 zQVEq(d6|}(nbcVe)>#BaumYQa0w&-CUU?9smnRr@5Xg$^h)Snl8$kY#E=h8Jn|d zpYMpD^H`62sgwNKpLnQ;RM3ZnS(pb}oJM(^3z`Z_x|E!t2sQ+r6pEP^ilG^bog8|l z97+Ip01WkzEGKB9sFVPdfSxy?0xD1q)t~|@x}q=oo-rz;EJ>pMMSO4yhp+@g(5h)_rfRyTY}%$U>Y8vmpEX*WGwBLyxC~}! zh8i%0LO2nJwS07dg@PaeQiuwYhyqHmphobhQb4M@q6n9YshY~E8S1H7N&pA&0>coD zZo&lz01T)Q3SD-pUzDnc&Y5Zjpbndm z37en}J|MDHBBt$n12@pBF&nS)3ahdITCcR)o^L9nNl*plXoi%~09ptWY{iKvp;Ts| z2u`q^n7RVlDzOt=1fEI&il7J<>#dc*1)Sim-s-U)3k)XknIlUSf^f2#aJFa*114Z4 zd>{aUfB*;pvvLcwG+VQ$NZ-0xLVVLE!^vd%CBa1!DUmm|y{|um)=Y zw{bhSbW5}Idb6_HrY_L8mJqZ;fh009qMP^+V=x9jl>mZ(ur`ndQJbY1+bK?PwHBMX zpIg0~TL~%~qNKYMrke>4@V(&w>j0+<13tjIu8RP$E4y$@yR{pyw~M#z>9eFD0JOle zcOkqZr&AI@umV8`f?xs)d!>O1y)PiW)N8$7TL}uRz*#W5+iMf18@>|!y{G#MC(^d9 zzyqU^`HnQzzPm2y3Q>Ud5&+!y2+>f$ zn(MhXjKewn0xuA{tINYGkp)7$#^OuFAHoNgkPAqx!RfoiOw7LVssdGj2vM9BJ5mgr zVplPdxSc{@&sB8gqiMJQb$N6^2qpjw7YoJ+EXE2<#+Lj74UEPsAp&gN$!p6Yw7>{S zyauDJ#CF`oOK=FM5V&1&as9`9_ttAiw*kd4Xt$Rv?p6%nC=i113N`=(lKi=pTnU$K z$qt~lC#=aHAqe360uJEJ(EQBO9L>)Bz2%!Bv=9h$yv=lM%1bP_)zAv6%*tSqfHJXz zrAozTV1v-+5P;yzlT69Qe96Zw0@|z0AdvttFwOi-&HX&hp>QGD90WnY&2tR9-ps)_ z&>zv) z2n0>g23^W_EY6hwuolT;6D05_A#hl}_792MkrzP+K41gaE5;~o%$b19B)t(LaMVsc z&zmsP2a*79@X{|G)7`ATtw0Kx;1d<8gU;<`)x^Ar31|bN3UD{+k*9(#W7H|ti_-&@T>T--B$w$@D~XMjB^x8nmr;R6xh z5q@_Su0_*&`!{e5h1Vv ztKv21P7t@J06{(%KG5v3o)pLa?EG%SHZULwkPEj^?RlOHr!E-Ey(uhy7fo%<=N;~6 z>*gMU#c@XvTlLBvQGU6ea|GdHzNZ)a?&(>e>_s66chK)BU%dqI;R5mo15fY)-UWUC z?jNKLJR%Xg6gn-I)4d%oggON;1Hkh9a75$0e70$ z6D06)2L*RPJr|H{`klY|Ss?g2;RAPI{LbI`sDBG9VEVYfAFgls+npFI-cG9jP8YAx z_{JUX%Kt(Gr+b(-{68UdmY+fP?G=LX{KjtsR8JELU<2@f{}!7HL2wIe01yNO4kReh z#y^A!6)t4h(BVUf5hYHfSkdA|j2Sg<; z`ncoO>{)`Tf`BA)76~x1aplgX%MzqWWh6`H;#1czQL4QD4(=!dr_P){du}4!*zse? z6OB=o03vcm08}4YzT6lP5+rCv54>X{tY1LDLXT!m(8yYlG0>vuT-)~lZHZk%jx1TS z%W2%f|9R0YyfYodI~8YAXWaR7=oF_&pt>P?!w`>Nl6d-v`}Lt@T8}3~hi(EgpvRAY z4HBdjw{XGFr$1D{?!3KAw!&2ZF>$7h@y{=Hl5=3WOiUPXK?Vn#?xiuNV6Yeo1XO4; zUKHT0L5>m-2s98w6mdinMKnkdu@W$$ECNb&@kJ7AS;M_s)_@U24s+D;Ap>&RO)vd& zIcdkZK=7iNGmII-IwFlqKm-F1Tn@!1tF$sD0FBvjK>)T4fT;+tr0A6!%e=9cOd{~nvLTu>tR1oQ`^9ejLb=9^mc+*deAZiuzRa_r?GSS3% zN-T?9gI%x~ULKK<(4_(t7LZSyb@o|kqm>p~(2B4k3ljig2@q72nD$$6!F8cdka`IB z(?Nw*H!ePM$#2$L;|+wCb?c4F2N-zmH4`aCu=ii#Ds@SM5C|oKx`82e)r4~!b{N__ zP%uG|bSFxH5HdDd0mO$phBgUuL2{T%fJ-h46L)8&HQrizV5BdTAp+spm?83E6MdE2 z$%mSICJY4am@0-)5HO?}p)(ZD69tc(emG)OOdv!MK}P-mhYqS=F#!c{i>Rd*r^7Dd ziI8tmVPv1vW(j4LS#FsJm+4g-ApplvpqQg;zWD{d595{JUag*c@T1NMY_md(DOjPY zjBQkG$Rq!HQzRmHx`(oFNy6b*2=_cAw)Jy62)IiRWXQCMRkmgoH^HgzoDGEGbJ~dl z0K4HhD`a5{ ztuU_4-J*FbxWflOh>0$&jAaaLpaa)X1x8Rpf>eRZx)R_6byT7f8O)%8L{K0DWkOpv zq@w4hV#4}W$pkClVi&#mMKFd@jAJBY8O?Y`G?FnGF}wu=(#S=@Rgrpc$i)JA=tBhL z5dlCHqP+kR0tJlW5$ZZ(5|yY#CTb8dn?MoHWG6>RGQahxkI7P)$85}^oA`1sNWG@q`NEmbwIZAKEi%JEG-h!!C zy(lVm1OO5gL#*RN$jo%Ql2ztn4cmidNYP1BxK^PPC>0Y46hH!>?&zH+(8^2wI?tFc zk%?)lSiTIzn6n8vAvU=I77D5U20i=?6 zVIhR54kif!&?Su_)Wk}}00L-557*$GP>dq(2oOFX zLly|~*S`ifgTM&ILj)0}O!zCd?3=;_U6R~?WhO8Q<&Y1i3d72pg0t&oZ+nXhvaG5% zvm^`7k(`ncOS%NL|4^+<6w+G%D&({Z2*7=}D-l{S!iTrTt!{nGTL@R98Y)o15;|cC zS`0Tji$y4h=s*a*u0|jSAOMM>>sRUWZ4;=7<3gYmC9fWsdQ&)qcE(V%#}cecCzxtd zX*7fwxL3$Sp6V#GdW#=ucC@(v2|%)V!KBnOWsnIW0aX-IWv2X>Dh8G@L=td{E-=`w z4!!||BTQimmq5)WSORbJ;{6G2oIHQeR7 zO@YfLJ7_blL{(FeivdX{a-}WJfFs{RzL|}&W46T8VqMwFtYwM>6w==tC*-k$Ny;M> z5CZTb8XuH2JGMvmS{jqg#+r-UdPT`+k_SN+#<8|+0XSn zq&j?X=!6W@K){l9x2qHaM<-=RN=QYy&3$fjqnAc5=mEOj{cd>2Ti)}gcfHSzg-qW< z74}BA5nzGstB|0^P>vM;RW<}*R?}^DQGUsOQ7vi+!FIc}W`a4%oa-U%I?cXTGq61l z>?<%~2OT&?1HN||8c#5OxB&!9@!YjfeT_uXF7dQ^8R%@7bITf9UyiC0FITE z$9gbW^8p0Nz_nTgPv(_#*37*kZRoCe&>8_oqNaFvpM`(~k|&%O5VrCL5kl*0EE^Ak z@patiUiY{cL%tdRuzMq{rABCdhmoVeB#Z2{=`3E2XW{?2O41VSGAlF5P$#*Pyh=rT7iq1*F4y$H6oh( zD;Tf=H9-lR;5i3CEY$OvWT_B>BAittulh)UuPcEV0y5ysKn=t_Fvz!C5Q87kz|*@5 zFKDcPSS+|hh^cTuiQ|YV2&>mh3aWr6p4tE}U_C@j07|fc?(;h|1H8Z+Kk`F7^g}-& zZ~-7lLLf*2CQO1I7=V#$zxQ*z`k;do5QO=I06gQGLO3cek^(Urp2_(?6ZyQFI{;vq z7?$vXoZvbC?OMQ|2@D*J896hdIRpkvPNJUjl#YS+2 z0eHm$Xg^t;!YP~xFF3x9NPrXA#a+xo`nx>)Q-U6lI&(w1E`+Ku5<~xc7SH2~1Gs}U zq%W3G2fo3n(St*i5w9#G91!3z6C4l-7{;s{#27*VQwT(K+`s_JG#&`S3{-;0O9}7V zuR0uqJG_cNQUD1^HH4_c#~Oo6ygOR31idSPjpH~^{6r!Q#l#!MBrL@wIK@?5#YSj^ z8MsLQjGTcZ*vKPD1~B+YkPHTq97$LlfObfOb|^_$M1fai$pJut2f%_Dm`R$PNi4X@ zAIM3a%*hz|0W9bNpqx5$JI1FuTx3D}*v`kC2TuZiG%XK`mvTRGWgvX;mw&)v!0Jz7w0IwP> zu9wiupW_2cu!J3`0)+I$!dpm&bjT$vMTwk9RVbXekk6}jLJWHh#Jbn+YS)GNx!O=W7hy z>94sHx8YO>K4<`fWP*fLNQN{&#)Qbngv^SZOv>a)kGxDU$V`!}0nZH3&E!l~C{5E8 zO$24l*0f2SjLn>!O`$9|qI9a;1RkX9JkUS^RN&3HpvE>tjB8BKe=(9tDu#emo7G#0 z?_3Zva8MW3$+yfLF-U?Jtx+4jQ5?1YJ-D zh0P%SO4^iA+XN$JY!)Oy0Wz4F7Q2e51WuiZM=2fD&H#X=c*Nk)03iJV%E~GR=*dNm zR7suGvGl-TnAF%L1eVB-mry=U{URZk@D4(C2sRiZ$Lg#aI@ z8ruK{Maqc~#aEGy4Bml|1yO=@z0pR^DlvEha(!8Hy@DPEi7R;7uu}>1Ou;MBMr|G0 zieQ5V(0~qT)g8zO7noHq{Zd=aNL=MlghkUd{nY~v)?tNM2R+uMtIeXqSQLN&so_}X zpabAM465{5vvmxfeTnIt4_kOmw|(2UwZKekS-7oRyS-by&0D>#TeLWug#e2P zA(Oq3C}orYpblJ=v!sz1? zzFAwO48WXWs^G^jz6xaHg*;B11b_h;PU1e!k&<-@g8Lw)U}E|JVsAY%A9iC(X5#~< zC>}6{HcnlN@Jq%DufVwBLsp5jJx>FYRz>2rQQld_@q*@)qil5vQHG3$t>w_1fgb%| zTi#_}?qy%@WiW{2BeSeuW>YK}fQLAP!bKQREg1^w)P)Fu#a(5M0D))zOAm5i;OsPN z?&c!ZjA@RHVn)zh9%pktXLJtDBp_mna)ERXO-@#bDOd^#0Iw=DhCs%Y+u;R1{s+Se z6K|dg0JtKZWuF&rA{c4f5iXe(8)5D3_q!=wO40_Q0j8Xr12aU=U_Dn&WrYfP^Sz zljcV6a%7nHHnE*4_D$*qF%rC?qLM9Y<2V5Wy=tt^YEvK^4PaGd!N zfYW5_(!^ysW^BlgY{@QbB0%eqxq->vYN1XDPqs(uS>joCYSoEB8ye(9;%3065A$5) z0~%w7C}YS#1z(+Q+V<5;)+Pq1ZQb5&-tKMx-~Mgj=FG}wqudVeh3)9F3Yx0rZaNf0z#H z^_=cnYM4{umvM`>?0!rB#(1BpL3`U1_mG{kXQpE zFY-v(avN&oQVZ=ok!gNbh(zY&RTK38(2j4c9zhVS3~FjJpLBUf!TXkr1SnHY*L0aZ zvJ%*IP!DxcA9YeMbyMGT7w~D@JN1#|hg&jKs_=7lIqKvBu>GpL=tT%)cy7o50crjq zV1V@Vkq=5Qc835es*Vf+VD*vQ@UjVlWMFn_pLS}mc9C?2A>hLyVD=}717j+hm*90* zK{!eRFrAeiuDV5*UtFod-s`OhxZpC_IdXYyV}R?W{7ov z3__>@X?KMqbAf?>d6*A)Y)=Aa?|^m^=?5ujMj?SQ*b-AU23cQ-eAEtkF3!%7GYLp` zM?a(tLhX=$4gjEQ9jBf~W^KY4fLzV`tXF}o8Un5Vdaw_Bu^)S~FMG2;`&`Wjg>Ih* zQFDE!j-J1=FMo(nGrP!e*8sWnp6PgaSHP#A4-Qj$8$$5zt_wOqdyq`y+CzNCZ+ypp ze8|7d&9;&};D8jk{LANnwOd=L{0E350tXT-Xz(DygbEijZ0PWz!5K3e{Htj3BF2mwH*)Og@#951Dazq0 zY4RjWTtX6nZ0Yjf6bZa$@>1~fCeEBXHyWc!lf+JsA&OlDAakY!ph1^rBym(J)2LFX zQmyEt5>~7)w!R2e^()x0V#ks#YxbW^2@0T|ZR?gPh(|sZfOy+70mOnlZt?Bw_m-F> zc?T0NZ1^zYWp>T~b{m=U@#Auf{{a{?)8^#N95?vnb*61kpN}+a1Yry_>C|Quz{sj~ zD=^ivXVb1dIY5ZIws)J2IWqw7e+?uOFK+yJqrb{;xj63pIrQk#r&F(P{rdGIe1hi` z=L9nN$3iB97=yYZ2;khwrnf9Xci8})B}^e*4F3B3IZe2J{X&p_0SY)Efdv{^Q3w)v z!CeQ~5y!xCelg%6g%w(OA%@qHav(;HVG|yRDdizsc{Oo36##6J<%MN5g+SUFygX9h zi3ifr-)phexFe4}`uO8ZASm#nYy(udSA@4rm?4u*I{74Y`LM)c9^Zv{WV6#`8Xc==^;HsDyvR)DZMC!GWWaK?Le7RV)yspaEJBY_G!D4~KT zk))D^D!M46jXL@$q>)NGDW!vIaG*FOVY#VE-H=vUXKU&?TL>GFDZvYlq;0-GH0CJc1EdoR9Iu{+af`I34RP3~q! zNh8-1fuwTF8oV&W4Lkhsw2e@tAGaTZ3$B)$`O3=%s_JPB$mDwL*2V&lC108V`0JJl z2h5BAqX{d^JTpjMbjFLQG`}^)&Blz}RtE+H5uw5m8+|m=Njn=ye%%OR@x|a`lNzh% z)!HIW3uXoaz%;2>mQyont+QBwxcstfn@mYI+d7j=lc{ang(gi_Pqxf4*WR#P(MtRM zH{b@ZZJX0k8(z}jli9rMxw-yEngX5KO_iBbJd(5AQUP-|j$UQ{dA@Y>VhrY>k0s{U zr0sAF>#e)~y6ZtGR|4#{+kQLlx$C|=@4fr}d+q}4R`}tEkC>Xu7@ZNfp2d`#TI4e^ z4weGInl9EWXQPdkj@NrnE7u#0{r$Q*kMC9z>8rm!`|CsaR|)OQKR^BT+kZd)`Rl*` zKmYeHQ5*4!XFS`0fHS%pmCXWHI=7T)fl-N`^~fg`I>11C{jrw>GpNB0a-@K~P|<4o zmjndBPlP5sAqrEd!V?Z_VyQ`h5CkYganS};E*g~tByfg4r3M7&`$ul15}d~eS&$dwL%-g6?>SmW6xS*saJt;~{ zNVTZYIltbQbNyBrHiZORM;7DwUfKVF+uzEM_yS zS+sI;DhYT)F5)^@y5cf=A5CM|?mC$b7=xWHQKw$x8O|&a;G%bQEN;J2z5C=ABny=+ zQ*L^N%@Vh`#x3p@E(MlThUhcR0NE!w^I&5w(^!qt z;SMfS^4T)#w46n^=MYj(#+xBQ5DkQ@YZYzBHyYt?5m3y3?Mv^ek8a zi3v;~9Xk1JbN!NLHA_=J2}HE3wn-BqFXnAqy-0$Pg35?uH38#Hvi1ldUP2SwC0hLz zoE__%j*Z0MKB_IBsZjEvPOB=jO@Kq{lHE%1R8yx<0pG!UYIfo(Iq)Gs+jeU&5b zaffma9P$FdcT}>}bnOx2v-JTJ`(1e-0N5^@)xA^Bky1(k)=rLI##_>fgVVg`HorNf zLp|z-H=G?Hp>KpHPVq60XHPK-te$L$bOiC%E!$Lp7kFKf>mntw7A3jK4{%$Ra~&hs zrAx}S8uO3r0Ow{uJK8;sZEf?M=XLN1EnplW(GGo#(#?+--ewF}3F(7Hk;&!U0)PO3 zAeVWvh(nnzSRcr{@g;w}pt~;lifmvE27Wmlqq2klA(Wu=p8q`PLofQ#lfLw(KRxPG zulm)qe)TL^c-vz?0iSE+#fYMN-DOx~{p@R%v?}6_rR)|w9s2K&$T2z@-*~K-eAttJ ze%GlU+o&v|_13>W_Oq}3?O%WGvj6_uF>+=^-8c84WV3*z|EqWPl_q<4i{p5K%BUw+ z`OBZT=b*2Di;%$Oy>(@R-30dyAORMj0Ulrpyx#XMAgR&bKa3yw$(Z>SkAduq$DK=D zbjJ7;!{zA&38c09#MpX1e5z3t!r0YGSUAa2o3016-i#-I$&U<`U612W*-K_H^s z!m3T609o9gq=o+_o#VV;MZD1?ti@84%2D+HiBWV$vVoNSk)X>Q779M!9<>Xx*_D|L z$xf&M7>1!3jv*PAp&6bb8m6Hdt|1$?p&Pzo8S2~(&fx?=0we68s`((%9Z0D}p{qcd z*j$8G?bQ+bp%R@a?a^sB_9$% zVk8m(%2DF;J)vl*9LihqcvV5HfEzXZX-8#qc?sdIEJG*Ru;DH94aoL zDzc(qj6fNL11t&<`t3*n5Mt7N%hyCSoS0VlF0QHl|}HW;H~mRHEZ5 zR;6EHWmfuwNpdBp+#P{DWAVxV%;0TcXVj!mutf+25ricG3iM<#-XBojkmaJHv=z9)Pl=5Q7#17biFB&S~}0~|2t zNQ%O9Zc0kl$Ra8LZm!G}oDT~^7$Dq15qPI-${P}dr+8jr033zzbsgO#g-2Kd0(7W{ zekh2BsECdziI%8|o+yf@sEV#=ht4N`E}$0NCw{IYe`e)o{%3f2<&N}Nfx^;(7J~xZ zqJlMOgMO#Hk%3)KXxG{Q5KRcFy}eUQt|yC5DV0{Km0szJ4gplc-iyMhAI#`qh-8g6 zCys`QXBx;9^ynW+AFbU|0+0cb7U_ddqLNagQD%+GZ47pG1czd&pZ+PJ1}cd9!6n3h4Af=E2psaSx(&72c=u0WhRXir)prJ~>>CZx*so>*#x1qdpt zrmCuzXcTAxM;>bIsp6tC>R&A9m}aFRL@H!}L!AJC0yHEZ-OOHIj}P2IEo>^M7UKzi zDkX-7hGJ-%sOLWf!WyiqwO*^ALS?qWYJDOnt@_1&-fC8ErLLw#(S3=pBE_(hPhKUc z2EA!594oR0q!LYkHjB!%IudU z04l6(yvpmQMre{!Eefij2twE*zF9>C0uP)m+NQ1At}WZPt=qmW+{Ufk&Mn)nfwoR; z(YD=jCT)^9D$|yMe*#rF>_Hjyry+Fgx11}GsNdLmZGNPz6+|x9&MTcdu54DQ?@1mr ziqy2O0o{K8F6f4?=#DPhLZ!s!?HpPx-$sb7GHqreuHkIK9@J<%9?r+k3DyLj<<>|7 z*nkwQKrKiv%f{&)EbH!$XIQQ)V&&C^nl0%*FZ4$5+zPF$qHZdV!mO??#=0(l=IR)Q zgDBi;?OqJj0tuQJgS#Ftcf7(KtbqGU?&LP86=XrosxKw_BgaUdQTarlIIr~nF94Hn z#9{!^TJHkdr}iSs(sFNB2Ce`Z0@I2D?1G5mvWc2H5daj}P;j6TrL6mYF#Ha05uifX z+OOo@Y5r<%i=<8~1c3{`Fbv1A49_qP*RT!WFb?N14D&z~j6(GqFdZK4mnJZx_$>pE zm3jR6whXw(B`0>fz{|R8JDpcpRo;p zK@_lnBw&D{0&(hkD-pBq5%Z^W8V>j}Fu8I{`Hl(+fUyR>lP`J?9gu+(9P$;vukhA_ z4U}*fPabE?nL&C18dtIAF&<`Y9Wy7SMhyhlF}PH4uNH$` zwCS35#vp@EAs@0WC-Mrw?-oC@c!EIV-Wv$4Lmw!E7!eLzXtF>*GeH-05U8<0PqQRw0Y4Y8HB;=~YO^{TaX0(J z9dAk=cXJYh2(Qu#$b~KWTo=eVDccYN3b->$<8m!1fmO{jrRJJCr(8O8DJ1Z-P2V(5 z=d>F?G*5eEC`+_DhHFJ{tT>}YDpzz9U&)z%324rQ^1Ad*B)}^uK_L_ZO1pC+-vI&}~mF(WA003YY z_eUgv57>ec=(b_=wr>OX@3ORcQ)+rWE01u1K-C>M%z+Sy19Z0qbq6@=Lc(cdcU8{y zcGopz;B|ug1C~g}eBVTY^ zL_-t=!W&QmCRBwz2sn+?^eDWwf#auYCwNJwgljjrW?qRbj<`!)>bao*w=ML*5B$Jx zb9j4u_*I8Ekuz%mfB`@|zr?kH{8XgUz$NrW7cjyb&_avtL_XIznbYAuvq1Ld_>5{d zkTd5Vc&~T2xm`a-AM*+cR8M(>j*-&}8$f^(U;&fsHhVj{VMDnsqW~9A`R@Aifuw-b z;)0ehM<94PEyVasq;;8RdfO5Bb*nj;%5|GZaHwbIoTtQ6>j??mn3Z$Tp3?~(Xn-xi z!VduYk_-Bv6FQU^0vViuq91SAq%UoB!=sx74W#*Ubb*VX0xcvdrfYhp6F3XtxTn+V zj*q&vyE%wp17EYrv7MG2wJV#jfdG(!62t(lFFCr~dXwXND!>8+Gm`e#;K#au)x315xeUL*&Z1ai%sl|?UaRQux0HDGEP=P5V{oYdn(=&a? zfBe&jyb-{{u2(%2gQf(mdTp4))<4D;T!IyevNZp5Lz6u^_Om2J|3S{9{Tw4W+xz@R zmjpIMx1H~h2{%B+$3fn!!rmwS#>c+W|9!>>zTks=4;X%;BYqc0e4bB69y~r|M7pO7 zG}wo|8;?Ct^Rqv1eiRtH=VN!ZhyJLSIxaLk@9zd1*nJ1s!T>zM6VyNrJb@~dzw7tC z?8iRs*S^!EyY6TIJOjWZF#c`?zwi%#zDNIZ2(&;0gd~9j2VyXI5TQYW1X)CJ80E_? zi4!SSw0IF?MvWUecH}t1Ur3Q7NtQHu5@kx2D$n6^`4Shl1pjE(w0YAeGz&SJ<*)z^ zXHcO-i54|_6lqeWOPMxxI&_K`62+(jc-oXr0}UNW4Hys%|7_S`Pso;iq6)1kwN%)) zWg7!-T)0@U{IF|R2?Rh_`S$hu7jR&~g9#Tld>C}{WXY2$S9a{iV`j~p zId}H_8FXlmx>lApeH!)2J&;o{h4#SYvc-!*c$s}0_f2EGc=NU`xIu#q9Xc!<@LaZ41XZNGgM7N|Q~GQ-ulUt6(7NLVN#_DFIF$T7jKt+Qc*PN8nN-e!qu@EFc(-Zgx+LNtAY4LNz`VMkNLyh1>9wUYqT< z-G1tf-i$%=M{s1mJ5)Esz?tv9tyBgMo&_gxHC(_^8z-T&Ec((jHwa*gw*kXMfJ2xl zfq~35*Zhjkuf{s!t-1F4Yp^qR4!Rl4CIx~M*pM9d*kzv`ghwQRQ^-&8-kr%UE>Xts z;Dyf*%fXF5O_VP42+DwEklB)POCz8C|0!CU9-@ToG2fi~&aM7D+0eJH*y0jR7aI(( zSf}I!I;eji{`gUzg6<&m;2r-ZY1n)C{{81j8O1x^082v;1Kgx}&tp~e6j;0{xQjDP*(H41DM16jDM1f1d!RZzhYsxd(dRxo=P zAf5O`C`vJN8LT42T#%1Ck)i_v{|FA^z*tF3UTO+wI1?{)!;42KKuv4&!lrsNMhA7n zjj5!J8-OQASJq62c9i7~_c$p46k`LVJj44aafqqiV3352pdl-WNCqgfVN2LSCx|o2 zOFk2t-@*`YC{&Yj_2v;~Bp377~h4l%fk};R|M-q5+bM=0FL$t2UuYL;rA7r;>Kf-blcd@8N(rDSDY!niHe{ ziDf!D%CiRX(OlJZ8zv5c00hvpp7z9NJ~g)lUvNPYX!r#$)L2lP-n3FjLzByBG6gsB zG-W=On>ODQ!-`gvoEcSW|9$}1QKuS@Oqe2NhTKMrK$z5|Csozu&@|SBo7lWb0BSm}UeOdt;yCJFaZ$si{*^O2d+K2A71c}OiZ^2j zWzwvQRjg{&o=hBO6*^&wZm|`!ne|Z!#vsyv=+u+Dq)7-id9IqQj2Fi6Y`7E| zsBi@AY&k+N!rssZLDv=airgl3uBTn9~IQ=PTR8Cn|@ z00cm~G^t&0#z53&!T8{_STkrb86?ELoMTv9k`0t?Qa=b2}FV#W=829v$X!0bo-SD7S-PmTYBTl-a}6g?gdgZ<9#dTT}JT zZ7d)}lLVJ3r|`D7|BTft&{{C$KKH~ajua3$G_BtXz}!4#?~1}9-};`gze!#afbVLW zrcF!+hL#d%I7Q*Fmif0$$^#1!+Aq8{>zU?Vai3oj01F1p6iUkrh$1=x*zmYG{h^7F z`)%YUm%2z!jwv#aK>=1C3^e+sk^wX&<}zn^tXn~gpPwD=G%3>K*8LR3au;RkmVnU9 zbK|AE|1l(*(8bejmforVo$BC%kQeA4nC6`(+e$e_5X3(74+p^l8c6%(eJ=1~3kC!N zmq3?WxXBv72ibRT;~qj1hQ0IsU494r>Z@K!me*=2n8*d=8}H{_@V6O4ru^~2@d$}`T*~lUN1G)NhlJa z3xa?FYmW!IAUvQUBoL4~`zzdKs{WcI~Yz^3kBxzFaNRDC|M8f_OBlPqS7s7!B z|6yfa_)jFlVEX=#KLT(7Z_pnCLpU(3MFUCHo&ZIO)1c}pOK&dpumJMjGU1u|4`xq zJS!T(HUe>C}{B)QKB7h1I&1V5|zRL@+eD^ z0uhv95yP()2#q8nkr~~@8le#(Es4dB4}MzCB>0dpl5Xk3p+H0-9LMo7wg3*zaR$+` zB6Pt9*HI+>0U2`9CNf|dk});DZZ=q~=a2##3Q0w@Uo96->?BKK836+b zjp3ROG9inyh~>7X3lQrdOK>G}KrNqwjWBIzEYc9lAWQs@`an_u#ga2EASHK3Du;yd_@V<`Wy6d@ z0B#Z|i;)q8LMBPVB>z%2sYl`vL#Q_39>K-*~fr?V*DKs8sB0i!H&PJ$-A(?G3IVAcvLRW2wb5H*nD zC;Ngb9c&ApO%veY^vr1l>JvXZl!xL>JbFtsWr&fNww?^Dpub$BgsY!r($P)PKN|IX#p~1t&HS0{}<>2wMy( zMwCR0;uQW9Km`gXOV7dCV`o77P22sfyWH;%$eU1dFSq7{_kL>aI(h2jZOqCmsc zO~{l}&tnR56fgkb3>>N{E2T8)(kRZ-G~kpmgmb(MVE~O34n{ye{Ud+~wNMXLSJf#u z1dbODQaX92C}=@aCp8GT^d=fL+Wx{^I@MX-X8nxhQuvY}|DS>}2?R_TqdocRJz>#L zFZ4obRa^sABE(f(FEjxCbXV6kouE?^r*J5ORal9F5tut4#-^9&^)2tn~i4uv}j9AJ8*P?zAr^YP_~GVtdwS?dUNLV`V<+Et$tl z7lsWqYJiL3FePu2gTrL1z+Alfv$f zsvLioZQGV-s`WNNAPno2XDAR6I`&@m5+}-5B|@`m|Nr(pqLnv#@hPS(11m*YPtzs@ zmy?E~Rmie0g!HJo)om-+atkC|)nh*g2LTh{7V!2&agHW7HznK^a8H*!e3fS5M_K&= zZfn9^oRMy`gjMsDuPXF%b60myqju55X7f`hC}C-t_9hV_UypVq6c=@uH$7Oa(pIQ- zL*h|WPS|WBY6qlul|oO&iD!4$d%rg%&UQAY%q9%LA%XV@KbLsLKyPnCUPZPqZs`KO zAWfOqeupAjH-IQ(2VqfyReR+THlP?t;J~i6RkAlJwpW$-6nq=FZ8H@&rVK+wigcfI zC?XYfRa12T;e1!3F9dgN?^lG0!gkaxQDt<0|IOo483ZSKv40;|e2+qb-3V44*oG|^ zRS%=+>`@GPNXYm=A~IMEo;E1ncO|CQDA-c?Mwp3SJ|39RGGzN3?65C6nU0~LLzQ?h=szN zRRWBG8J&}&-O$8k`!$Ly!~l@DB-XPTRdjF|nJFZ5g#RHjlX!(fM zmNU40X+on<rh~%PjxKht*ff;6p2^vGa`-6npkec;dq`GT&fLPkNgs{eJ;{~n>8*QT8J)h1rF7?{$T^YwVr@-Gs=3EHTd(;Bi# zPI=2CF(n`Z+&U;~d8IwJ6>#FDgG^J=M6dVyS$h;QrVIl|doi|pCDs}#1}Jl%LZ5es zpCjA0-At@=Bd*RGr87IOZ<&Z`BAzWQw5yG@fw>s!x6ppdK?1upN*Zbhh?0#W56%p4 zi1fCz8_1?xO?lxD&}sQ{p{YIAcx@u72?V-}!kCYHgaM#jT&ypQ@0W9fwM)aNapJrM z=f07GvGXUDwHv^l>OG9%vN0Pce*2cyxF%khK)Bl`kWvuYyNP#=H$Lzwc6g!_!?{f( zk8$FqdB(wuf&+q@T^>8Y|4;m>_B$~GU?0FcC=eWU75pDCR!c&>r_Sn3h*`o>*Z6p$ zYn9?g(Il%6gRV`ZL59{C>P)#e9BBt50ZzF|wqU4HoXTgZB-H}|c)PcS5ym}M!4-N- zT6xE-+SXFbp(Es>-eZrm1R5%~D8L%T3nLG>QB<_N%J006`dB^Esr6`~%WpE0i`+EE zoX~?}nv}9H`sl}VgTb@p0pzK4QjBe4x1)2v04GhvKcu35h!qP*fwl^Kuj|ZT~gUJP_&;MKuXra+D_tcBMr);_= zv6I5;LG#mj zo%Sgw|1XZYl_KQ`zC4(HXP~|&1b=`6W82E z|E~DaNw-i<7m#21354>S|F|`O^Dc$=XVQ;h-d?y~D7v5iOYZvd+xTnZ^+q7;0b+!| zfdmU0JXrAAzl97NI(!H*qQr?5D_XpWF{8$f96Nga2r{I|kt9o+JP8rR7+x${27nj> zrO6Bk|7+U3IWUJs0XTd5EJ!lP&!I$%8a;|MsnVrPn>u|8HLBF9Hjx>~xQwdLWecGR z9XLXnSZ2(`h!Mz?Yse|aiWLjsWs1zXbnDu^i#M;{y?p0xXxY-s!w|e4#q4{LrdEO$ zDk@%F@Ty3jkSklhj5)LB&73L8urcb@nU2RDrU|=@|ox9)T>*+jy?NpjmkA6LkQ>A*s^As>J(XQ zwgTkr+q-`cKR(|8EjKtM@h|=C!@j#~btaia-(3dXNF8X0V1f!R$Y6s=otIGq+G z|7vE1u~dN}>DN**@_h(mh$4M%sl@A$ek8m{c<){RNnZBt8jclu}M9(iCzrMBHpG@rPWFm8lhxX_tX1(f~i6 ziDsH=s;Ll1G-5~OcxH(Khf)HN6hbk^7{iiaRQ?HQpn_7#r;99&@!p%4bs&*wmXR3} zG;2;uX{DBW#wbT-W>@5#bt2^qT`3MqYN@84iW_}^NdSTu#`xKuqn0rj5oM2>B<7{M z?#gSgL?Ppmt=EO68hJsWbySe3F3W7Q&ib|zV8X@cA7r#XR4rAIhLo+Z-hK`5T6cZ zl}SRg#3j&XpN;lOXJ{z_zH2wh-pH3Ry-==CqvW#HdhgA*LFw<)Y!h8 zIDIHK#1=1?GxP{xy4$9FX2>wwEmW?XM11q|&wuH^B7IyPg{ucavrnd%1b`4IpnPZG zo&gW&E)X~ge8Ovp7kDO{-VKjBT(OLdPU1BBX>fx)3foAIM;)>lsc=5IpiBUeIRvV3 zg``pmFE-GzjEs#JDg4J6&d|WSq)#&$)D~Sp$iW~AF*_`a4t7ScIv%(NC(j5F3!ex@ z#<}oNF|-Km>eGNjAY+{}kT{$BIp{C1a?e zMZ7RCp3RFFh_XxOQ1>k-qC{zBETkbJ^S+UYaXOlq9uK~TkpGoL0U@Yj950DU-{1}~ zT+|2v&d|q&fM7PA{Kp0)uz_ND!IByQ0SV6V2njGGVZ91fUA%b6TEU<3&vLpVa# z7|tL?JjoR|P{~XxbD6l=B)vwWMV2(MMPzf}Dkp-%hRg;(|7d{RELF_8Y;Kq2Eay<3 zcalYV28j)0O%Q5gOPM&cFwCr{J@Zni?~P=SvSi2=rRmSuF$8-Hv>_)M0!Uun$u99T zr$Zll5J{5cm!5$`FoVFqgV60HMq^1S?+Mb7a)d20REa)c{|8IpWlW$8iC{|_!l7p_ z#FBUG^m z)tJ_1ptBL40e)JkM9!s$J1y%W&v=sdWkwf54FV4eV$`{WKxkWSs$K6oQ}fwoZ0E}l zVCw2uz<|$1-0aFX)nvcAP}Ho9En_%Ovct`cK&WEiLI3>G%aeEkIAz1DXYaWpQrf0f zdg9U^0gF9eIOGu<2!K+%c~~@|buQ{`tZj!l+mhgtk!|(bKqOk1*M4@m%bX_-eWRyn z0oJ9o5pF~lizcxS^0w4xs20PD49D{hdEIRcFrKR~`TuJRb2k%MIj;K>?s@MR|3 zwGE#lUeuf%lFwYb0dLc19k}p40r1x# z^)95zwyyS0TkH_*X66B>@H0kv&bE6%O^2XX5A`M-PGu+6BTljjrpAJcA5Bl+(3kr|ph3T5N=kDrc zI_N1cx-Q|&5Q#hG0XPlB%z40!m2YH@|1D{@&aWOK5=aiJ7gIo~9hU+2dd7qhA-d7e zUPgF3WVc$8`ae$bbRGnGEoJUF*6$u4LGw&cReE} zg{Nsg=8cdbhF7#Z-Y>r!5O4_qvjOZI!)~2%mEG{9Hzd;kf$*d-og!~|{C2~g5fH4+ zK3SB&;xLbU-H?DurtK;w|4yBFR%F+umtPZ(kB7n!U?S8;3dJqL2iI>m@^>6HFQz5R z+-rX~;-!5{&G+>c&e}7XZiv{6ZxB2*fdUj5g!|v`{x-Cs3BCG28``gf{Oex_-WWt1 zl8o*AAuzw|Jf;!-T0JtV)Kn{a&f-mTQA3$#&7!hiP6rvCTAchgNpnwZV3l|}1CnbSH z7%wC+K;ko4($Wwb2uHw&8N!DV(1w2cAOjJA16&XR9`J%&n16#ne`xXyvXBBeaD`gP zh5P3X6ku&lG!YDE6c_LZEa7<+;dE}bfDC9c*argECq58Jgn#%E|HKPC!-r7SRM6mt zO(B4LmU;~*3KSp|^1Xbsk24rX|UCg_DevObgOiSB?6nW%r>@P(9?S@Px-$PfX5 zVR;Wh2^e>WKL|d_rXnq2{|rnBh`IO>Q;;XdAOS{JW~Eet8&Gadl>x5?6>et{Hi&Zg zUJChm@_7NN!Jw4kXt*GMZWia zws;C!(lkxFji5D9v9%=nWQiV^); zW$@6X#9$8Xsek#&8F)Z~IzSB2NTOU&n1i67@HM4OF`HK87R8_dofMrX$)$4#rj4qg zi}sNvCD?rCo8JMp2n1K?@7Wr3kPF%IK)E zI)oPX{}jkDsL$nacz^?XN~BM^o5i^qc%XmS00M8in-s8=jMbu>))c2#5`nO#t-1!7 zAgk4?i?ulvuxb%LI%%A`m`!R8Myjct!KwNe0hj8l)%c-1wVVvWtW43abb$%c8kz`z z0BC8g`6_{@sxE38XOwzd!pf^eDz5Dbte7FJ`p2Yiny#YQ2FbcpP}6)vajPQ12wlph zYhbSk;BWfsvD!zgQ1N^n0fIE9q;D#T>?x;R$Qhlwq;J{}4V#H5T2_9!5J9>WsVWnE zkPBwns;*iELiVvkdwCN}6gdE26>*SuMuI3y3@W>_Isl)Sai=fKuJEda=z@|O{ zlzGt#8N0EeNv&p!xF5%_Nr7t{fu_~vf_RFud>XacB=z;+pCqEe{@?J)T@6UkN`~@ss;`{}%!v zw(Og_@aw=wCcn_DUPjwkAFzL)N}?u;z5F-6i&3?A+oYUZjSr;|4KS2O(W17|1?;Q5 z5A4GJ6~R*R3>JV>6x>+kP=ZfMq?EhC>#7yb@^5#hw+zQh^K)fHgMUST37?ToA$=oPS3w72Laj<50W7dAqwwv*=lsxY5Kxy~yq##V;DyMYN+z|Gx^ z0FNBb{|rYQNX|cD&ijYI)I87F+s;2+jf-Hto)`ho9JPX6Yxm3>0>A^O+szx@u>kGS zPXxLH?Gpuk!Jo{H-k8byx6oA41PoX{fFa+H^|KEpQ4Lfk(3l1+C@V59hFNQ_{n{IT;6qRi zDxd-&F5)-NEU<}wySw80*WS9@UMiEEauFWfgV4c{ zA?2k_<<^!1{(9t9(BU+$0wuuYPEO`j0_7-I2IhU@nUUr_zQ&aH4B7l4lpkITz2+!qfPaGM z2oC0V&ghR0>hBTh=qKoUjR#d8==*))=xMy|aSAFR|KpwR>0(~rp>FH9G3t~I+>l-A zTCwY7?F~atZ)^J@q!8`1*9HRS;>Ii$@O)%{Z*9=J;B8Q;p$$sZd zP|VFP?s&ni?c1Hf%)I^PMK`iiatN|M>kM!ORlo-0j_-7VwuWcoyngMO(eGrP z?uKWxBog0?p6m?309DY@`R?!{vG1f8@WB4>Y+mYedXdWf-cj=E-d+L!Aqp0YBfTEC0$F z4ka_6@HJ2G%dYcJ4-tF(${%p?NSWmc((+(T|B5_65H4=qRWR>NPwyiS^*Jx~Q@|6plO0fwK6n}{ZbfBO`WySbF+Edczn{{gaZ9nMsGE^y;5y}tfogiiC~_cIacjjwrveIkvXu;T(@=Y-o=}@ObfGT z{r>Hh2#>(L3BMjjoLKQ<#*H06h8$V)WXe+wJS%Ll%u2v81ryF3TJ&hrrA?m>xwrG` zziCH5s~lVQY}&PL-^QI=x9n!5ok3ykw+SB9#f=|Fo?Q9D4=sYfEbzQn3($p|?Z%#6 z`*!Z#y?+OfGXv=d|MW;dn~3^y|Mu{r&$3FhBtZ%0~VCQfE)^IfItyPB(X#jPeie|&^pLzfeZa= zfw&20q_IXD8B*=VXl`;5KWc8W058~3B(g{&k3=#_#8ljGCk0>h%(WY*q_WE8KA3Mo zL$1L|2K;2mNr1a7P_j%j&qOm#Bbmf22cFJ@@-Ouqw6ab+?_8_GzAmg!r)hlDNx!y` zF+fd24@ERlMQ8i4unc+vE>6Hc!?RLLFMX)FJuigf$DH7lZbzNiTnoWPPenCVRaa#y zNJo1z;7>_AlZj4DZ^e~L|2(q1umEXziuAbx=-7$Wu?S@~S!I`HR?P-7BeN$^@sssA zN^`}wi8eN(V55$(y^=3U`{Zdu^z`5fv$jlawq1AMh1Wl43McXtmd7yX~CL#%lne9F|UJoTy1-Y3X9L zb?f6=un23xFY=}h|G&pVcxIL^_!X&Wq`SbUW!OU2ZObpm9Nq!);<%@K@hdy0r}dU@ zIIRyit__t3XI%#hPDkCN#1-#_@ui+zZ5o?*YE-RgGY39+;T63sF9Cx37`+0@jRyCq zV~pK23RpD!>AE<`f^YW_~>Bok^U5LoRmFi*;LULtZcA+7x_6V3fifsXl zgi;6Hn5ospk$>dyo6PK(!ZffDD&hMJ41WSQlO(c`r$l8clVX5Fx@C+%Q5xi&at#^8 zFOqzlWI{q{NfCO(h_|YcxHeh4D^5j?ektZobf`sDHq)8U+zBh~!Xh;O@RP^nU>kE8 zKwid?0v@bWa}qYd0(3GfE<_+wS_w#Kw$q)c3`<=)_me8J24Yb$zyh$D&5|`SiQVMi z)a>NM|6vjkU0eBFXGHmvp4G&Z?}TVXBN$J&P!lQpJc}otl244zOeTZLA07beO>j;q zm`_BZA*9%qSXmJ%G9<|sB|1}@LXe`Ao8nLC07;mD$toNzXiM1GPneNo5S1J$^?qs6 z69$zfQPgNw==PUB{$!ma31m&Rx>exav{XuoWpJX3&jr-dsVspD9+uj#T{VFm-J-xC z-BSK{mCrUSIh%|&vF&7kHQYJ`}^C8x?uMOI2zp9jacBYZ%)aO}W0+*TG z|J7u4YU^9OwON4)G&v}Ni*8+_(!P47qjywF7DIxJ3c}X9*M*mCkCjt02B)fCiR@(U zI8?Q6b0KiCENSvO&a*;c0nPmiIz!1_Sec}-*u`&tS*6{Y)N7UE?Zs`qLXEQ8GN`;Q zuX&RL+O{eeCgnWnWHtzx2nPkGA!)6CHN4?P@z*5PMM_vZ^IgSUL!SnADuO@YTLsr- zy`t5vd*Qp7mICIUNxAArsLNp<_t;P#W-U`3dlmsN#>7wna8QRDqx7~oA`DgujFn5; zB1@*WFtJ-w8ak5W`Pj=}P7;tu@-wAO)h`*04S{7~KL-Ow#Z7K8l#|roC|MZoO zfvdce3x6bSUd|%8*`-o$*W?Gry#1 zOUbcB7%ON}m)bvs28l(J;<42%FE<2E1I{oV95%{?$&|*{2Xx&6l{7)hvu?=+q9Kh< z--f3JDE3Yk7?LiV+S$+k&N59hYO$d>nYS@ZV-hxeB1{= zc)`y-@Xizf3d07Ftj{8GVrD4xU9XAl8qj|9H%0J~MSM zfY<{`WK169rXcz(f&EX*V=eHRH{Iz^6*jv~q;Z(EJ1Ik#$U}p^ zb*_5_>pgc0o6mA%5=jH#GH~?M)!y_1L_OXD%K2HYEm)Bly6bnxJ6FCAl6fL!r=6)V zk<1>ASc`q_hqvQ2E+B(w=)vkoqPj~+Y~kr>E0 z|76(`$*}FEKmDKJJ|fYlZD(eTs6|U-0ns3Q>LX8l#kKwTjhDvb|5=rC=sLyv3ZXXj z5Jjb6vz+|N!@wPk+=+x# zwWD;(R0_(4ObULB!KXwFs$>a@d&yR0@qV#M2by>#lLK!mN3e-gouqa%*GTRn3T$0 zYzeJ&Oq7E+s@Tgftjx`PppSHhy3~n;;!LQB6tjd1m1KxP9L?2yOpo}-rAVG}WX+%W zlWEk+hiJ;#)J?a^00`tueEO3j+)Y?w3E5-{SxgA89M0tw8m~gasJI|uWKI{MO!sq$ zv1HEdtQhCqL8)*N?erYmbc$;HP4G0&=Jd^k|LDu5@Wkx29jYkDgm}&Kq)&L^PK9X8 z`lO8XB#1uL&;N9n`%DPN1W?4FK}Rgm1br6)MTnkMP{r6ugAmLIrO-uTP=k=m3grs& zL?S65z&Fj&lSar%q)oYgwYx;6KFU>gCI~FWeU{v zM;-;zG?Av%bkHERPKEGKBUMr&8PbLDuO)qo^h8i7rP3m4QiUkeD*Xu`H3$;b(k~s5 zEA3D)Rf^5?P%=f+@DS63pwTqF3EzBCH)K!($*ic4;=sQY12uGdOU2Tl7>qao;uS@0CVPytHQEJsz$hgOW_*5#b#yH*9as3Lfde#yprBo%? zb;XLC@=_|5q4{LjdCdxnid8CAMscOreZ7i!`qm{?Mrq~Oft88|zzjE?oNFc6g>4E0 za8(|)fO}=wiKW3L{+p#6vvNhYYMccGh+qGrewsqULh1 + + + + + + + + +
+

200 Success

+

Hello World! Welcome to your Workers Site.

+ a happy crab is wearing a cowboy hat and holding a lasso. 200 success. +
+ + diff --git a/fixtures/sites-app/src/modules.js b/fixtures/sites-app/src/modules.js new file mode 100644 index 0000000..7073b4b --- /dev/null +++ b/fixtures/sites-app/src/modules.js @@ -0,0 +1,88 @@ +import { + getAssetFromKV, + mapRequestToAsset, +} from "@cloudflare/kv-asset-handler"; +import manifestJSON from "__STATIC_CONTENT_MANIFEST"; + +const assetManifest = JSON.parse(manifestJSON); + +/** + * The DEBUG flag will do two things that help during development: + * 1. we will skip caching on the edge, which makes it easier to + * debug. + * 2. we will return an error message on exception in your Response rather + * than the default 404.html page. + */ +const DEBUG = false; + +export default { + async fetch(request, env, ctx) { + let options = { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: assetManifest, + }; + + /** + * You can add custom logic to how we fetch your assets + * by configuring the function `mapRequestToAsset` + */ + // options.mapRequestToAsset = handlePrefix(/^\/docs/) + + try { + if (DEBUG) { + // customize caching + options.cacheControl = { + bypassCache: true, + }; + } + + const page = await getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise); + }, + }, + options + ); + + // allow headers to be altered + const response = new Response(page.body, page); + + response.headers.set("X-XSS-Protection", "1; mode=block"); + response.headers.set("X-Content-Type-Options", "nosniff"); + response.headers.set("X-Frame-Options", "DENY"); + response.headers.set("Referrer-Policy", "unsafe-url"); + response.headers.set("Feature-Policy", "none"); + + return response; + } catch (e) { + // if an error is thrown try to serve the asset at 404.html + if (!DEBUG) { + try { + let notFoundResponse = await getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise); + }, + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: assetManifest, + mapRequestToAsset: (req) => + new Request(`${new URL(req.url).origin}/404.html`, req), + } + ); + + return new Response(notFoundResponse.body, { + ...notFoundResponse, + status: 404, + }); + } catch (e) {} + } + + return new Response(e.message || e.toString(), { status: 500 }); + } + }, +}; diff --git a/fixtures/sites-app/src/service-worker.js b/fixtures/sites-app/src/service-worker.js new file mode 100644 index 0000000..9cff3ea --- /dev/null +++ b/fixtures/sites-app/src/service-worker.js @@ -0,0 +1,66 @@ +import { + getAssetFromKV, + mapRequestToAsset, +} from "@cloudflare/kv-asset-handler"; + +/** + * The DEBUG flag will do two things that help during development: + * 1. we will skip caching on the edge, which makes it easier to + * debug. + * 2. we will return an error message on exception in your Response rather + * than the default 404.html page. + */ +const DEBUG = false; + +addEventListener("fetch", (event) => { + event.respondWith(handleEvent(event)); +}); + +async function handleEvent(event) { + let options = {}; + + /** + * You can add custom logic to how we fetch your assets + * by configuring the function `mapRequestToAsset` + */ + // options.mapRequestToAsset = handlePrefix(/^\/docs/) + + try { + if (DEBUG) { + // customize caching + options.cacheControl = { + bypassCache: true, + }; + } + + const page = await getAssetFromKV(event, options); + + // allow headers to be altered + const response = new Response(page.body, page); + + response.headers.set("X-XSS-Protection", "1; mode=block"); + response.headers.set("X-Content-Type-Options", "nosniff"); + response.headers.set("X-Frame-Options", "DENY"); + response.headers.set("Referrer-Policy", "unsafe-url"); + response.headers.set("Feature-Policy", "none"); + + return response; + } catch (e) { + // if an error is thrown try to serve the asset at 404.html + if (!DEBUG) { + try { + let notFoundResponse = await getAssetFromKV(event, { + mapRequestToAsset: (req) => + new Request(`${new URL(req.url).origin}/404.html`, req), + }); + + return new Response(notFoundResponse.body, { + ...notFoundResponse, + status: 404, + }); + } catch (e) {} + } + + return new Response(e.message || e.toString(), { status: 500 }); + } +} diff --git a/fixtures/sites-app/wrangler.jsonc b/fixtures/sites-app/wrangler.jsonc new file mode 100644 index 0000000..ea93578 --- /dev/null +++ b/fixtures/sites-app/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "sites-app", + "site": { + "bucket": "./public", + }, + "main": "src/modules.js", +} diff --git a/fixtures/start-worker-node-test/package.json b/fixtures/start-worker-node-test/package.json new file mode 100644 index 0000000..28c6e4d --- /dev/null +++ b/fixtures/start-worker-node-test/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/start-worker-node", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "scripts": { + "test:ci": "node --test --test-timeout=15000" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@types/is-even": "^1.0.2", + "get-port": "^7.1.0", + "is-even": "^1.0.0", + "miniflare": "workspace:*", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/start-worker-node-test/src/config-errors.test.js b/fixtures/start-worker-node-test/src/config-errors.test.js new file mode 100644 index 0000000..a1dcadc --- /dev/null +++ b/fixtures/start-worker-node-test/src/config-errors.test.js @@ -0,0 +1,102 @@ +import assert from "node:assert"; +import test, { describe } from "node:test"; +import getPort from "get-port"; +import { unstable_startWorker } from "wrangler"; + +describe("startWorker - configuration errors", () => { + test("providing an incorrect entrypoint to startWorker", async () => { + await assert.rejects( + unstable_startWorker({ + entrypoint: "not a real entrypoint", + }), + (err) => { + assert(err instanceof Error); + assert.strictEqual( + err.message, + "An error occurred when starting the server" + ); + const cause = err.cause; + assert(cause instanceof Error); + assert.match( + cause.message, + /The entry-point file at "not a real entrypoint" was not found\./ + ); + return true; + } + ); + }); + + test("providing a non existing config file to startWorker", async () => { + await assert.rejects( + unstable_startWorker({ config: "non-existing-config" }), + (err) => { + assert(err instanceof Error); + assert.strictEqual( + err.message, + "An error occurred when starting the server" + ); + const cause = err.cause; + assert(cause instanceof Error); + assert.match( + cause.message, + /Missing entry-point to Worker script or to assets directory/ + ); + return true; + } + ); + }); + + test("providing an incorrect config to setConfig", async () => { + const worker = await unstable_startWorker({ + config: "wrangler.json", + dev: { + persist: false, + server: { + port: await getPort(), + }, + inspector: false, + }, + }); + + await assert.rejects( + worker.setConfig({ config: "non-existing-config" }, true), + (err) => { + assert(err instanceof Error); + assert.match( + err.message, + /Missing entry-point to Worker script or to assets directory/ + ); + return true; + } + ); + + await worker.dispose(); + }); + + test("providing an incorrect entrypoint to setConfig", async () => { + const worker = await unstable_startWorker({ + config: "wrangler.json", + dev: { + persist: false, + server: { + port: await getPort(), + }, + inspector: false, + }, + }); + + await assert.rejects( + worker.setConfig({ entrypoint: "not a real entrypoint" }, true), + (err) => { + assert(err instanceof Error); + assert.match( + err.message, + /The entry-point file at "not a real entrypoint" was not found\./ + ); + return true; + } + ); + + await worker.dispose(); + }); +}); diff --git a/fixtures/start-worker-node-test/src/index.test.js b/fixtures/start-worker-node-test/src/index.test.js new file mode 100644 index 0000000..451c705 --- /dev/null +++ b/fixtures/start-worker-node-test/src/index.test.js @@ -0,0 +1,28 @@ +import assert from "node:assert"; +import test, { after, before, describe } from "node:test"; +import { unstable_startWorker } from "wrangler"; + +describe("worker", () => { + /** + * @type {Awaited>} + */ + let worker; + + before(async () => { + worker = await unstable_startWorker({ + config: "wrangler.json", + dev: { persist: false }, + }); + }); + + test("hello world", async () => { + assert.strictEqual( + await (await worker.fetch("http://example.com")).text(), + "Hello from even" + ); + }); + + after(async () => { + await worker.dispose(); + }); +}); diff --git a/fixtures/start-worker-node-test/src/index.ts b/fixtures/start-worker-node-test/src/index.ts new file mode 100644 index 0000000..ac53f66 --- /dev/null +++ b/fixtures/start-worker-node-test/src/index.ts @@ -0,0 +1,13 @@ +import isEven from "is-even"; +import { sayHello } from "./say-hello"; + +export default { + async fetch(request): Promise { + const url = new URL(request.url); + return new Response( + sayHello( + isEven(Number(url.searchParams.get("number") ?? "0")) ? "even" : "odd" + ) + ); + }, +} satisfies ExportedHandler; diff --git a/fixtures/start-worker-node-test/src/say-hello.ts b/fixtures/start-worker-node-test/src/say-hello.ts new file mode 100644 index 0000000..b83bb8b --- /dev/null +++ b/fixtures/start-worker-node-test/src/say-hello.ts @@ -0,0 +1,3 @@ +export function sayHello(url: string) { + return `Hello from ${url}`; +} diff --git a/fixtures/start-worker-node-test/tsconfig.json b/fixtures/start-worker-node-test/tsconfig.json new file mode 100644 index 0000000..3f9d8e2 --- /dev/null +++ b/fixtures/start-worker-node-test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node", "@cloudflare/workers-types"], + "checkJs": true + }, + "include": ["**/*.js", "**/*.ts"] +} diff --git a/fixtures/start-worker-node-test/wrangler.json b/fixtures/start-worker-node-test/wrangler.json new file mode 100644 index 0000000..46ed31d --- /dev/null +++ b/fixtures/start-worker-node-test/wrangler.json @@ -0,0 +1,5 @@ +{ + "name": "build-miniflare", + "main": "src/index.ts", + "compatibility_date": "2025-01-16" +} diff --git a/fixtures/unbound-durable-object/package.json b/fixtures/unbound-durable-object/package.json new file mode 100644 index 0000000..e3b68ba --- /dev/null +++ b/fixtures/unbound-durable-object/package.json @@ -0,0 +1,15 @@ +{ + "name": "@fixture/unbound-durable-object", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/unbound-durable-object/src/index.ts b/fixtures/unbound-durable-object/src/index.ts new file mode 100644 index 0000000..b8b6164 --- /dev/null +++ b/fixtures/unbound-durable-object/src/index.ts @@ -0,0 +1,60 @@ +import { DurableObject } from "cloudflare:workers"; + +export class Counter extends DurableObject { + async getCounterValue() { + let value = (await this.ctx.storage.get("value")) || 0; + return value; + } + + async increment(amount = 1) { + let value = (await this.ctx.storage.get("value")) || 0; + value += amount; + await this.ctx.storage.put("value", value); + return value; + } + + async decrement(amount = 1) { + let value = (await this.ctx.storage.get("value")) || 0; + value -= amount; + await this.ctx.storage.put("value", value); + return value; + } +} + +export default { + async fetch( + request: Request, + _env: never, + ctx: ExecutionContext + ): Promise { + let url = new URL(request.url); + let name = url.searchParams.get("name"); + if (!name) { + return new Response( + "Select a Durable Object to contact by using" + + " the `name` URL query string parameter, for example, ?name=A" + ); + } + + let stub = ctx.exports.Counter.getByName(name); + + // Send a request to the Durable Object using RPC methods, then await its response. + let count = null; + switch (url.pathname) { + case "/increment": + count = await stub.increment(); + break; + case "/decrement": + count = await stub.decrement(); + break; + case "/": + // Serves the current value. + count = await stub.getCounterValue(); + break; + default: + return new Response("Not found", { status: 404 }); + } + + return new Response(`count: ${count}`); + }, +}; diff --git a/fixtures/unbound-durable-object/tests/index.test.ts b/fixtures/unbound-durable-object/tests/index.test.ts new file mode 100644 index 0000000..e47febd --- /dev/null +++ b/fixtures/unbound-durable-object/tests/index.test.ts @@ -0,0 +1,30 @@ +import { resolve } from "path"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { createTestHarness } from "wrangler"; + +const basePath = resolve(__dirname, ".."); +const server = createTestHarness({ + root: basePath, + workers: [{ configPath: "wrangler.jsonc" }], +}); + +describe("Unbound DO is available through `ctx.exports`", () => { + beforeAll(async () => { + await server.listen(); + }); + + afterAll(async () => { + await server.close(); + }); + + it("can execute storage operations", async ({ expect }) => { + const doName = crypto.randomUUID(); + let response = await server.fetch(`/?name=${doName}`); + let content = await response.text(); + expect(content).toMatchInlineSnapshot(`"count: 0"`); + + response = await server.fetch(`/increment?name=${doName}`); + content = await response.text(); + expect(content).toMatchInlineSnapshot(`"count: 1"`); + }); +}); diff --git a/fixtures/unbound-durable-object/tests/tsconfig.json b/fixtures/unbound-durable-object/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/unbound-durable-object/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/unbound-durable-object/tsconfig.json b/fixtures/unbound-durable-object/tsconfig.json new file mode 100644 index 0000000..2431bac --- /dev/null +++ b/fixtures/unbound-durable-object/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "types": ["@cloudflare/workers-types/experimental"], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/unbound-durable-object/vitest.config.mts b/fixtures/unbound-durable-object/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/unbound-durable-object/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/unbound-durable-object/wrangler.jsonc b/fixtures/unbound-durable-object/wrangler.jsonc new file mode 100644 index 0000000..e4f9f04 --- /dev/null +++ b/fixtures/unbound-durable-object/wrangler.jsonc @@ -0,0 +1,21 @@ +{ + "name": "unbound-durable-object", + "main": "src/index.ts", + "compatibility_date": "2025-01-01", + "compatibility_flags": ["enable_ctx_exports"], + // This DO intentionally doesn't have a binding + // "durable_objects": { + // "bindings": [ + // { + // "name": "COUNTER", + // "class_name": "Counter", + // }, + // ], + // }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["Counter"], + }, + ], +} diff --git a/fixtures/unsafe-external-plugin/README.md b/fixtures/unsafe-external-plugin/README.md new file mode 100644 index 0000000..e9ded4e --- /dev/null +++ b/fixtures/unsafe-external-plugin/README.md @@ -0,0 +1,3 @@ +# Unsafe External Plugin + +This folder contains an example of an Unsafe External Miniflare Plugin that extends Miniflare's suite of plugins with local development simulators. An example of usage can be found in the [worker-with-unsafe-exteral-plugin fixture](../worker-with-unsafe-external-plugin/). diff --git a/fixtures/unsafe-external-plugin/package.json b/fixtures/unsafe-external-plugin/package.json new file mode 100644 index 0000000..dbaccda --- /dev/null +++ b/fixtures/unsafe-external-plugin/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/unsafe-external-plugin", + "private": true, + "description": "An example of an unsafe external plugin for Miniflare", + "license": "ISC", + "author": "", + "main": "dist/index.js", + "scripts": { + "build": "tsx tools/build.ts" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "esbuild": "catalog:default", + "miniflare": "workspace:*", + "tsx": "^3.12.8", + "zod": "3.22.3" + } +} diff --git a/fixtures/unsafe-external-plugin/src/index.ts b/fixtures/unsafe-external-plugin/src/index.ts new file mode 100644 index 0000000..75d1444 --- /dev/null +++ b/fixtures/unsafe-external-plugin/src/index.ts @@ -0,0 +1,8 @@ +import { + UNSAFE_PLUGIN_NAME, + UNSAFE_SERVICE_PLUGIN, +} from "./plugins/unsafe-service"; + +export const plugins = { + [UNSAFE_PLUGIN_NAME]: UNSAFE_SERVICE_PLUGIN, +}; diff --git a/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts b/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts new file mode 100644 index 0000000..1218e9a --- /dev/null +++ b/fixtures/unsafe-external-plugin/src/plugins/unsafe-service.ts @@ -0,0 +1,149 @@ +import fs from "node:fs/promises"; +import { + getMiniflareObjectBindings, + getPersistPath, + Plugin, + ProxyNodeBinding, + SERVICE_LOOPBACK, + SharedBindings, +} from "miniflare"; +// The below imports (prefixed with `worker:`) +// will be converted by our ESBuild plugin +// into functions that load the transpiled Workers as JS +import BINDING_WORKER from "worker:binding.worker"; +import OBJECT_WORKER from "worker:object.worker"; +import { z } from "zod"; +import type { Service, Worker_Binding } from "miniflare"; + +export const UNSAFE_PLUGIN_NAME = "unsafe-plugin"; + +export const UnsafeServiceBindingOptionSchema = z + .array( + z.object({ + name: z.string(), + type: z.string(), + plugin: z.object({ + package: z.string(), + name: z.string(), + }), + options: z.object({ emitLogs: z.boolean() }), + }) + ) + .or(z.undefined()); + +export const UNSAFE_SERVICE_PLUGIN: Plugin< + typeof UnsafeServiceBindingOptionSchema +> = { + options: UnsafeServiceBindingOptionSchema, + /** + * getBindings will add bindings to the user's Workers. Specifically, we add a binding to a service + * that will expose an `UnsafeBindingServiceEntrypoint` + * @param options - A map of bindings names to options provided for that binding. + * @returns + */ + async getBindings(options) { + return options?.map((binding) => { + return { + name: binding.name, + service: { + name: `${UNSAFE_PLUGIN_NAME}:${binding.name}`, + entrypoint: "UnsafeBindingServiceEntrypoint", + }, + }; + }); + }, + getNodeBindings(options) { + return Object.fromEntries( + options?.map((binding) => [binding.name, new ProxyNodeBinding()]) ?? [] + ); + }, + async getServices({ + options, + tmpPath, + defaultPersistRoot, + unsafeStickyBlobs, + }) { + if (!options || options.length === 0) { + return []; + } + + const persistPath = getPersistPath( + UNSAFE_PLUGIN_NAME, + tmpPath, + defaultPersistRoot, + undefined + ); + + await fs.mkdir(persistPath, { recursive: true }); + + // Create a service that will persist any data + const storageService = { + name: `${UNSAFE_PLUGIN_NAME}:storage`, + disk: { path: persistPath, writable: true }, + } satisfies Service; + + const objectService = { + name: `${UNSAFE_PLUGIN_NAME}:object`, + worker: { + compatibilityDate: "2025-01-01", + modules: [ + { + name: "object.worker.js", + esModule: OBJECT_WORKER(), + }, + ], + durableObjectNamespaces: [ + { + className: "UnsafeBindingObject", + uniqueKey: `miniflare-unsafe-binding-UnsafeBindingObject`, + }, + ], + // Store Durable Object SQL databases in persist path + durableObjectStorage: { localDisk: storageService.name }, + // Bind blob disk directory service to object + bindings: [ + { + name: SharedBindings.MAYBE_SERVICE_BLOBS, + service: { name: storageService.name }, + }, + { + name: SharedBindings.MAYBE_SERVICE_LOOPBACK, + service: { name: SERVICE_LOOPBACK }, + }, + ...getMiniflareObjectBindings(unsafeStickyBlobs), + ], + }, + } satisfies Service; + + const bindingWorker = options.map( + (binding) => + ({ + name: `${UNSAFE_PLUGIN_NAME}:${binding.name}`, + worker: { + compatibilityDate: "2025-01-01", + modules: [ + { + name: "binding.worker.js", + esModule: BINDING_WORKER(), + }, + ], + bindings: [ + { + name: "config", + json: JSON.stringify(binding.options), + }, + { + name: "store", + durableObjectNamespace: { + className: "UnsafeBindingObject", + serviceName: objectService.name, + }, + }, + ], + }, + }) satisfies Service + ); + + return [...bindingWorker, storageService, objectService]; + }, +}; diff --git a/fixtures/unsafe-external-plugin/src/plugins/worker-shim.ts b/fixtures/unsafe-external-plugin/src/plugins/worker-shim.ts new file mode 100644 index 0000000..a3f08ac --- /dev/null +++ b/fixtures/unsafe-external-plugin/src/plugins/worker-shim.ts @@ -0,0 +1,7 @@ +/** + * ESBuild will build the Workers from '../workers' and provide the built script + * files as variables on the global scope. + */ +declare module "worker:*" { + export default function (): string; +} diff --git a/fixtures/unsafe-external-plugin/src/workers/binding.worker.ts b/fixtures/unsafe-external-plugin/src/workers/binding.worker.ts new file mode 100644 index 0000000..df3db25 --- /dev/null +++ b/fixtures/unsafe-external-plugin/src/workers/binding.worker.ts @@ -0,0 +1,61 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; +import type { UnsafeBindingObject } from "./object.worker"; + +// ENV configuration +interface Env { + config: { emitLogs: boolean }; + store: DurableObjectNamespace; +} + +/** + * UnsafeBinding offers two RPCs, `performUnsafeWrite` and `performUnsafeRead`. + */ +export class UnsafeBindingServiceEntrypoint extends WorkerEntrypoint { + override async fetch(_request: Request): Promise { + return new Response("This is a development stub for an unsafe worker.", { + status: 200, + statusText: "OK", + headers: { + "content-type": "text/plain", + }, + }); + } + + async performUnsafeWrite(key: string, value: number) { + if (this.env.config.emitLogs) { + console.log("Emitting a log for write operation"); + } + const objectNamespace = this.env.store; + const namespaceId = JSON.stringify(this.env.config); + const id = objectNamespace.idFromName(namespaceId); + const stub = objectNamespace.get(id); + await stub.set(key, value); + + return { + ok: true, + result: `Set key ${key} to ${value}`, + meta: { + workersVersion: "my-version-from-dev", + }, + }; + } + + async performUnsafeRead(key: string) { + if (this.env.config.emitLogs) { + console.log("Emitting a log for read operation"); + } + const objectNamespace = this.env.store; + const namespaceId = JSON.stringify(this.env.config); + const id = objectNamespace.idFromName(namespaceId); + const stub = objectNamespace.get(id); + const value = await stub.get(key); + + return { + ok: true, + result: value, + meta: { + workersVersion: "my-version-from-dev", + }, + }; + } +} diff --git a/fixtures/unsafe-external-plugin/src/workers/object.worker.ts b/fixtures/unsafe-external-plugin/src/workers/object.worker.ts new file mode 100644 index 0000000..13e179e --- /dev/null +++ b/fixtures/unsafe-external-plugin/src/workers/object.worker.ts @@ -0,0 +1,11 @@ +import { DurableObject } from "cloudflare:workers"; + +export class UnsafeBindingObject extends DurableObject { + async get(tag: string) { + return await this.ctx.storage.get(tag); + } + + async set(key: string, value: number) { + await this.ctx.storage.put(key, value); + } +} diff --git a/fixtures/unsafe-external-plugin/tools/build.ts b/fixtures/unsafe-external-plugin/tools/build.ts new file mode 100644 index 0000000..095c36b --- /dev/null +++ b/fixtures/unsafe-external-plugin/tools/build.ts @@ -0,0 +1,110 @@ +import { join, resolve } from "node:path"; +import { + BuildContext, + BuildOptions, + context, + Plugin, + build as runBuild, +} from "esbuild"; + +type EmbedWorkersOptions = { + /** + * workersRootDir is a path to a directory containing external Workers that + * will be bundled + */ + workersRootDir: string; + workerOutputDir: string; +}; + +/** + * embedWorkerPlugin is an ESBuild plugin that will look for imports to Workers specified by `worker:` + * and bundle them into the final output. + */ +export const embedWorkersPlugin: (options: EmbedWorkersOptions) => Plugin = ({ + workerOutputDir, + workersRootDir, +}) => { + return { + name: "embed-workers", + setup(build) { + const namespace = "embed-worker"; + // For imports prefixed with `worker:`, attempt to resolve them from a directory containing + // your Workers + build.onResolve({ filter: /^worker:/ }, async (args) => { + let name = args.path.substring("worker:".length); + // Allow `.worker` to be omitted + if (!name.endsWith(".worker")) name += ".worker"; + // Use `build.resolve()` API so Workers can be written as `m?[jt]s` files + const result = await build.resolve("./" + name, { + kind: "import-statement", + // Resolve relative to the directory containing the Workers + resolveDir: workersRootDir, + }); + if (result.errors.length > 0) return { errors: result.errors }; + return { path: result.path, namespace }; + }); + build.onLoad({ filter: /.*/, namespace }, async (args) => { + await runBuild({ + platform: "node", // Marks `node:*` imports as external + format: "esm", + target: "esnext", + bundle: true, + sourcemap: true, + sourcesContent: false, + external: ["cloudflare:workers"], + entryPoints: [args.path], + minifySyntax: true, + outdir: workerOutputDir, + plugins: [], + }); + + let outPath = args.path.substring(workersRootDir.length + 1); + outPath = outPath.substring(0, outPath.lastIndexOf(".")) + ".js"; + outPath = JSON.stringify(outPath); + const contents = ` + import fs from "fs"; + import path from "path"; + import url from "url"; + let contents; + export default function() { + if (contents !== undefined) return contents; + const filePath = path.join(__dirname, "workers", ${outPath}); + contents = fs.readFileSync(filePath, "utf8") + "//# sourceURL=" + url.pathToFileURL(filePath); + return contents; + } + `; + return { contents, loader: "js" }; + }); + }, + }; +}; + +const distDir = resolve(__dirname, "../dist"); +// When outputting the Worker, map to the structure of 'src'. +// This means the plugin will outout the build Workers to a `workers` dist in `dir` +const workerOutputDir = resolve(distDir, "workers"); +const workersDir = resolve(__dirname, "../src/workers"); + +export async function buildPackage() { + console.log("Building the module"); + await runBuild({ + platform: "node", + target: "esnext", + format: "cjs", + bundle: true, + sourcemap: true, + entryPoints: ["src/index.ts"], + plugins: [ + embedWorkersPlugin({ + workersRootDir: workersDir, + workerOutputDir, + }), + ], + external: ["@cloudflare/workers-types", "miniflare"], + outdir: distDir, + }); +} + +buildPackage().catch((exc) => { + console.error("Failed to build external package", `${exc}`); +}); diff --git a/fixtures/unsafe-external-plugin/tsconfig.json b/fixtures/unsafe-external-plugin/tsconfig.json new file mode 100644 index 0000000..35cdf79 --- /dev/null +++ b/fixtures/unsafe-external-plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "types": ["@cloudflare/workers-types/experimental"], + "noEmit": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/unsafe-external-plugin/turbo.json b/fixtures/unsafe-external-plugin/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/unsafe-external-plugin/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/vitest-pool-workers-examples/README.md b/fixtures/vitest-pool-workers-examples/README.md new file mode 100644 index 0000000..d883b88 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/README.md @@ -0,0 +1,26 @@ +# `@cloudflare/vitest-pool-workers` Examples + +This directory contains example projects tested with `@cloudflare/vitest-pool-workers`. It aims to provide the building blocks for you to write tests for your own Workers. + +| Directory | Overview | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [✅ basics-unit-integration-self](basics-unit-integration-self) | Basic unit tests and integration tests using `exports.default` | +| [⚠️ basics-integration-auxiliary](basics-integration-auxiliary) | Basic integration tests using an auxiliary worker[^1] | +| [⚡️ pages-functions-unit-integration-self](pages-functions-unit-integration-self) | Functions unit tests and integration tests using `exports.default` | +| [📦 kv-r2-caches](kv-r2-caches) | Tests using KV, R2 and the Cache API | +| [📚 d1](d1) | Tests using D1 with migrations | +| [📌 durable-objects](durable-objects) | Tests using Durable Objects with direct access, alarms and eviction | +| [🔁 workflows](workflows) | Tests using Workflows | +| [🚥 queues](queues) | Tests using Queue producers and consumers | +| [🚰 pipelines](pipelines) | Tests using Pipelines | +| [🚀 hyperdrive](hyperdrive) | Tests using Hyperdrive with a Vitest managed TCP server | +| [🤹 request-mocking](request-mocking) | Tests using declarative (MSW) / imperative outbound request mocks | +| [🔌 multiple-workers](multiple-workers) | Tests using multiple auxiliary workers and request mocks | +| [⚙️ web-assembly](web-assembly) | Tests importing WebAssembly modules | +| [🤯 rpc](rpc) | Tests using named entrypoints, Durable Objects and RPC | +| [🧠 ai-vectorize](ai-vectorize) | Tests using Workers AI and Vectorize | +| [🔄 context-exports](context-exports) | Tests using context exports | +| [📥 dynamic-import](dynamic-import) | Tests using dynamic imports | +| [🖼️ images](images) | Tests using the Images binding | + +[^1]: When using `exports.default` for integration tests, your worker code runs in the same context as the test runner. This means you can use global mocks to control your worker, but also means your worker uses the same subtly different module resolution behaviour provided by Vite. Usually this isn't a problem, but if you'd like to run your worker in a fresh environment that's as close to production as possible, using an auxiliary worker may be a good idea. Note this prevents global mocks from controlling your worker, and requires you to build your worker ahead-of-time. This means your tests won't re-run automatically if you change your worker's source code, but could be useful if you have a complicated build process (e.g. full-stack framework). diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/README.md b/fixtures/vitest-pool-workers-examples/ai-vectorize/README.md new file mode 100644 index 0000000..e8d3ae2 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/README.md @@ -0,0 +1,7 @@ +# 🤖 AI and Vectorize + +This Worker uses the AI and Vectorize bindings. @cloudflare/vitest-pool-workers@^0.8.1 is required to use AI and Vectorize bindings in the Vitest integration. + +[!WARNING] + +Because Workers AI and Vectorize bindings do not have a local simulator, usage of these bindings will always access your Cloudflare account, and so will incur usage charges even in local development and testing. We recommend mocking any usage of these bindings in your tests as demonstrated in this fixture. diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/global-setup.ts b/fixtures/vitest-pool-workers-examples/ai-vectorize/global-setup.ts new file mode 100644 index 0000000..4f7d9cb --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/global-setup.ts @@ -0,0 +1,9 @@ +import childProcess from "node:child_process"; + +// Global setup runs inside Node.js, not `workerd` +export default function () { + const label = "Built ai-vectorize Worker"; + console.time(label); + childProcess.execSync("wrangler build", { cwd: __dirname }); + console.timeEnd(label); +} diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/src/env.d.ts b/fixtures/vitest-pool-workers-examples/ai-vectorize/src/env.d.ts new file mode 100644 index 0000000..e0d7259 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/src/env.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./ai-vectorize/src/env.d.ts -c ./ai-vectorize/wrangler.jsonc --no-include-runtime` (hash: 54950e4bb5477ca70c1306130c0748c4) +interface __BaseEnv_Env { + VECTORIZE: VectorizeIndex; + AI: Ai; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/src/index.ts b/fixtures/vitest-pool-workers-examples/ai-vectorize/src/index.ts new file mode 100644 index 0000000..d693e06 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/src/index.ts @@ -0,0 +1,31 @@ +export default { + async fetch(request, env, ctx): Promise { + const path = new URL(request.url).pathname; + + if (path === "/ai") { + const stories = [ + "This is a story about an orange cloud", + "This is a story about a llama", + ]; + const modelResp: Ai_Cf_Baai_Bge_Base_En_V1_5_Output = await env.AI.run( + "@cf/baai/bge-base-en-v1.5", + { + text: stories, + } + ); + return Response.json(modelResp); + } + + if (path === "/vectorize") { + const vectors: VectorizeVector[] = [ + { id: "123", values: [...Array(768).keys()] }, + { id: "456", values: [...Array(768).keys()] }, + ]; + + let inserted = await env.VECTORIZE.upsert(vectors); + return Response.json(inserted); + } + + return new Response("Hello World!"); + }, +} satisfies ExportedHandler; diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/ai-vectorize/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/test/index.spec.ts b/fixtures/vitest-pool-workers-examples/ai-vectorize/test/index.spec.ts new file mode 100644 index 0000000..ae22f4c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/test/index.spec.ts @@ -0,0 +1,80 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { describe, it, vi } from "vitest"; +import worker from "../src/index"; + +// For now, you'll need to do something like this to get a correctly-typed +// `Request` to pass to `worker.fetch()`. +const IncomingRequest = Request; + +describe("Tests that do hit the AI binding", () => { + describe("unit style", () => { + it("lets you mock the ai binding", async ({ expect }) => { + const request = new IncomingRequest("http://example.com/ai"); + + const ctx = createExecutionContext(); + + // mock the AI run function by directly modifying `env` + vi.spyOn(env.AI, "run").mockResolvedValue({ + shape: [1, 2], + data: [[0, 0]], + }); + const response = await worker.fetch(request, env, ctx); + + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot( + `"{"shape":[1,2],"data":[[0,0]]}"` + ); + }); + + it("lets you mock the vectorize binding", async ({ expect }) => { + const request = new IncomingRequest("http://example.com/vectorize"); + const ctx = createExecutionContext(); + + // mock the vectorize upsert function by directly modifying `env` + const mockVectorizeStore: VectorizeVector[] = []; + + vi.spyOn(env.VECTORIZE, "upsert").mockImplementation(async (vectors) => { + mockVectorizeStore.push(...vectors); + return { + mutationId: "123", + count: vectors.length, + ids: vectors.map((v) => v.id), + }; + }); + + const response = await worker.fetch(request, env, ctx); + + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot( + `"{"mutationId":"123","count":2,"ids":["123","456"]}"` + ); + expect(mockVectorizeStore.map((v) => v.id)).toMatchInlineSnapshot(` + [ + "123", + "456", + ] + `); + }); + }); +}); + +describe("Tests that do not hit the AI binding", () => { + it("responds with Hello World! (unit style)", async ({ expect }) => { + const request = new IncomingRequest("http://example.com"); + // Create an empty context to pass to `worker.fetch()`. + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); + }); + + it("responds with Hello World! (integration style)", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com"); + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/ai-vectorize/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/tsconfig.json b/fixtures/vitest-pool-workers-examples/ai-vectorize/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/vitest.config.ts b/fixtures/vitest-pool-workers-examples/ai-vectorize/vitest.config.ts new file mode 100644 index 0000000..1215075 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/vitest.config.ts @@ -0,0 +1,18 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + remoteBindings: false, + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + test: { + globalSetup: ["./global-setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/ai-vectorize/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/ai-vectorize/wrangler.jsonc new file mode 100644 index 0000000..e710172 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/ai-vectorize/wrangler.jsonc @@ -0,0 +1,15 @@ +{ + "name": "ai-vectorize", + "main": "src/index.ts", + // compatibility_date is required so we can run `wrangler build` + "compatibility_date": "2025-03-20", + "ai": { + "binding": "AI", + }, + "vectorize": [ + { + "binding": "VECTORIZE", + "index_name": "embeddings", + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/README.md b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/README.md new file mode 100644 index 0000000..294fc96 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/README.md @@ -0,0 +1,8 @@ +# ⚠️ basics-integration-auxiliary + +This Worker contains basic `fetch` and `scheduled` handlers. Instead of running the Worker-under-test in the same Worker as the test runner, this example defines the Worker-under-test as an _auxiliary_ Worker. This means the Worker runs in a separate isolate to the test runner, with a different global scope. The Worker-under-test runs in an environment closer to production, but Vite transformations and hot-module-reloading aren't applied to the Worker—you must compile your TypeScript to JavaScript beforehand. This is done in [global-setup.ts](global-setup.ts). Note auxiliary workers cannot be configured from `wrangler.toml` files—you must use Miniflare `WorkerOptions`. + +| Test | Overview | +| ----------------------------------------------------------------------------------------- | ---------------------------------- | +| [fetch-integration-auxiliary.test.ts](test/fetch-integration-auxiliary.test.ts) | Basic `fetch` integration test | +| [scheduled-integration-auxiliary.test.ts](test%2Fscheduled-integration-auxiliary.test.ts) | Basic `scheduled` integration test | diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/global-setup.ts b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/global-setup.ts new file mode 100644 index 0000000..a0cbf0e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/global-setup.ts @@ -0,0 +1,9 @@ +import childProcess from "node:child_process"; + +// Global setup runs inside Node.js, not `workerd` +export default function () { + const label = "Built basics-integration-auxiliary worker"; + console.time(label); + childProcess.execSync("wrangler build", { cwd: __dirname }); + console.timeEnd(label); +} diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/index.ts b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/index.ts new file mode 100644 index 0000000..bcd8d09 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/index.ts @@ -0,0 +1,8 @@ +export default { + async fetch(request, env, ctx) { + return new Response("👋"); + }, + async scheduled(controller, env, ctx) { + // ... + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/env.d.ts b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/env.d.ts new file mode 100644 index 0000000..8792875 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/env.d.ts @@ -0,0 +1,5 @@ +declare namespace Cloudflare { + interface Env { + WORKER: Fetcher; + } +} diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/fetch-integration-auxiliary.test.ts b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/fetch-integration-auxiliary.test.ts new file mode 100644 index 0000000..d3f0d87 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/fetch-integration-auxiliary.test.ts @@ -0,0 +1,7 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +it("dispatches fetch event", async ({ expect }) => { + const response = await env.WORKER.fetch("http://example.com"); + expect(await response.text()).toBe("👋"); +}); diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/scheduled-integration-auxiliary.test.ts b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/scheduled-integration-auxiliary.test.ts new file mode 100644 index 0000000..8d7de82 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/scheduled-integration-auxiliary.test.ts @@ -0,0 +1,12 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +it("dispatches scheduled event", async ({ expect }) => { + // Note the `Fetcher#scheduled()` method is experimental, and requires the + // `service_binding_extra_handlers` compatibility flag to be enabled. + const result = await env.WORKER.scheduled({ + scheduledTime: new Date(1000), + cron: "30 * * * *", + }); + expect(result.outcome).toBe("ok"); +}); diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/tsconfig.json new file mode 100644 index 0000000..40d2455 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/tsconfig.json b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/vitest.config.ts b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/vitest.config.ts new file mode 100644 index 0000000..3631a5b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/vitest.config.ts @@ -0,0 +1,46 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + miniflare: { + // Configuration for the test runner Worker + compatibilityDate: "2024-01-01", + compatibilityFlags: [ + // This illustrates a Worker that in production only wants v1 of Node.js compatibility. + // The Vitest pool integration will need to remove this flag since the `MockAgent` requires v2. + "no_nodejs_compat_v2", + "nodejs_compat", + // Required to use `WORKER.scheduled()`. This is an experimental + // compatibility flag, and cannot be enabled in production. + "service_binding_extra_handlers", + ], + serviceBindings: { + WORKER: "worker-under-test", + }, + + workers: [ + // Configuration for the "auxiliary" Worker under test. + // Unfortunately, auxiliary Workers cannot load their configuration + // from `wrangler.toml` files, and must be configured with Miniflare + // `WorkerOptions`. + { + name: "worker-under-test", + modules: true, + scriptPath: "./dist/index.js", // Built by `global-setup.ts` + compatibilityDate: "2024-01-01", + compatibilityFlags: ["nodejs_compat"], + }, + ], + }, + }), + ], + test: { + globalSetup: ["./global-setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/wrangler.jsonc new file mode 100644 index 0000000..2631c09 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-integration-auxiliary/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "basics-integration-auxiliary", + "main": "src/index.ts", + // compatibility_date is required so we can run `wrangler build` + "compatibility_date": "2024-01-01", +} diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/README.md b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/README.md new file mode 100644 index 0000000..2035e31 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/README.md @@ -0,0 +1,10 @@ +# ✅ basics-unit-integration-self + +This Worker contains basic `fetch` and `scheduled` handlers. Integration tests dispatch events using `exports` from the `cloudflare:workers` module. Unit tests call handler functions directly. + +| Test | Overview | +| ----------------------------------------------------------------------------- | ----------------------------------------------------- | +| [fetch-integration-self.test.ts](test/fetch-integration-self.test.ts) | Basic `fetch` integration test using `exports` | +| [fetch-unit.test.ts](test/fetch-unit.test.ts) | Basic unit test calling `worker.fetch()` directly | +| [scheduled-integration-self.test.ts](test/scheduled-integration-self.test.ts) | Basic `scheduled` integration test using `exports` | +| [scheduled-unit.test.ts](test/scheduled-unit.test.ts) | Basic unit test calling `worker.scheduled()` directly | diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/env.d.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/env.d.ts new file mode 100644 index 0000000..cb439c0 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/env.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./basics-unit-integration-self/src/env.d.ts -c ./basics-unit-integration-self/wrangler.jsonc --no-include-runtime` (hash: 0c8cd5ae12176ebab8337cbfef98b516) +interface __BaseEnv_Env {} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/greet.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/greet.ts new file mode 100644 index 0000000..9c92f3b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/greet.ts @@ -0,0 +1,3 @@ +export function greet(request: Request): string { + return `👋 ${request.url}`; +} diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/index.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/index.ts new file mode 100644 index 0000000..1d87415 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/index.ts @@ -0,0 +1,13 @@ +import { greet } from "./greet"; + +export default { + async fetch(request, env, ctx) { + return new Response(greet(request)); + }, + async scheduled(controller, env, ctx) { + // ... + }, +} satisfies ExportedHandler; +// ^ Using `satisfies` provides type checking/completions for `ExportedHandler` +// whilst still allowing us to call `worker.fetch()` in tests without +// asserting `worker.fetch` is defined. diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-integration-self.test.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-integration-self.test.ts new file mode 100644 index 0000000..45bd1a0 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-integration-self.test.ts @@ -0,0 +1,11 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("dispatches fetch event", async ({ expect }) => { + // `exports.default` here points to the worker running in the current isolate. + // This gets its handler from the `main` option in `vitest.config.mts`. + // Importantly, it uses the exact `import("../src").default` instance we could + // import in this file as its handler. + const response = await exports.default.fetch("http://example.com"); + expect(await response.text()).toBe("👋 http://example.com/"); +}); diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-unit.test.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-unit.test.ts new file mode 100644 index 0000000..75fda07 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/fetch-unit.test.ts @@ -0,0 +1,26 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import { greet } from "../src/greet"; +import worker from "../src/index"; + +// This will improve in the next major version of `@cloudflare/workers-types`, +// but for now you'll need to do something like this to get a correctly-typed +// `Request` to pass to `worker.fetch()`. +const IncomingRequest = Request; + +it("dispatches fetch event", async ({ expect }) => { + const request = new IncomingRequest("http://example.com"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(await response.text()).toBe("👋 http://example.com/"); +}); + +it("calls arbitrary function", ({ expect }) => { + const request = new Request("http://example.com"); + expect(greet(request)).toBe("👋 http://example.com/"); +}); diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-integration-self.test.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-integration-self.test.ts new file mode 100644 index 0000000..ed7c197 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-integration-self.test.ts @@ -0,0 +1,16 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("dispatches scheduled event", async ({ expect }) => { + // `exports.default` here points to the worker running in the current isolate. + // This gets its handler from the `main` option in `vitest.config.mts`. + // Importantly, it uses the exact `import("../src").default` instance we could + // import in this file as its handler. Note the `exports.default.scheduled()` method + // is experimental, and requires the `service_binding_extra_handlers` + // compatibility flag to be enabled. + const result = await exports.default.scheduled({ + scheduledTime: new Date(1000), + cron: "30 * * * *", + }); + expect(result.outcome).toBe("ok"); +}); diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-unit.test.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-unit.test.ts new file mode 100644 index 0000000..af0ea7c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/scheduled-unit.test.ts @@ -0,0 +1,18 @@ +import { + createExecutionContext, + createScheduledController, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import worker from "../src/index"; + +it("dispatches scheduled event", async () => { + const controller = createScheduledController({ + scheduledTime: new Date(1000), + cron: "30 * * * *", + }); + const ctx = createExecutionContext(); + await worker.scheduled(controller, env, ctx); + await waitOnExecutionContext(ctx); +}); diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/tsconfig.json b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/vitest.config.ts b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/vitest.config.ts new file mode 100644 index 0000000..3d82d8a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/vitest.config.ts @@ -0,0 +1,21 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + miniflare: { + // Required to use `exports.default.scheduled()`. This is an experimental + // compatibility flag, and cannot be enabled in production. + compatibilityFlags: ["service_binding_extra_handlers"], + }, + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/wrangler.jsonc new file mode 100644 index 0000000..7edd213 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/basics-unit-integration-self/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "basics-unit-integration-self", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one +} diff --git a/fixtures/vitest-pool-workers-examples/container-app/Dockerfile b/fixtures/vitest-pool-workers-examples/container-app/Dockerfile new file mode 100644 index 0000000..4666db8 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-alpine + +WORKDIR /usr/src/app +RUN echo '{"name": "simple-node-app", "version": "1.0.0", "dependencies": {"ws": "^8.0.0"}}' > package.json +RUN npm install + +COPY ./container/simple-node-app.js app.js +EXPOSE 8787 +CMD ["node", "app.js"] diff --git a/fixtures/vitest-pool-workers-examples/container-app/README.md b/fixtures/vitest-pool-workers-examples/container-app/README.md new file mode 100644 index 0000000..f0ad4a9 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/README.md @@ -0,0 +1,3 @@ +# Apps with no-op containers + +Currently, `vitest-pool-workers` does not support testing containers yet. It should still let you test your application as long as you do not test with any code paths that interact with containers. diff --git a/fixtures/vitest-pool-workers-examples/container-app/container/simple-node-app.js b/fixtures/vitest-pool-workers-examples/container-app/container/simple-node-app.js new file mode 100644 index 0000000..4579b16 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/container/simple-node-app.js @@ -0,0 +1,12 @@ +const { createServer } = require("http"); + +// Create HTTP server +const server = createServer(function (req, res) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("Hello World! Have an env var! " + process.env.MESSAGE); + res.end(); +}); + +server.listen(8787, function () { + console.log("Server listening on port 8080"); +}); diff --git a/fixtures/vitest-pool-workers-examples/container-app/src/env.d.ts b/fixtures/vitest-pool-workers-examples/container-app/src/env.d.ts new file mode 100644 index 0000000..3866e24 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/src/env.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./container-app/src/env.d.ts -c ./container-app/wrangler.jsonc --no-include-runtime` (hash: 45be8189fa807ec0c802f13cfd9b1a25) +interface __BaseEnv_Env { + MY_CONTAINER: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "MyContainer"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/container-app/src/index.ts b/fixtures/vitest-pool-workers-examples/container-app/src/index.ts new file mode 100644 index 0000000..ff5125f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/src/index.ts @@ -0,0 +1,57 @@ +import { Container, getContainer, getRandom } from "@cloudflare/containers"; // in a real Worker + +export class MyContainer extends Container { + defaultPort = 8787; // The default port for the container to listen on + sleepAfter = "3m"; // Sleep the container if no requests are made in this timeframe + + envVars = { + MESSAGE: "I was passed in via the container class!", + }; + + override onStart() { + console.log("Container successfully started"); + } + + override onStop() { + console.log("Container successfully shut down"); + } + + override onError(error: unknown) { + console.log("Container error:", error); + } +} + +export default { + async fetch( + request: Request, + env: { MY_CONTAINER: DurableObjectNamespace } + ): Promise { + const pathname = new URL(request.url).pathname; + // If you want to route requests to a specific container, + // pass a unique container identifier to .get() + + if (pathname.startsWith("/container")) { + const containerInstance = getContainer(env.MY_CONTAINER, pathname); + return containerInstance.fetch(request); + } + + if (pathname.startsWith("/error")) { + const containerInstance = getContainer(env.MY_CONTAINER, "error-test"); + return containerInstance.fetch(request); + } + + if (pathname.startsWith("/lb")) { + const containerInstance = await getRandom(env.MY_CONTAINER, 3); + return containerInstance.fetch(request); + } + + if (pathname.startsWith("/singleton")) { + // getContainer will return a specific instance if no second argument is provided + return getContainer(env.MY_CONTAINER).fetch(request); + } + + return new Response( + "Call /container to start a container with a 10s timeout.\nCall /error to start a container that errors\nCall /lb to test load balancing" + ); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/container-app/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/container-app/src/tsconfig.json new file mode 100644 index 0000000..3a4e18a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.workerd.json", + "compilerOptions": { + // @cloudflare/containers@0.0.25 has an alarm() signature incompatible + // with the updated DurableObject base type in @cloudflare/workers-types. + // Remove when https://github.com/cloudflare/containers/issues/167 is resolved. + "skipLibCheck": true + }, + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/container-app/test/container.test.ts b/fixtures/vitest-pool-workers-examples/container-app/test/container.test.ts new file mode 100644 index 0000000..3c034c5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/test/container.test.ts @@ -0,0 +1,16 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("dispatches fetch event", { timeout: 10_000 }, async ({ expect }) => { + // requests to code paths that do not interact with a container should work fine + const res = await exports.default.fetch("http://example.com/"); + expect(await res.text()).toMatchInlineSnapshot(` + "Call /container to start a container with a 10s timeout. + Call /error to start a container that errors + Call /lb to test load balancing" + `); + // however if you attempt to start a container, you should expect an error + await expect( + exports.default.fetch("http://example.com/container/hello") + ).rejects.toThrow(); +}); diff --git a/fixtures/vitest-pool-workers-examples/container-app/test/container.unit.test.ts b/fixtures/vitest-pool-workers-examples/container-app/test/container.unit.test.ts new file mode 100644 index 0000000..5173f97 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/test/container.unit.test.ts @@ -0,0 +1,9 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +it("dispatches fetch event", { timeout: 10000 }, async ({ expect }) => { + const id = env.MY_CONTAINER.idFromName("helloagain"); + const stub = env.MY_CONTAINER.get(id); + // the DO constructor will now throw + await expect(() => stub.fetch("http://example.com/")).rejects.toThrow(); +}); diff --git a/fixtures/vitest-pool-workers-examples/container-app/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/container-app/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/container-app/tsconfig.json b/fixtures/vitest-pool-workers-examples/container-app/tsconfig.json new file mode 100644 index 0000000..7d4e80e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.node.json", + "compilerOptions": { + // @cloudflare/containers@0.0.25 has an alarm() signature incompatible + // with the updated DurableObject base type in @cloudflare/workers-types. + // Remove when https://github.com/cloudflare/containers/issues/167 is resolved. + "skipLibCheck": true + }, + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/container-app/vitest.config.ts b/fixtures/vitest-pool-workers-examples/container-app/vitest.config.ts new file mode 100644 index 0000000..d9a889a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/vitest.config.ts @@ -0,0 +1,14 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/container-app/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/container-app/wrangler.jsonc new file mode 100644 index 0000000..ba12103 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/container-app/wrangler.jsonc @@ -0,0 +1,27 @@ +{ + "name": "container-app", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "containers": [ + { + "image": "./Dockerfile", + "class_name": "MyContainer", + "name": "container", + "max_instances": 2, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "MyContainer", + "name": "MY_CONTAINER", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["MyContainer"], + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/README.md b/fixtures/vitest-pool-workers-examples/context-exports/README.md new file mode 100644 index 0000000..5c29d61 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/README.md @@ -0,0 +1,9 @@ +# Context exports + +The Workers here demonstrate how to access and test the `ctx.exports` property. + +| Test | Overview | +| ----------------------------------------------- | ----------------------------------------------------------------------- | +| [auxiliary.test.ts](test/auxiliary.test.ts) | Integration test with an auxiliary Worker that accesses its ctx.exports | +| [integration.test.ts](test/integration.test.ts) | Integration test with a Worker that accesses its ctx.exports | +| [unit.test.ts](test/unit.test.ts) | Unit tests of ctx.exports, constructed, imported and via a Worker | diff --git a/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/index.ts b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/index.ts new file mode 100644 index 0000000..3e1c70a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/index.ts @@ -0,0 +1,13 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +export default { + async fetch(request, env, ctx) { + return new Response("👋 " + (await ctx.exports.NamedEntryPoint.greet())); + }, +} satisfies ExportedHandler; + +export class NamedEntryPoint extends WorkerEntrypoint { + greet() { + return `Hello ${this.env.NAME} from Auxiliary NamedEntryPoint!`; + } +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/tsconfig.json b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/worker-configuration.d.ts b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/worker-configuration.d.ts new file mode 100644 index 0000000..902467f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/worker-configuration.d.ts @@ -0,0 +1,9 @@ +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env { + NAME: string; + } +} +interface Env extends Cloudflare.Env {} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/wrangler.jsonc new file mode 100644 index 0000000..8fd3cf3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/auxiliary-worker/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "auxiliary-worker", + "main": "./index.ts", + // compatibility_date is required so we can run `wrangler build` + "compatibility_date": "2025-11-01", +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/global-setup.ts b/fixtures/vitest-pool-workers-examples/context-exports/global-setup.ts new file mode 100644 index 0000000..bf90b23 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/global-setup.ts @@ -0,0 +1,11 @@ +import childProcess from "node:child_process"; + +// Global setup runs inside Node.js, not `workerd` +export default function () { + const label = "Built auxiliary worker"; + console.time(label); + childProcess.execSync("wrangler build -c auxiliary-worker/wrangler.jsonc", { + cwd: __dirname, + }); + console.timeEnd(label); +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/src/index.ts b/fixtures/vitest-pool-workers-examples/context-exports/src/index.ts new file mode 100644 index 0000000..e4a4aa1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/src/index.ts @@ -0,0 +1,79 @@ +import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; + +// This re-export will not be detected by the export guessing logic +export * from "@virtual-module"; + +// This explicit re-export will be detected by the export guessing logic +// even though it won't be able to tell what it is at build time. +export { ExplicitVirtualEntryPoint } from "@virtual-module"; + +export default { + async fetch(request, env, ctx) { + const { pathname } = new URL(request.url); + if (pathname === "/durable-object") { + const id = ctx.exports.Counter.idFromName(pathname); + const stub = ctx.exports.Counter.get(id); + return stub.fetch(request); + } + if (pathname === "/props") { + return new Response( + "👋 " + + (await ctx.exports + .NamedEntryPoint({ props: { extra: "\nAdditional props!!" } }) + .greet()) + ); + } + if (pathname === "/invalid-export") { + // @ts-expect-error we are testing invalid export access + return new Response("👋 " + (await ctx.exports.InvalidExport)); + } + + if (pathname === "/virtual-implicit") { + // We don't try to call greet() here because the entry-point will be undefined. + return new Response("👋 " + ctx.exports.ReexportedVirtualEntryPoint); + } + + if (pathname === "/virtual-explicit") { + return new Response( + "👋 " + (await ctx.exports.ExplicitVirtualEntryPoint.greet()) + ); + } + + if (pathname === "/virtual-configured") { + return new Response( + "👋 " + (await ctx.exports.ConfiguredVirtualEntryPoint.greet()) + ); + } + + if (pathname === "/virtual-durable-object") { + const id = ctx.exports.ConfiguredVirtualDurableObject.newUniqueId(); + const stub = ctx.exports.ConfiguredVirtualDurableObject.get(id); + return new Response("👋 " + (await stub.greet())); + } + + return new Response("👋 " + (await ctx.exports.NamedEntryPoint.greet())); + }, +} satisfies ExportedHandler; + +export class NamedEntryPoint extends WorkerEntrypoint { + greet() { + return ( + `Hello ${this.env.NAME} from Main NamedEntryPoint!` + + (this.ctx.props.extra ?? "") + ); + } +} + +export class Counter extends DurableObject { + count: number = 0; + + increment(by = 1) { + this.count += by; + void this.ctx.storage.put("count", this.count); + } + + fetch(request: Request) { + this.increment(); + return new Response(this.count.toString()); + } +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/context-exports/src/tsconfig.json new file mode 100644 index 0000000..38455c0 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/src/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"], + "compilerOptions": { + "paths": { + // This path is used to simulate a virtual module that Vitest and TypeScript can understand, + // but esbuild (used by the vitest-pool-workers to guess exports) cannot. + "@virtual-module": ["./virtual.ts"] + } + } +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/src/virtual.ts b/fixtures/vitest-pool-workers-examples/context-exports/src/virtual.ts new file mode 100644 index 0000000..6766491 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/src/virtual.ts @@ -0,0 +1,29 @@ +import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; + +// This is wildcard re-exported in src/index.ts so it is not detected by the export guessing logic +export class ReexportedVirtualEntryPoint extends WorkerEntrypoint { + greet() { + return `Hello ${this.env.NAME} from ReexportedVirtualEntryPoint!`; + } +} + +// This is explictly re-exported in src/index.ts so it is detected by the export guessing logic +export class ExplicitVirtualEntryPoint extends WorkerEntrypoint { + greet() { + return `Hello ${this.env.NAME} from ExplicitVirtualEntryPoint!`; + } +} + +// Although this export cannot be inferred by esbuild, it is explicitly configured in the Wrangler config via `migrations`. +export class ConfiguredVirtualDurableObject extends DurableObject { + greet() { + return `Hello ${this.env.NAME} from ConfiguredVirtualDurableObject!`; + } +} + +// Although this export cannot be inferred by esbuild, it is explicitly configured in vitest-pool-workers config via `additionalExports`. +export class ConfiguredVirtualEntryPoint extends WorkerEntrypoint { + greet() { + return `Hello ${this.env.NAME} from ConfiguredVirtualEntryPoint!`; + } +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/src/worker-configuration.d.ts b/fixtures/vitest-pool-workers-examples/context-exports/src/worker-configuration.d.ts new file mode 100644 index 0000000..25f6ac9 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/src/worker-configuration.d.ts @@ -0,0 +1,11 @@ +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "Counter" | "ConfiguredVirtualDurableObject"; + } + interface Env { + NAME: string; + AUXILIARY_WORKER: Fetcher; + } +} +interface Env extends Cloudflare.Env {} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/src/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/context-exports/src/wrangler.jsonc new file mode 100644 index 0000000..af756df --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/src/wrangler.jsonc @@ -0,0 +1,22 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "main-worker", + "main": "./index.ts", + "compatibility_date": "2025-11-01", + "compatibility_flags": ["enable_ctx_exports"], + "vars": { + "NAME": "MainWorker", + }, + "services": [ + { + "binding": "AUXILIARY_WORKER", + "service": "auxiliary-worker", + }, + ], + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["Counter", "ConfiguredVirtualDurableObject"], + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/test/auxiliary.test.ts b/fixtures/vitest-pool-workers-examples/context-exports/test/auxiliary.test.ts new file mode 100644 index 0000000..0ebc86d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/test/auxiliary.test.ts @@ -0,0 +1,11 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +it("uses the correct context exports on the auxiliary worker", async ({ + expect, +}) => { + const response = await env.AUXILIARY_WORKER.fetch("http://example.com"); + expect(await response.text()).toMatchInlineSnapshot( + `"👋 Hello AuxiliaryWorker from Auxiliary NamedEntryPoint!"` + ); +}); diff --git a/fixtures/vitest-pool-workers-examples/context-exports/test/durable-objects.test.ts b/fixtures/vitest-pool-workers-examples/context-exports/test/durable-objects.test.ts new file mode 100644 index 0000000..5041290 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/test/durable-objects.test.ts @@ -0,0 +1,40 @@ +import { runInDurableObject } from "cloudflare:test"; +import { exports } from "cloudflare:workers"; +import { it, vi } from "vitest"; + +it("can access imported context exports for Durable Objects", async ({ + expect, +}) => { + const id = exports.Counter.idFromName("/path"); + const stub = exports.Counter.get(id); + const response = await runInDurableObject(stub, async (obj) => { + return obj.fetch(new Request("http://example.com")); + }); + expect(await response.text()).toMatchInlineSnapshot(`"1"`); + const count = await runInDurableObject(stub, async (obj) => obj.count); + expect(count).toBe(1); +}); + +it("can access context exports for Durable Objects on exports.default", async ({ + expect, +}) => { + const response = await exports.default.fetch( + "https://example.com/durable-object" + ); + expect(await response.text()).toMatchInlineSnapshot(`"1"`); +}); + +it("will can access Durable Object context exports that could not be guessed on the exports.default worker but are described by DO migrations", async ({ + expect, +}) => { + // In this test, we are trying to access a durable-object that is wildcard (*) re-exported from a virtual module. + // This virtual module is understood by Vitest and TypeScript but not the lightweight esbuild that we use to guess exports. + // But since Durable Objects require explicit "migration" configuration in wrangler, we can still make this work. + const warnSpy = vi.spyOn(console, "warn"); + const response = await exports.default.fetch( + "http://example.com/virtual-durable-object" + ); + expect(await response.text()).toMatchInlineSnapshot( + `"👋 Hello MainWorker from ConfiguredVirtualDurableObject!"` + ); +}); diff --git a/fixtures/vitest-pool-workers-examples/context-exports/test/integration-self.test.ts b/fixtures/vitest-pool-workers-examples/context-exports/test/integration-self.test.ts new file mode 100644 index 0000000..e92516a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/test/integration-self.test.ts @@ -0,0 +1,85 @@ +import { exports } from "cloudflare:workers"; +import { it, vi } from "vitest"; + +it("can use context exports on the main worker", async ({ expect }) => { + const response = await exports.default.fetch("http://example.com"); + expect(await response.text()).toBe( + "👋 Hello MainWorker from Main NamedEntryPoint!" + ); +}); + +it("can use context exports (parameterized with props) on the main worker", async ({ + expect, +}) => { + const response = await exports.default.fetch("http://example.com/props"); + expect(await response.text()).toBe( + "👋 Hello MainWorker from Main NamedEntryPoint!\nAdditional props!!" + ); +}); + +it("will warn on missing context exports on the main worker", async ({ + expect, +}) => { + const warnSpy = vi.spyOn(console, "warn"); + const response = await exports.default.fetch( + "http://example.com/invalid-export" + ); + expect(await response.text()).toMatchInlineSnapshot(`"👋 undefined"`); + expect(warnSpy).toHaveBeenCalledWith( + "Attempted to access 'ctx.exports.InvalidExport', which was not defined for the main Worker.\n" + + "Check that 'InvalidExport' is exported as an entry-point from the Worker.\n" + + "The '@cloudflare/vitest-pool-workers' integration tries to infer these exports by analyzing the source code of the main Worker.\n" + ); +}); + +it("will warn on implicit re-exports that will exist in production but cannot not be guessed on the main worker", async ({ + expect, +}) => { + // In this test, we are trying to access an entry-point that is wildcard (*) re-exported from a virtual module. + // This virtual module is understood by Vitest and TypeScript but not the lightweight esbuild that we use to guess exports. + const warnSpy = vi.spyOn(console, "warn"); + const response = await exports.default.fetch( + "http://example.com/virtual-implicit" + ); + expect(await response.text()).toMatchInlineSnapshot(`"👋 undefined"`); + expect(warnSpy).toHaveBeenCalledWith( + "Attempted to access 'ctx.exports.ReexportedVirtualEntryPoint', which was not defined for the main Worker.\n" + + "Check that 'ReexportedVirtualEntryPoint' is exported as an entry-point from the Worker.\n" + + "The '@cloudflare/vitest-pool-workers' integration tries to infer these exports by analyzing the source code of the main Worker.\n" + ); +}); + +it("will still guess re-exports on the main worker that cannot be fully analyzed by esbuild", async ({ + expect, +}) => { + // In this test, we are trying to access an entry-point that is explicitly re-exported from a virtual module. + // Although esbuild cannot really analyze what is being re-exported, it can at least see that something is being re-exported with that name. + const response = await exports.default.fetch( + "http://example.com/virtual-explicit" + ); + expect(await response.text()).toBe( + "👋 Hello MainWorker from ExplicitVirtualEntryPoint!" + ); +}); + +it("can access configured virtual entry points on the main worker that cannot be fully analyzed by esbuild", async ({ + expect, +}) => { + // In this test, we are trying to access an entry-point that is explicitly re-exported from a virtual module. + // Although esbuild cannot really analyze what is being re-exported, it can at least see that something is being re-exported with that name. + const response = await exports.default.fetch( + "http://example.com/virtual-configured" + ); + expect(await response.text()).toBe( + "👋 Hello MainWorker from ConfiguredVirtualEntryPoint!" + ); +}); + +it("can access imported context exports for the main worker", async ({ + expect, +}) => { + const msg = await exports.NamedEntryPoint.greet(); + expect(msg).toMatchInlineSnapshot( + `"Hello MainWorker from Main NamedEntryPoint!"` + ); +}); diff --git a/fixtures/vitest-pool-workers-examples/context-exports/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/context-exports/test/tsconfig.json new file mode 100644 index 0000000..a13714b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/worker-configuration.d.ts"], + "compilerOptions": { + "paths": { + // This path is used to simulate a virtual module that Vitest and TypeScript can understand, + // but esbuild (used by the vitest-pool-workers to guess exports) cannot. + "@virtual-module": ["../src/virtual.ts"] + } + } +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/test/unit.test.ts b/fixtures/vitest-pool-workers-examples/context-exports/test/unit.test.ts new file mode 100644 index 0000000..c09734f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/test/unit.test.ts @@ -0,0 +1,38 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { exports as importedExports } from "cloudflare:workers"; +import { it } from "vitest"; +import worker from "../src/index"; + +// This will improve in the next major version of `@cloudflare/workers-types`, +// but for now you'll need to do something like this to get a correctly-typed +// `Request` to pass to `worker.fetch()`. +const IncomingRequest = Request; + +it("has the correct context exports from `createExecutionContext()`", async ({ + expect, +}) => { + const ctx = createExecutionContext(); + expect(await ctx.exports.NamedEntryPoint.greet()).toMatchInlineSnapshot( + `"Hello MainWorker from Main NamedEntryPoint!"` + ); +}); + +it("has the correct imported context exports", async ({ expect }) => { + expect(await importedExports.NamedEntryPoint.greet()).toMatchInlineSnapshot( + `"Hello MainWorker from Main NamedEntryPoint!"` + ); +}); + +it("can pass the context exports to a worker", async ({ expect }) => { + const request = new IncomingRequest("http://example.com"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot( + `"👋 Hello MainWorker from Main NamedEntryPoint!"` + ); +}); diff --git a/fixtures/vitest-pool-workers-examples/context-exports/tsconfig.json b/fixtures/vitest-pool-workers-examples/context-exports/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/context-exports/vitest.config.ts b/fixtures/vitest-pool-workers-examples/context-exports/vitest.config.ts new file mode 100644 index 0000000..6b8c11a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/context-exports/vitest.config.ts @@ -0,0 +1,44 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +// Configuration for the "auxiliary" Worker under test. +// Unfortunately, auxiliary Workers cannot load their configuration +// from wrangler config files, and must be configured with Miniflare +// `WorkerOptions`. +export const auxiliaryWorker = { + name: "auxiliary-worker", + modules: true, + scriptPath: "./auxiliary-worker/dist/index.js", // Built by `global-setup.ts` + compatibilityDate: "2025-11-01", + compatibilityFlags: ["nodejs_compat", "enable_ctx_exports"], + bindings: { + NAME: "AuxiliaryWorker", + }, +}; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./src/wrangler.jsonc" }, + miniflare: { + workers: [auxiliaryWorker], + }, + additionalExports: { + // This entrypoint is wildcard re-exported from a virtual module so we cannot automatically infer it. + ConfiguredVirtualEntryPoint: "WorkerEntrypoint", + }, + }), + ], + test: { + globalSetup: ["./global-setup.ts"], + alias: { + // This alias is used to simulate a virtual module that Vitest and TypeScript can understand, + // but esbuild (used by the vitest-pool-workers to guess exports) cannot. + "@virtual-module": "./virtual.ts", + }, + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/d1/README.md b/fixtures/vitest-pool-workers-examples/d1/README.md new file mode 100644 index 0000000..c6b6d91 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/README.md @@ -0,0 +1,8 @@ +# 📚 d1 + +This Worker implements a simple blog using D1 bindings. It uses migrations for setting up the database. These migrations are read in `vitest.config.mts`, and applied with the [test/apply-migrations.ts](test/apply-migrations.ts) setup file. + +| Test | Overview | +| --------------------------------------- | -------------------------------- | +| [queries.test.ts](test/queries.test.ts) | Unit tests for SQL query helpers | +| [routes.test.ts](test/routes.test.ts) | Integration for endpoints | diff --git a/fixtures/vitest-pool-workers-examples/d1/migrations/0000_initial.sql b/fixtures/vitest-pool-workers-examples/d1/migrations/0000_initial.sql new file mode 100644 index 0000000..e00faff --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/migrations/0000_initial.sql @@ -0,0 +1,13 @@ +-- Migration number: 0000 2024-03-01T15:05:23.955Z +CREATE TABLE users ( + username TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL +); +CREATE TABLE posts ( + slug TEXT PRIMARY KEY, + author TEXT NOT NULL, + body TEXT NOT NULL, + FOREIGN KEY (author) REFERENCES users(username) +); +-- Migration ends here diff --git a/fixtures/vitest-pool-workers-examples/d1/migrations/0001_admin.sql b/fixtures/vitest-pool-workers-examples/d1/migrations/0001_admin.sql new file mode 100644 index 0000000..6d7fc1f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/migrations/0001_admin.sql @@ -0,0 +1,2 @@ +-- Migration number: 0001 2024-03-01T15:17:12.235Z +INSERT INTO users (username, name, email) VALUES ('admin', 'Ada Min', 'admin@example.com'); diff --git a/fixtures/vitest-pool-workers-examples/d1/src/env.d.ts b/fixtures/vitest-pool-workers-examples/d1/src/env.d.ts new file mode 100644 index 0000000..cb34da6 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/src/env.d.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./d1/src/env.d.ts -c ./d1/wrangler.jsonc --no-include-runtime` (hash: db529685c9defc6baa1cc744b765c359) +interface __BaseEnv_Env { + DATABASE?: D1Database; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface ProductionEnv { + DATABASE: D1Database; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/d1/src/index.ts b/fixtures/vitest-pool-workers-examples/d1/src/index.ts new file mode 100644 index 0000000..20ce2ea --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/src/index.ts @@ -0,0 +1,39 @@ +import { listPosts, readPost, upsertPost } from "./utils"; + +async function handleListRequest(env: Env, origin: string): Promise { + const posts = await listPosts(env); + const body = posts + .map((post) => `${origin}${post.slug}\n${post.body}`) + .join(`\n\n${"-".repeat(20)}\n`); + return new Response(body); +} + +async function handleReadRequest(env: Env, slug: string): Promise { + const post = await readPost(env, slug); + if (post === null) return new Response("Not Found", { status: 404 }); + else return new Response(post.body); +} + +async function handlePutRequest( + request: Request, + env: Env, + slug: string +): Promise { + if (slug === "/") return new Response("Method Not Allowed", { status: 405 }); + await upsertPost(env, slug, await request.text()); + return new Response(null, { status: 204 }); +} + +export default >{ + fetch(request, env) { + const { origin, pathname } = new URL(request.url); + if (request.method === "GET") { + if (pathname === "/") return handleListRequest(env, origin); + else return handleReadRequest(env, pathname); + } else if (request.method === "PUT") { + return handlePutRequest(request, env, pathname); + } else { + return new Response("Method Not Allowed", { status: 405 }); + } + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/d1/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/d1/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/d1/src/utils.ts b/fixtures/vitest-pool-workers-examples/d1/src/utils.ts new file mode 100644 index 0000000..a9e1bd7 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/src/utils.ts @@ -0,0 +1,92 @@ +// ============================================================================= +// Types + +interface User { + username: string; + name: string; + email: string; +} + +interface Post { + slug: string; + author: User; + body: string; +} + +// ============================================================================= +// Queries + +type QueryResult = User & Omit; +function queryResultToPost(result: QueryResult): Post { + return { + slug: result.slug, + body: result.body, + author: { + username: result.username, + name: result.name, + email: result.email, + }, + }; +} + +export async function listPosts(env: Env): Promise { + const database = env.DATABASE; + if (!database) { + throw new Error("DATABASE binding is required"); + } + + const results = await database + .prepare( + ` + SELECT slug, body, username, name, email + FROM posts + INNER JOIN users ON posts.author = users.username + ` + ) + .all(); + return results.results.map(queryResultToPost); +} + +export async function readPost(env: Env, slug: string): Promise { + const database = env.DATABASE; + if (!database) { + throw new Error("DATABASE binding is required"); + } + + const result = await database + .prepare( + ` + SELECT slug, body, username, name, email + FROM posts + INNER JOIN users ON posts.author = users.username + WHERE slug = ?1 + ` + ) + .bind(slug) + .first(); + return result === null ? null : queryResultToPost(result); +} + +export async function upsertPost( + env: Env, + slug: string, + body: string +): Promise { + const database = env.DATABASE; + if (!database) { + throw new Error("DATABASE binding is required"); + } + + await database + .prepare( + ` + INSERT INTO posts (slug, author, body) + VALUES (?1, ?2, ?3) + ON CONFLICT (slug) DO UPDATE SET + author = ?2, + body = ?3 + ` + ) + .bind(slug, "admin", body) + .run(); +} diff --git a/fixtures/vitest-pool-workers-examples/d1/test/apply-migrations.ts b/fixtures/vitest-pool-workers-examples/d1/test/apply-migrations.ts new file mode 100644 index 0000000..1dd6e31 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/test/apply-migrations.ts @@ -0,0 +1,7 @@ +import { applyD1Migrations } from "cloudflare:test"; +import { env } from "cloudflare:workers"; + +// Setup files run outside the per-test-file storage isolation, and may be run +// multiple times. `applyD1Migrations()` only applies migrations that haven't +// already been applied, therefore it is safe to call this function here. +await applyD1Migrations(env.DATABASE, env.TEST_MIGRATIONS); diff --git a/fixtures/vitest-pool-workers-examples/d1/test/env.d.ts b/fixtures/vitest-pool-workers-examples/d1/test/env.d.ts new file mode 100644 index 0000000..afabea1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/test/env.d.ts @@ -0,0 +1,6 @@ +declare namespace Cloudflare { + interface Env { + DATABASE: D1Database; + TEST_MIGRATIONS: import("cloudflare:test").D1Migration[]; // Defined in `vitest.config.mts` + } +} diff --git a/fixtures/vitest-pool-workers-examples/d1/test/queries.test.ts b/fixtures/vitest-pool-workers-examples/d1/test/queries.test.ts new file mode 100644 index 0000000..8a7a1bd --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/test/queries.test.ts @@ -0,0 +1,33 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import { listPosts, readPost, upsertPost } from "../src/utils"; + +it("should create and read post", async ({ expect }) => { + await upsertPost(env, "/hello", "👋"); + + const post = await readPost(env, "/hello"); + expect(post).toMatchInlineSnapshot(` + { + "author": { + "email": "admin@example.com", + "name": "Ada Min", + "username": "admin", + }, + "body": "👋", + "slug": "/hello", + } + `); +}); + +it("should list posts", async ({ expect }) => { + await upsertPost(env, "/one", "1"); + await upsertPost(env, "/two", "2"); + await upsertPost(env, "/three", "3"); + + const posts = await listPosts(env); + expect(posts.length).toBe(4); + expect(posts[0].body).toBe("👋"); + expect(posts[1].body).toBe("1"); + expect(posts[2].body).toBe("2"); + expect(posts[3].body).toBe("3"); +}); diff --git a/fixtures/vitest-pool-workers-examples/d1/test/routes.test.ts b/fixtures/vitest-pool-workers-examples/d1/test/routes.test.ts new file mode 100644 index 0000000..26eb508 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/test/routes.test.ts @@ -0,0 +1,61 @@ +import { env, exports } from "cloudflare:workers"; +import { it } from "vitest"; +import { upsertPost } from "../src/utils"; + +it("should create and read post", async ({ expect }) => { + let response = await exports.default.fetch("https://example.com/hello", { + method: "PUT", + body: "👋", + }); + expect(response.status).toBe(204); + + response = await exports.default.fetch("https://example.com/hello"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("👋"); +}); + +it("should list posts", async ({ expect }) => { + await upsertPost(env, "/one", "1"); + await upsertPost(env, "/two", "2"); + await upsertPost(env, "/three", "3"); + + const response = await exports.default.fetch("https://example.com/"); + expect(response.status).toBe(200); + expect(await response.text()).toMatchInlineSnapshot(` + "https://example.com/hello + 👋 + + -------------------- + https://example.com/one + 1 + + -------------------- + https://example.com/two + 2 + + -------------------- + https://example.com/three + 3" + `); +}); + +it("should reject invalid method", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/hello", { + method: "POST", + body: "👋", + }); + expect(response.status).toBe(405); +}); + +it("should respond with not found for invalid slugs", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/bad"); + expect(response.status).toBe(404); +}); + +it("shouldn't allow creating post at root", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/", { + method: "PUT", + body: "👋", + }); + expect(response.status).toBe(405); +}); diff --git a/fixtures/vitest-pool-workers-examples/d1/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/d1/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/d1/tsconfig.json b/fixtures/vitest-pool-workers-examples/d1/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/d1/vitest.config.ts b/fixtures/vitest-pool-workers-examples/d1/vitest.config.ts new file mode 100644 index 0000000..0d05cab --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/vitest.config.ts @@ -0,0 +1,35 @@ +import path from "node:path"; +import { + cloudflareTest, + readD1Migrations, +} from "@cloudflare/vitest-pool-workers"; +import { defineConfig, defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default defineConfig(async () => { + // Read all migrations in the `migrations` directory + const migrationsPath = path.join(__dirname, "migrations"); + const migrations = await readD1Migrations(migrationsPath); + + return mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + environment: "production", + }, + miniflare: { + // Add a test-only binding for migrations, so we can apply them in a + // setup file + bindings: { TEST_MIGRATIONS: migrations }, + }, + }), + ], + test: { + setupFiles: ["./test/apply-migrations.ts"], + }, + }) + ); +}); diff --git a/fixtures/vitest-pool-workers-examples/d1/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/d1/wrangler.jsonc new file mode 100644 index 0000000..c8bff04 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/d1/wrangler.jsonc @@ -0,0 +1,16 @@ +{ + "name": "d1", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "env": { + "production": { + "d1_databases": [ + { + "binding": "DATABASE", + "database_name": "database", + "database_id": "00000000-0000-0000-0000-000000000000", + }, + ], + }, + }, +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/README.md b/fixtures/vitest-pool-workers-examples/durable-objects-exports/README.md new file mode 100644 index 0000000..8142b0c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/README.md @@ -0,0 +1,9 @@ +# 📌 durable-objects-exports + +This Worker mirrors the simple [`durable-objects`](../durable-objects/) fixture, but configures its Durable Objects via the declarative [`exports`](https://developers.cloudflare.com/durable-objects/reference/durable-object-exports) field in `wrangler.jsonc` instead of the legacy `migrations` array. + +`Counter` is also declared as a regular binding under `durable_objects.bindings`. `UnboundCounter` has no binding and is reachable only via `ctx.exports.UnboundCounter` from inside the Worker — both forms must work for `exports` to be a full replacement for `migrations`. + +| Test | Overview | +| --------------------------------------- | ---------------------------------------------------------------------------------- | +| [exports.test.ts](test/exports.test.ts) | Bound + unbound DOs declared via `exports`, with SQLite storage and direct access. | diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/env.d.ts b/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/env.d.ts new file mode 100644 index 0000000..b31a1a2 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/env.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --config=./durable-objects-exports/wrangler.jsonc --include-runtime=false ./durable-objects-exports/src/env.d.ts` (hash: a1db05673b096df6d90243959ccaa4bd) +interface __BaseEnv_Env { + COUNTER: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "Counter" | "UnboundCounter"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/index.ts b/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/index.ts new file mode 100644 index 0000000..e9d35c6 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/index.ts @@ -0,0 +1,52 @@ +import { DurableObject } from "cloudflare:workers"; + +class SqlCounter extends DurableObject { + count: number = 0; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + void ctx.blockConcurrencyWhile(async () => { + this.count = (await ctx.storage.get("count")) ?? 0; + }); + } + + increment(by = 1) { + this.count += by; + void this.ctx.storage.put("count", this.count); + return this.count; + } + + // Touches the SQLite-backed API so callers can confirm the class is + // provisioned with `storage: "sqlite"` (this throws on a legacy-KV DO). + sqliteOk() { + return this.ctx.storage.sql.databaseSize; + } +} + +// Bound class — has a `durable_objects.bindings` entry. +export class Counter extends SqlCounter { + fetch(_request: Request) { + return new Response(this.increment().toString()); + } +} + +// Unbound class — declared only via `exports`, reached via `ctx.exports`. +export class UnboundCounter extends SqlCounter { + fetch(_request: Request) { + return new Response(this.increment().toString()); + } +} + +export default >{ + fetch(request, env, ctx) { + const url = new URL(request.url); + if (url.pathname === "/unbound") { + const id = ctx.exports.UnboundCounter.idFromName("/unbound"); + const stub = ctx.exports.UnboundCounter.get(id); + return stub.fetch(request); + } + const id = env.COUNTER.idFromName(url.pathname); + const stub = env.COUNTER.get(id); + return stub.fetch(request); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/test/exports.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects-exports/test/exports.test.ts new file mode 100644 index 0000000..4815deb --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/test/exports.test.ts @@ -0,0 +1,45 @@ +import { listDurableObjectIds, runInDurableObject } from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { it } from "vitest"; +import { Counter } from "../src/"; + +it("provisions a bound DO declared via `exports` with SQLite storage", async ({ + expect, +}) => { + // Hit the default fetch handler — exercises the bound DO end-to-end. + let response = await exports.default.fetch("https://example.com/path"); + expect(await response.text()).toBe("1"); + + // Direct access via `runInDurableObject` — proves the class is the same + // instance class and that its SQLite storage works (would throw on a + // legacy-KV DO). + const id = env.COUNTER.idFromName("/path"); + const stub = env.COUNTER.get(id); + response = await runInDurableObject(stub, (instance: Counter) => { + expect(instance).toBeInstanceOf(Counter); + // `sqliteOk()` only succeeds for sqlite-backed DOs; the assertion below + // is the actual signal that `exports.Counter.storage = "sqlite"` was + // correctly threaded through the local-dev SQLite class-name map. + expect(typeof instance.sqliteOk()).toBe("number"); + return instance.fetch(new Request("https://example.com/path")); + }); + expect(await response.text()).toBe("2"); + + // And that the bound namespace exposes the usual helpers. + const ids = await listDurableObjectIds(env.COUNTER); + expect(ids.map((i) => i.toString())).toContain(id.toString()); +}); + +it("reaches an unbound DO declared only via `exports` through `ctx.exports`", async ({ + expect, +}) => { + // `UnboundCounter` has no `durable_objects.bindings` entry — it is reachable + // only via `ctx.exports.UnboundCounter` from inside the Worker. The default + // handler routes `/unbound` to it; if `additionalUnboundDurableObjects` was + // not threaded through from `exports`, this fetch would fail. + let response = await exports.default.fetch("https://example.com/unbound"); + expect(await response.text()).toBe("1"); + + response = await exports.default.fetch("https://example.com/unbound"); + expect(await response.text()).toBe("2"); +}); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/durable-objects-exports/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/tsconfig.json b/fixtures/vitest-pool-workers-examples/durable-objects-exports/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/vitest.config.ts b/fixtures/vitest-pool-workers-examples/durable-objects-exports/vitest.config.ts new file mode 100644 index 0000000..dfaddb1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/vitest.config.ts @@ -0,0 +1,19 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + test: { + name: "@scoped/durable-objects-exports", + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects-exports/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/durable-objects-exports/wrangler.jsonc new file mode 100644 index 0000000..14f5b2e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects-exports/wrangler.jsonc @@ -0,0 +1,20 @@ +{ + "name": "durable-objects-exports", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "durable_objects": { + "bindings": [ + { + "name": "COUNTER", + "class_name": "Counter", + }, + ], + }, + // Declarative DO `exports` — replaces the legacy `migrations` array. + // `Counter` is also bound (above); `UnboundCounter` is reachable only via + // `ctx.exports.UnboundCounter`. + "exports": { + "Counter": { "type": "durable-object", "storage": "sqlite" }, + "UnboundCounter": { "type": "durable-object", "storage": "sqlite" }, + }, +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/README.md b/fixtures/vitest-pool-workers-examples/durable-objects/README.md new file mode 100644 index 0000000..018855f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/README.md @@ -0,0 +1,9 @@ +# 📌 durable-objects + +This Worker implements a counter with Durable Objects. Each object holds a single count. + +| Test | Overview | +| --------------------------------------------------- | ---------------------------------------------------------------------- | +| [direct-access.test.ts](test/direct-access.test.ts) | Tests for endpoints that also access object instance members directly | +| [alarm.test.ts](test/alarm.test.ts) | Tests that immediately execute object alarms | +| [eviction.test.ts](test/eviction.test.ts) | Tests eviction, storage preservation and WebSocket hibernation/closing | diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/src/env.d.ts b/fixtures/vitest-pool-workers-examples/durable-objects/src/env.d.ts new file mode 100644 index 0000000..1dfacd4 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/src/env.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./durable-objects/src/env.d.ts -c ./durable-objects/wrangler.jsonc --no-include-runtime` (hash: dbeecb58f42b6b6ca9742dccb5c058cf) +interface __BaseEnv_Env { + COUNTER: DurableObjectNamespace; + SQL: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "Counter" | "SQLiteDurableObject"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts b/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts new file mode 100644 index 0000000..ce2dd7f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/src/index.ts @@ -0,0 +1,78 @@ +import { DurableObject } from "cloudflare:workers"; + +export class Counter extends DurableObject { + count: number = 0; + #webSocketMessages: string[] = []; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + void ctx.blockConcurrencyWhile(async () => { + this.count = (await ctx.storage.get("count")) ?? 0; + }); + } + + increment(by = 1) { + this.count += by; + void this.ctx.storage.put("count", this.count); + } + + fetch(request: Request) { + const url = new URL(request.url); + if (url.pathname === "/websocket-order") { + const { 0: client, 1: server } = new WebSocketPair(); + this.ctx.acceptWebSocket(server); + return new Response(null, { status: 101, webSocket: client }); + } + if (url.pathname === "/websocket-order-log") { + return Response.json(this.#webSocketMessages); + } + if (url.pathname === "/redirect") { + return Response.redirect("https://example.com/redirected", 302); + } + this.increment(); + return new Response(this.count.toString()); + } + + alarm() { + this.count = 0; + void this.ctx.storage.put("count", this.count); + } + + scheduleReset(afterMillis: number) { + void this.ctx.storage.setAlarm(Date.now() + afterMillis); + } + + webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { + const value = + typeof message === "string" ? message : new TextDecoder().decode(message); + this.#webSocketMessages.push(value); + ws.send(value); + } + + webSocketClose() {} + + webSocketError() {} +} + +export class SQLiteDurableObject extends DurableObject { + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + } + fetch() { + return new Response(this.ctx.storage.sql.databaseSize.toString()); + } +} + +export default >{ + fetch(request, env) { + const { pathname } = new URL(request.url); + if (pathname === "/sql") { + const id = env.SQL.idFromName(pathname); + const stub = env.SQL.get(id); + return stub.fetch(request); + } + const id = env.COUNTER.idFromName(pathname); + const stub = env.COUNTER.get(id); + return stub.fetch(request); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/durable-objects/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/alarm.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/alarm.test.ts new file mode 100644 index 0000000..2430c44 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/alarm.test.ts @@ -0,0 +1,30 @@ +import { runDurableObjectAlarm, runInDurableObject } from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import { Counter } from "../src/"; + +it("immediately executes alarm", async ({ expect }) => { + // Schedule alarm by directly calling instance method + const id = env.COUNTER.newUniqueId(); + const stub = env.COUNTER.get(id); + await runInDurableObject(stub, (instance: Counter) => { + instance.increment(3); + instance.scheduleReset(60_000); + }); + + // Check counter has non-zero value + let response = await stub.fetch("http://placeholder"); + expect(await response.text()).toBe("4"); + + // Immediately execute the alarm to reset the counter + let ran = await runDurableObjectAlarm(stub); + expect(ran).toBe(true); // ...as there was an alarm scheduled + + // Check counter value was reset + response = await stub.fetch("http://placeholder"); + expect(await response.text()).toBe("1"); + + // Check trying to execute the alarm again does nothing + ran = await runDurableObjectAlarm(stub); + expect(ran).toBe(false); // ...as there was no alarm scheduled +}); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/direct-access.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/direct-access.test.ts new file mode 100644 index 0000000..cbab619 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/direct-access.test.ts @@ -0,0 +1,48 @@ +import { listDurableObjectIds, runInDurableObject } from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { it } from "vitest"; +import { Counter } from "../src/"; + +// Regression test for https://github.com/cloudflare/workers-sdk/issues/9907 +it("handles redirect responses returned from runInDurableObject callback", async ({ + expect, +}) => { + const id = env.COUNTER.idFromName("/redirect-test"); + const stub = env.COUNTER.get(id); + const response = await runInDurableObject(stub, (instance: Counter) => { + const request = new Request("https://example.com/redirect"); + return instance.fetch(request); + }); + expect(response.status).toBe(302); + expect(response.headers.get("Location")).toBe( + "https://example.com/redirected" + ); +}); + +it("increments count and allows direct access to instance/storage", async ({ + expect, +}) => { + // Check access through `fetch()` handler + let response = await exports.default.fetch("https://example.com/path"); + expect(await response.text()).toBe("1"); + + // Check sending request directly to instance + const id = env.COUNTER.idFromName("/path"); + const stub = env.COUNTER.get(id); + response = await runInDurableObject(stub, (instance: Counter) => { + expect(instance).toBeInstanceOf(Counter); // Exact same class as import + const request = new Request("https://example.com/path"); + return instance.fetch(request); + }); + expect(await response.text()).toBe("2"); + + // Check direct access to instance fields and storage + await runInDurableObject(stub, async (instance: Counter, state) => { + expect(instance.count).toBe(2); + expect(await state.storage.get("count")).toBe(2); + }); + + // Check IDs can be listed + const ids = await listDurableObjectIds(env.COUNTER); + expect(ids.map((i) => i.toString())).toContain(id.toString()); +}); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts new file mode 100644 index 0000000..5e1b1e0 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/eviction.test.ts @@ -0,0 +1,121 @@ +import { + evictAllDurableObjects, + evictDurableObject, + runInDurableObject, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import { Counter } from "../src/"; + +function getResponseWebSocket(response: Response) { + const socket = response.webSocket; + if (socket === null || socket === undefined) { + throw new TypeError("Expected WebSocket response"); + } + return socket; +} + +function waitForMessage(socket: WebSocket) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timed out waiting for WebSocket message")); + }, 10_000); + socket.addEventListener("message", (event) => { + clearTimeout(timeout); + resolve( + typeof event.data === "string" + ? event.data + : new TextDecoder().decode(event.data as ArrayBuffer) + ); + }); + socket.addEventListener("error", () => { + clearTimeout(timeout); + reject(new Error("WebSocket error while waiting for message")); + }); + }); +} + +function waitForClose(socket: WebSocket) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timed out waiting for WebSocket close")); + }, 10_000); + socket.addEventListener("close", (event) => { + clearTimeout(timeout); + resolve(event); + }); + socket.addEventListener("error", () => { + clearTimeout(timeout); + reject(new Error("WebSocket error while waiting for close")); + }); + }); +} + +it("resets in-memory state but preserves storage on targeted eviction", async ({ + expect, +}) => { + const id = env.COUNTER.idFromName(`evict-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + + // Persist `count = 2` through the `fetch()` handler + expect(await (await stub.fetch("https://example.com/")).text()).toBe("1"); + expect(await (await stub.fetch("https://example.com/")).text()).toBe("2"); + + // Corrupt in-memory state without persisting it to storage + await runInDurableObject(stub, (instance: Counter) => { + instance.count = 999; + }); + + await evictDurableObject(stub, { webSockets: "hibernate" }); + + // After eviction the instance is torn down: the in-memory `999` is discarded + // and `count` is reloaded from storage (`2`), so the next increment yields `3` + expect(await (await stub.fetch("https://example.com/")).text()).toBe("3"); +}); + +it("resets all running instances with bulk eviction", async ({ expect }) => { + const id = env.COUNTER.idFromName(`evict-all-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + + expect(await (await stub.fetch("https://example.com/")).text()).toBe("1"); + await runInDurableObject(stub, (instance: Counter) => { + instance.count = 999; + }); + + await evictAllDurableObjects(); + + expect(await (await stub.fetch("https://example.com/")).text()).toBe("2"); +}); + +it("hibernates WebSockets across eviction", async ({ expect }) => { + const id = env.COUNTER.idFromName(`evict-ws-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com/websocket-order", { + headers: { Upgrade: "websocket" }, + }); + const socket = getResponseWebSocket(response); + socket.accept(); + + await evictDurableObject(stub); + + // The WebSocket should be hibernated rather than closed, so messages still + // round-trip after eviction (waking the Durable Object back up) + const messagePromise = waitForMessage(socket); + socket.send("after-eviction"); + expect(await messagePromise).toBe("after-eviction"); + socket.close(1000, "done"); +}); + +it("closes WebSockets when requested during eviction", async ({ expect }) => { + const id = env.COUNTER.idFromName(`evict-ws-close-${crypto.randomUUID()}`); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com/websocket-order", { + headers: { Upgrade: "websocket" }, + }); + const socket = getResponseWebSocket(response); + socket.accept(); + + const closePromise = waitForClose(socket); + await evictDurableObject(stub, { webSockets: "close" }); + expect(await closePromise).toBeDefined(); +}); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/sqlite-in-do.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/sqlite-in-do.test.ts new file mode 100644 index 0000000..b65daef --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/sqlite-in-do.test.ts @@ -0,0 +1,7 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("enables SQL API with migrations", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/sql"); + expect(await response.text()).toBe("4096"); +}); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/durable-objects/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts b/fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts new file mode 100644 index 0000000..b72bf3d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/test/websockets.test.ts @@ -0,0 +1,79 @@ +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +const orderingAttempts = 5; +const orderingMessages = 100; + +function expectedOrderingMessages() { + return Array.from({ length: orderingMessages }, (_, i) => `message-${i}`); +} + +function getMessageData(event: MessageEvent) { + if (typeof event.data === "string") { + return event.data; + } + if (event.data instanceof ArrayBuffer) { + return new TextDecoder().decode(event.data); + } + throw new TypeError( + `Unexpected WebSocket message type: ${typeof event.data}` + ); +} + +function waitForMessages(socket: WebSocket, count: number) { + return new Promise((resolve, reject) => { + const messages: string[] = []; + const timeout = setTimeout(() => { + reject(new Error(`Timed out waiting for ${count} WebSocket messages`)); + }, 10_000); + + socket.addEventListener("message", (event) => { + messages.push(getMessageData(event)); + if (messages.length === count) { + clearTimeout(timeout); + resolve(messages); + } + }); + socket.addEventListener("error", () => { + clearTimeout(timeout); + reject(new Error("WebSocket error while waiting for messages")); + }); + }); +} + +function getResponseWebSocket(response: Response) { + const socket = response.webSocket; + if (socket === null || socket === undefined) { + throw new TypeError("Expected WebSocket response"); + } + return socket; +} + +it("preserves hibernatable WebSocket message order", async ({ expect }) => { + for (let attempt = 0; attempt < orderingAttempts; attempt++) { + const id = env.COUNTER.idFromName( + `websocket-ordering-${crypto.randomUUID()}-${attempt}` + ); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com/websocket-order", { + headers: { Upgrade: "websocket" }, + }); + const socket = getResponseWebSocket(response); + const expected = expectedOrderingMessages(); + const messagesPromise = waitForMessages(socket, orderingMessages); + + socket.accept(); + for (const message of expected) { + socket.send(message); + } + + expect(await messagesPromise).toEqual(expected); + + const logResponse = await stub.fetch( + "https://example.com/websocket-order-log" + ); + expect(await logResponse.json()).toEqual(expected); + + socket.close(1000, "done"); + } +}); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/tsconfig.json b/fixtures/vitest-pool-workers-examples/durable-objects/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/vitest.config.ts b/fixtures/vitest-pool-workers-examples/durable-objects/vitest.config.ts new file mode 100644 index 0000000..ce613dc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/vitest.config.ts @@ -0,0 +1,19 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + test: { + name: "@scoped/durable-objects", + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/durable-objects/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/durable-objects/wrangler.jsonc new file mode 100644 index 0000000..53dc8c9 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/durable-objects/wrangler.jsonc @@ -0,0 +1,24 @@ +{ + "name": "durable-objects", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "durable_objects": { + "bindings": [ + { + "name": "COUNTER", + "class_name": "Counter", + }, + { + "name": "SQL", + "class_name": "SQLiteDurableObject", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_classes": ["Counter"], + "new_sqlite_classes": ["SQLiteDurableObject"], + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/src/greeting.ts b/fixtures/vitest-pool-workers-examples/dynamic-import/src/greeting.ts new file mode 100644 index 0000000..df34ef5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/src/greeting.ts @@ -0,0 +1,3 @@ +export function greet(name: string): string { + return `Hello, ${name}!`; +} diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/src/index.ts b/fixtures/vitest-pool-workers-examples/dynamic-import/src/index.ts new file mode 100644 index 0000000..f50ae23 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/src/index.ts @@ -0,0 +1,25 @@ +import { DurableObject } from "cloudflare:workers"; + +// Durable Object that uses dynamic import() in fetch handler. +// Regression test for https://github.com/cloudflare/workers-sdk/issues/5387 +export class GreeterDO extends DurableObject { + async fetch(request: Request): Promise { + const { greet } = await import("./greeting"); + return new Response(greet("DO")); + } +} + +export default { + async fetch( + request: Request, + _env: unknown, + _ctx: ExecutionContext + ): Promise { + // Dynamic import inside a fetch handler — this is the pattern that + // triggers the cross-DO I/O violation in vitest-pool-workers 0.13.x + // when called via `exports.default.fetch()` in tests. + // See: https://github.com/cloudflare/workers-sdk/issues/12924 + const { greet } = await import("./greeting"); + return new Response(greet("World")); + }, +} satisfies ExportedHandler; diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/dynamic-import/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/src/worker-configuration.d.ts b/fixtures/vitest-pool-workers-examples/dynamic-import/src/worker-configuration.d.ts new file mode 100644 index 0000000..e497300 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/src/worker-configuration.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./dynamic-import/src/worker-configuration.d.ts -c ./dynamic-import/wrangler.jsonc --no-include-runtime` (hash: 3db8a5d08906b2ae83ef9a0539ebae04) +interface __BaseEnv_Env { + GREETER: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "GreeterDO"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/test/dynamic-import.test.ts b/fixtures/vitest-pool-workers-examples/dynamic-import/test/dynamic-import.test.ts new file mode 100644 index 0000000..1c950f9 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/test/dynamic-import.test.ts @@ -0,0 +1,29 @@ +import { env } from "cloudflare:workers"; +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +// Regression test for https://github.com/cloudflare/workers-sdk/issues/12924 +// +// Calling exports.default.fetch() on a worker whose fetch handler uses a +// dynamic import() would hang with "Cannot perform I/O on behalf of a +// different Durable Object". Pre-loading the module (e.g. via a static +// import of the worker) masks the bug by caching the module. +it("exports.default.fetch() with dynamic import()", async ({ expect }) => { + const response = await exports.default.fetch( + new Request("https://example.com/") + ); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello, World!"); +}); + +// Regression test for https://github.com/cloudflare/workers-sdk/issues/5387 +// +// Dynamic import() inside a Durable Object fetch handler has the same +// cross-context I/O violation when the module isn't already cached. +it("Durable Object fetch with dynamic import()", async ({ expect }) => { + const id = env.GREETER.idFromName("test"); + const stub = env.GREETER.get(id); + const response = await stub.fetch(new Request("https://example.com/")); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello, DO!"); +}); diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/dynamic-import/test/tsconfig.json new file mode 100644 index 0000000..20248fc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/tsconfig.json b/fixtures/vitest-pool-workers-examples/dynamic-import/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/vitest.config.ts b/fixtures/vitest-pool-workers-examples/dynamic-import/vitest.config.ts new file mode 100644 index 0000000..4217062 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/vitest.config.ts @@ -0,0 +1,16 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/dynamic-import/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/dynamic-import/wrangler.jsonc new file mode 100644 index 0000000..715d079 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/dynamic-import/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "name": "dynamic-import", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "durable_objects": { + "bindings": [{ "name": "GREETER", "class_name": "GreeterDO" }], + }, + "migrations": [{ "tag": "v1", "new_classes": ["GreeterDO"] }], +} diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/README.md b/fixtures/vitest-pool-workers-examples/hyperdrive/README.md new file mode 100644 index 0000000..9997f46 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/README.md @@ -0,0 +1,7 @@ +# 🚀 hyperdrive + +This Worker establishes a TCP connection with an echo server via Hyperdrive. The echo server is started/stopped by [global-setup.ts](global-setup.ts) on a random port, which is then provided to [vitest.config.mts](vitest.config.mts). In a real worker, Hyperdrive would be used with a database instead. + +| Test | Overview | +| --------------------------------- | -------------------------------- | +| [echo.test.ts](test/echo.test.ts) | Integration test using `exports` | diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/env.d.ts b/fixtures/vitest-pool-workers-examples/hyperdrive/env.d.ts new file mode 100644 index 0000000..d409ce6 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/env.d.ts @@ -0,0 +1,7 @@ +declare module "vitest" { + interface ProvidedContext { + echoServerPort: number; + } +} + +export {}; diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/global-setup.ts b/fixtures/vitest-pool-workers-examples/hyperdrive/global-setup.ts new file mode 100644 index 0000000..aa60834 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/global-setup.ts @@ -0,0 +1,40 @@ +import assert from "node:assert"; +import events from "node:events"; +import net from "node:net"; +import util from "node:util"; +import type { TestProject } from "vitest/node"; + +export const POSTGRES_SSL_REQUEST_PACKET = Buffer.from([ + 0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x2f, +]); + +// Global setup runs inside Node.js, not `workerd` +export default async function ({ provide }: TestProject) { + // Start echo server on random port + const server = net.createServer((socket) => { + socket.on("data", (chunk) => { + // on postgres ssl request packet respond with 'N' to indicate no SSL support + if (POSTGRES_SSL_REQUEST_PACKET.equals(chunk)) { + socket.write("N"); + } else { + socket.write(chunk); + } + }); + }); + const listeningPromise = events.once(server, "listening"); + server.listen(0, "127.0.0.1"); + await listeningPromise; + + // Get randomly assigned port and provide for config + const address = server.address(); + assert(typeof address === "object" && address !== null); + const port = address.port; + provide("echoServerPort", port); + console.log(`Started echo server on port ${port}`); + + return async () => { + // Stop echo server on teardown + await util.promisify(server.close.bind(server))(); + console.log("Stopped echo server"); + }; +} diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/src/env.d.ts b/fixtures/vitest-pool-workers-examples/hyperdrive/src/env.d.ts new file mode 100644 index 0000000..41053a1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/src/env.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./hyperdrive/src/env.d.ts -c ./hyperdrive/wrangler.jsonc --no-include-runtime` (hash: b649c979f0061521ef2170b65cdd0101) +interface __BaseEnv_Env { + ECHO_SERVER_HYPERDRIVE: Hyperdrive; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/src/index.ts b/fixtures/vitest-pool-workers-examples/hyperdrive/src/index.ts new file mode 100644 index 0000000..e4d3aa6 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/src/index.ts @@ -0,0 +1,17 @@ +export default >{ + async fetch(request, env, ctx) { + if (request.method !== "POST") return new Response("Method Not Allowed"); + if (request.body === null) return new Response(); + + const socket = env.ECHO_SERVER_HYPERDRIVE.connect(); + const writer = socket.writable.getWriter(); + + // parse body + const value = await request.text(); + await writer.write(new TextEncoder().encode(value)); + const result = await socket.readable.getReader().read(); + + writer.close(); + return new Response(result.value); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/hyperdrive/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/test/echo.test.ts b/fixtures/vitest-pool-workers-examples/hyperdrive/test/echo.test.ts new file mode 100644 index 0000000..8cafd1e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/test/echo.test.ts @@ -0,0 +1,11 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("responds with request body", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/", { + method: "POST", + body: "body", + }); + expect(response.status).toBe(200); + expect(await response.text()).toBe("body"); +}); diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/hyperdrive/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/tsconfig.json b/fixtures/vitest-pool-workers-examples/hyperdrive/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/vitest.config.ts b/fixtures/vitest-pool-workers-examples/hyperdrive/vitest.config.ts new file mode 100644 index 0000000..c2be5c3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/vitest.config.ts @@ -0,0 +1,29 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest(({ inject }) => { + // Provided in `global-setup.ts` + const echoServerPort = inject("echoServerPort"); + + return { + miniflare: { + hyperdrives: { + ECHO_SERVER_HYPERDRIVE: `postgres://user:pass@127.0.0.1:${echoServerPort}/db`, + }, + }, + wrangler: { + configPath: "./wrangler.jsonc", + }, + }; + }), + ], + test: { + globalSetup: ["./global-setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/hyperdrive/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/hyperdrive/wrangler.jsonc new file mode 100644 index 0000000..12d2f75 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/hyperdrive/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "hyperdrive", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "hyperdrive": [ + { + "binding": "ECHO_SERVER_HYPERDRIVE", + "id": "00000000000000000000000000000000", + "localConnectionString": "postgres://user:pass@127.0.0.1/db", // Overridden in `vitest.config.mts` + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/images/README.md b/fixtures/vitest-pool-workers-examples/images/README.md new file mode 100644 index 0000000..9410bf7 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/README.md @@ -0,0 +1,3 @@ +# images + +This Worker returns information about an image that is POSTed to it and can perform Hosted Images CRUD operations. diff --git a/fixtures/vitest-pool-workers-examples/images/src/env.d.ts b/fixtures/vitest-pool-workers-examples/images/src/env.d.ts new file mode 100644 index 0000000..af4e8db --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/src/env.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./images/src/env.d.ts -c ./images/wrangler.jsonc --no-include-runtime` (hash: 8c4b4847f688035a3be996ec1a9112ba) +interface __BaseEnv_Env { + IMAGES: ImagesBinding; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/images/src/index.ts b/fixtures/vitest-pool-workers-examples/images/src/index.ts new file mode 100644 index 0000000..75901c9 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/src/index.ts @@ -0,0 +1,8 @@ +export default { + async fetch(request, env, _ctx) { + return Response.json(await env.IMAGES.info(request.body!)); + }, +} satisfies ExportedHandler; +// ^ Using `satisfies` provides type checking/completions for `ExportedHandler` +// whilst still allowing us to call `worker.fetch()` and `worker.queue()` in +// tests without asserting they're defined. diff --git a/fixtures/vitest-pool-workers-examples/images/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/images/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/images/test/images.test.ts b/fixtures/vitest-pool-workers-examples/images/test/images.test.ts new file mode 100644 index 0000000..1686f20 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/test/images.test.ts @@ -0,0 +1,100 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +const TINY_PNG = new Uint8Array([ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 18, 0, + 0, 0, 18, 8, 6, 0, 0, 0, 86, 206, 142, 87, 0, 0, 0, 4, 103, 65, 77, 65, 0, 0, + 177, 143, 11, 252, 97, 5, 0, 0, 0, 32, 99, 72, 82, 77, 0, 0, 122, 38, 0, 0, + 128, 132, 0, 0, 250, 0, 0, 0, 128, 232, 0, 0, 117, 48, 0, 0, 234, 96, 0, 0, + 58, 152, 0, 0, 23, 112, 156, 186, 81, 60, 0, 0, 0, 120, 101, 88, 73, 102, 77, + 77, 0, 42, 0, 0, 0, 8, 0, 4, 1, 18, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 1, 26, 0, 5, + 0, 0, 0, 1, 0, 0, 0, 62, 1, 27, 0, 5, 0, 0, 0, 1, 0, 0, 0, 70, 135, 105, 0, 4, + 0, 0, 0, 1, 0, 0, 0, 78, 0, 0, 0, 0, 0, 0, 0, 72, 0, 0, 0, 1, 0, 0, 0, 72, 0, + 0, 0, 1, 0, 3, 160, 1, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 160, 2, 0, 4, 0, 0, 0, 1, + 0, 0, 0, 18, 160, 3, 0, 4, 0, 0, 0, 1, 0, 0, 0, 18, 0, 0, 0, 0, 117, 55, 28, + 226, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 11, 19, 0, 0, 11, 19, 1, 0, 154, 156, + 24, 0, 0, 2, 146, 105, 84, 88, 116, 88, 77, 76, 58, 99, 111, 109, 46, 97, 100, + 111, 98, 101, 46, 120, 109, 112, 0, 0, 0, 0, 0, 60, 120, 58, 120, 109, 112, + 109, 101, 116, 97, 32, 120, 109, 108, 110, 115, 58, 120, 61, 34, 97, 100, 111, + 98, 101, 58, 110, 115, 58, 109, 101, 116, 97, 47, 34, 32, 120, 58, 120, 109, + 112, 116, 107, 61, 34, 88, 77, 80, 32, 67, 111, 114, 101, 32, 54, 46, 48, 46, + 48, 34, 62, 10, 32, 32, 32, 60, 114, 100, 102, 58, 82, 68, 70, 32, 120, 109, + 108, 110, 115, 58, 114, 100, 102, 61, 34, 104, 116, 116, 112, 58, 47, 47, 119, + 119, 119, 46, 119, 51, 46, 111, 114, 103, 47, 49, 57, 57, 57, 47, 48, 50, 47, + 50, 50, 45, 114, 100, 102, 45, 115, 121, 110, 116, 97, 120, 45, 110, 115, 35, + 34, 62, 10, 32, 32, 32, 32, 32, 32, 60, 114, 100, 102, 58, 68, 101, 115, 99, + 114, 105, 112, 116, 105, 111, 110, 32, 114, 100, 102, 58, 97, 98, 111, 117, + 116, 61, 34, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 120, 109, + 108, 110, 115, 58, 116, 105, 102, 102, 61, 34, 104, 116, 116, 112, 58, 47, 47, + 110, 115, 46, 97, 100, 111, 98, 101, 46, 99, 111, 109, 47, 116, 105, 102, 102, + 47, 49, 46, 48, 47, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 120, 109, 108, 110, 115, 58, 101, 120, 105, 102, 61, 34, 104, 116, 116, 112, + 58, 47, 47, 110, 115, 46, 97, 100, 111, 98, 101, 46, 99, 111, 109, 47, 101, + 120, 105, 102, 47, 49, 46, 48, 47, 34, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 60, 116, 105, 102, 102, 58, 89, 82, 101, 115, 111, 108, 117, 116, 105, + 111, 110, 62, 55, 50, 60, 47, 116, 105, 102, 102, 58, 89, 82, 101, 115, 111, + 108, 117, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, + 116, 105, 102, 102, 58, 88, 82, 101, 115, 111, 108, 117, 116, 105, 111, 110, + 62, 55, 50, 60, 47, 116, 105, 102, 102, 58, 88, 82, 101, 115, 111, 108, 117, + 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 105, + 102, 102, 58, 79, 114, 105, 101, 110, 116, 97, 116, 105, 111, 110, 62, 49, 60, + 47, 116, 105, 102, 102, 58, 79, 114, 105, 101, 110, 116, 97, 116, 105, 111, + 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 101, 120, 105, 102, 58, + 80, 105, 120, 101, 108, 88, 68, 105, 109, 101, 110, 115, 105, 111, 110, 62, + 54, 52, 60, 47, 101, 120, 105, 102, 58, 80, 105, 120, 101, 108, 88, 68, 105, + 109, 101, 110, 115, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 60, 101, 120, 105, 102, 58, 67, 111, 108, 111, 114, 83, 112, 97, 99, 101, 62, + 49, 60, 47, 101, 120, 105, 102, 58, 67, 111, 108, 111, 114, 83, 112, 97, 99, + 101, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 101, 120, 105, 102, 58, + 80, 105, 120, 101, 108, 89, 68, 105, 109, 101, 110, 115, 105, 111, 110, 62, + 54, 52, 60, 47, 101, 120, 105, 102, 58, 80, 105, 120, 101, 108, 89, 68, 105, + 109, 101, 110, 115, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 60, 47, + 114, 100, 102, 58, 68, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 62, + 10, 32, 32, 32, 60, 47, 114, 100, 102, 58, 82, 68, 70, 62, 10, 60, 47, 120, + 58, 120, 109, 112, 109, 101, 116, 97, 62, 10, 104, 194, 171, 3, 0, 0, 2, 38, + 73, 68, 65, 84, 56, 17, 221, 83, 75, 104, 19, 81, 20, 61, 239, 51, 153, 73, + 172, 197, 46, 84, 208, 69, 141, 165, 65, 4, 65, 232, 78, 193, 79, 93, 73, 169, + 136, 216, 224, 70, 16, 92, 21, 4, 17, 177, 59, 237, 168, 117, 229, 66, 5, 247, + 130, 203, 180, 43, 215, 74, 179, 113, 227, 194, 141, 32, 130, 24, 20, 162, 96, + 45, 90, 219, 152, 100, 38, 243, 222, 243, 188, 244, 131, 17, 68, 196, 149, 94, + 114, 185, 111, 114, 207, 59, 247, 158, 123, 103, 128, 127, 202, 220, 4, 148, + 171, 208, 29, 68, 247, 28, 67, 255, 177, 0, 79, 240, 171, 75, 158, 248, 231, + 92, 79, 133, 56, 134, 156, 222, 75, 208, 75, 8, 81, 70, 150, 222, 24, 26, 113, + 210, 92, 200, 12, 74, 82, 98, 73, 65, 206, 229, 174, 214, 30, 8, 1, 114, 17, + 3, 48, 172, 218, 6, 179, 35, 137, 136, 97, 215, 19, 233, 76, 241, 44, 145, 15, + 181, 18, 176, 214, 49, 201, 76, 32, 209, 106, 102, 115, 133, 107, 239, 38, 92, + 165, 162, 102, 81, 70, 185, 12, 227, 239, 116, 137, 214, 73, 86, 226, 161, + 109, 188, 182, 95, 106, 219, 128, 21, 79, 36, 16, 117, 44, 82, 226, 40, 211, + 55, 1, 211, 215, 159, 15, 147, 47, 175, 46, 69, 55, 113, 215, 19, 116, 71, 80, + 38, 218, 15, 81, 204, 194, 52, 103, 74, 59, 133, 75, 159, 70, 129, 28, 252, + 150, 202, 140, 215, 188, 108, 118, 40, 200, 183, 106, 66, 230, 76, 96, 63, 40, + 51, 48, 254, 58, 127, 108, 116, 50, 121, 113, 238, 109, 116, 6, 111, 124, 86, + 163, 66, 48, 251, 210, 162, 51, 22, 68, 122, 112, 101, 185, 157, 8, 91, 15, + 169, 222, 119, 176, 65, 226, 193, 78, 66, 181, 23, 224, 242, 71, 199, 134, + 177, 88, 125, 172, 228, 102, 99, 30, 141, 220, 81, 39, 170, 87, 132, 139, 119, + 69, 137, 20, 183, 89, 250, 180, 177, 217, 118, 200, 126, 136, 190, 97, 54, + 162, 5, 4, 21, 117, 157, 124, 66, 195, 101, 45, 232, 61, 199, 17, 30, 58, 233, + 204, 179, 123, 214, 53, 107, 50, 232, 111, 136, 236, 243, 194, 148, 78, 148, + 184, 31, 22, 228, 249, 70, 131, 29, 216, 101, 131, 168, 36, 100, 113, 148, 83, + 9, 233, 84, 167, 2, 138, 99, 244, 59, 202, 69, 8, 15, 142, 243, 153, 53, 118, + 28, 0, 26, 187, 59, 230, 227, 124, 78, 186, 250, 164, 200, 110, 21, 157, 234, + 174, 196, 47, 148, 149, 93, 27, 72, 222, 123, 37, 60, 175, 185, 63, 123, 145, + 75, 244, 139, 207, 25, 231, 129, 218, 101, 32, 220, 199, 77, 14, 192, 180, + 117, 71, 167, 153, 61, 165, 148, 242, 189, 19, 149, 209, 11, 252, 177, 90, + 143, 249, 119, 147, 203, 43, 108, 129, 90, 172, 65, 213, 167, 128, 77, 220, + 187, 225, 127, 73, 139, 111, 126, 250, 181, 7, 254, 55, 15, 28, 246, 239, 190, + 159, 35, 107, 252, 85, 70, 158, 15, 79, 51, 94, 167, 255, 96, 159, 182, 250, + 33, 252, 175, 246, 29, 136, 209, 199, 77, 51, 27, 182, 48, 0, 0, 0, 0, 73, 69, + 78, 68, 174, 66, 96, 130, +]); + +it("can return image info", async ({ expect }) => { + const resp = (await ( + await exports.default.fetch("https://example.com/", { + method: "POST", + body: new Blob([TINY_PNG]).stream(), + }) + ).json()) as ImageInfoResponse; + + expect(resp.format).toEqual("image/png"); +}); diff --git a/fixtures/vitest-pool-workers-examples/images/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/images/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/images/tsconfig.json b/fixtures/vitest-pool-workers-examples/images/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/images/vitest.config.ts b/fixtures/vitest-pool-workers-examples/images/vitest.config.ts new file mode 100644 index 0000000..4217062 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/vitest.config.ts @@ -0,0 +1,16 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/images/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/images/wrangler.jsonc new file mode 100644 index 0000000..1d36b37 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/images/wrangler.jsonc @@ -0,0 +1,8 @@ +{ + "name": "images", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "images": { + "binding": "IMAGES", + }, +} diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/README.md b/fixtures/vitest-pool-workers-examples/kv-r2-caches/README.md new file mode 100644 index 0000000..5451091 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/README.md @@ -0,0 +1,8 @@ +# 📦 kv-r2-caches + +This Worker makes use of KV and R2 bindings, along with the Cache API. The Worker accepts `GET`/`PUT` requests to `/kv/...` and `/r2/...`. Request URLs are used as keys. `PUT` requests store their body at the corresponding key. `GET` requests read the value at the key. R2 reads are cached using the Cache API. + +| Test | Overview | +| ----------------------------- | ------------------------------------------- | +| [kv.test.ts](test/kv.test.ts) | Integration tests for KV endpoints | +| [r2.test.ts](test/r2.test.ts) | Integration and unit tests for R2 endpoints | diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/env.d.ts b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/env.d.ts new file mode 100644 index 0000000..65522bb --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/env.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./kv-r2-caches/src/env.d.ts -c ./kv-r2-caches/wrangler.jsonc --no-include-runtime` (hash: 7e0cf6d569a493341235c16658e7e4d0) +interface __BaseEnv_Env { + KV_NAMESPACE: KVNamespace; + R2_BUCKET: R2Bucket; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/helpers.ts b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/helpers.ts new file mode 100644 index 0000000..9b97bc7 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/helpers.ts @@ -0,0 +1,39 @@ +export async function handleKVRequest(request: Request, env: Env) { + if (request.method === "GET") { + const value = await env.KV_NAMESPACE.get(request.url, "stream"); + return new Response(value, { status: value === null ? 204 : 200 }); + } else if (request.method === "PUT") { + await env.KV_NAMESPACE.put(request.url, request.body ?? ""); + return new Response(null, { status: 204 }); + } else { + return new Response("Method Not Allowed", { status: 405 }); + } +} + +export async function handleR2Request( + request: Request, + env: Env, + ctx: ExecutionContext +) { + if (request.method === "GET") { + let response = await caches.default.match(request); + if (response !== undefined) return response; + + const object = await env.R2_BUCKET.get(request.url); + if (object === null) return new Response(null, { status: 204 }); + + const headers = new Headers(); + object.writeHttpMetadata(headers); + response = new Response(object.body, { headers }); + + ctx.waitUntil(caches.default.put(request, response.clone())); + return response; + } else if (request.method === "PUT") { + await env.R2_BUCKET.put(request.url, request.body, { + httpMetadata: request.headers, + }); + return new Response(null, { status: 204 }); + } else { + return new Response("Method Not Allowed", { status: 405 }); + } +} diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/index.ts b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/index.ts new file mode 100644 index 0000000..7242b1a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/index.ts @@ -0,0 +1,14 @@ +import { handleKVRequest, handleR2Request } from "./helpers"; + +export default >{ + fetch(request, env, ctx) { + const url = new URL(request.url); + if (url.pathname.startsWith("/kv/")) { + return handleKVRequest(request, env); + } else if (url.pathname.startsWith("/r2/")) { + return handleR2Request(request, env, ctx); + } else { + return new Response("Not Found", { status: 404 }); + } + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/kv.test.ts b/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/kv.test.ts new file mode 100644 index 0000000..3dfc11e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/kv.test.ts @@ -0,0 +1,14 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("stores in KV namespace", async ({ expect }) => { + let response = await exports.default.fetch("https://example.com/kv/key", { + method: "PUT", + body: "value", + }); + expect(response.status).toBe(204); + + response = await exports.default.fetch("https://example.com/kv/key"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("value"); +}); diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/r2.test.ts b/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/r2.test.ts new file mode 100644 index 0000000..d229924 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/r2.test.ts @@ -0,0 +1,32 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { it } from "vitest"; +import { handleR2Request } from "../src/helpers"; // Note we can import any function + +it("stores in R2 bucket", async ({ expect }) => { + let response = await exports.default.fetch("https://example.com/r2/key", { + method: "PUT", + headers: { "Cache-Control": "max-age=3600" }, + body: "value", + }); + expect(response.status).toBe(204); + + const request = new Request("https://example.com/r2/key"); + let ctx = createExecutionContext(); + response = await handleR2Request(request, env, ctx); + await waitOnExecutionContext(ctx); // Wait for `caches.default.put()` + expect(response.status).toBe(200); + expect(response.headers.get("CF-Cache-Status")).toBe(null); + expect(await response.text()).toBe("value"); + + // Check 2nd request to the same resource is cached + ctx = createExecutionContext(); + response = await handleR2Request(request, env, ctx); + await waitOnExecutionContext(ctx); + expect(response.status).toBe(200); + expect(response.headers.get("CF-Cache-Status")).toBe("HIT"); + expect(await response.text()).toBe("value"); +}); diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/tsconfig.json b/fixtures/vitest-pool-workers-examples/kv-r2-caches/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/vitest.config.ts b/fixtures/vitest-pool-workers-examples/kv-r2-caches/vitest.config.ts new file mode 100644 index 0000000..4217062 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/vitest.config.ts @@ -0,0 +1,16 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/kv-r2-caches/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/kv-r2-caches/wrangler.jsonc new file mode 100644 index 0000000..43d1d43 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/kv-r2-caches/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "name": "kv-r2-caches", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "kv_namespaces": [ + { + "binding": "KV_NAMESPACE", + "id": "00000000000000000000000000000000", + }, + ], + "r2_buckets": [ + { + "binding": "R2_BUCKET", + "bucket_name": "bucket", + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/misc/README.md b/fixtures/vitest-pool-workers-examples/misc/README.md new file mode 100644 index 0000000..24cbb33 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/README.md @@ -0,0 +1,3 @@ +# 🤷 misc + +We run all the tests in `vitest-pool-workers-examples` on every change to `workers-sdk`. This directory contains a bunch of tests to validate assorted Vitest features that aren't tested elsewhere. diff --git a/fixtures/vitest-pool-workers-examples/misc/global-setup.ts b/fixtures/vitest-pool-workers-examples/misc/global-setup.ts new file mode 100644 index 0000000..3ebff2e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/global-setup.ts @@ -0,0 +1,12 @@ +// Regression test for #9957: In v3, `provide` data was sent via HTTP headers +// (~8 KB limit). Large payloads caused silent failures. v4 sends provide data +// through WebSocket messages (32 MiB limit). +import type { TestProject } from "vitest/node"; + +export default function ({ provide }: TestProject) { + // No ProvidedContext declaration for this key, so cast is needed + (provide as (key: string, value: unknown) => void)( + "largePayload", + "x".repeat(50_000) + ); +} diff --git a/fixtures/vitest-pool-workers-examples/misc/public/test.txt b/fixtures/vitest-pool-workers-examples/misc/public/test.txt new file mode 100644 index 0000000..b45ef6f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/public/test.txt @@ -0,0 +1 @@ +Hello, World! \ No newline at end of file diff --git a/fixtures/vitest-pool-workers-examples/misc/src/other-worker.mjs b/fixtures/vitest-pool-workers-examples/misc/src/other-worker.mjs new file mode 100644 index 0000000..8cc2d57 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/src/other-worker.mjs @@ -0,0 +1,5 @@ +export class OtherObject { + async fetch(request) { + return new Response("OtherObject body"); + } +} diff --git a/fixtures/vitest-pool-workers-examples/misc/test/bare-specifiers.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/bare-specifiers.test.ts new file mode 100644 index 0000000..6fbeea5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/bare-specifiers.test.ts @@ -0,0 +1,26 @@ +// Regression test for #6214: bare Node.js module specifiers (without `node:` +// prefix) should resolve. The `module-fallback.ts` `viteResolve()` codepath +// prepends `node:` when a bare specifier isn't found in workerd built-ins. +import { describe, it } from "vitest"; + +describe("bare Node module specifiers", () => { + it("resolves 'url' without node: prefix", async ({ expect }) => { + const urlModule = await import("url"); + expect(urlModule.URL).toBeDefined(); + expect(new urlModule.URL("https://example.com").hostname).toBe( + "example.com" + ); + }); + + it("resolves 'path' without node: prefix", async ({ expect }) => { + const pathModule = await import("path"); + expect(pathModule.join).toBeDefined(); + expect(pathModule.join("/foo", "bar")).toBe("/foo/bar"); + }); + + it("resolves 'buffer' without node: prefix", async ({ expect }) => { + const bufferModule = await import("buffer"); + expect(bufferModule.Buffer).toBeDefined(); + expect(bufferModule.Buffer.from("hello").toString()).toBe("hello"); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/defines.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/defines.test.ts new file mode 100644 index 0000000..9eb8e64 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/defines.test.ts @@ -0,0 +1,13 @@ +import { it } from "vitest"; + +it("replaces defines from wrangler.toml", async ({ expect }) => { + expect(WRANGLER_DEFINED_THING).toBe("thing"); + expect(WRANGLER_NESTED.DEFINED.THING).toStrictEqual([1, 2, 3]); + expect(WRANGLER_NESTED.DEFINED.THING).toBe(WRANGLER_NESTED.DEFINED.THING); +}); + +it("replaces defines from vitest.config.mts", async ({ expect }) => { + expect(CONFIG_DEFINED_THING).toBe("thing"); + expect(CONFIG_NESTED.DEFINED.THING).toStrictEqual([1, 2, 3]); + // Note that, unlike ESBuild, Oxc does not share object references when using `define` (https://oxc.rs/docs/guide/usage/transformer/global-variable-replacement#define) +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/durable-objects.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/durable-objects.test.ts new file mode 100644 index 0000000..10f0fd1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/durable-objects.test.ts @@ -0,0 +1,23 @@ +import { + listDurableObjectIds, + runDurableObjectAlarm, + runInDurableObject, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +it("uses other object", async ({ expect }) => { + const id = env.OTHER_OBJECT.idFromName("other-test"); + const stub = env.OTHER_OBJECT.get(id); + const response = await stub.fetch("http://x"); + expect(await response.text()).toBe("OtherObject body"); + + // Check can only use run in helpers for same-isolate classes... + await expect(runInDurableObject(stub, () => {})).rejects.toThrow(); + await expect(runDurableObjectAlarm(stub)).rejects.toThrow(); + + // ...but can list IDs for any class + const ids = await listDurableObjectIds(env.OTHER_OBJECT); + expect(ids.length).toBe(1); + expect(ids[0].equals(id)).toBe(true); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/env.d.ts b/fixtures/vitest-pool-workers-examples/misc/test/env.d.ts new file mode 100644 index 0000000..9e4376d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/env.d.ts @@ -0,0 +1,7 @@ +declare namespace Cloudflare { + interface Env { + ASSETS: Fetcher; + KV_NAMESPACE: KVNamespace; + OTHER_OBJECT: DurableObjectNamespace; + } +} diff --git a/fixtures/vitest-pool-workers-examples/misc/test/fake-timers.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/fake-timers.test.ts new file mode 100644 index 0000000..1ae970c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/fake-timers.test.ts @@ -0,0 +1,23 @@ +import { afterEach, beforeEach, it, vi } from "vitest"; + +beforeEach(() => { + vi.useFakeTimers(); +}); +afterEach(() => { + vi.useRealTimers(); +}); + +it("fake system time", ({ expect }) => { + const date = new Date(2023, 0, 1); + vi.setSystemTime(date); + expect(new Date()).toMatchInlineSnapshot(`2023-01-01T00:00:00.000Z`); +}); + +it("advances fake time", ({ expect }) => { + const fn = vi.fn(() => {}); + setTimeout(fn, 1000); + vi.advanceTimersByTime(500); + expect(fn).not.toHaveBeenCalled(); + vi.advanceTimersByTime(500); + expect(fn).toHaveBeenCalled(); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/global.d.ts b/fixtures/vitest-pool-workers-examples/misc/test/global.d.ts new file mode 100644 index 0000000..60644ea --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/global.d.ts @@ -0,0 +1,9 @@ +declare global { + const WRANGLER_DEFINED_THING: string; + const WRANGLER_NESTED: { DEFINED: { THING: boolean } }; + + const CONFIG_DEFINED_THING: string; + const CONFIG_NESTED: { DEFINED: { THING: boolean } }; +} + +export {}; diff --git a/fixtures/vitest-pool-workers-examples/misc/test/large-provide.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/large-provide.test.ts new file mode 100644 index 0000000..3e69986 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/large-provide.test.ts @@ -0,0 +1,12 @@ +// Regression test for #9957: In v3, `provide` data was sent via HTTP headers +// (~8 KB limit). v4 sends provide data through WebSocket messages (32 MiB +// limit). This test injects a 50 KB string provided by global-setup.ts. +import { inject, it } from "vitest"; + +it("receives 50 KB of provided data without truncation", ({ expect }) => { + const data = inject("largePayload" as never); + expect(data).toBeDefined(); + expect(typeof data).toBe("string"); + expect((data as string).length).toBe(50_000); + expect(data).toBe("x".repeat(50_000)); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/module-mocking.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/module-mocking.test.ts new file mode 100644 index 0000000..09c0511 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/module-mocking.test.ts @@ -0,0 +1,14 @@ +import { jwtVerify, JWTVerifyResult } from "jose"; +import { it, vi } from "vitest"; + +vi.mock("jose", () => { + const jwtVerify = async (): Promise> => { + return { payload: { mock: true }, protectedHeader: { alg: "" } }; + }; + return { jwtVerify }; +}); + +it("uses mocked module", async ({ expect }) => { + const result = await jwtVerify("", new Uint8Array()); + expect(result.payload).toStrictEqual({ mock: true }); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/msw.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/msw.test.ts new file mode 100644 index 0000000..aac8a34 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/msw.test.ts @@ -0,0 +1,61 @@ +import { http, HttpResponse } from "msw"; +import { expect, it } from "vitest"; +import { server } from "./server"; + +it("falls through to global fetch() if unmatched", async () => { + server.use( + http.get( + "https://example.com", + () => { + return HttpResponse.text("body"); + }, + { once: true } + ) + ); + + let response = await fetch("https://example.com"); + expect(response.url).toEqual("https://example.com/"); + expect(await response.text()).toBe("body"); + + response = await fetch("https://example.com/bad"); + expect(response.url).toEqual("https://example.com/bad"); + expect(await response.text()).toBe("fallthrough:GET https://example.com/bad"); +}); + +it("intercepts URLs with query parameters with repeated keys", async () => { + server.use( + http.get( + "https://example.com/foo?key=value", + () => { + return HttpResponse.text("foo"); + }, + { once: true } + ), + http.get( + "https://example.com/bar?a=1&a=2", + () => { + return HttpResponse.text("bar"); + }, + { once: true } + ), + http.get( + "https://example.com/baz?key1=a&key2=c&key1=b", + () => { + return HttpResponse.text("baz"); + }, + { once: true } + ) + ); + + let response1 = await fetch("https://example.com/foo?key=value"); + expect(response1.url).toEqual("https://example.com/foo?key=value"); + expect(await response1.text()).toBe("foo"); + + let response2 = await fetch("https://example.com/bar?a=1&a=2"); + expect(response2.url).toEqual("https://example.com/bar?a=1&a=2"); + expect(await response2.text()).toBe("bar"); + + let response3 = await fetch("https://example.com/baz?key1=a&key2=c&key1=b"); + expect(response3.url).toEqual("https://example.com/baz?key1=a&key2=c&key1=b"); + expect(await response3.text()).toBe("baz"); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/nodejs.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/nodejs.test.ts new file mode 100644 index 0000000..f3c5132 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/nodejs.test.ts @@ -0,0 +1,8 @@ +import { it } from "vitest"; + +it("runs in Node.js compatibility mode", ({ expect }) => { + expect(typeof process).toBe("object"); + expect(process.versions).toBeDefined(); + expect(process.versions.node).toBeDefined(); + expect(typeof Buffer).toBe("function"); +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/pages-functions.test.ts b/fixtures/vitest-pool-workers-examples/misc/test/pages-functions.test.ts new file mode 100644 index 0000000..b07f00d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/pages-functions.test.ts @@ -0,0 +1,183 @@ +import { + createPagesEventContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it, onTestFinished } from "vitest"; + +// This will improve in the next major version of `@cloudflare/workers-types`, +// but for now you'll need to do something like this to get a correctly-typed +// `Request` to pass to `createPagesEventContext()`. +const IncomingRequest = Request; + +type BareFunction = PagesFunction>; + +it("can consume body in middleware and in next request", async ({ expect }) => { + const fn: BareFunction = async (ctx) => { + const requestText = await ctx.request.text(); + const nextResponse = await ctx.next(); + const nextResponseText = await nextResponse.text(); + return Response.json({ requestText, nextResponseText }); + }; + const request = new IncomingRequest("https://example.com", { + method: "POST", + body: "body", + }); + const ctx = createPagesEventContext({ + request, + async next(nextRequest) { + const nextRequestText = await nextRequest.text(); + return new Response(nextRequestText); + }, + }); + const response = await fn(ctx); + await waitOnExecutionContext(ctx); + expect(await response.json()).toStrictEqual({ + requestText: "body", + nextResponseText: "body", + }); +}); + +it("can rewrite to absolute and relative urls in next", async ({ expect }) => { + const fn: BareFunction = async (ctx) => { + const { pathname } = new URL(ctx.request.url); + if (pathname === "/absolute") { + return ctx.next("https://example.com/new-absolute", { method: "PUT" }); + } else if (pathname === "/relative/") { + return ctx.next("./new", { method: "PATCH" }); + } else { + return new Response(null, { status: 404 }); + } + }; + + // Check with absolute rewrite + let request = new IncomingRequest("https://example.com/absolute"); + let ctx = createPagesEventContext({ + request, + async next(nextRequest) { + return new Response(`next:${nextRequest.method} ${nextRequest.url}`); + }, + }); + let response = await fn(ctx); + await waitOnExecutionContext(ctx); + expect(await response.text()).toBe( + "next:PUT https://example.com/new-absolute" + ); + + // Check with relative rewrite + request = new IncomingRequest("https://example.com/relative/"); + ctx = createPagesEventContext({ + request, + async next(nextRequest) { + return new Response(`next:${nextRequest.method} ${nextRequest.url}`); + }, + }); + response = await fn(ctx); + await waitOnExecutionContext(ctx); + expect(await response.text()).toBe( + "next:PATCH https://example.com/relative/new" + ); +}); + +it("requires next property to call next()", async ({ expect }) => { + const fn: BareFunction = (ctx) => ctx.next(); + const request = new IncomingRequest("https://example.com"); + const ctx = createPagesEventContext({ request }); + await expect(fn(ctx)).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: Cannot call \`EventContext#next()\` without including \`next\` property in 2nd argument to \`createPagesEventContext()\`]` + ); +}); + +it("requires ASSETS service binding", async ({ expect }) => { + let originalASSETS = env.ASSETS; + onTestFinished(() => { + env.ASSETS = originalASSETS; + }); + delete (env as Partial).ASSETS; + + const request = new IncomingRequest("https://example.com", { + method: "POST", + body: "body", + }); + expect(() => + createPagesEventContext({ request }) + ).toThrowErrorMatchingInlineSnapshot( + `[TypeError: Cannot call \`createPagesEventContext()\` without defining \`ASSETS\` service binding]` + ); +}); + +it("waits for waitUntil()ed promises", async ({ expect }) => { + const fn: BareFunction = (ctx) => { + ctx.waitUntil(ctx.env.KV_NAMESPACE.put("key", "value")); + return new Response(); + }; + const request = new IncomingRequest("https://example.com"); + const ctx = createPagesEventContext({ request }); + await fn(ctx); + await waitOnExecutionContext(ctx); + expect(await env.KV_NAMESPACE.get("key")).toBe("value"); +}); + +it("correctly types parameters", async ({ expect }) => { + const request = new IncomingRequest("https://example.com"); + + // Check no params and no data required + { + type Fn = PagesFunction>; + createPagesEventContext({ request }); + createPagesEventContext({ request, params: {} }); + // @ts-expect-error no params required + createPagesEventContext({ request, params: { a: "1" } }); + createPagesEventContext({ request, data: {} }); + // @ts-expect-error no data required + createPagesEventContext({ request, data: { b: "1" } }); + } + + // Check no params but data required + { + type Fn = PagesFunction; + // @ts-expect-error data required + createPagesEventContext({ request }); + // @ts-expect-error data required + createPagesEventContext({ request, params: {} }); + // @ts-expect-error no params but data required + createPagesEventContext({ request, params: { a: "1" } }); + // @ts-expect-error data required + createPagesEventContext({ request, data: {} }); + createPagesEventContext({ request, data: { b: "1" } }); + } + + // Check no data but params required + { + type Fn = PagesFunction>; + // @ts-expect-error params required + createPagesEventContext({ request }); + // @ts-expect-error params required + createPagesEventContext({ request, params: {} }); + createPagesEventContext({ request, params: { a: ["1"] } }); + // @ts-expect-error no data but params required + createPagesEventContext({ request, data: {} }); + // @ts-expect-error no data but params required + createPagesEventContext({ request, data: { b: "1" } }); + } + + // Check params and data required + { + type Fn = PagesFunction; + // @ts-expect-error params required + createPagesEventContext({ request }); + // @ts-expect-error params required + createPagesEventContext({ request, params: {} }); + // @ts-expect-error data required + createPagesEventContext({ request, params: { a: "1" } }); + // @ts-expect-error params required + createPagesEventContext({ request, data: {} }); + // @ts-expect-error params required + createPagesEventContext({ request, data: { b: "1" } }); + createPagesEventContext({ + request, + params: { a: "1" }, + data: { b: "1" }, + }); + } +}); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/server.ts b/fixtures/vitest-pool-workers-examples/misc/test/server.ts new file mode 100644 index 0000000..bd0bda5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/server.ts @@ -0,0 +1,3 @@ +import { setupServer } from "msw/node"; + +export const server = setupServer(); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/setup.ts b/fixtures/vitest-pool-workers-examples/misc/test/setup.ts new file mode 100644 index 0000000..f37c96e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/setup.ts @@ -0,0 +1,10 @@ +import { afterAll, afterEach, beforeAll } from "vitest"; +import { server } from "./server"; + +beforeAll(() => + server.listen({ + onUnhandledRequest: "bypass", + }) +); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/fixtures/vitest-pool-workers-examples/misc/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/misc/test/tsconfig.json new file mode 100644 index 0000000..40d2455 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/misc/tsconfig.json b/fixtures/vitest-pool-workers-examples/misc/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/misc/vitest.assets.config.ts b/fixtures/vitest-pool-workers-examples/misc/vitest.assets.config.ts new file mode 100644 index 0000000..597a234 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/vitest.assets.config.ts @@ -0,0 +1,27 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + miniflare: { + assets: { + directory: "./public", + binding: "ASSETS", + }, + }, + wrangler: { + configPath: "./wrangler.assets.jsonc", + }, + }), + ], + + test: { + name: "misc-assets", + include: ["test/assets.test.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/misc/vitest.config.ts b/fixtures/vitest-pool-workers-examples/misc/vitest.config.ts new file mode 100644 index 0000000..a249478 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/vitest.config.ts @@ -0,0 +1,46 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { Response } from "miniflare"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + miniflare: { + kvNamespaces: ["KV_NAMESPACE"], + outboundService(request) { + return new Response(`fallthrough:${request.method} ${request.url}`); + }, + serviceBindings: { + ASSETS(request) { + return new Response(`assets:${request.method} ${request.url}`); + }, + }, + workers: [ + { + name: "other", + modules: true, + scriptPath: "./src/other-worker.mjs", + }, + ], + }, + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + + define: { + CONFIG_DEFINED_THING: '"thing"', + "CONFIG_NESTED.DEFINED.THING": "[1,2,3]", + }, + + test: { + exclude: ["test/assets.test.ts", "test/nodejs.test.ts"], + globalSetup: ["./global-setup.ts"], + setupFiles: ["test/setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/misc/vitest.nodejs.config.ts b/fixtures/vitest-pool-workers-examples/misc/vitest.nodejs.config.ts new file mode 100644 index 0000000..4c33e22 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/vitest.nodejs.config.ts @@ -0,0 +1,21 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.nodejs.jsonc", + }, + }), + ], + + test: { + name: "misc-nodejs", + include: ["test/nodejs.test.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/misc/wrangler.assets.jsonc b/fixtures/vitest-pool-workers-examples/misc/wrangler.assets.jsonc new file mode 100644 index 0000000..87f70bd --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/wrangler.assets.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "../node_modules/wrangler/config-schema.json", + "compatibility_date": "2024-01-01", + "assets": { + // This will be overridden by the assets config in vitest.config.ts + // See https://github.com/cloudflare/workers-sdk/issues/9130 + "directory": "./does-not-exist", + "binding": "ASSETS", + }, +} diff --git a/fixtures/vitest-pool-workers-examples/misc/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/misc/wrangler.jsonc new file mode 100644 index 0000000..093c15a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "../node_modules/wrangler/config-schema.json", + // don't provide compatibility_date so that vitest will infer the latest one + "define": { + "WRANGLER_DEFINED_THING": "\"thing\"", + "WRANGLER_NESTED.DEFINED.THING": "[1,2,3]", + }, + "durable_objects": { + "bindings": [ + { + "name": "OTHER_OBJECT", + "class_name": "OtherObject", + "script_name": "other", + }, + ], + }, +} diff --git a/fixtures/vitest-pool-workers-examples/misc/wrangler.nodejs.jsonc b/fixtures/vitest-pool-workers-examples/misc/wrangler.nodejs.jsonc new file mode 100644 index 0000000..4f45603 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/misc/wrangler.nodejs.jsonc @@ -0,0 +1,5 @@ +{ + "$schema": "../node_modules/wrangler/config-schema.json", + // don't provide compatibility_date so that vitest will infer the latest one + "compatibility_flags": ["nodejs_compat"], +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/README.md b/fixtures/vitest-pool-workers-examples/module-resolution/README.md new file mode 100644 index 0000000..a2c8672 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/README.md @@ -0,0 +1,36 @@ +# ⚡ module-resolution + +This fixture demonstrates that the Vitest integration correctly resolves modules, including: + +- A CommonJS package that requires a directory rather than a specific file. +- An ESM dependency that imports `*.wasm?module` from the same directory. +- A package without a main entrypoint or with browser field mapping, handled via [Dependency Pre-Bundling](#dependency-pre-bundling). + +## Dependency Pre-Bundling + +[Dependency Pre-Bundling](https://vite.dev/guide/dep-pre-bundling) is a Vite feature that converts dependencies shipped as CommonJS or UMD into ESM. If you encounter module resolution issues—such as: `Error: Cannot use require() to import an ES Module` or `Error: No such module`—you can pre-bundle these dependencies using the [deps.optimizer](https://vitest.dev/config/#deps-optimizer) option: + +```ts +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + test: { + deps: { + optimizer: { + ssr: { + enabled: true, + include: ["your-package-name"], + }, + }, + }, + }, +}); +``` + +See our [vitest config](./vitest.config.ts) for an example of how we pre-bundled `discord-api-types/v10` and `@microlabs/otel-cf-workers`. diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/src/env.d.ts b/fixtures/vitest-pool-workers-examples/module-resolution/src/env.d.ts new file mode 100644 index 0000000..a91c854 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/src/env.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./module-resolution/src/env.d.ts -c ./module-resolution/wrangler.jsonc --no-include-runtime` (hash: 0c8cd5ae12176ebab8337cbfef98b516) +interface __BaseEnv_Env {} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/src/index.ts b/fixtures/vitest-pool-workers-examples/module-resolution/src/index.ts new file mode 100644 index 0000000..ba0c8cd --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/src/index.ts @@ -0,0 +1,41 @@ +import * as bAuthStripe from "@better-auth/stripe"; +// Testing dependency with browser field mapping +// @see https://github.com/cloudflare/workers-sdk/issues/6581 +import * as otel from "@microlabs/otel-cf-workers"; +import * as bAuth from "better-auth"; +// Testing dependency without a main entrypoint +// @see https://github.com/cloudflare/workers-sdk/issues/6591 +import * as discord from "discord-api-types/v10"; +import * as jose from "jose"; +import nunjucks from "nunjucks"; +import Stripe from "stripe"; +import { Toucan } from "toucan-js"; + +export default { + async fetch(): Promise { + // Verify that we're able to import & create the toucan class. This is a library where `"module": "..."` is set, and `"type": "module"` is not + const _ = new Toucan({ + dsn: "https://foo@sentry.com/123456", + }); + const a = Stripe; + + nunjucks.configure({ autoescape: true }); + + console.log( + nunjucks.renderString("Hello {{ username }}", { username: "James" }) + ); + + // Make sure none of the imports are tree shaken + return new Response( + JSON.stringify({ + discord, + otel, + jose, + bAuth, + bAuthStripe, + a, + n: nunjucks.renderString("Hello {{ username }}", { username: "James" }), + }) + ); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/src/test.sql b/fixtures/vitest-pool-workers-examples/module-resolution/src/test.sql new file mode 100644 index 0000000..c0e59fb --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/src/test.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE id = 1; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/module-resolution/src/tsconfig.json new file mode 100644 index 0000000..b9d513a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"], + "compilerOptions": { + // In this fixture, we're including the toucan-js library, which references `@cloudflare/workers-types`, whereas the rest of the fixture is referencing + // `@cloudflare/workers-types/experimental`. This causes a conflict in types defined, so we need to skip the lib check specifically in this directory + "skipLibCheck": true + } +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/test/index.d.ts b/fixtures/vitest-pool-workers-examples/module-resolution/test/index.d.ts new file mode 100644 index 0000000..f20a628 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/test/index.d.ts @@ -0,0 +1,10 @@ +declare module "ext-dep" { + var x: number; + export default x; +} + +// .sql files are loaded as Text modules by default in wrangler +declare module "*.sql" { + const content: string; + export default content; +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/test/index.spec.ts b/fixtures/vitest-pool-workers-examples/module-resolution/test/index.spec.ts new file mode 100644 index 0000000..105ad29 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/test/index.spec.ts @@ -0,0 +1,63 @@ +import { instrument } from "@microlabs/otel-cf-workers"; +import addFromCjsWasmModuleDep from "cjs-wasm-module-dep"; +import { exports } from "cloudflare:workers"; +import { Utils } from "discord-api-types/v10"; +import { value as esmDepValue } from "esm-dep"; +import dep from "ext-dep"; +import mime from "mime-types"; +import { assert, describe, expect, test } from "vitest"; +import addFromWasmModuleDep from "wasm-module-dep"; +import worker from "../src/index"; +import sqlPlain from "../src/test.sql"; +import sqlRaw from "../src/test.sql?raw"; + +describe("test", () => { + test("resolves commonjs directory dependencies correctly", async () => { + assert.equal(dep, 123); + }); + + test("resolves dependency without a default entrypoint", async () => { + assert.isFunction(Utils.isDMInteraction); + }); + + test("resolves a dependency importing .wasm?module", async () => { + assert.equal(await addFromWasmModuleDep(2, 3), 5); + }); + + test("resolves a CJS dependency requiring .wasm?module", async () => { + assert.equal(await addFromCjsWasmModuleDep(2, 3), 5); + }); + + test("resolves dependency with mapping on the browser field", async () => { + assert.isFunction(instrument); + }); + + test("can use toucan-js (integration)", async () => { + expect((await exports.default.fetch("http://example.com")).status).toBe( + 200 + ); + }); + + test("can use toucan-js (unit)", async () => { + const response = await worker.fetch(); + expect(response.status).toBe(200); + }); + + // Regression test for https://github.com/cloudflare/workers-sdk/issues/12049 + // Vite query parameters like ?raw should be handled by Vite, not module rules + test("resolves file with ?raw query parameter", async () => { + assert.equal(sqlRaw, sqlPlain); + }); + + // Verify CommonJS require() of JSON files works + test("resolves dependency that requires JSON files", async () => { + assert.equal(mime.lookup("test.html"), "text/html"); + }); + + // Regression test for https://github.com/cloudflare/workers-sdk/issues/12022 + // ESM imports with extensionless specifiers should be resolved by Vite, not + // by the module fallback's extension-probing loop (which is only for require()). + test("resolves ESM dependency with extensionless internal import", async () => { + assert.equal(esmDepValue, 456); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/module-resolution/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/tsconfig.json b/fixtures/vitest-pool-workers-examples/module-resolution/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/add.wasm b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..fa370ae4187f2b0e9a65181bee6f50db6b3c6026 GIT binary patch literal 53 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9erX;9|*3%uQut H2C4=C$J7S% literal 0 HcmV?d00001 diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.d.ts b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.d.ts new file mode 100644 index 0000000..6094be8 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.d.ts @@ -0,0 +1,3 @@ +declare const add: (a: number, b: number) => Promise; + +export = add; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.js b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.js new file mode 100644 index 0000000..dbfd00d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/index.js @@ -0,0 +1,5 @@ +module.exports = async function add(a, b) { + const { default: addModule } = require("./add.wasm?module"); + const addInstance = new WebAssembly.Instance(addModule); + return addInstance.exports.add(a, b); +}; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/package.json b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/package.json new file mode 100644 index 0000000..edc60dc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/cjs-wasm-module-dep/package.json @@ -0,0 +1,5 @@ +{ + "name": "cjs-wasm-module-dep", + "main": "index.js", + "types": "index.d.ts" +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.d.mts b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.d.mts new file mode 100644 index 0000000..052dbfe --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.d.mts @@ -0,0 +1 @@ +export declare const value: number; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.mjs b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.mjs new file mode 100644 index 0000000..a315aa5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/index.mjs @@ -0,0 +1,5 @@ +// Intentionally extensionless — valid in bundler/Vite contexts but technically +// requires explicit extensions in strict ESM. The module fallback service must +// NOT add extensions for `import` (only for `require()`); it should defer to +// Vite's resolver for this. +export { value } from "./value"; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/package.json b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/package.json new file mode 100644 index 0000000..791e24f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/package.json @@ -0,0 +1,5 @@ +{ + "name": "esm-dep", + "type": "module", + "main": "index.mjs" +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/value.mjs b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/value.mjs new file mode 100644 index 0000000..851a1e3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/esm-dep/value.mjs @@ -0,0 +1 @@ +export const value = 456; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/index.js b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/index.js new file mode 100644 index 0000000..901fd81 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/index.js @@ -0,0 +1 @@ +module.exports = require("./subfolder"); diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/package.json b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/package.json new file mode 100644 index 0000000..2a833aa --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/package.json @@ -0,0 +1,5 @@ +{ + "name": "ext-dep", + "type": "commonjs", + "main": "index.js" +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/subfolder/index.js b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/subfolder/index.js new file mode 100644 index 0000000..1918562 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/ext-dep/subfolder/index.js @@ -0,0 +1 @@ +module.exports = 123; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/add.wasm b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..fa370ae4187f2b0e9a65181bee6f50db6b3c6026 GIT binary patch literal 53 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9erX;9|*3%uQut H2C4=C$J7S% literal 0 HcmV?d00001 diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.d.mts b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.d.mts new file mode 100644 index 0000000..1930b8e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.d.mts @@ -0,0 +1,3 @@ +declare const add: (a: number, b: number) => Promise; + +export default add; diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.mjs b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.mjs new file mode 100644 index 0000000..d91f94a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/index.mjs @@ -0,0 +1,7 @@ +export default async function add(a, b) { + const { default: addModule } = await import( + /* @vite-ignore */ "./add.wasm?module" + ); + const addInstance = new WebAssembly.Instance(addModule); + return addInstance.exports.add(a, b); +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/package.json b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/package.json new file mode 100644 index 0000000..17398c1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vendor/wasm-module-dep/package.json @@ -0,0 +1,6 @@ +{ + "name": "wasm-module-dep", + "type": "module", + "main": "index.mjs", + "types": "index.d.mts" +} diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/vitest.config.ts b/fixtures/vitest-pool-workers-examples/module-resolution/vitest.config.ts new file mode 100644 index 0000000..d9a889a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/vitest.config.ts @@ -0,0 +1,14 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/module-resolution/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/module-resolution/wrangler.jsonc new file mode 100644 index 0000000..2c5be6a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/module-resolution/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "module-resolution", + "main": "src/index.ts", + "compatibility_date": "2025-12-13", + "compatibility_flags": ["nodejs_compat"], + // don't provide compatibility_date so that vitest will infer the latest one +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/README.md b/fixtures/vitest-pool-workers-examples/multiple-workers/README.md new file mode 100644 index 0000000..7619eae --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/README.md @@ -0,0 +1,13 @@ +# 🔌 multiple-workers + +This example uses multiple Workers: + +- `api-service`: entrypoint to the project, forwards requests on to other services as needed +- `auth-service`: handles signing and verifying JWTs +- `database-service`: handles reading/writing values from a KV namespace + +In a real project, only the `api-service` would be publicly routable. The `auth-service` sends request to an external endpoint to login and sign JWTs. This endpoint is mocked in tests using the `outboundService` Miniflare option. The `database-service` assumes the user has been authenticated and allows reads/writes to any key. + +| Test | Overview | +| ----------------------------------------------- | --------------------------------- | +| [integration.test.ts](test/integration.test.ts) | Integration tests using `exports` | diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/env.d.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/env.d.ts new file mode 100644 index 0000000..03583ec --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/env.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./multiple-workers/api-service/src/env.d.ts -c ./multiple-workers/api-service/wrangler.jsonc --no-include-runtime` (hash: b9086500665f32aa0533c30a22adce89) +interface __BaseEnv_Env { + AUTH_SERVICE: Fetcher /* auth-service */; + DATABASE_SERVICE: Fetcher /* database-service */; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/index.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/index.ts new file mode 100644 index 0000000..7ffa915 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/index.ts @@ -0,0 +1,23 @@ +export default >{ + async fetch(request, env, ctx) { + const url = new URL(request.url); + // Forward login requests to the auth service + if (url.pathname === "/login") return env.AUTH_SERVICE.fetch(request); + + // Verify the user's token with the auth service... + const verifyResponse = await env.AUTH_SERVICE.fetch( + "http://placeholder/verify", + { method: "POST", headers: request.headers } + ); + + if (!verifyResponse.ok) return verifyResponse; + + // ...then prefix the path with the username and forward to database service + const payload = await verifyResponse.json<{ + "urn:example:username": string; + }>(); + const username = payload["urn:example:username"]; + url.pathname = `/${username}${url.pathname}`; + return env.DATABASE_SERVICE.fetch(url, request); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/tsconfig.json new file mode 100644 index 0000000..75cdccc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/wrangler.jsonc new file mode 100644 index 0000000..3c22e07 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/api-service/wrangler.jsonc @@ -0,0 +1,19 @@ +{ + "name": "api-service", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "services": [ + { + "binding": "AUTH_SERVICE", + "service": "auth-service", + }, + { + "binding": "DATABASE_SERVICE", + "service": "database-service", + }, + ], + "tail_consumers": [ + { "service": "this-tail-does-not-exist" }, + { "service": "tail-consumer" }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/env.d.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/env.d.ts new file mode 100644 index 0000000..16d5dda --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/env.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./multiple-workers/auth-service/src/env.d.ts -c ./multiple-workers/auth-service/wrangler.jsonc --no-include-runtime` (hash: 371cc12bcdcb91caf0579ce1fe3dcf6b) +interface __BaseEnv_Env { + AUTH_PUBLIC_KEY: string; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/index.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/index.ts new file mode 100644 index 0000000..e9678be --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/index.ts @@ -0,0 +1,45 @@ +import { importSPKI, jwtVerify } from "jose"; + +export default >{ + async fetch(request, env, ctx) { + if (request.method !== "POST") { + return new Response("Method Not Allowed", { status: 405 }); + } + + const { pathname } = new URL(request.url); + if (pathname === "/login") { + // Forward login request to upstream service mocked in test. + const formData = await request.formData(); + const username = formData.get("username"); + const password = formData.get("password"); + if (typeof username !== "string" || typeof password !== "string") { + return new Response("Bad Request", { status: 400 }); + } + return fetch("https://example.com/login", { + method: "POST", + body: JSON.stringify({ username, password }), + }); + } else if (pathname === "/verify") { + // Verify token with public key + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return new Response("Unauthorized", { status: 401 }); + } + const token = authHeader.substring("Bearer ".length); + + const alg = "RS256"; + const publicKey = await importSPKI(env.AUTH_PUBLIC_KEY, alg); + try { + const { payload } = await jwtVerify(token, publicKey, { + issuer: "urn:example:issuer", + audience: "urn:example:audience", + }); + return Response.json(payload); + } catch { + return new Response("Unauthorized", { status: 401 }); + } + } else { + return new Response("Not Found", { status: 404 }); + } + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/tsconfig.json new file mode 100644 index 0000000..75cdccc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/wrangler.jsonc new file mode 100644 index 0000000..ed2f14a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/auth-service/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "name": "auth-service", + "main": "src/index.ts", + // compatibility_date is required so we can run `wrangler build` + "compatibility_date": "2024-01-01", + /* + `AUTH_PUBLIC_KEY` would be a secret created with `wrangler secret put`, and + stored in `.dev.vars` during development + */ + "secrets": { + "required": ["AUTH_PUBLIC_KEY"], + }, +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/env.d.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/env.d.ts new file mode 100644 index 0000000..aec38ae --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/env.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./multiple-workers/database-service/src/env.d.ts -c ./multiple-workers/database-service/wrangler.jsonc --no-include-runtime` (hash: 836c8ec92bcd45633fe61ef63340d336) +interface __BaseEnv_Env { + KV_NAMESPACE: KVNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/index.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/index.ts new file mode 100644 index 0000000..c05f2db --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/index.ts @@ -0,0 +1,14 @@ +export default >{ + async fetch(request, env, ctx) { + const { pathname } = new URL(request.url); + if (request.method === "GET") { + const value = await env.KV_NAMESPACE.get(pathname, "stream"); + return new Response(value, { status: value === null ? 204 : 200 }); + } else if (request.method === "PUT") { + await env.KV_NAMESPACE.put(pathname, request.body ?? ""); + return new Response(null, { status: 204 }); + } else { + return new Response("Method Not Allowed", { status: 405 }); + } + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/tsconfig.json new file mode 100644 index 0000000..75cdccc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/wrangler.jsonc new file mode 100644 index 0000000..a37f625 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/database-service/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "database-service", + "main": "src/index.ts", + // compatibility_date is required so we can run `wrangler build` + "compatibility_date": "2024-01-01", + "kv_namespaces": [ + { + "binding": "KV_NAMESPACE", + "id": "00000000000000000000000000000000", + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/global-setup.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/global-setup.ts new file mode 100644 index 0000000..5d82857 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/global-setup.ts @@ -0,0 +1,21 @@ +import childProcess from "node:child_process"; +import path from "node:path"; + +// Global setup runs inside Node.js, not `workerd` +export default function () { + // Build `api-service`'s dependencies + + let label = "Built multiple-workers auth-service"; + console.time(label); + childProcess.execSync("wrangler build", { + cwd: path.join(__dirname, "auth-service"), + }); + console.timeEnd(label); + + label = "Built multiple-workers database-service"; + console.time(label); + childProcess.execSync("wrangler build", { + cwd: path.join(__dirname, "database-service"), + }); + console.timeEnd(label); +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/test/env.d.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/test/env.d.ts new file mode 100644 index 0000000..20f2ff0 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/test/env.d.ts @@ -0,0 +1,5 @@ +declare namespace Cloudflare { + interface Env { + TEST_AUTH_PUBLIC_KEY: string; // Defined in `vitest.config.mts` + } +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/test/integration.test.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/test/integration.test.ts new file mode 100644 index 0000000..83aacac --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/test/integration.test.ts @@ -0,0 +1,65 @@ +import { env, exports } from "cloudflare:workers"; +import { importSPKI, jwtVerify } from "jose"; // Check importing external module +import { it } from "vitest"; +import type { ExpectStatic } from "vitest"; + +async function login( + username: string, + password: string, + expect: ExpectStatic +): Promise { + const formData = new FormData(); + formData.set("username", username); + formData.set("password", password); + const response = await exports.default.fetch("https://example.com/login", { + method: "POST", + body: formData, + }); + expect(response.status).toBe(200); + const { token } = await response.json<{ token: string }>(); + expect(token).toMatch(/^ey/); + return token; +} + +it("logs in and generates token for user", async ({ expect }) => { + // Login and get token + const token = await login("admin", "lovelace", expect); + + // Verify token is valid + const alg = "RS256"; + const publicKey = await importSPKI(env.TEST_AUTH_PUBLIC_KEY, alg); + const { payload } = await jwtVerify(token, publicKey, { + issuer: "urn:example:issuer", + audience: "urn:example:audience", + }); + expect(payload).toStrictEqual({ + "urn:example:username": "admin", + iss: "urn:example:issuer", + aud: "urn:example:audience", + iat: expect.any(Number), + exp: expect.any(Number), + }); +}); + +it("stores in user's database", async ({ expect }) => { + // Login and get token + const token = await login("admin", "lovelace", expect); + + // Read and write from the database + let response = await exports.default.fetch("https://example.com/key", { + method: "PUT", + body: "value", + headers: { Authorization: `Bearer ${token}` }, + }); + expect(response.status).toBe(204); + response = await exports.default.fetch("https://example.com/key", { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(response.status).toBe(200); + expect(await response.text()).toBe("value"); + + // Check key written under user's namespace + response = await env.DATABASE_SERVICE.fetch("https://placeholder/admin/key"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("value"); +}); diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/multiple-workers/test/tsconfig.json new file mode 100644 index 0000000..eea7b50 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../api-service/src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/tsconfig.json b/fixtures/vitest-pool-workers-examples/multiple-workers/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/multiple-workers/vitest.config.ts b/fixtures/vitest-pool-workers-examples/multiple-workers/vitest.config.ts new file mode 100644 index 0000000..4058552 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/multiple-workers/vitest.config.ts @@ -0,0 +1,123 @@ +import crypto from "node:crypto"; +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { importPKCS8, SignJWT } from "jose"; +import { Request, Response } from "miniflare"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +// Generate RSA keypair for signing/verifying JWTs +const authKeypair = crypto.generateKeyPairSync("rsa", { + modulusLength: 4096, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +const authAlg = "RS256"; +const authPrivateKey = await importPKCS8(authKeypair.privateKey, authAlg); + +function isCredentialsObject( + value: unknown +): value is { username: string; password: string } { + return ( + typeof value === "object" && + value !== null && + "username" in value && + typeof value.username === "string" && + "password" in value && + typeof value.password === "string" + ); +} + +// Mapping between usernames and passwords +const passwords: Record = { + admin: "lovelace", +}; + +async function handleAuthServiceOutbound(request: Request): Promise { + const url = new URL(request.url); + + if (request.method === "POST" && url.pathname === "/login") { + // If this is a login request, verify the username/password, then sign a JWT + const body = await request.json(); + if (!isCredentialsObject(body)) { + return new Response("Bad Request", { status: 400 }); + } + if (passwords[body.username] !== body.password) { + return new Response("Unauthorized", { status: 401 }); + } + const payload = { "urn:example:username": body.username }; + const token = await new SignJWT(payload) + .setProtectedHeader({ alg: authAlg }) + .setIssuedAt() + .setIssuer("urn:example:issuer") + .setAudience("urn:example:audience") + .setExpirationTime("1h") + .sign(authPrivateKey); + return Response.json({ token }); + } + + return new Response("Not Found", { status: 404 }); +} + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + // Configuration for the test runner and "API service" Worker + wrangler: { + configPath: "./api-service/wrangler.jsonc", + }, + miniflare: { + bindings: { + TEST_AUTH_PUBLIC_KEY: authKeypair.publicKey, + }, + + workers: [ + // Configuration for "auxiliary" Worker dependencies. + // Unfortunately, auxiliary Workers cannot load their configuration + // from `wrangler.toml` files, and must be configured with Miniflare + // `WorkerOptions`. + { + name: "auth-service", + modules: true, + scriptPath: "./auth-service/dist/index.js", // Built by `global-setup.ts` + compatibilityDate: "2024-01-01", + compatibilityFlags: ["nodejs_compat"], + bindings: { AUTH_PUBLIC_KEY: authKeypair.publicKey }, + // Mock outbound `fetch()`es from the `auth-service` + outboundService: handleAuthServiceOutbound, + }, + { + name: "database-service", + modules: true, + scriptPath: "./database-service/dist/index.js", // Built by `global-setup.ts` + compatibilityDate: "2024-01-01", + compatibilityFlags: ["nodejs_compat"], + kvNamespaces: ["KV_NAMESPACE"], + }, + { + name: "tail-consumer", + modules: [ + { + path: "index.js", + type: "ESModule", + contents: /* javascript */ ` + export default { + tail(event) { + console.log("tail event received") + } + } + `, + }, + ], + compatibilityDate: "2024-01-01", + }, + ], + }, + }), + ], + test: { + globalSetup: ["./global-setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/package.json b/fixtures/vitest-pool-workers-examples/package.json new file mode 100644 index 0000000..b82c2e0 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/package.json @@ -0,0 +1,62 @@ +{ + "name": "@fixture/vitest-pool-workers", + "private": true, + "type": "module", + "scripts": { + "cf-typegen": "pnpm run \"/^typegen:.*/\"", + "check:type": "node tsc-all.mjs", + "list": "vitest list", + "test": "vitest", + "test:ci": "vitest run", + "typegen:ai-vectorize": "wrangler types ./ai-vectorize/src/env.d.ts -c ./ai-vectorize/wrangler.jsonc --no-include-runtime", + "typegen:basics-unit-integration-self": "wrangler types ./basics-unit-integration-self/src/env.d.ts -c ./basics-unit-integration-self/wrangler.jsonc --no-include-runtime", + "typegen:container-app": "wrangler types ./container-app/src/env.d.ts -c ./container-app/wrangler.jsonc --no-include-runtime", + "typegen:d1": "wrangler types ./d1/src/env.d.ts -c ./d1/wrangler.jsonc --no-include-runtime", + "typegen:durable-objects": "wrangler types ./durable-objects/src/env.d.ts -c ./durable-objects/wrangler.jsonc --no-include-runtime", + "typegen:durable-objects-exports": "wrangler types ./durable-objects-exports/src/env.d.ts -c ./durable-objects-exports/wrangler.jsonc --no-include-runtime", + "typegen:dynamic-import": "wrangler types ./dynamic-import/src/worker-configuration.d.ts -c ./dynamic-import/wrangler.jsonc --no-include-runtime", + "typegen:hyperdrive": "wrangler types ./hyperdrive/src/env.d.ts -c ./hyperdrive/wrangler.jsonc --no-include-runtime", + "typegen:images": "wrangler types ./images/src/env.d.ts -c ./images/wrangler.jsonc --no-include-runtime", + "typegen:kv-r2-caches": "wrangler types ./kv-r2-caches/src/env.d.ts -c ./kv-r2-caches/wrangler.jsonc --no-include-runtime", + "typegen:module-resolution": "wrangler types ./module-resolution/src/env.d.ts -c ./module-resolution/wrangler.jsonc --no-include-runtime", + "typegen:multiple-workers:api": "wrangler types ./multiple-workers/api-service/src/env.d.ts -c ./multiple-workers/api-service/wrangler.jsonc --no-include-runtime", + "typegen:multiple-workers:auth": "wrangler types ./multiple-workers/auth-service/src/env.d.ts -c ./multiple-workers/auth-service/wrangler.jsonc --no-include-runtime", + "typegen:multiple-workers:database": "wrangler types ./multiple-workers/database-service/src/env.d.ts -c ./multiple-workers/database-service/wrangler.jsonc --no-include-runtime", + "typegen:pipelines": "wrangler types ./pipelines/src/env.d.ts -c ./pipelines/wrangler.jsonc --no-include-runtime", + "typegen:queues": "wrangler types ./queues/src/env.d.ts -c ./queues/wrangler.jsonc --no-include-runtime", + "typegen:request-mocking": "wrangler types ./request-mocking/src/env.d.ts -c ./request-mocking/wrangler.jsonc --no-include-runtime", + "typegen:reset": "wrangler types ./reset/src/env.d.ts -c ./reset/wrangler.jsonc --no-include-runtime", + "typegen:rpc": "wrangler types ./rpc/src/env.d.ts -c ./rpc/wrangler.jsonc --no-include-runtime", + "typegen:web-assembly": "wrangler types ./web-assembly/src/env.d.ts -c ./web-assembly/wrangler.jsonc --no-include-runtime", + "typegen:workers-assets": "wrangler types ./workers-assets/src/env.d.ts -c ./workers-assets/wrangler.jsonc --no-include-runtime", + "typegen:workers-assets-no-dir": "wrangler types ./workers-assets-no-dir/src/env.d.ts -c ./workers-assets-no-dir/wrangler.jsonc --no-include-runtime", + "typegen:workflows": "wrangler types ./workflows/src/env.d.ts -c ./workflows/wrangler.jsonc --no-include-runtime" + }, + "devDependencies": { + "@better-auth/stripe": "^1.4.6", + "@cloudflare/containers": "^0.2.2", + "@cloudflare/vitest-pool-workers": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@microlabs/otel-cf-workers": "1.0.0-rc.45", + "@types/mime-types": "^3.0.1", + "@types/node": "catalog:default", + "@types/nunjucks": "^3.2.6", + "better-auth": "^1.4.6", + "cjs-wasm-module-dep": "file:./module-resolution/vendor/cjs-wasm-module-dep", + "discord-api-types": "0.37.98", + "esm-dep": "file:./module-resolution/vendor/esm-dep", + "ext-dep": "file:./module-resolution/vendor/ext-dep", + "jose": "^5.9.3", + "mime-types": "^2.1.35", + "miniflare": "workspace:*", + "msw": "catalog:default", + "nunjucks": "^3.2.4", + "stripe": "^20.0.0", + "toucan-js": "4.0.0", + "typescript": "catalog:default", + "vite": "catalog:default", + "vitest": "catalog:default", + "wasm-module-dep": "file:./module-resolution/vendor/wasm-module-dep", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/.gitignore b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/.gitignore new file mode 100644 index 0000000..83e6278 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/.gitignore @@ -0,0 +1 @@ +dist-functions diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/README.md b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/README.md new file mode 100644 index 0000000..65945f2 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/README.md @@ -0,0 +1,8 @@ +# ⚡️ pages-functions-unit-integration-self + +This project uses Pages Functions. Integration tests dispatch events using `exports.default` from the `cloudflare:workers` module. Unit tests call handler functions directly. [`global-setup.ts`](global-setup.ts) builds Pages Functions into a Worker for integration testing, watching for changes. + +| Test | Overview | +| --------------------------------------------------------- | ------------------------------------------------------------------------ | +| [integration-self.test.ts](test/integration-self.test.ts) | Basic `fetch` integration test using `exports.default` **(recommended)** | +| [unit.test.ts](test/unit.test.ts) | Basic unit test calling `worker.fetch()` directly | diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/_middleware.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/_middleware.ts new file mode 100644 index 0000000..2aaa5c5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/_middleware.ts @@ -0,0 +1,11 @@ +// Add data to the request and make all bodies uppercase +export const onRequest: PagesFunction< + Env, + never, + Data | Record +> = async (ctx) => { + ctx.data = { user: "ada" }; + const response = await ctx.next(); + const text = await response.text(); + return new Response(text.toUpperCase(), response); +}; diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/kv/[key].ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/kv/[key].ts new file mode 100644 index 0000000..67a0fc9 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/kv/[key].ts @@ -0,0 +1,11 @@ +export const onRequestGet: PagesFunction = async (ctx) => { + const key = `${ctx.data.user}:${ctx.params.key}`; + const value = await ctx.env.KV_NAMESPACE.get(key, "stream"); + return new Response(value, { status: value === null ? 204 : 200 }); +}; + +export const onRequestPut: PagesFunction = async (ctx) => { + const key = `${ctx.data.user}:${ctx.params.key}`; + await ctx.env.KV_NAMESPACE.put(key, ctx.request.body ?? ""); + return new Response(null, { status: 204 }); +}; diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/ping.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/ping.ts new file mode 100644 index 0000000..d8842ea --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/api/ping.ts @@ -0,0 +1,3 @@ +export const onRequest: PagesFunction = ({ request }) => { + return new Response(`${request.method} pong`); +}; diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/env.d.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/env.d.ts new file mode 100644 index 0000000..055338d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/env.d.ts @@ -0,0 +1,6 @@ +interface Env { + KV_NAMESPACE: KVNamespace; + ASSETS: Fetcher; +} + +type Data = { user: string }; diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/tsconfig.json b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/functions/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/global-setup.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/global-setup.ts new file mode 100644 index 0000000..0ba0b7f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/global-setup.ts @@ -0,0 +1,25 @@ +import childProcess from "node:child_process"; +import events from "node:events"; + +// Global setup runs inside Node.js, not `workerd` +export default async function () { + console.log( + "Building pages-functions-unit-integration-self and watching for changes..." + ); + + // Not building to `dist` here as Vitest ignores changes in `dist` by default + const buildProcess = childProcess.spawn( + "wrangler pages functions build --outdir dist-functions --watch", + { cwd: __dirname, shell: true } + ); + buildProcess.stdout.pipe(process.stdout); + buildProcess.stderr.pipe(process.stderr); + + // Wait for first build + await events.once(buildProcess.stdout, "data"); + + // Stop watching for changes on teardown + return () => { + buildProcess.kill(); + }; +} diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/404.html b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/404.html new file mode 100644 index 0000000..5854f2f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/404.html @@ -0,0 +1 @@ +

Not found 😭

diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_headers b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_headers new file mode 100644 index 0000000..0b19dba --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_headers @@ -0,0 +1,4 @@ +/secure + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy: no-referrer diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_redirects b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_redirects new file mode 100644 index 0000000..54dab31 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/_redirects @@ -0,0 +1 @@ +/take-me-home / 302 diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/index.html b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/index.html new file mode 100644 index 0000000..640142f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/index.html @@ -0,0 +1 @@ +

Homepage 🏡

diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/secure.html b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/secure.html new file mode 100644 index 0000000..acbd884 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/public/secure.html @@ -0,0 +1 @@ +

Secure page 🔐

diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/env.d.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/env.d.ts new file mode 100644 index 0000000..49c7bb1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/env.d.ts @@ -0,0 +1,10 @@ +declare namespace Cloudflare { + interface GlobalProps { + // Pages Functions compile to a worker with an ExportedHandler default export + mainModule: { default: ExportedHandler }; + } + interface Env { + KV_NAMESPACE: KVNamespace; + ASSETS: Fetcher; + } +} diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/integration-self.test.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/integration-self.test.ts new file mode 100644 index 0000000..d6ec247 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/integration-self.test.ts @@ -0,0 +1,71 @@ +import { exports } from "cloudflare:workers"; +import { describe, it } from "vitest"; + +describe("functions", () => { + it("calls function", async ({ expect }) => { + // `exports.default` here points to the worker running in the current isolate. + // This gets its handler from the `main` option in `vitest.config.mts`. + const response = await exports.default.fetch("http://example.com/api/ping"); + // All `/api/*` requests go through `functions/api/_middleware.ts`, + // which makes all response bodies uppercase + expect(await response.text()).toBe("GET PONG"); + }); + + it("calls function with params", async ({ expect }) => { + let response = await exports.default.fetch( + "https://example.com/api/kv/key", + { + method: "PUT", + body: "value", + } + ); + expect(response.status).toBe(204); + + response = await exports.default.fetch("https://example.com/api/kv/key"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("VALUE"); + }); +}); + +describe("assets", () => { + it("serves static assets", async ({ expect }) => { + const response = await exports.default.fetch("http://example.com/"); + expect(await response.text()).toMatchInlineSnapshot(` + "

Homepage 🏡

+ " + `); + }); + + it("respects 404.html", async ({ expect }) => { + // `404.html` should be served for all unmatched requests + const response = await exports.default.fetch( + "http://example.com/not-found" + ); + expect(await response.text()).toMatchInlineSnapshot(` + "

Not found 😭

+ " +`); + }); + + it("respects _redirects", async ({ expect }) => { + const response = await exports.default.fetch( + "http://example.com/take-me-home", + { + redirect: "manual", + } + ); + expect(response.status).toBe(302); + expect(response.headers.get("Location")).toBe("/"); + }); + + it("respects _headers", async ({ expect }) => { + let response = await exports.default.fetch("http://example.com/secure"); + expect(response.headers.get("X-Frame-Options")).toBe("DENY"); + expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff"); + expect(response.headers.get("Referrer-Policy")).toBe("no-referrer"); + + // Check headers only added to matching requests + response = await exports.default.fetch("http://example.com/"); + expect(response.headers.get("X-Frame-Options")).toBe(null); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/tsconfig.json new file mode 100644 index 0000000..3da5187 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../functions/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/unit.test.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/unit.test.ts new file mode 100644 index 0000000..e70873f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/test/unit.test.ts @@ -0,0 +1,66 @@ +import { + createPagesEventContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { describe, it } from "vitest"; +import * as apiMiddleware from "../functions/api/_middleware"; +import * as apiKVKeyFunction from "../functions/api/kv/[key]"; +import * as apiPingFunction from "../functions/api/ping"; + +// This will improve in the next major version of `@cloudflare/workers-types`, +// but for now you'll need to do something like this to get a correctly-typed +// `Request` to pass to `createPagesEventContext()`. +const IncomingRequest = Request; + +describe("functions", () => { + it("calls function", async ({ expect }) => { + const request = new IncomingRequest("http://example.com/api/ping"); + const ctx = createPagesEventContext({ + request, + data: { user: "test" }, + }); + const response = await apiPingFunction.onRequest(ctx); + await waitOnExecutionContext(ctx); + expect(await response.text()).toBe("GET pong"); + }); + + it("calls function with params", async ({ expect }) => { + let request = new IncomingRequest("http://example.com/api/kv/key", { + method: "PUT", + body: "value", + }); + let ctx = createPagesEventContext({ + request, + data: { user: "test" }, + params: { key: "key" }, + }); + let response = await apiKVKeyFunction.onRequestPut(ctx); + await waitOnExecutionContext(ctx); + expect(response.status).toBe(204); + + request = new IncomingRequest("http://example.com/api/kv/key"); + ctx = createPagesEventContext({ + request, + data: { user: "test" }, + params: { key: "key" }, + }); + response = await apiKVKeyFunction.onRequestGet(ctx); + await waitOnExecutionContext(ctx); + expect(response.status).toBe(200); + expect(await response.text()).toBe("value"); + }); + + it("calls middleware", async ({ expect }) => { + const request = new IncomingRequest("http://example.com/api/ping"); + const ctx = createPagesEventContext({ + request, + async next(request) { + expect(ctx.data).toStrictEqual({ user: "ada" }); + return new Response(`next:${request.method} ${request.url}`); + }, + }); + const response = await apiMiddleware.onRequest(ctx); + await waitOnExecutionContext(ctx); + expect(await response.text()).toBe("NEXT:GET HTTP://EXAMPLE.COM/API/PING"); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/tsconfig.json b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/vitest.config.ts b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/vitest.config.ts new file mode 100644 index 0000000..89818ce --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-functions-unit-integration-self/vitest.config.ts @@ -0,0 +1,32 @@ +import path from "node:path"; +import { + buildPagesASSETSBinding, + cloudflareTest, +} from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +const assetsPath = path.join(__dirname, "public"); + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + main: "./dist-functions/index.js", // Built by `global-setup.ts` + miniflare: { + compatibilityFlags: ["nodejs_compat"], + compatibilityDate: "2024-01-01", + kvNamespaces: ["KV_NAMESPACE"], + serviceBindings: { + ASSETS: await buildPagesASSETSBinding(assetsPath), + }, + }, + }), + ], + test: { + // Only required for integration tests + globalSetup: ["./global-setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/pages-with-config/README.md b/fixtures/vitest-pool-workers-examples/pages-with-config/README.md new file mode 100644 index 0000000..4ca02e8 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-with-config/README.md @@ -0,0 +1,3 @@ +# pages-with-config + +Tests for regression of [#5768](https://github.com/cloudflare/workers-sdk/issues/5768) diff --git a/fixtures/vitest-pool-workers-examples/pages-with-config/pages-config.test.ts b/fixtures/vitest-pool-workers-examples/pages-with-config/pages-config.test.ts new file mode 100644 index 0000000..440de74 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-with-config/pages-config.test.ts @@ -0,0 +1,7 @@ +import { it } from "vitest"; + +it("should run tests even if Pages project specifies wrangler config file", ({ + expect, +}) => { + expect(1).toBe(1); +}); diff --git a/fixtures/vitest-pool-workers-examples/pages-with-config/tsconfig.json b/fixtures/vitest-pool-workers-examples/pages-with-config/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-with-config/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pages-with-config/vitest.config.ts b/fixtures/vitest-pool-workers-examples/pages-with-config/vitest.config.ts new file mode 100644 index 0000000..d9a889a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-with-config/vitest.config.ts @@ -0,0 +1,14 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/pages-with-config/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/pages-with-config/wrangler.jsonc new file mode 100644 index 0000000..5916924 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pages-with-config/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "pages-with-config", + // don't provide compatibility_date so that vitest will infer the latest one + "pages_build_output_dir": "public", +} diff --git a/fixtures/vitest-pool-workers-examples/pipelines/README.md b/fixtures/vitest-pool-workers-examples/pipelines/README.md new file mode 100644 index 0000000..2bdc62f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/README.md @@ -0,0 +1,8 @@ +# 🚰 pipelines + +This Worker implements endpoint that send details of the incoming HTTP request to a Pipeline. + +| Test | Overview | +| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [pipeline-send-integration-self.test.ts](test/pipeline-send-integration-self.test.ts) | Integration tests for endpoints using `exports` | +| [pipeline-send-unit.test.ts](test/pipeline-send-unit.test.ts) | Unit tests calling `worker.fetch()` directly mocking the Pipeline | diff --git a/fixtures/vitest-pool-workers-examples/pipelines/src/env.d.ts b/fixtures/vitest-pool-workers-examples/pipelines/src/env.d.ts new file mode 100644 index 0000000..e0059f8 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/src/env.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./pipelines/src/env.d.ts -c ./pipelines/wrangler.jsonc --no-include-runtime` (hash: 75410e3d85500fe5c403446edfdba23c) +interface __BaseEnv_Env { + PIPELINE: import("cloudflare:pipelines").Pipeline< + import("cloudflare:pipelines").PipelineRecord + >; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/pipelines/src/index.ts b/fixtures/vitest-pool-workers-examples/pipelines/src/index.ts new file mode 100644 index 0000000..1d1fc05 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/src/index.ts @@ -0,0 +1,11 @@ +export default { + async fetch(request, env, ctx) { + await env.PIPELINE.send([ + { + method: request.method.toUpperCase(), + url: request.url, + }, + ]); + return new Response("Accepted", { status: 202 }); + }, +} satisfies ExportedHandler; diff --git a/fixtures/vitest-pool-workers-examples/pipelines/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/pipelines/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-integration-self.test.ts b/fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-integration-self.test.ts new file mode 100644 index 0000000..6d9a48b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-integration-self.test.ts @@ -0,0 +1,12 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("sends message to pipeline", async ({ expect }) => { + // Send data to the Pipeline + const response = await exports.default.fetch("https://example.com/ingest", { + method: "POST", + body: "value", + }); + expect(response.status).toBe(202); + expect(await response.text()).toBe("Accepted"); +}); diff --git a/fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-unit.test.ts b/fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-unit.test.ts new file mode 100644 index 0000000..702071d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/test/pipeline-send-unit.test.ts @@ -0,0 +1,45 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { afterEach, it, vi } from "vitest"; +import worker from "../src/index"; + +// This will improve in the next major version of `@cloudflare/workers-types`, +// but for now you'll need to do something like this to get a correctly-typed +// `Request` to pass to `worker.fetch()`. +const IncomingRequest = Request; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +it("produces message to pipeline", async ({ expect }) => { + const mockPipeline = { + send: vi.fn().mockResolvedValue(undefined), + }; + + const testEnv = { + ...env, + PIPELINE: mockPipeline, + }; + + // Send data to pipeline + const request = new IncomingRequest("https://example.com/ingest", { + method: "POST", + body: "value", + }); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, testEnv, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(202); + expect(await response.text()).toBe("Accepted"); + + // Check `PIPELINE.send()` was called + expect(mockPipeline.send).toHaveBeenCalledTimes(1); + expect(mockPipeline.send).toHaveBeenCalledWith([ + { method: "POST", url: "https://example.com/ingest" }, + ]); +}); diff --git a/fixtures/vitest-pool-workers-examples/pipelines/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/pipelines/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pipelines/tsconfig.json b/fixtures/vitest-pool-workers-examples/pipelines/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/pipelines/vitest.config.ts b/fixtures/vitest-pool-workers-examples/pipelines/vitest.config.ts new file mode 100644 index 0000000..4217062 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/vitest.config.ts @@ -0,0 +1,16 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/pipelines/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/pipelines/wrangler.jsonc new file mode 100644 index 0000000..8eccd5a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/pipelines/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "name": "pipelines", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "pipelines": [ + { + "binding": "PIPELINE", + "stream": "my-pipeline", + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/queues/README.md b/fixtures/vitest-pool-workers-examples/queues/README.md new file mode 100644 index 0000000..7551af7 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/README.md @@ -0,0 +1,10 @@ +# 🚥 queues + +This Worker implements a `PUT` endpoint that queues jobs on a queue, storing results in a KV namespace, and a `GET` endpoint that retrieves results from KV. Each job converts the request body to uppercase. + +| Test | Overview | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [queue-consumer-integration-self.test.ts](test/queue-consumer-integration-self.test.ts) | `queue` handler integration test using `exports` | +| [queue-consumer-unit.test.ts](test/queue-consumer-unit.test.ts) | Unit tests calling `worker.queue()` directly | +| [queue-producer-integration-self.test.ts](test/queue-producer-integration-self.test.ts) | Integration tests for endpoints using `exports` | +| [queue-producer-unit.test.ts](test/queue-producer-unit.test.ts) | Unit tests calling `worker.fetch()` directly mocking enqueuing and the consumer | diff --git a/fixtures/vitest-pool-workers-examples/queues/src/env.d.ts b/fixtures/vitest-pool-workers-examples/queues/src/env.d.ts new file mode 100644 index 0000000..d1ef862 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/src/env.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./queues/src/env.d.ts -c ./queues/wrangler.jsonc --no-include-runtime` (hash: d686d455d34c5b143559729c82938b10) +interface __BaseEnv_Env { + QUEUE_RESULTS: KVNamespace; + QUEUE_PRODUCER: Queue; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/queues/src/index.ts b/fixtures/vitest-pool-workers-examples/queues/src/index.ts new file mode 100644 index 0000000..bbf413b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/src/index.ts @@ -0,0 +1,34 @@ +export interface QueueJob { + key: string; + value: string; +} + +async function processJob(env: Env, job: QueueJob) { + const result = job.value.toUpperCase(); + await env.QUEUE_RESULTS.put(job.key, result); +} + +export default { + async fetch(request, env, ctx) { + const { pathname } = new URL(request.url); + if (request.method === "GET") { + const value = await env.QUEUE_RESULTS.get(pathname, "stream"); + return new Response(value, { status: value === null ? 404 : 200 }); + } else if (request.method === "POST") { + const value = await request.text(); + await env.QUEUE_PRODUCER.send({ key: pathname, value }); + return new Response("Accepted", { status: 202 }); + } else { + return new Response("Method Not Allowed", { status: 405 }); + } + }, + async queue(batch, env, ctx) { + for (const message of batch.messages) { + await processJob(env, message.body); + message.ack(); + } + }, +} satisfies ExportedHandler; +// ^ Using `satisfies` provides type checking/completions for `ExportedHandler` +// whilst still allowing us to call `worker.fetch()` and `worker.queue()` in +// tests without asserting they're defined. diff --git a/fixtures/vitest-pool-workers-examples/queues/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/queues/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-integration-self.test.ts b/fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-integration-self.test.ts new file mode 100644 index 0000000..f311567 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-integration-self.test.ts @@ -0,0 +1,36 @@ +import { randomBytes } from "node:crypto"; +import { env, exports } from "cloudflare:workers"; +import { it } from "vitest"; +import type { QueueJob } from "../src/index"; + +it("consumes queue messages", async ({ expect }) => { + // `exports.default` here points to the worker running in the current isolate. + // This gets its handler from the `main` option in `vitest.config.mts`. + // Importantly, it uses the exact `import("../src").default` instance we could + // import in this file as its handler. Note the `exports.default.queue()` method + // is experimental, and requires the `service_binding_extra_handlers` + // compatibility flag to be enabled. + const messages: ServiceBindingQueueMessage[] = [ + { + id: randomBytes(16).toString("hex"), + timestamp: new Date(1000), + attempts: 1, + body: { key: "/1", value: "one" }, + }, + { + id: randomBytes(16).toString("hex"), + timestamp: new Date(2000), + attempts: 1, + body: { key: "/2", value: "two" }, + }, + ]; + const result = await exports.default.queue("queue", messages); + expect(result.outcome).toBe("ok"); + expect(result.retryBatch.retry).toBe(false); // `true` if `batch.retryAll()` called + expect(result.ackAll).toBe(false); // `true` if `batch.ackAll()` called + expect(result.retryMessages).toStrictEqual([]); + expect(result.explicitAcks).toStrictEqual([messages[0].id, messages[1].id]); + + expect(await env.QUEUE_RESULTS.get("/1")).toBe("ONE"); + expect(await env.QUEUE_RESULTS.get("/2")).toBe("TWO"); +}); diff --git a/fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-unit.test.ts b/fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-unit.test.ts new file mode 100644 index 0000000..fd1e5dd --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/test/queue-consumer-unit.test.ts @@ -0,0 +1,42 @@ +import { randomBytes } from "node:crypto"; +import { + createExecutionContext, + createMessageBatch, + getQueueResult, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; +import worker from "../src/index"; +import type { QueueJob } from "../src/index"; + +it("consumes queue messages", async ({ expect }) => { + // Call `queue()` handler directly + const messages: ServiceBindingQueueMessage[] = [ + { + id: randomBytes(16).toString("hex"), + timestamp: new Date(1000), + attempts: 1, + body: { key: "/1", value: "one" }, + }, + { + id: randomBytes(16).toString("hex"), + timestamp: new Date(2000), + attempts: 1, + body: { key: "/2", value: "two" }, + }, + ]; + const batch = createMessageBatch("queue", messages); + const ctx = createExecutionContext(); + await worker.queue(batch, env, ctx); + + // `getQueueResult()` implicitly calls `waitOnExecutionContext()` + const result = await getQueueResult(batch, ctx); + expect(result.outcome).toBe("ok"); + expect(result.retryBatch.retry).toBe(false); // `true` if `batch.retryAll()` called + expect(result.ackAll).toBe(false); // `true` if `batch.ackAll()` called + expect(result.retryMessages).toStrictEqual([]); + expect(result.explicitAcks).toStrictEqual([messages[0].id, messages[1].id]); + + expect(await env.QUEUE_RESULTS.get("/1")).toBe("ONE"); + expect(await env.QUEUE_RESULTS.get("/2")).toBe("TWO"); +}); diff --git a/fixtures/vitest-pool-workers-examples/queues/test/queue-producer-integration-self.test.ts b/fixtures/vitest-pool-workers-examples/queues/test/queue-producer-integration-self.test.ts new file mode 100644 index 0000000..95371e3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/test/queue-producer-integration-self.test.ts @@ -0,0 +1,20 @@ +import { exports } from "cloudflare:workers"; +import { it, vi } from "vitest"; + +it("produces and consumers queue message", async ({ expect }) => { + // Enqueue job on queue + let response = await exports.default.fetch("https://example.com/key", { + method: "POST", + body: "value", + }); + expect(response.status).toBe(202); + expect(await response.text()).toBe("Accepted"); + + // Wait for job to be processed + const result = await vi.waitUntil(async () => { + const response = await exports.default.fetch("https://example.com/key"); + const text = await response.text(); + if (response.ok) return text; + }); + expect(result).toBe("VALUE"); +}); diff --git a/fixtures/vitest-pool-workers-examples/queues/test/queue-producer-unit.test.ts b/fixtures/vitest-pool-workers-examples/queues/test/queue-producer-unit.test.ts new file mode 100644 index 0000000..59244e3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/test/queue-producer-unit.test.ts @@ -0,0 +1,79 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { afterEach, assert, it, vi } from "vitest"; +import worker from "../src/index"; + +// This will improve in the next major version of `@cloudflare/workers-types`, +// but for now you'll need to do something like this to get a correctly-typed +// `Request` to pass to `worker.fetch()`. +const IncomingRequest = Request; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +it("produces queue message with mocked send", async ({ expect }) => { + // Intercept calls to `QUEUE_PRODUCER.send()` + const sendSpy = vi + .spyOn(env.QUEUE_PRODUCER, "send") + .mockImplementation(async () => ({ + metadata: { + metrics: { + backlogCount: 0, + backlogBytes: 0, + oldestMessageTimestamp: new Date(0), + }, + }, + })); + + // Enqueue job on queue + const request = new IncomingRequest("https://example.com/key", { + method: "POST", + body: "value", + }); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(202); + expect(await response.text()).toBe("Accepted"); + + // Check `QUEUE_PRODUCER.send()` was called + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith({ key: "/key", value: "value" }); +}); + +it("produces queue message with mocked consumer", async ({ expect }) => { + // Intercept calls to `worker.queue()`. Note the runner worker has a queue + // consumer configured that gets its handler from the `main` option in + // `vitest.config.mts`. Importantly, this uses the exact `worker` instance + // we're spying on here. + const consumerSpy = vi + .spyOn(worker, "queue") + .mockImplementation(async () => {}); + + // Enqueue job on queue + const request = new IncomingRequest("https://example.com/key", { + method: "POST", + body: "another value", + }); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(202); + expect(await response.text()).toBe("Accepted"); + + // Wait for consumer to be called + await vi.waitUntil(() => consumerSpy.mock.calls.length > 0); + expect(consumerSpy).toHaveBeenCalledTimes(1); + const batch = consumerSpy.mock.lastCall?.[0]; + assert(batch); + expect(batch.messages[0].body).toStrictEqual({ + key: "/key", + value: "another value", + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/queues/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/queues/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/queues/tsconfig.json b/fixtures/vitest-pool-workers-examples/queues/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/queues/vitest.config.ts b/fixtures/vitest-pool-workers-examples/queues/vitest.config.ts new file mode 100644 index 0000000..fe3a8af --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/vitest.config.ts @@ -0,0 +1,25 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + miniflare: { + // Required to use `exports.default.queue()`. This is an experimental + // compatibility flag, and cannot be enabled in production. + compatibilityFlags: ["service_binding_extra_handlers"], + // Use a shorter `max_batch_timeout` in tests + queueConsumers: { + queue: { maxBatchTimeout: 0.05 /* 50ms */ }, + }, + }, + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/queues/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/queues/wrangler.jsonc new file mode 100644 index 0000000..373766a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/queues/wrangler.jsonc @@ -0,0 +1,25 @@ +{ + "name": "queues", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "kv_namespaces": [ + { + "binding": "QUEUE_RESULTS", + "id": "00000000000000000000000000000000", + }, + ], + "queues": { + "producers": [ + { + "binding": "QUEUE_PRODUCER", + "queue": "queue", + }, + ], + "consumers": [ + { + "queue": "queue", + "max_batch_timeout": 3, // Overridden in `vitest.config.mts` + }, + ], + }, +} diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/README.md b/fixtures/vitest-pool-workers-examples/request-mocking/README.md new file mode 100644 index 0000000..d52ba33 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/README.md @@ -0,0 +1,8 @@ +# 🤹 request-mocking + +This Worker rewrites the host of all incoming requests to `cloudflare.com` then forwards the request on. Tests demonstrate declarative mocking with [MSW (Mock Service Worker)](https://mswjs.io/), and imperative mocks of `globalThis.fetch()`. Note mocking WebSocket requests is only supported with imperative mocking. + +| Test | Overview | +| ----------------------------------------------- | ----------------------------------------------------------------------- | +| [declarative.test.ts](test/declarative.test.ts) | Integration tests with declarative request mocking using MSW | +| [imperative.test.ts](test/imperative.test.ts) | Integration tests with imperative request mocking, including WebSockets | diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/src/env.d.ts b/fixtures/vitest-pool-workers-examples/request-mocking/src/env.d.ts new file mode 100644 index 0000000..f2c3b51 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/src/env.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./request-mocking/src/env.d.ts -c ./request-mocking/wrangler.jsonc --no-include-runtime` (hash: 0c8cd5ae12176ebab8337cbfef98b516) +interface __BaseEnv_Env {} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/src/index.ts b/fixtures/vitest-pool-workers-examples/request-mocking/src/index.ts new file mode 100644 index 0000000..b6a4b83 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/src/index.ts @@ -0,0 +1,11 @@ +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + url.host = "cloudflare.com"; + try { + return await fetch(url, request); + } catch (e) { + return new Response(String(e), { status: 500 }); + } + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/request-mocking/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/test/declarative.test.ts b/fixtures/vitest-pool-workers-examples/request-mocking/test/declarative.test.ts new file mode 100644 index 0000000..e785184 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/test/declarative.test.ts @@ -0,0 +1,65 @@ +import { exports } from "cloudflare:workers"; +import { http, HttpResponse } from "msw"; +import { expect, it } from "vitest"; +import { server } from "./server"; + +it("mocks GET requests", async () => { + server.use( + http.get( + "https://cloudflare.com/once", + () => { + return HttpResponse.text("😉"); + }, + { once: true } + ), + http.get("https://cloudflare.com/persistent", () => { + return HttpResponse.text("📌"); + }) + ); + + // Host `example.com` will be rewritten to `cloudflare.com` by the Worker + let response = await exports.default.fetch("https://example.com/once"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("😉"); + + // Subsequent `fetch()`es fail... + response = await exports.default.fetch("https://example.com/once"); + expect(response.status).toBe(500); + expect(await response.text()).toMatch("Cannot bypass"); + + // ...but calling `.persist()` will match forever, with `.times(n)` matching + // `n` times + for (let i = 0; i < 3; i++) { + response = await exports.default.fetch("https://example.com/persistent"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("📌"); + } +}); + +it("mocks POST requests", async () => { + server.use( + http.post("https://cloudflare.com/path", async ({ request }) => { + const text = await request.text(); + if (text !== "✨") { + return HttpResponse.text("Bad request body", { status: 400 }); + } + return HttpResponse.text("✅"); + }) + ); + + // Sending a request without the expected body returns an error response... + let response = await exports.default.fetch("https://example.com/path", { + method: "POST", + body: "🙃", + }); + expect(response.status).toBe(400); + expect(await response.text()).toBe("Bad request body"); + + // ...but the correct body should succeed + response = await exports.default.fetch("https://example.com/path", { + method: "POST", + body: "✨", + }); + expect(response.status).toBe(200); + expect(await response.text()).toBe("✅"); +}); diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/test/imperative.test.ts b/fixtures/vitest-pool-workers-examples/request-mocking/test/imperative.test.ts new file mode 100644 index 0000000..e19c13c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/test/imperative.test.ts @@ -0,0 +1,104 @@ +import events from "node:events"; +import { exports } from "cloudflare:workers"; +import { afterEach, assert, it, vi } from "vitest"; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +it("mocks GET requests", async ({ expect }) => { + vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const request = new Request(input, init); + const url = new URL(request.url); + + if ( + request.method === "GET" && + url.origin === "https://cloudflare.com" && + url.pathname === "/path" + ) { + return new Response("✅"); + } + + throw new Error("No mock found"); + }); + + // Host `example.com` will be rewritten to `cloudflare.com` by the Worker + let response = await exports.default.fetch("https://example.com/path"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("✅"); + + // Invalid paths shouldn't match + response = await exports.default.fetch("https://example.com/bad"); + expect(response.status).toBe(500); + expect(await response.text()).toMatch("No mock found"); +}); + +it("mocks POST requests", async ({ expect }) => { + vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const request = new Request(input, init); + const url = new URL(request.url); + const body = await request.text(); + + if ( + request.method === "POST" && + url.origin === "https://cloudflare.com" && + url.pathname === "/path" && + body === "✨" + ) { + return new Response("✅"); + } + + throw new Error("No mock found"); + }); + + const response = await exports.default.fetch("https://example.com/path", { + method: "POST", + body: "✨", + }); + expect(response.status).toBe(200); + expect(await response.text()).toBe("✅"); +}); + +it("mocks WebSocket requests", async ({ expect }) => { + vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const request = new Request(input, init); + const url = new URL(request.url); + + if ( + request.method === "GET" && + url.origin === "https://cloudflare.com" && + url.pathname === "/ws" && + request.headers.get("Upgrade") === "websocket" + ) { + const { 0: socket, 1: responseSocket } = new WebSocketPair(); + socket.addEventListener("message", (event) => { + assert(typeof event.data === "string"); + socket.send(event.data.toUpperCase()); + }); + socket.accept(); + return new Response(null, { + status: 101, + webSocket: responseSocket, + }); + } + + throw new Error("No mock found"); + }); + + // Send WebSocket request and assert WebSocket response received... + const response = await exports.default.fetch("https://example.com/ws", { + headers: { Upgrade: "websocket" }, + }); + expect(response.status).toBe(101); + const webSocket = response.webSocket; + assert(webSocket !== null); // Using `assert()` for type narrowing + + // ...then accept WebSocket and send/receive message + const eventPromise = events.once(webSocket, "message") as Promise< + [MessageEvent] /* args */ + >; + webSocket.accept(); + webSocket.send("hello"); + const args = await eventPromise; + expect(args[0].data).toBe("HELLO"); +}); diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/test/server.ts b/fixtures/vitest-pool-workers-examples/request-mocking/test/server.ts new file mode 100644 index 0000000..bd0bda5 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/test/server.ts @@ -0,0 +1,3 @@ +import { setupServer } from "msw/node"; + +export const server = setupServer(); diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/test/setup.ts b/fixtures/vitest-pool-workers-examples/request-mocking/test/setup.ts new file mode 100644 index 0000000..891a479 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/test/setup.ts @@ -0,0 +1,10 @@ +import { afterAll, afterEach, beforeAll } from "vitest"; +import { server } from "./server"; + +beforeAll(() => + server.listen({ + onUnhandledRequest: "error", + }) +); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/request-mocking/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/tsconfig.json b/fixtures/vitest-pool-workers-examples/request-mocking/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/vitest.config.ts b/fixtures/vitest-pool-workers-examples/request-mocking/vitest.config.ts new file mode 100644 index 0000000..20f7975 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/vitest.config.ts @@ -0,0 +1,19 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + test: { + setupFiles: ["test/setup.ts"], + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/request-mocking/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/request-mocking/wrangler.jsonc new file mode 100644 index 0000000..17f88cb --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/request-mocking/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "request-mocking", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one +} diff --git a/fixtures/vitest-pool-workers-examples/reset/src/env.d.ts b/fixtures/vitest-pool-workers-examples/reset/src/env.d.ts new file mode 100644 index 0000000..d9ccb38 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/src/env.d.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --config=./reset/wrangler.jsonc --include-runtime=false ./reset/src/env.d.ts` (hash: 33577604668b74d20af09cc4348f7c27) +interface __BaseEnv_Env { + KV_NAMESPACE: KVNamespace; + R2_BUCKET: R2Bucket; + DATABASE: D1Database; + RATE_LIMITER: RateLimit; + RATE_LIMITER_ALIAS: RateLimit; + COUNTER: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "Counter"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/reset/src/index.ts b/fixtures/vitest-pool-workers-examples/reset/src/index.ts new file mode 100644 index 0000000..5c719f8 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/src/index.ts @@ -0,0 +1,27 @@ +import { DurableObject } from "cloudflare:workers"; + +export class Counter extends DurableObject { + count: number = 0; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + void ctx.blockConcurrencyWhile(async () => { + this.count = (await ctx.storage.get("count")) ?? 0; + }); + } + + fetch() { + this.count++; + void this.ctx.storage.put("count", this.count); + return new Response(this.count.toString()); + } +} + +export default >{ + fetch(request, env) { + const { pathname } = new URL(request.url); + const id = env.COUNTER.idFromName(pathname); + const stub = env.COUNTER.get(id); + return stub.fetch(request); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/reset/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/reset/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/reset/test/reset.test.ts b/fixtures/vitest-pool-workers-examples/reset/test/reset.test.ts new file mode 100644 index 0000000..e995eaa --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/test/reset.test.ts @@ -0,0 +1,105 @@ +import { reset } from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { afterEach, it } from "vitest"; + +afterEach(async () => { + await reset(); +}); + +it("clears KV storage between tests", async ({ expect }) => { + expect(await env.KV_NAMESPACE.get("key")).toBe(null); + await env.KV_NAMESPACE.put("key", "value"); + expect(await env.KV_NAMESPACE.get("key")).toBe("value"); +}); + +it("sees empty KV storage after reset", async ({ expect }) => { + expect(await env.KV_NAMESPACE.get("key")).toBe(null); +}); + +it("clears R2 storage between tests", async ({ expect }) => { + const listed = await env.R2_BUCKET.list(); + expect(listed.objects.length).toBe(0); + await env.R2_BUCKET.put("object", new Uint8Array([1, 2, 3])); + const listedAfter = await env.R2_BUCKET.list(); + expect(listedAfter.objects.length).toBe(1); +}); + +it("sees empty R2 storage after reset", async ({ expect }) => { + const listed = await env.R2_BUCKET.list(); + expect(listed.objects.length).toBe(0); +}); + +it("clears D1 storage between tests", async ({ expect }) => { + await env.DATABASE.prepare( + "CREATE TABLE IF NOT EXISTS test (key TEXT PRIMARY KEY, value TEXT)" + ).run(); + await env.DATABASE.prepare( + "INSERT INTO test (key, value) VALUES ('key', 'value')" + ).run(); + const result = await env.DATABASE.prepare( + "SELECT value FROM test WHERE key = 'key'" + ).first(); + expect(result?.value).toBe("value"); +}); + +it("sees empty D1 storage after reset", async ({ expect }) => { + const result = await env.DATABASE.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='test'" + ).first(); + expect(result).toBe(null); +}); + +it("clears Durable Object storage between tests", async ({ expect }) => { + const id = env.COUNTER.idFromName("/test"); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com"); + expect(await response.text()).toBe("1"); +}); + +it("sees reset Durable Object storage after reset", async ({ expect }) => { + const id = env.COUNTER.idFromName("/test"); + const stub = env.COUNTER.get(id); + const response = await stub.fetch("https://example.com"); + expect(await response.text()).toBe("1"); +}); + +it("exhausts ratelimit then resets between tests", async ({ expect }) => { + // First call succeeds + const first = await env.RATE_LIMITER.limit({ key: "test-key" }); + expect(first.success).toBe(true); + + // Exhaust the full limit of 100 + for (let i = 1; i < 100; i++) { + await env.RATE_LIMITER.limit({ key: "test-key" }); + } + + // 101st call should fail + const over = await env.RATE_LIMITER.limit({ key: "test-key" }); + expect(over.success).toBe(false); +}); + +it("sees reset ratelimit state after reset", async ({ expect }) => { + // After reset(), buckets should be cleared — first call succeeds again + const result = await env.RATE_LIMITER.limit({ key: "test-key" }); + expect(result.success).toBe(true); +}); + +it("shares ratelimit state across bindings with the same namespace_id", async ({ + expect, +}) => { + // RATE_LIMITER and RATE_LIMITER_ALIAS both reference namespace_id "1", so + // they increment the same underlying counter for a given key. + for (let i = 0; i < 100; i++) { + const res = await env.RATE_LIMITER.limit({ key: "shared" }); + expect(res.success).toBe(true); + } + + // The 101st call — made through the *other* binding — is rejected because + // the counter is shared across both bindings. + const viaAlias = await env.RATE_LIMITER_ALIAS.limit({ key: "shared" }); + expect(viaAlias.success).toBe(false); + + // A different key is still tracked independently within the namespace. + const otherKey = await env.RATE_LIMITER_ALIAS.limit({ key: "fresh" }); + expect(otherKey.success).toBe(true); +}); diff --git a/fixtures/vitest-pool-workers-examples/reset/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/reset/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/reset/tsconfig.json b/fixtures/vitest-pool-workers-examples/reset/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/reset/vitest.config.ts b/fixtures/vitest-pool-workers-examples/reset/vitest.config.ts new file mode 100644 index 0000000..2590f0b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/vitest.config.ts @@ -0,0 +1,19 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + test: { + name: "@scoped/reset", + }, + }) +); diff --git a/fixtures/vitest-pool-workers-examples/reset/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/reset/wrangler.jsonc new file mode 100644 index 0000000..90b3721 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/reset/wrangler.jsonc @@ -0,0 +1,51 @@ +{ + "name": "reset", + "main": "src/index.ts", + "durable_objects": { + "bindings": [ + { + "name": "COUNTER", + "class_name": "Counter", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_classes": ["Counter"], + }, + ], + "kv_namespaces": [ + { + "binding": "KV_NAMESPACE", + "id": "00000000000000000000000000000000", + }, + ], + "r2_buckets": [ + { + "binding": "R2_BUCKET", + "bucket_name": "bucket", + }, + ], + "d1_databases": [ + { + "binding": "DATABASE", + "database_name": "database", + "database_id": "00000000-0000-0000-0000-000000000000", + }, + ], + "ratelimits": [ + { + "name": "RATE_LIMITER", + "namespace_id": "1", + "simple": { "limit": 100, "period": 60 }, + }, + { + // Deliberately shares namespace_id "1" with RATE_LIMITER: two bindings + // pointing at the same namespace must share a single counter. + "name": "RATE_LIMITER_ALIAS", + "namespace_id": "1", + "simple": { "limit": 100, "period": 60 }, + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/rpc/README.md b/fixtures/vitest-pool-workers-examples/rpc/README.md new file mode 100644 index 0000000..59ecefe --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/README.md @@ -0,0 +1,8 @@ +# 🤯 rpc + +This Worker defines `WorkerEntrypoint` default and named exports. It also defines Durable Objects subclassing `DurableObject`. All of these classes define properties and methods that can be called over RPC. Integration tests dispatch events, access properties and call methods via `exports` imported from `cloudflare:workers`. + +| Test | Overview | +| --------------------------------------------------------- | -------------------------------------------- | +| [integration-self.test.ts](test/integration-self.test.ts) | Integration test using `exports` | +| [unit.test.ts](test/unit.test.ts) | Unit tests calling exported classes directly | diff --git a/fixtures/vitest-pool-workers-examples/rpc/src/counter.ts b/fixtures/vitest-pool-workers-examples/rpc/src/counter.ts new file mode 100644 index 0000000..645cf54 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/src/counter.ts @@ -0,0 +1,26 @@ +import { RpcTarget } from "cloudflare:workers"; + +export class Counter extends RpcTarget { + #value: number; + + constructor(value: number) { + super(); + this.#value = value; + } + + get value() { + return this.#value; + } + + increment(by = 1) { + return (this.#value += by); + } + + clone() { + return new Counter(this.#value); + } + + asObject() { + return { val: this.#value }; + } +} diff --git a/fixtures/vitest-pool-workers-examples/rpc/src/env.d.ts b/fixtures/vitest-pool-workers-examples/rpc/src/env.d.ts new file mode 100644 index 0000000..2534a64 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/src/env.d.ts @@ -0,0 +1,19 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./rpc/src/env.d.ts -c ./rpc/wrangler.jsonc --no-include-runtime` (hash: 86dc5f25d2f0268adc69f5c054dadba6) +interface __BaseEnv_Env { + KV_NAMESPACE: KVNamespace; + TEST_OBJECT: DurableObjectNamespace; + PROXIED_TEST_OBJECT: DurableObjectNamespace< + import("./index").ProxiedTestObject + >; + TEST_NAMED_HANDLER: Service; + TEST_NAMED_ENTRYPOINT: Service; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + durableNamespaces: "TestObject" | "ProxiedTestObject"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/rpc/src/index.ts b/fixtures/vitest-pool-workers-examples/rpc/src/index.ts new file mode 100644 index 0000000..92f4252 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/src/index.ts @@ -0,0 +1,188 @@ +import { DurableObject, RpcTarget, WorkerEntrypoint } from "cloudflare:workers"; +import { Counter } from "./counter"; + +export class TestObject extends DurableObject { + #value: number = 0; + #log: string[] = []; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.overriddenPrototypeMethod = () => "instance"; + void ctx.blockConcurrencyWhile(async () => { + this.#value = (await ctx.storage.get("count")) ?? 0; + }); + } + + get value() { + return this.#value; + } + + increment(by = 1) { + this.#value += by; + void this.ctx.storage.put("count", this.#value); + return this.#value; + } + + scheduleReset(afterMillis: number) { + void this.ctx.storage.setAlarm(Date.now() + afterMillis); + } + + record(value: string) { + this.#log.push(value); + } + + getLog() { + return this.#log; + } + + async recordFromDurableObject(targetName: string, calls: number) { + const id = this.env.TEST_OBJECT.idFromName(targetName); + const stub = this.env.TEST_OBJECT.get(id); + const promises: Promise[] = []; + + for (let i = 0; i < calls; i++) { + promises.push(stub.record(`call-${i}`)); + } + + await Promise.all(promises); + return stub.getLog(); + } + + async fetch(request: Request) { + return Response.json({ + source: "TestObject", + method: request.method, + url: request.url, + ctxWaitUntil: typeof this.ctx.waitUntil, + envKeys: Object.keys(this.env).sort(), + }); + } + + overriddenPrototypeMethod() { + return "prototype"; + } + + getCounter() { + return new Counter(0); + } + + getObject() { + return { hello: "world" }; + } + + alarm() { + this.#value = 0; + void this.ctx.storage.put("count", this.#value); + } + + instanceProperty = "👻"; +} + +export class ProxiedTestObject extends DurableObject { + #value = "private value"; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + return new Proxy(this, { + get(target, key, receiver) { + const value = Reflect.get(target, key, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + }); + } + + readPrivateValue() { + return this.#value; + } +} + +export const testNamedHandler = >{ + fetch(request, env, ctx) { + return Response.json({ + source: "testNamedHandler", + method: request.method, + url: request.url, + ctxWaitUntil: typeof ctx.waitUntil, + envKeys: Object.keys(env).sort(), + }); + }, +}; + +export class TestNamedEntrypoint extends WorkerEntrypoint { + fetch(request: Request) { + return Response.json({ + source: "TestNamedEntrypoint", + method: request.method, + url: request.url, + ctxWaitUntil: typeof this.ctx.waitUntil, + envKeys: Object.keys(this.env).sort(), + }); + } + + ping() { + return "pong"; + } + + getCounter() { + return new Counter(0); + } + + async recordFromWorkerEntrypoint(targetName: string, calls: number) { + const id = this.env.TEST_OBJECT.idFromName(targetName); + const stub = this.env.TEST_OBJECT.get(id); + const promises: Promise[] = []; + + for (let i = 0; i < calls; i++) { + promises.push(stub.record(`call-${i}`)); + } + + await Promise.all(promises); + return stub.getLog(); + } +} + +export class TestSuperEntrypoint extends WorkerEntrypoint { + superMethod() { + return "🦸"; + } +} + +let lastController: ScheduledController | undefined; +export default class TestDefaultEntrypoint extends TestSuperEntrypoint { + async fetch(request: Request) { + return Response.json({ + source: "TestDefaultEntrypoint", + method: request.method, + url: request.url, + ctxWaitUntil: typeof this.ctx.waitUntil, + envKeys: Object.keys(this.env).sort(), + }); + } + + async scheduled(controller: ScheduledController) { + lastController = controller; + } + + get lastControllerCron() { + return lastController?.cron; + } + + sum(...args: number[]) { + return args.reduce((acc, value) => acc + value, 0); + } + + backgroundWrite(key: string, value: string) { + this.ctx.waitUntil(this.env.KV_NAMESPACE.put(key, value)); + } + + async read(key: string) { + return this.env.KV_NAMESPACE.get(key); + } + + createCounter(value = 0) { + return new Counter(value); + } + + instanceProperty = "👻"; + instanceMethod = () => "👻"; +} diff --git a/fixtures/vitest-pool-workers-examples/rpc/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/rpc/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/rpc/test/integration-self.test.ts b/fixtures/vitest-pool-workers-examples/rpc/test/integration-self.test.ts new file mode 100644 index 0000000..3b43b88 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/test/integration-self.test.ts @@ -0,0 +1,78 @@ +import { env, exports } from "cloudflare:workers"; +import { it, vi } from "vitest"; + +it("dispatches fetch event", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/"); + expect(await response.json()).toMatchObject({ + ctxWaitUntil: "function", + method: "GET", + source: "TestDefaultEntrypoint", + url: "https://example.com/", + }); +}); + +it("dispatches scheduled event and accesses property with rpc", async ({ + expect, +}) => { + await exports.default.scheduled({ cron: "* * * * 30" }); + const lastControllerCron = await exports.default.lastControllerCron; + expect(lastControllerCron).toBe("* * * * 30"); +}); + +it("calls multi-argument methods with rpc", async ({ expect }) => { + const result = await exports.default.sum(1, 2, 3); + expect(result).toBe(6); +}); + +it("calls methods using ctx and env with rpc", async ({ expect }) => { + expect(await env.KV_NAMESPACE.get("key")).toBe(null); + await exports.default.backgroundWrite("key", "value"); + await vi.waitUntil( + async () => (await env.KV_NAMESPACE.get("key")) === "value" + ); +}); + +it("calls async methods with rpc", async ({ expect }) => { + await env.KV_NAMESPACE.put("key", "value"); + expect(await exports.default.read("key")).toBe("value"); +}); + +it("calls methods with rpc and pipelining", async ({ expect }) => { + const result = await exports.default.createCounter(5).clone().increment(3); + expect(result).toBe(8); +}); + +it("can access methods from superclass", async ({ expect }) => { + const result = await exports.default.superMethod(); + expect(result).toBe("🦸"); +}); +it("cannot access instance properties or methods", async ({ expect }) => { + await expect(async () => await exports.default.instanceProperty).rejects + .toThrowErrorMatchingInlineSnapshot(` + [TypeError: The RPC receiver's prototype does not implement "instanceProperty", but the receiver instance does. + Only properties and methods defined on the prototype can be accessed over RPC. + Ensure properties are declared like \`get instanceProperty() { ... }\` instead of \`instanceProperty = ...\`, + and methods are declared like \`instanceProperty() { ... }\` instead of \`instanceProperty = () => { ... }\`.] + `); + await expect(async () => await exports.default.instanceMethod()).rejects + .toThrowErrorMatchingInlineSnapshot(` + [TypeError: The RPC receiver's prototype does not implement "instanceMethod", but the receiver instance does. + Only properties and methods defined on the prototype can be accessed over RPC. + Ensure properties are declared like \`get instanceMethod() { ... }\` instead of \`instanceMethod = ...\`, + and methods are declared like \`instanceMethod() { ... }\` instead of \`instanceMethod = () => { ... }\`.] + `); +}); +it("cannot access non-existent properties or methods", async ({ expect }) => { + await expect( + // @ts-expect-error intentionally testing incorrect types + async () => await exports.default.nonExistentProperty + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: The RPC receiver does not implement "nonExistentProperty".]` + ); + await expect( + // @ts-expect-error intentionally testing incorrect types + async () => await exports.default.nonExistentMethod() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: The RPC receiver does not implement "nonExistentMethod".]` + ); +}); diff --git a/fixtures/vitest-pool-workers-examples/rpc/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/rpc/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/rpc/test/unit.test.ts b/fixtures/vitest-pool-workers-examples/rpc/test/unit.test.ts new file mode 100644 index 0000000..d22d282 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/test/unit.test.ts @@ -0,0 +1,292 @@ +import { + createExecutionContext, + runDurableObjectAlarm, + runInDurableObject, +} from "cloudflare:test"; +import { env, RpcStub } from "cloudflare:workers"; +import { describe, it, onTestFinished } from "vitest"; +import TestDefaultEntrypoint, { TestObject } from "../src"; +import { Counter } from "../src/counter"; + +describe("named entrypoints", () => { + it("dispatches fetch request to named ExportedHandler", async ({ + expect, + }) => { + const response = await env.TEST_NAMED_HANDLER.fetch("https://example.com"); + expect(await response.json()).toMatchObject({ + ctxWaitUntil: "function", + method: "GET", + source: "testNamedHandler", + url: "https://example.com/", + }); + }); + it("dispatches fetch request to named WorkerEntrypoint", async ({ + expect, + }) => { + const response = await env.TEST_NAMED_ENTRYPOINT.fetch( + "https://example.com" + ); + expect(await response.json()).toMatchObject({ + ctxWaitUntil: "function", + method: "GET", + source: "TestNamedEntrypoint", + url: "https://example.com/", + }); + }); + it("calls method with rpc", async ({ expect }) => { + const result = await env.TEST_NAMED_ENTRYPOINT.ping(); + expect(result).toBe("pong"); + }); + + it("receives RpcTarget over RPC", async ({ expect }) => { + const result = await env.TEST_NAMED_ENTRYPOINT.getCounter(); + expect(await result.value).toBe(0); + result.increment(); + result.increment(); + expect(await result.value).toBe(2); + const counter2 = result.clone(); + counter2.increment(); + expect(await counter2.value).toBe(3); + expect(await result.value).toBe(2); + }); + + it("receives plain objects over RPC", async ({ expect }) => { + const result = await env.TEST_NAMED_ENTRYPOINT.getCounter(); + result.increment(); + expect(await result.asObject()).toMatchObject({ val: 1 }); + }); +}); + +describe("Durable Object", () => { + const orderingAttempts = 5; + const orderingCalls = 100; + + function expectedOrderingLog() { + return Array.from({ length: orderingCalls }, (_, i) => `call-${i}`); + } + function orderingTargetName(prefix: string, attempt: number) { + return `${prefix}-${crypto.randomUUID()}-${attempt}`; + } + + it("dispatches fetch request", async ({ expect }) => { + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + const response = await stub.fetch("https://example.com"); + expect(await response.json()).toMatchObject({ + ctxWaitUntil: "function", + method: "GET", + source: "TestObject", + url: "https://example.com/", + }); + }); + it("increments count and allows direct/rpc access to instance/storage", async ({ + expect, + }) => { + // Check sending request directly to instance + const id = env.TEST_OBJECT.idFromName("/path"); + const stub = env.TEST_OBJECT.get(id); + const result = await runInDurableObject(stub, (instance: TestObject) => { + expect(instance).toBeInstanceOf(TestObject); // Exact same class as import + return instance.increment(1); + }); + expect(result).toBe(1); + + // Check direct access to properties and storage + await runInDurableObject(stub, async (instance: TestObject, state) => { + expect(instance.value).toBe(1); + expect(await state.storage.get("count")).toBe(1); + }); + + // Check calling method over RPC + expect(await stub.increment(3)).toBe(4); + + // Check accessing property over RPC + expect(await stub.value).toBe(4); + }); + it("dispatches RPC methods from proxy-returning Durable Objects", async ({ + expect, + }) => { + const id = env.PROXIED_TEST_OBJECT.newUniqueId(); + const stub = env.PROXIED_TEST_OBJECT.get(id); + expect(await stub.readPrivateValue()).toBe("private value"); + }); + it("rejects instance overrides of prototype methods", async ({ expect }) => { + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + await expect( + async () => await stub.overriddenPrototypeMethod() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: The RPC receiver does not implement the method "overriddenPrototypeMethod".]` + ); + }); + it("immediately executes alarm", async ({ expect }) => { + // Schedule alarm by directly calling method over RPC + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + await stub.increment(3); + await stub.scheduleReset(60_000); + + // Check counter has non-zero value + expect(await stub.value).toBe(3); + + // Immediately execute the alarm to reset the counter + let ran = await runDurableObjectAlarm(stub); + expect(ran).toBe(true); // ...as there was an alarm scheduled + + // Check counter value was reset + expect(await stub.value).toBe(0); + }); + it("cannot access instance properties or methods", async ({ expect }) => { + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + await expect(async () => await stub.instanceProperty).rejects + .toThrowErrorMatchingInlineSnapshot(` + [TypeError: The RPC receiver's prototype does not implement "instanceProperty", but the receiver instance does. + Only properties and methods defined on the prototype can be accessed over RPC. + Ensure properties are declared like \`get instanceProperty() { ... }\` instead of \`instanceProperty = ...\`, + and methods are declared like \`instanceProperty() { ... }\` instead of \`instanceProperty = () => { ... }\`.] + `); + }); + it("cannot access non-existent properties or methods", async ({ expect }) => { + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + await expect( + // @ts-expect-error intentionally testing incorrect types + async () => await stub.nonExistentProperty + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[TypeError: The RPC receiver does not implement "nonExistentProperty".]` + ); + }); + it("receives RpcTarget over RPC", async ({ expect }) => { + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + using result = await stub.getCounter(); + expect(await result.value).toBe(0); + await result.increment(); + await result.increment(); + expect(await result.value).toBe(2); + // TODO: Improve RPC types so this casting isn't required + using counter2 = (await result.clone()) as unknown as Counter & Disposable; + await counter2.increment(); + expect(await counter2.value).toBe(3); + expect(await result.value).toBe(2); + }); + + it("receives plain objects over RPC", async ({ expect }) => { + const id = env.TEST_OBJECT.newUniqueId(); + const stub = env.TEST_OBJECT.get(id); + using result = await stub.getObject(); + expect(result).toMatchObject({ hello: "world" }); + }); + + // Regression repro for https://github.com/cloudflare/workers-sdk/issues/13433. + it("preserves same-type RPC call order", async ({ expect }) => { + for (let attempt = 0; attempt < orderingAttempts; attempt++) { + const id = env.TEST_OBJECT.idFromName( + orderingTargetName("ordering", attempt) + ); + const stub = env.TEST_OBJECT.get(id); + const promises: Promise[] = []; + + for (let i = 0; i < orderingCalls; i++) { + promises.push(stub.record(`call-${i}`)); + } + + await Promise.all(promises); + + expect(await stub.getLog()).toEqual(expectedOrderingLog()); + } + }); + + it("preserves same-type RPC call order after prewarming instance", async ({ + expect, + }) => { + for (let attempt = 0; attempt < orderingAttempts; attempt++) { + const id = env.TEST_OBJECT.idFromName( + orderingTargetName("prewarmed-ordering", attempt) + ); + const stub = env.TEST_OBJECT.get(id); + await stub.getLog(); + const promises: Promise[] = []; + + for (let i = 0; i < orderingCalls; i++) { + promises.push(stub.record(`call-${i}`)); + } + + await Promise.all(promises); + + expect(await stub.getLog()).toEqual(expectedOrderingLog()); + } + }); + + it("preserves same-type RPC call order from a WorkerEntrypoint caller", async ({ + expect, + }) => { + for (let attempt = 0; attempt < orderingAttempts; attempt++) { + expect( + await env.TEST_NAMED_ENTRYPOINT.recordFromWorkerEntrypoint( + orderingTargetName("worker-entrypoint-ordering", attempt), + orderingCalls + ) + ).toEqual(expectedOrderingLog()); + } + }); + + it("preserves same-type RPC call order from a Durable Object caller", async ({ + expect, + }) => { + for (let attempt = 0; attempt < orderingAttempts; attempt++) { + const id = env.TEST_OBJECT.idFromName( + orderingTargetName("do-caller", attempt) + ); + const stub = env.TEST_OBJECT.get(id); + expect( + await stub.recordFromDurableObject( + orderingTargetName("do-caller-ordering", attempt), + orderingCalls + ) + ).toEqual(expectedOrderingLog()); + } + }); +}); + +// Regression: https://github.com/cloudflare/workers-sdk/issues/7077 +// Fixed in workerd by https://github.com/cloudflare/workerd/pull/3782 +it("can construct a WorkerEntrypoint with mocked env", async ({ expect }) => { + const data = new Map([["mocked-key", "mocked-value"]]); + const mockedKv = new Proxy(env.KV_NAMESPACE, { + get: (target, prop, receiver) => + prop === "get" + ? async (key: string) => data.get(key) ?? null + : Reflect.get(target, prop, receiver), + }); + + const ctx = createExecutionContext(); + const worker = new TestDefaultEntrypoint(ctx, { + ...env, + KV_NAMESPACE: mockedKv, + }); + expect(await worker.read("mocked-key")).toBe("mocked-value"); +}); + +describe("counter", () => { + it("increments count", ({ expect }) => { + const counter = new Counter(3); + expect(counter.increment()).toBe(4); + expect(counter.increment(2)).toBe(6); + expect(counter.value).toBe(6); + }); + it("clones counters", ({ expect }) => { + const counter = new Counter(3); + const clone = counter.clone(); + expect(counter.increment()).toBe(4); + expect(clone.value).toBe(3); + }); + it("calls methods with loopback rpc and pipelining", async ({ expect }) => { + const stub = new RpcStub(new Counter(1)); + // TODO(soon): replace with `using` when supported + onTestFinished(() => stub[Symbol.dispose]()); + const result = await stub.clone().increment(3); + expect(result).toBe(4); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/rpc/tsconfig.json b/fixtures/vitest-pool-workers-examples/rpc/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/rpc/vitest.config.ts b/fixtures/vitest-pool-workers-examples/rpc/vitest.config.ts new file mode 100644 index 0000000..515584f --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/vitest.config.ts @@ -0,0 +1,24 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + miniflare: { + // Required to use `exports.default.scheduled()`. This is an experimental + // compatibility flag, and cannot be enabled in production. + compatibilityFlags: [ + "service_binding_extra_handlers", + "nodejs_compat", + ], + }, + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/rpc/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/rpc/wrangler.jsonc new file mode 100644 index 0000000..ff8af95 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/rpc/wrangler.jsonc @@ -0,0 +1,45 @@ +{ + "name": "rpc", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "kv_namespaces": [ + { + "binding": "KV_NAMESPACE", + "id": "00000000000000000000000000000000", + }, + ], + "durable_objects": { + "bindings": [ + { + "name": "TEST_OBJECT", + "class_name": "TestObject", + }, + { + "name": "PROXIED_TEST_OBJECT", + "class_name": "ProxiedTestObject", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_classes": ["TestObject"], + }, + { + "tag": "v2", + "new_classes": ["ProxiedTestObject"], + }, + ], + "services": [ + { + "binding": "TEST_NAMED_HANDLER", + "service": "rpc", + "entrypoint": "testNamedHandler", + }, + { + "binding": "TEST_NAMED_ENTRYPOINT", + "service": "rpc", + "entrypoint": "TestNamedEntrypoint", + }, + ], +} diff --git a/fixtures/vitest-pool-workers-examples/tsc-all.mjs b/fixtures/vitest-pool-workers-examples/tsc-all.mjs new file mode 100644 index 0000000..c6bec73 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/tsc-all.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert"; +import childProcess from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import url from "node:url"; + +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function* walkTsConfigs(rootPath) { + for (const entry of await fs.readdir(rootPath, { withFileTypes: true })) { + if (entry.name === "node_modules") continue; + const filePath = path.join(rootPath, entry.name); + if (entry.isDirectory()) yield* walkTsConfigs(filePath); + else if (entry.name === "tsconfig.json") yield filePath; + } +} + +assert( + process.env.PATH?.includes(path.join(__dirname, "node_modules/.bin")), + "Expected `tsc-all.mjs` to be run with `pnpm check:type`" +); + +for await (const tsconfigPath of walkTsConfigs(__dirname)) { + console.log(`Checking ${path.relative(__dirname, tsconfigPath)}...`); + const result = childProcess.spawnSync("tsc", ["-p", tsconfigPath], { + stdio: "inherit", + cwd: __dirname, + shell: true, + }); + if (result.status !== 0) process.exitCode = 1; +} diff --git a/fixtures/vitest-pool-workers-examples/tsconfig.node.json b/fixtures/vitest-pool-workers-examples/tsconfig.node.json new file mode 100644 index 0000000..c071db3 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/fixtures/vitest-pool-workers-examples/tsconfig.workerd-test.json b/fixtures/vitest-pool-workers-examples/tsconfig.workerd-test.json new file mode 100644 index 0000000..3aa2158 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/tsconfig.workerd-test.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "types": [ + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers/types", + "@types/node", // For tests using Node.js compat APIs (process, Buffer, node:*) + "vite/client" // For `?raw`, `?url`, etc. import types + ] + } +} diff --git a/fixtures/vitest-pool-workers-examples/tsconfig.workerd.json b/fixtures/vitest-pool-workers-examples/tsconfig.workerd.json new file mode 100644 index 0000000..b827fd6 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/tsconfig.workerd.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "skipLibCheck": false, + "types": ["@cloudflare/workers-types/experimental"] + } +} diff --git a/fixtures/vitest-pool-workers-examples/vitest.config.ts b/fixtures/vitest-pool-workers-examples/vitest.config.ts new file mode 100644 index 0000000..b292e26 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/vitest.config.ts @@ -0,0 +1,22 @@ +// Root vitest config for the vitest-pool-workers-examples fixture. +// Per the Vitest 4 docs, only `globalSetup`, `reporters`, `coverage`, and +// other "global" options are inherited from this root config; project test +// options (testTimeout, retry, etc.) are NOT inherited. Each project under +// `*/vitest.*config.*ts` extends `vitest.shared.ts` directly via mergeConfig. +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + reporters: ["default"], + projects: [ + "*/vitest.*config.*ts", + // workerd's Windows SQLite VFS uses kj::Path::toString() (Unix-style + // paths) with the win32 VFS, causing SQLITE_CANTOPEN for disk-backed + // SQLite DOs. Exclude until workerd ships the fix (cloudflare/workerd#6110). + ...(process.platform === "win32" + ? ["!durable-objects/vitest.*config.*ts"] + : []), + ], + globalSetup: ["./vitest.global.ts"], + }, +}); diff --git a/fixtures/vitest-pool-workers-examples/vitest.global.ts b/fixtures/vitest-pool-workers-examples/vitest.global.ts new file mode 100644 index 0000000..bfabe36 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/vitest.global.ts @@ -0,0 +1,3 @@ +export function setup(): void { + process.env.TZ = "UTC"; +} diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/README.md b/fixtures/vitest-pool-workers-examples/web-assembly/README.md new file mode 100644 index 0000000..c101816 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/README.md @@ -0,0 +1,7 @@ +# ⚙️ web-assembly + +This Worker makes use of a WebAssembly module to add two numbers together. Wrangler's default module rules are enabled by `@cloudflare/vitest-pool-workers` if you have a `wrangler.configPath` configured. This means `.wasm` files can be imported as `WebAssembly.Module`s. + +| Test | Overview | +| ------------------------------- | -------------------------------- | +| [add.test.ts](test/add.test.ts) | Integration test using `exports` | diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/src/add.wasm b/fixtures/vitest-pool-workers-examples/web-assembly/src/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..fa370ae4187f2b0e9a65181bee6f50db6b3c6026 GIT binary patch literal 53 zcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9erX;9|*3%uQut H2C4=C$J7S% literal 0 HcmV?d00001 diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/src/env.d.ts b/fixtures/vitest-pool-workers-examples/web-assembly/src/env.d.ts new file mode 100644 index 0000000..5e3887c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/src/env.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./web-assembly/src/env.d.ts -c ./web-assembly/wrangler.jsonc --no-include-runtime` (hash: 0c8cd5ae12176ebab8337cbfef98b516) +interface __BaseEnv_Env {} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/src/index.ts b/fixtures/vitest-pool-workers-examples/web-assembly/src/index.ts new file mode 100644 index 0000000..1a5c49a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/src/index.ts @@ -0,0 +1,25 @@ +import addModule from "./add.wasm"; +// Verify that we can also load this with "?module". +// Generated Prisma clients can include this import style as an example. +import addModule2 from "./add.wasm?module"; + +const addInstance = new WebAssembly.Instance(addModule); +const add = addInstance.exports.add as (a: number, b: number) => number; + +const add2Instance = new WebAssembly.Instance(addModule2); +const add2 = add2Instance.exports.add as (a: number, b: number) => number; + +export default { + fetch(request, env, ctx) { + const url = new URL(request.url); + const a = parseInt(url.searchParams.get("a") ?? "0"); + const b = parseInt(url.searchParams.get("b") ?? "0"); + const result = add(a, b); + + if (result !== add2(a, b)) { + return new Response("add mismatch"); + } + + return new Response(result.toString()); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/web-assembly/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/src/wasm-shim.d.ts b/fixtures/vitest-pool-workers-examples/web-assembly/src/wasm-shim.d.ts new file mode 100644 index 0000000..ee84755 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/src/wasm-shim.d.ts @@ -0,0 +1,9 @@ +declare module "*.wasm" { + const value: WebAssembly.Module; + export default value; +} + +declare module "*.wasm?module" { + const value: WebAssembly.Module; + export default value; +} diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/test/add.test.ts b/fixtures/vitest-pool-workers-examples/web-assembly/test/add.test.ts new file mode 100644 index 0000000..e5dcd0c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/test/add.test.ts @@ -0,0 +1,7 @@ +import { exports } from "cloudflare:workers"; +import { it } from "vitest"; + +it("adds numbers together", async ({ expect }) => { + const response = await exports.default.fetch("https://example.com/?a=1&b=2"); + expect(await response.text()).toBe("3"); +}); diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts b/fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts new file mode 100644 index 0000000..36eb0f7 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/test/not-actually.wasm.test.ts @@ -0,0 +1,10 @@ +// Regression test for https://github.com/cloudflare/workers-sdk/issues/8280 +// A test file whose name contains ".wasm" (but whose actual extension is .ts) +// must NOT be treated as a WebAssembly module by the module rules. +import { it } from "vitest"; + +it("loads .wasm.test.ts files as JavaScript, not as WebAssembly", ({ + expect, +}) => { + expect(true).toBe(true); +}); diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/web-assembly/test/tsconfig.json new file mode 100644 index 0000000..ff294d1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts", "../src/wasm-shim.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/tsconfig.json b/fixtures/vitest-pool-workers-examples/web-assembly/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/vitest.config.ts b/fixtures/vitest-pool-workers-examples/web-assembly/vitest.config.ts new file mode 100644 index 0000000..f8b2e2b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/vitest.config.ts @@ -0,0 +1,20 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + // Specifying a `wrangler.configPath` will enable Wrangler's default + // module rules including support for `.wasm` files. Refer to + // https://developers.cloudflare.com/workers/wrangler/bundling/#files-which-will-not-be-bundled + // for more information. + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/web-assembly/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/web-assembly/wrangler.jsonc new file mode 100644 index 0000000..a604e80 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/web-assembly/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "web-assembly", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/env.d.ts b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/env.d.ts new file mode 100644 index 0000000..72d663b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/env.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./workers-assets-no-dir/src/env.d.ts -c ./workers-assets-no-dir/wrangler.jsonc --no-include-runtime` (hash: 1177c209bb457d419779798c493147d2) +interface __BaseEnv_Env { + ASSETS: Fetcher; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/index.ts b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/index.ts new file mode 100644 index 0000000..eefdc49 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/index.ts @@ -0,0 +1,5 @@ +export default { + async fetch(request): Promise { + return new Response("Hello from worker!"); + }, +} satisfies ExportedHandler; diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/assets-no-dir.test.ts b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/assets-no-dir.test.ts new file mode 100644 index 0000000..9e1ab2c --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/assets-no-dir.test.ts @@ -0,0 +1,19 @@ +import { exports } from "cloudflare:workers"; +import { describe, it } from "vitest"; + +// Regression test for https://github.com/cloudflare/workers-sdk/issues/9381 +// +// When a wrangler config has `assets: { binding: "ASSETS" }` but no `directory` +// (as is common when using @cloudflare/vite-plugin, which handles asset serving +// independently via its dev server), vitest-pool-workers must not throw: +// "The `assets` property in your configuration is missing the required `directory` property." + +describe("workers-assets-no-dir", () => { + it("worker starts and responds when assets binding has no directory configured", async ({ + expect, + }) => { + const response = await exports.default.fetch("http://example.com/"); + expect(response.status).toBe(200); + expect(await response.text()).toBe("Hello from worker!"); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/tsconfig.json b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/vitest.config.ts b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/vitest.config.ts new file mode 100644 index 0000000..d9a889a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/vitest.config.ts @@ -0,0 +1,14 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/wrangler.jsonc new file mode 100644 index 0000000..49d877d --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets-no-dir/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "name": "workers-assets-no-directory", + "main": "src/index.ts", + // Simulate a project using @cloudflare/vite-plugin where assets are configured + // with a binding but no directory (vite-plugin serves them via its dev server). + // vitest-pool-workers must not throw "missing required `directory` property" here. + "assets": { + "binding": "ASSETS", + }, +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/README.md b/fixtures/vitest-pool-workers-examples/workers-assets/README.md new file mode 100644 index 0000000..6b5ab16 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/README.md @@ -0,0 +1,5 @@ +# ✅ workers-static-assets-with-user-worker + +This Worker contains assets as well as a Worker script + +Neither integration nor unit tests should expose assets. However, you can mock them (demonstrated in this example), or write an integration test using [`unstable_startWorker()`](https://developers.cloudflare.com/workers/testing/unstable_startworker/). diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/public/binding.html b/fixtures/vitest-pool-workers-examples/workers-assets/public/binding.html new file mode 100644 index 0000000..7f4f36e --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/public/binding.html @@ -0,0 +1 @@ +

binding.html

diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/public/index.html b/fixtures/vitest-pool-workers-examples/workers-assets/public/index.html new file mode 100644 index 0000000..29df18b --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/public/index.html @@ -0,0 +1,11 @@ + + + + + + Hello, World! + + +

Asset index.html

+ + diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/src/env.d.ts b/fixtures/vitest-pool-workers-examples/workers-assets/src/env.d.ts new file mode 100644 index 0000000..e29b455 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/src/env.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./workers-assets/src/env.d.ts -c ./workers-assets/wrangler.jsonc --no-include-runtime` (hash: 1177c209bb457d419779798c493147d2) +interface __BaseEnv_Env { + ASSETS: Fetcher; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/src/index.ts b/fixtures/vitest-pool-workers-examples/workers-assets/src/index.ts new file mode 100644 index 0000000..62d1b4a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/src/index.ts @@ -0,0 +1,15 @@ +export default { + async fetch(request, env, ctx): Promise { + const url = new URL(request.url); + switch (url.pathname) { + case "/message": + return new Response("Hello, World!"); + case "/random": + return new Response(crypto.randomUUID()); + case "/binding": + return env.ASSETS.fetch(request); + default: + return new Response(null, { status: 404 }); + } + }, +} satisfies ExportedHandler; diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/workers-assets/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/test/assets.test.ts b/fixtures/vitest-pool-workers-examples/workers-assets/test/assets.test.ts new file mode 100644 index 0000000..4b8e508 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/test/assets.test.ts @@ -0,0 +1,34 @@ +import { env, exports, withEnv } from "cloudflare:workers"; +import { describe, it } from "vitest"; +import worker from "../src"; + +describe("Hello World user worker", () => { + it('responds with "Hello, World!" (integration style)', async ({ + expect, + }) => { + const response = await exports.default.fetch("http://example.com/message"); + expect(await response.text()).toMatchInlineSnapshot(`"Hello, World!"`); + }); + + it("returns 404 for unknown routes (assets are not routed through exports.default)", async ({ + expect, + }) => { + const response = await exports.default.fetch("http://example.com/"); + expect(response.status).toBe(404); + }); + + it("can mock the ASSETS binding using withEnv", async ({ expect }) => { + const mockAssets = { + fetch: async () => new Response("mocked asset response"), + }; + + await withEnv({ ...env, ASSETS: mockAssets }, async () => { + const response = await worker.fetch( + new Request("http://example.com/binding"), + env, + {} as ExecutionContext + ); + expect(await response.text()).toBe("mocked asset response"); + }); + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/workers-assets/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/tsconfig.json b/fixtures/vitest-pool-workers-examples/workers-assets/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/vitest.config.ts b/fixtures/vitest-pool-workers-examples/workers-assets/vitest.config.ts new file mode 100644 index 0000000..d9a889a --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/vitest.config.ts @@ -0,0 +1,14 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/workers-assets/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/workers-assets/wrangler.jsonc new file mode 100644 index 0000000..a7babfd --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workers-assets/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "name": "workers-static-assets-with-user-worker", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "assets": { + "directory": "./public", + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "none", + }, +} diff --git a/fixtures/vitest-pool-workers-examples/workflows/README.md b/fixtures/vitest-pool-workers-examples/workflows/README.md new file mode 100644 index 0000000..bf98b73 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/README.md @@ -0,0 +1,9 @@ +# 🔁 workflows + +This Worker includes a ModeratorWorkflow that serves as a template for an automated content moderation process. +The testing suite uses workflow mocking to validate the logic of each step. + +| Test | Overview | +| ----------------------------------------------- | ----------------------------------------------------------------------------------------- | +| [integration.test.ts](test/integration.test.ts) | Tests on the Worker's endpoints, ensuring that workflows are created and run correctly. | +| [unit.test.ts](test/unit.test.ts) | Tests on the internal logic of each workflow. It uses mocking to test steps in isolation. | diff --git a/fixtures/vitest-pool-workers-examples/workflows/src/env.d.ts b/fixtures/vitest-pool-workers-examples/workflows/src/env.d.ts new file mode 100644 index 0000000..a76ffa1 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/src/env.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types ./workflows/src/env.d.ts -c ./workflows/wrangler.jsonc --no-include-runtime` (hash: 84529125ee80eb4b92700c4efdce1e33) +interface __BaseEnv_Env { + MODERATOR: Workflow< + Parameters[0]["payload"] + >; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-examples/workflows/src/index.ts b/fixtures/vitest-pool-workers-examples/workflows/src/index.ts new file mode 100644 index 0000000..7f753b4 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/src/index.ts @@ -0,0 +1,89 @@ +import { + WorkflowEntrypoint, + WorkflowEvent, + WorkflowStep, +} from "cloudflare:workers"; + +export class ModeratorWorkflow extends WorkflowEntrypoint { + async run(_event: Readonly>, step: WorkflowStep) { + await step.sleep("sleep for a while", "10 seconds"); + + // Get an initial analysis from an AI model + const aiResult = await step.do("AI content scan", async () => { + // Call to an workers-ai to scan the text content and return a violation score + + // Simulated score: + const violationScore = Math.floor(Math.random() * 100); + + return { violationScore: violationScore }; + }); + + // Triage based on the AI score + if (aiResult.violationScore < 10) { + await step.do("auto approve content", async () => { + // API call to set app content status to "approved" + return { status: "auto_approved" }; + }); + return { status: "auto_approved" }; + } + if (aiResult.violationScore > 90) { + await step.do("auto reject content", async () => { + // API call to set app content status to "rejected" + return { status: "auto_rejected" }; + }); + return { status: "auto_rejected" }; + } + + // If the score is ambiguous, require human review + type EventPayload = { + moderatorAction: string; + }; + const eventPayload = await step.waitForEvent("human review", { + type: "moderation-decision", + timeout: "1 day", + }); + + if (eventPayload) { + // The moderator responded in time. + const decision = eventPayload.payload.moderatorAction; // e.g., "approve" or "reject" + await step.do("apply moderator decision", async () => { + // API call to update content status based on the decision + return { status: "moderated", decision: decision }; + }); + return { status: "moderated", decision: decision }; + } + } +} + +export default { + async fetch(request: Request, env: Env) { + const url = new URL(request.url); + const maybeId = url.searchParams.get("id"); + if (maybeId !== null) { + const instance = await env.MODERATOR.get(maybeId); + + return Response.json(await instance.status()); + } + + if (url.pathname === "/moderate") { + const workflow = await env.MODERATOR.create(); + return Response.json({ + id: workflow.id, + details: await workflow.status(), + }); + } + + if (url.pathname === "/moderate-batch") { + const workflows = await env.MODERATOR.createBatch([ + {}, + { id: "321" }, + {}, + ]); + + const ids = workflows.map((workflow) => workflow.id); + return Response.json({ ids: ids }); + } + + return new Response("Not found", { status: 404 }); + }, +}; diff --git a/fixtures/vitest-pool-workers-examples/workflows/src/tsconfig.json b/fixtures/vitest-pool-workers-examples/workflows/src/tsconfig.json new file mode 100644 index 0000000..0141323 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/src/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd.json", + "include": ["./**/*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workflows/test/integration.test.ts b/fixtures/vitest-pool-workers-examples/workflows/test/integration.test.ts new file mode 100644 index 0000000..3254819 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/test/integration.test.ts @@ -0,0 +1,174 @@ +import { introspectWorkflow } from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { describe, it } from "vitest"; + +const STATUS_COMPLETE = "complete"; +const STEP_NAME = "AI content scan"; +const mockResult = { violationScore: 0 }; + +// This example implicitly disposes the Workflow instance +it("workflow should be able to reach the end and be successful", async ({ + expect, +}) => { + // CONFIG with `await using` to ensure Workflow instances cleanup: + await using introspector = await introspectWorkflow(env.MODERATOR); + await introspector.modifyAll(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + }); + + await exports.default.fetch(`https://mock-worker.local/moderate`); + + const instances = await introspector.get(); + expect(instances.length).toBe(1); + + // ASSERTIONS: + const instance = instances[0]; + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + await expect(instance.waitForStatus(STATUS_COMPLETE)).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ status: "auto_approved" }); + + // DISPOSE: ensured by `await using` +}); + +// This example explicitly disposes the Workflow instances +it("workflow batch should be able to reach the end and be successful", async ({ + expect, +}) => { + // CONFIG: + let introspector = await introspectWorkflow(env.MODERATOR); + try { + await introspector.modifyAll(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + }); + + await exports.default.fetch(`https://mock-worker.local/moderate-batch`); + + const instances = await introspector.get(); + expect(instances.length).toBe(3); + + // ASSERTIONS: + for (const instance of instances) { + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + await expect( + instance.waitForStatus(STATUS_COMPLETE) + ).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ status: "auto_approved" }); + } + } finally { + // DISPOSE: + // Workflow introspector should be disposed the end of each test, if no `await using` dyntax is used + // Also disposes all intercepted instances + await introspector.dispose(); + } +}); + +describe("workflow instance lifecycle methods", () => { + it("should terminate a workflow instance", async ({ expect }) => { + // CONFIG: + await using introspector = await introspectWorkflow(env.MODERATOR); + await introspector.modifyAll(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, { violationScore: 50 }); + }); + + const res = await exports.default.fetch( + "https://mock-worker.local/moderate" + ); + const data = (await res.json()) as { id: string; details: unknown }; + + const instances = await introspector.get(); + expect(instances.length).toBe(1); + const instance = instances[0]; + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual({ + violationScore: 50, + }); + + const handle = await env.MODERATOR.get(data.id); + await handle.terminate(); + + // ASSERTIONS: + await expect(instance.waitForStatus("terminated")).resolves.not.toThrow(); + + // DISPOSE: ensured by `await using` + }); + + it("should restart a workflow instance", async ({ expect }) => { + // CONFIG: + await using introspector = await introspectWorkflow(env.MODERATOR); + await introspector.modifyAll(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, { violationScore: 50 }); + await m.mockEvent({ + type: "moderation-decision", + payload: { moderatorAction: "approve" }, + }); + }); + + const res = await exports.default.fetch( + "https://mock-worker.local/moderate" + ); + const data = (await res.json()) as { id: string; details: unknown }; + + const instances = await introspector.get(); + expect(instances.length).toBe(1); + const instance = instances[0]; + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual({ + violationScore: 50, + }); + + const handle = await env.MODERATOR.get(data.id); + await handle.restart(); + + // Mocks survive instace restart, so the restarted workflow re-runs + // with the same config + await expect( + instance.waitForStatus(STATUS_COMPLETE) + ).resolves.not.toThrow(); + + // DISPOSE: ensured by `await using` + }); + + // TODO(vaish): add restart-from-step test here once @cloudflare/workers-types ships restart options + + it("should pause a workflow instance", async ({ expect }) => { + // CONFIG: + await using introspector = await introspectWorkflow(env.MODERATOR); + await introspector.modifyAll(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, { violationScore: 50 }); + }); + + const res = await exports.default.fetch( + "https://mock-worker.local/moderate" + ); + const data = (await res.json()) as { id: string; details: unknown }; + + const instances = await introspector.get(); + expect(instances.length).toBe(1); + const instance = instances[0]; + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual({ + violationScore: 50, + }); + + const handle = await env.MODERATOR.get(data.id); + await handle.pause(); + + // ASSERTIONS: + await expect(instance.waitForStatus("paused")).resolves.not.toThrow(); + + // DISPOSE: ensured by `await using` + }); +}); diff --git a/fixtures/vitest-pool-workers-examples/workflows/test/tsconfig.json b/fixtures/vitest-pool-workers-examples/workflows/test/tsconfig.json new file mode 100644 index 0000000..49d6632 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.workerd-test.json", + "include": ["./**/*.ts", "../src/env.d.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workflows/test/unit.test.ts b/fixtures/vitest-pool-workers-examples/workflows/test/unit.test.ts new file mode 100644 index 0000000..58f33bc --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/test/unit.test.ts @@ -0,0 +1,217 @@ +import { introspectWorkflowInstance } from "cloudflare:test"; +import { env } from "cloudflare:workers"; +import { it } from "vitest"; + +const STATUS_COMPLETE = "complete"; +const STATUS_ERROR = "errored"; +const STEP_NAME = "AI content scan"; + +// This example implicitly disposes the Workflow instance +it("should mock a non-violation score and complete", async ({ expect }) => { + const instanceId = crypto.randomUUID(); + const mockResult = { violationScore: 0 }; + + // CONFIG with `await using` to ensure Workflow instances cleanup: + await using instance = await introspectWorkflowInstance( + env.MODERATOR, + instanceId + ); + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + }); + + await env.MODERATOR.create({ + id: instanceId, + }); + + // ASSERTIONS: + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + expect( + await instance.waitForStepResult({ name: "auto approve content" }) + ).toEqual({ status: "auto_approved" }); + + await expect(instance.waitForStatus(STATUS_COMPLETE)).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ status: "auto_approved" }); + + // DISPOSE: ensured by `await using` +}); + +// This example explicitly disposes the Workflow instance +it("should mock the violation score calculation to fail 2 times and then complete", async ({ + expect, +}) => { + const instanceId = crypto.randomUUID(); + const mockResult = { violationScore: 0 }; + + // CONFIG: + const instance = await introspectWorkflowInstance(env.MODERATOR, instanceId); + + try { + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.mockStepError( + { name: STEP_NAME }, + new Error("Something went wrong!"), + 2 + ); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + }); + + await env.MODERATOR.create({ + id: instanceId, + }); + + // ASSERTIONS: + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + expect( + await instance.waitForStepResult({ name: "auto approve content" }) + ).toEqual({ status: "auto_approved" }); + + await expect( + instance.waitForStatus(STATUS_COMPLETE) + ).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ status: "auto_approved" }); + } finally { + // DISPOSE: + // Workflow introspector should be disposed the end of each test, if no `await using` dyntax is used + // Also disposes all intercepted instances + await instance.dispose(); + } +}); + +it("should mock a violation score and complete", async ({ expect }) => { + const instanceId = crypto.randomUUID(); + const mockResult = { violationScore: 99 }; + + await using instance = await introspectWorkflowInstance( + env.MODERATOR, + instanceId + ); + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + }); + + await env.MODERATOR.create({ + id: instanceId, + }); + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + expect( + await instance.waitForStepResult({ name: "auto reject content" }) + ).toEqual({ status: "auto_rejected" }); + + await expect(instance.waitForStatus(STATUS_COMPLETE)).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ status: "auto_rejected" }); +}); + +it("should be reviewed, accepted and complete", async ({ expect }) => { + const instanceId = crypto.randomUUID(); + const mockResult = { violationScore: 50 }; + + await using instance = await introspectWorkflowInstance( + env.MODERATOR, + instanceId + ); + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + await m.mockEvent({ + type: "moderation-decision", + payload: { moderatorAction: "approve" }, + }); + }); + + await env.MODERATOR.create({ + id: instanceId, + }); + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + expect( + await instance.waitForStepResult({ name: "apply moderator decision" }) + ).toEqual({ status: "moderated", decision: "approve" }); + + await expect(instance.waitForStatus(STATUS_COMPLETE)).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ decision: "approve", status: "moderated" }); +}); + +it("should disable retry delays when mocking step errors", async ({ + expect, +}) => { + const instanceId = crypto.randomUUID(); + const mockResult = { violationScore: 0 }; + + await using instance = await introspectWorkflowInstance( + env.MODERATOR, + instanceId + ); + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.disableRetryDelays(); + await m.mockStepError( + { name: STEP_NAME }, + new Error("Transient failure"), + 2 + ); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + }); + + await env.MODERATOR.create({ + id: instanceId, + }); + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + + await expect(instance.waitForStatus(STATUS_COMPLETE)).resolves.not.toThrow(); + + const output = await instance.getOutput(); + expect(output).toEqual({ status: "auto_approved" }); +}); + +it("should force human review to timeout and error", async ({ expect }) => { + const instanceId = crypto.randomUUID(); + const mockResult = { violationScore: 50 }; + + await using instance = await introspectWorkflowInstance( + env.MODERATOR, + instanceId + ); + await instance.modify(async (m) => { + await m.disableSleeps(); + await m.mockStepResult({ name: STEP_NAME }, mockResult); + await m.forceEventTimeout({ name: "human review" }); + }); + + await env.MODERATOR.create({ + id: instanceId, + }); + + expect(await instance.waitForStepResult({ name: STEP_NAME })).toEqual( + mockResult + ); + + await expect(instance.waitForStatus(STATUS_ERROR)).resolves.not.toThrow(); + + const error = await instance.getError(); + expect(error.name).toEqual("Error"); + expect(error.message).toContain("Execution timed out"); +}); diff --git a/fixtures/vitest-pool-workers-examples/workflows/tsconfig.json b/fixtures/vitest-pool-workers-examples/workflows/tsconfig.json new file mode 100644 index 0000000..90e58bf --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.node.json", + "include": ["./*.ts"] +} diff --git a/fixtures/vitest-pool-workers-examples/workflows/vitest.config.ts b/fixtures/vitest-pool-workers-examples/workflows/vitest.config.ts new file mode 100644 index 0000000..4217062 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/vitest.config.ts @@ -0,0 +1,16 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + }) +); diff --git a/fixtures/vitest-pool-workers-examples/workflows/wrangler.jsonc b/fixtures/vitest-pool-workers-examples/workflows/wrangler.jsonc new file mode 100644 index 0000000..df0fe05 --- /dev/null +++ b/fixtures/vitest-pool-workers-examples/workflows/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "workflows", + "main": "src/index.ts", + // don't provide compatibility_date so that vitest will infer the latest one + "workflows": [ + { + "binding": "MODERATOR", + "class_name": "ModeratorWorkflow", + "name": "moderator-workflow", + }, + ], +} diff --git a/fixtures/vitest-pool-workers-remote-bindings/env.d.ts b/fixtures/vitest-pool-workers-remote-bindings/env.d.ts new file mode 100644 index 0000000..c9d4878 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/env.d.ts @@ -0,0 +1,4 @@ +declare module "cloudflare:workers" { + // ProvidedEnv controls the type of `import("cloudflare:workers").env` + interface ProvidedEnv extends Env {} +} diff --git a/fixtures/vitest-pool-workers-remote-bindings/package.json b/fixtures/vitest-pool-workers-remote-bindings/package.json new file mode 100644 index 0000000..72ac0b9 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixture/vitest-pool-workers-remote-bindings", + "private": true, + "type": "module", + "scripts": { + "cf-typegen": "wrangler types --no-include-runtime", + "test": "vitest run --config vitest.workers.config.ts", + "test:ci": "node run-tests.mjs", + "test:vitest": "vitest run" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "workspace:*", + "typescript": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/vitest-pool-workers-remote-bindings/remote-worker.js b/fixtures/vitest-pool-workers-remote-bindings/remote-worker.js new file mode 100644 index 0000000..3e6f023 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/remote-worker.js @@ -0,0 +1,7 @@ +export default { + fetch() { + return new Response( + "Hello from a remote Worker part of the vitest-pool-workers remote bindings fixture!" + ); + }, +}; diff --git a/fixtures/vitest-pool-workers-remote-bindings/remote-worker.staging.js b/fixtures/vitest-pool-workers-remote-bindings/remote-worker.staging.js new file mode 100644 index 0000000..2857097 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/remote-worker.staging.js @@ -0,0 +1,7 @@ +export default { + fetch() { + return new Response( + "Hello from a remote Worker, defined for the staging environment, part of the vitest-pool-workers remote bindings fixture!" + ); + }, +}; diff --git a/fixtures/vitest-pool-workers-remote-bindings/run-tests.mjs b/fixtures/vitest-pool-workers-remote-bindings/run-tests.mjs new file mode 100644 index 0000000..72c90aa --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/run-tests.mjs @@ -0,0 +1,200 @@ +/** + * This fixture is particular since it needs to communicate with remote resources, namely + * two remote workers. + * + * This script is used to deploy the remote workers and run the fixture using said workers + * with the appropriate vitest configurations. + */ +import assert from "node:assert"; +import { execSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { cpSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { setTimeout } from "node:timers/promises"; + +const env = getAuthenticatedEnv(); +if (!env) { + console.warn("No credentials provided, skipping test..."); + process.exit(0); +} + +rmSync("./.tmp", { recursive: true, force: true }); + +cpSync("./src", "./.tmp/src", { recursive: true }); +cpSync("./test", "./.tmp/test", { recursive: true }); +cpSync("./vitest.workers.config.ts", "./.tmp/vitest.workers.config.ts"); +cpSync( + "./vitest.workers.config.staging.ts", + "./.tmp/vitest.workers.config.staging.ts" +); + +const remoteWorkerName = `tmp-e2e-worker-test-remote-bindings-${ + randomUUID().split("-")[0] +}`; +const remoteStagingWorkerName = `tmp-e2e-staging-worker-test-remote-bindings-${ + randomUUID().split("-")[0] +}`; + +const wranglerJson = JSON.parse(readFileSync("./wrangler.json", "utf8")); +wranglerJson.services[0].service = remoteWorkerName; +wranglerJson.env.staging.services[0].service = remoteStagingWorkerName; + +writeFileSync( + "./.tmp/wrangler.json", + JSON.stringify(wranglerJson, undefined, 2), + "utf8" +); + +writeFileSync( + "./.tmp/remote-wrangler.json", + JSON.stringify( + { + name: remoteWorkerName, + main: "../remote-worker.js", + compatibility_date: "2025-06-01", + }, + undefined, + 2 + ), + "utf8" +); + +const deployOut = execSync( + "pnpm wrangler deploy -c .tmp/remote-wrangler.json", + { + stdio: "pipe", + cwd: "./.tmp", + env, + encoding: "utf8", + } +); + +if (!new RegExp(`Deployed\\s+${remoteWorkerName}\\b`).test(`${deployOut}`)) { + throw new Error(`Failed to deploy ${remoteWorkerName}`); +} + +const urlMatcher = new RegExp( + `(?https:\\/\\/${remoteWorkerName}\\..+?\\.workers\\.dev)` +); + +const deployedUrl = deployOut.match(urlMatcher)?.groups?.url; +assert(deployedUrl, "Failed to find deployed worker URL"); + +writeFileSync( + "./.tmp/remote-wrangler.staging.json", + JSON.stringify( + { + name: remoteStagingWorkerName, + main: "../remote-worker.staging.js", + compatibility_date: "2025-06-01", + }, + undefined, + 2 + ), + "utf8" +); + +const deployStagingOut = execSync( + "pnpm wrangler deploy -c .tmp/remote-wrangler.staging.json", + { + stdio: "pipe", + cwd: "./.tmp", + env, + encoding: "utf8", + } +); +if ( + !new RegExp(`Deployed\\s+${remoteStagingWorkerName}\\b`).test( + `${deployStagingOut}` + ) +) { + throw new Error(`Failed to deploy ${remoteStagingWorkerName}`); +} +const deployedStagingUrl = deployStagingOut.match( + /(?https:\/\/tmp-e2e-.+?\..+?\.workers\.dev)/ +)?.groups?.url; +assert(deployedStagingUrl, "Failed to find deployed staging worker URL"); + +await setTimeout(2000); + +// Wait for the workers to become available +await Promise.all([ + waitFor(async () => { + const response = await fetch(deployedUrl); + assert.equal(response.status, 200); + }), + waitFor(async () => { + const response = await fetch(deployedStagingUrl); + assert.equal(response.status, 200); + }), +]); + +try { + try { + try { + console.log("Running vitest-pool-workers remote bindings tests..."); + execSync("pnpm test:vitest --config ./.tmp/vitest.workers.config.ts", { + env, + stdio: "inherit", + }); + console.log( + "Running vitest-pool-workers remote bindings staging tests..." + ); + execSync( + "pnpm test:vitest --config ./.tmp/vitest.workers.config.staging.ts", + { + env, + stdio: "inherit", + } + ); + } finally { + execSync(`pnpm wrangler delete --name ${remoteWorkerName}`, { env }); + } + } finally { + execSync(`pnpm wrangler delete --name ${remoteStagingWorkerName}`, { env }); + } +} finally { + rmSync("./.tmp", { recursive: true, force: true }); +} + +/** + * Gets an env object containing Cloudflare credentials or undefined if not authenticated. + * + * In the Github actions we convert the TEST_CLOUDFLARE_ACCOUNT_ID and TEST_CLOUDFLARE_API_TOKEN env variables. + * In local development we can rely on CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN env variables directly. + */ +function getAuthenticatedEnv() { + const CLOUDFLARE_ACCOUNT_ID = + process.env.TEST_CLOUDFLARE_ACCOUNT_ID || process.env.CLOUDFLARE_ACCOUNT_ID; + const CLOUDFLARE_API_TOKEN = + process.env.TEST_CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_API_TOKEN; + + if (CLOUDFLARE_ACCOUNT_ID && CLOUDFLARE_API_TOKEN) { + return { + ...process.env, + CLOUDFLARE_API_TOKEN, + CLOUDFLARE_ACCOUNT_ID, + }; + } + console.warn( + "Skipping vitest-pool-workers remote bindings tests because the environment is not authenticated with Cloudflare." + ); +} + +/** + * Wait for the callback to execute successfully. + * + * If the callback throws an error or returns a rejected promise it will continue to wait until it succeeds or times out. + */ +export async function waitFor(callback) { + const start = Date.now(); + while (true) { + try { + return callback(); + } catch (error) { + if (Date.now() < start + 15_000) { + throw error; + } + } + await setTimeout(1000); + } +} diff --git a/fixtures/vitest-pool-workers-remote-bindings/src/index.ts b/fixtures/vitest-pool-workers-remote-bindings/src/index.ts new file mode 100644 index 0000000..2316494 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/src/index.ts @@ -0,0 +1,7 @@ +export default { + async fetch(request, env) { + const remoteWorkerResp = await env.MY_WORKER.fetch(request); + const remoteWorkerRespText = await remoteWorkerResp.text(); + return new Response(`Response from remote worker: ${remoteWorkerRespText}`); + }, +} satisfies ExportedHandler; diff --git a/fixtures/vitest-pool-workers-remote-bindings/test-staging/index.spec.ts b/fixtures/vitest-pool-workers-remote-bindings/test-staging/index.spec.ts new file mode 100644 index 0000000..5af2c12 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/test-staging/index.spec.ts @@ -0,0 +1,30 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { describe, test } from "vitest"; + +describe("Vitest pool workers remote bindings with a staging environment", () => { + test( + "fetching unit-style from a remote service binding", + { timeout: 50_000 }, + async ({ expect }) => { + const response = await env.MY_WORKER.fetch("http://example.com"); + const ctx = createExecutionContext(); + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot( + `"Hello from a remote Worker, defined for the staging environment, part of the vitest-pool-workers remote bindings fixture!"` + ); + } + ); + + test("fetching integration-style from the local worker (which uses remote bindings)", async ({ + expect, + }) => { + const response = await exports.default.fetch("https://example.com"); + expect(await response.text()).toMatchInlineSnapshot( + `"Response from remote worker: Hello from a remote Worker, defined for the staging environment, part of the vitest-pool-workers remote bindings fixture!"` + ); + }); +}); diff --git a/fixtures/vitest-pool-workers-remote-bindings/test/index.spec.ts b/fixtures/vitest-pool-workers-remote-bindings/test/index.spec.ts new file mode 100644 index 0000000..7e3206d --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/test/index.spec.ts @@ -0,0 +1,30 @@ +import { + createExecutionContext, + waitOnExecutionContext, +} from "cloudflare:test"; +import { env, exports } from "cloudflare:workers"; +import { describe, test } from "vitest"; + +describe("Vitest pool workers remote bindings", () => { + test( + "fetching unit-style from a remote service binding", + { timeout: 50_000 }, + async ({ expect }) => { + const response = await env.MY_WORKER.fetch("http://example.com"); + const ctx = createExecutionContext(); + await waitOnExecutionContext(ctx); + expect(await response.text()).toMatchInlineSnapshot( + `"Hello from a remote Worker part of the vitest-pool-workers remote bindings fixture!"` + ); + } + ); + + test("fetching integration-style from the local worker (which uses remote bindings)", async ({ + expect, + }) => { + const response = await exports.default.fetch("https://example.com"); + expect(await response.text()).toMatchInlineSnapshot( + `"Response from remote worker: Hello from a remote Worker part of the vitest-pool-workers remote bindings fixture!"` + ); + }); +}); diff --git a/fixtures/vitest-pool-workers-remote-bindings/tsconfig.json b/fixtures/vitest-pool-workers-remote-bindings/tsconfig.json new file mode 100644 index 0000000..67d8573 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": [ + "./worker-configuration.d.ts", + "@cloudflare/vitest-pool-workers/types" + ], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src", "env.d.ts", "test", "test-staging"] +} diff --git a/fixtures/vitest-pool-workers-remote-bindings/turbo.json b/fixtures/vitest-pool-workers-remote-bindings/turbo.json new file mode 100644 index 0000000..789f1de --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/turbo.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "test:ci": { + "env": [ + "VITEST", + "NODE_DEBUG", + "MINIFLARE_WORKERD_PATH", + "WRANGLER", + "WRANGLER_IMPORT", + "MINIFLARE_IMPORT", + "TEST_CLOUDFLARE_ACCOUNT_ID", + "TEST_CLOUDFLARE_API_TOKEN", + "WRANGLER_E2E_TEST_FILE" + ] + } + } +} diff --git a/fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.staging.ts b/fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.staging.ts new file mode 100644 index 0000000..8a8c9e6 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.staging.ts @@ -0,0 +1,22 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.json", environment: "staging" }, + }), + ], + test: { + reporters: ["default"], + include: ["test-staging/**/*.spec.ts"], + server: { + deps: { + external: [ + /packages\/vitest-pool-workers\/dist/, + /packages\/wrangler\//, + ], + }, + }, + }, +}); diff --git a/fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.ts b/fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.ts new file mode 100644 index 0000000..38e9fcd --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/vitest.workers.config.ts @@ -0,0 +1,22 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.json" }, + }), + ], + test: { + reporters: ["default"], + include: ["test/**/*.spec.ts"], + server: { + deps: { + external: [ + /packages\/vitest-pool-workers\/dist/, + /packages\/wrangler\//, + ], + }, + }, + }, +}); diff --git a/fixtures/vitest-pool-workers-remote-bindings/worker-configuration.d.ts b/fixtures/vitest-pool-workers-remote-bindings/worker-configuration.d.ts new file mode 100644 index 0000000..a374ca3 --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/worker-configuration.d.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --no-include-runtime` (hash: ad45d03e3bc69c49113dbe042afdb1d6) +interface __BaseEnv_Env { + MY_WORKER: + | Fetcher /* my-staging-worker-test */ + | Fetcher /* my-worker-test */; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + interface StagingEnv { + MY_WORKER: Fetcher /* my-staging-worker-test */; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/fixtures/vitest-pool-workers-remote-bindings/wrangler.json b/fixtures/vitest-pool-workers-remote-bindings/wrangler.json new file mode 100644 index 0000000..f21535c --- /dev/null +++ b/fixtures/vitest-pool-workers-remote-bindings/wrangler.json @@ -0,0 +1,23 @@ +{ + "name": "vitest-remote-bindings-fixture-test", + "main": "src/index.ts", + "compatibility_date": "2025-06-01", + "services": [ + { + "binding": "MY_WORKER", + "service": "my-worker-test", + "remote": true + } + ], + "env": { + "staging": { + "services": [ + { + "binding": "MY_WORKER", + "service": "my-staging-worker-test", + "remote": true + } + ] + } + } +} diff --git a/fixtures/wasm-app/README.md b/fixtures/wasm-app/README.md new file mode 100644 index 0000000..72907e3 --- /dev/null +++ b/fixtures/wasm-app/README.md @@ -0,0 +1,17 @@ +## wasm-app + +There are 3 workers in this package. They were created with a [`workers-rs`](https://github.com/cloudflare/workers-rs) project, running a build, removing unneeded artifacts, and copying the output folders. + +The wasm file generated, `index_bg.wasm` is copied into `./worker`, and shared by the 3 workers. + +- `./worker/module` contains a "modules" format worker and imports the wasm module as a regular ES module. +- `./worker/service-worker` contains a "service-worker" format worker and uses `wrangler.toml` to bind the wasm module as a global `MYWASM`. +- `./worker/service-worker-module` contains a "service-worker" format worker and imports the wasm module as a regular ES module. + +They're otherwise identical. + +You can run the module worker with `npx wrangler dev worker/module/index.js` (or from the `wrangler` package directory with `npm start -- dev ../../examples/wasm-app/worker/module/index.js`). + +You can run the service-worker worker with `npx wrangler dev worker/service-worker/index.js --config worker/service-worker/wrangler.jsonc` (or from the `wrangler` package directory with `npm start -- dev ../../examples/wasm-app/worker/service-worker/index.js --config ../../examples/wasm-app/worker/service-worker/wrangler.jsonc`). + +You can run the service-worker-module worker with `npx wrangler dev worker/service-worker-module/index.js` (or from the `wrangler` package directory with `npm start -- dev ../../examples/wasm-app/worker/service-worker-module/index.js`). diff --git a/fixtures/wasm-app/package.json b/fixtures/wasm-app/package.json new file mode 100644 index 0000000..568fdab --- /dev/null +++ b/fixtures/wasm-app/package.json @@ -0,0 +1,4 @@ +{ + "name": "@fixture/wasm-basic", + "private": true +} diff --git a/fixtures/wasm-app/worker/index_bg.wasm b/fixtures/wasm-app/worker/index_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9ff1b9d24a4ca952c6b54d0700a50b49f4fbec45 GIT binary patch literal 563460 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

Uq#u8lg3uSC}&}_#t$T&ifYnA_2?WC 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 Ryf};7t)K;e*IS9={{vCq)S& 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, (len = offset + arg.length * 3)); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if ( + cachegetInt32Memory0 === null || + cachegetInt32Memory0.buffer !== wasm.memory.buffer + ) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +const lTextDecoder = + typeof TextDecoder === "undefined" + ? (0, module.require)("util").TextDecoder + : TextDecoder; + +let cachedTextDecoder = new lTextDecoder("utf-8", { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == "number" || type == "boolean" || val == null) { + return `${val}`; + } + if (type == "string") { + return `"${val}"`; + } + if (type == "symbol") { + const description = val.description; + if (description == null) { + return "Symbol"; + } else { + return `Symbol(${description})`; + } + } + if (type == "function") { + const name = val.name; + if (typeof name == "string" && name.length > 0) { + return `Function(${name})`; + } else { + return "Function"; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = "["; + if (length > 0) { + debug += debugString(val[0]); + } + for (let i = 1; i < length; i++) { + debug += ", " + debugString(val[i]); + } + debug += "]"; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == "Object") { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return "Object(" + JSON.stringify(val) + ")"; + } catch (_) { + return "Object"; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + } else { + state.a = a; + } + } + }; + real.original = state; + + return real; +} +function __wbg_adapter_22(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h29cf9a4fd4f08c73( + arg0, + arg1, + addHeapObject(arg2) + ); +} + +/** + * @param {any} req + * @param {any} env + * @param {any} ctx + * @returns {Promise} + */ +export function fetch(req, env, ctx) { + var ret = wasm.fetch( + addHeapObject(req), + addHeapObject(env), + addHeapObject(ctx) + ); + return takeObject(ret); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +function __wbg_adapter_86(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h70365e5614c937ba( + arg0, + arg1, + addHeapObject(arg2), + addHeapObject(arg3) + ); +} + +/** + * Configuration options for Cloudflare's image optimization feature: + * + */ +export const PolishConfig = Object.freeze({ + Off: 0, + 0: "Off", + Lossy: 1, + 1: "Lossy", + Lossless: 2, + 2: "Lossless", +}); +/** + */ +export const RequestRedirect = Object.freeze({ + Error: 0, + 0: "Error", + Follow: 1, + 1: "Follow", + Manual: 2, + 2: "Manual", +}); +/** + * Configuration options for Cloudflare's minification features: + * + */ +export class MinifyConfig { + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_minifyconfig_free(ptr); + } + /** + */ + get js() { + var ret = wasm.__wbg_get_minifyconfig_js(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set js(arg0) { + wasm.__wbg_set_minifyconfig_js(this.ptr, arg0); + } + /** + */ + get html() { + var ret = wasm.__wbg_get_minifyconfig_html(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set html(arg0) { + wasm.__wbg_set_minifyconfig_html(this.ptr, arg0); + } + /** + */ + get css() { + var ret = wasm.__wbg_get_minifyconfig_css(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set css(arg0) { + wasm.__wbg_set_minifyconfig_css(this.ptr, arg0); + } +} + +export function __wbindgen_object_drop_ref(arg0) { + takeObject(arg0); +} + +export function __wbindgen_string_get(arg0, arg1) { + const obj = getObject(arg1); + var ret = typeof obj === "string" ? obj : undefined; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_new_59cb74e423758ede() { + var ret = new Error(); + return addHeapObject(ret); +} + +export function __wbg_stack_558ba5917b466edd(arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_error_4bb6c2a97407129a(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } +} + +export function __wbindgen_string_new(arg0, arg1) { + var ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); +} + +export function __wbindgen_is_undefined(arg0) { + var ret = getObject(arg0) === undefined; + return ret; +} + +export function __wbindgen_number_new(arg0) { + var ret = arg0; + return addHeapObject(ret); +} + +export function __wbindgen_object_clone_ref(arg0) { + var ret = getObject(arg0); + return addHeapObject(ret); +} + +export function __wbindgen_cb_drop(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + var ret = false; + return ret; +} + +export function __wbg_body_b67afdc865ca6d95(arg0) { + var ret = getObject(arg0).body; + return isLikeNone(ret) ? 0 : addHeapObject(ret); +} + +export function __wbg_newwithoptu8arrayandinit_2358601704784951() { + return handleError(function (arg0, arg1) { + var ret = new Response(takeObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_newwithoptstrandinit_cd8a4402e68873df() { + return handleError(function (arg0, arg1, arg2) { + var ret = new Response( + arg0 === 0 ? undefined : getStringFromWasm0(arg0, arg1), + getObject(arg2) + ); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_newwithoptstreamandinit_2cdcade777fddab8() { + return handleError(function (arg0, arg1) { + var ret = new Response(takeObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_latitude_6d0dc7510853aaea(arg0, arg1) { + var ret = getObject(arg1).latitude; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_longitude_b566ab6d05581b27(arg0, arg1) { + var ret = getObject(arg1).longitude; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_region_5b42be38a5fb9fee(arg0, arg1) { + var ret = getObject(arg1).region; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_method_8ec82ee079ce2702(arg0, arg1) { + var ret = getObject(arg1).method; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_url_9b689d511a7995b5(arg0, arg1) { + var ret = getObject(arg1).url; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_headers_62f682054d74e541(arg0) { + var ret = getObject(arg0).headers; + return addHeapObject(ret); +} + +export function __wbg_formData_c5b7ee7b1f027402() { + return handleError(function (arg0) { + var ret = getObject(arg0).formData(); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_cf_619e9c1d3e10de88(arg0) { + var ret = getObject(arg0).cf; + return addHeapObject(ret); +} + +export function __wbg_new_9e449026aa04d852() { + return handleError(function () { + var ret = new Headers(); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_set_ecc7ab7b550ca8b7() { + return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).set( + getStringFromWasm0(arg1, arg2), + getStringFromWasm0(arg3, arg4) + ); + }, arguments); +} + +export function __wbg_log_9c5d40d7de6fd4f7(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); +} + +export function __wbg_instanceof_File_05917b01e27498d9(arg0) { + var ret = getObject(arg0) instanceof File; + return ret; +} + +export function __wbg_get_4139ec5751043532(arg0, arg1, arg2) { + var ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); +} + +export function __wbg_get_4d0f21c2f823742e() { + return handleError(function (arg0, arg1) { + var ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_new_0b83d3df67ecb33e() { + var ret = new Object(); + return addHeapObject(ret); +} + +export function __wbg_instanceof_Error_561efcb1265706d8(arg0) { + var ret = getObject(arg0) instanceof Error; + return ret; +} + +export function __wbg_toString_0ef1ea57b966aed4(arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); +} + +export function __wbg_call_346669c262382ad7() { + return handleError(function (arg0, arg1, arg2) { + var ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_name_9a3ff1e21a0e3304(arg0) { + var ret = getObject(arg0).name; + return addHeapObject(ret); +} + +export function __wbg_new0_fd3a3a290b25cdac() { + var ret = new Date(); + return addHeapObject(ret); +} + +export function __wbg_toString_646e437de608a0a1(arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); +} + +export function __wbg_constructor_9fe544cc0957fdd0(arg0) { + var ret = getObject(arg0).constructor; + return addHeapObject(ret); +} + +export function __wbg_new_b1d61b5687f5e73a(arg0, arg1) { + try { + var state0 = { a: arg0, b: arg1 }; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_86(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + var ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } +} + +export function __wbg_resolve_d23068002f584f22(arg0) { + var ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); +} + +export function __wbg_then_2fcac196782070cc(arg0, arg1) { + var ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); +} + +export function __wbg_then_8c2d62e8ae5978f7(arg0, arg1, arg2) { + var ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); +} + +export function __wbg_buffer_397eaa4d72ee94dd(arg0) { + var ret = getObject(arg0).buffer; + return addHeapObject(ret); +} + +export function __wbg_newwithbyteoffsetandlength_4b9b8c4e3f5adbff( + arg0, + arg1, + arg2 +) { + var ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); +} + +export function __wbg_set_969ad0a60e51d320(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); +} + +export function __wbg_length_1eb8fc608a0d4cdb(arg0) { + var ret = getObject(arg0).length; + return ret; +} + +export function __wbg_newwithlength_929232475839a482(arg0) { + var ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); +} + +export function __wbg_set_82a4e8a85e31ac42() { + return handleError(function (arg0, arg1, arg2) { + var ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments); +} + +export function __wbindgen_debug_string(arg0, arg1) { + var ret = debugString(getObject(arg1)); + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbindgen_throw(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +} + +export function __wbindgen_memory() { + var ret = wasm.memory; + return addHeapObject(ret); +} + +export function __wbindgen_closure_wrapper515(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 100, __wbg_adapter_22); + return addHeapObject(ret); +} diff --git a/fixtures/wasm-app/worker/service-worker-module/export_wasm.js b/fixtures/wasm-app/worker/service-worker-module/export_wasm.js new file mode 100644 index 0000000..5469c2c --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker-module/export_wasm.js @@ -0,0 +1,10 @@ +import _wasm from "../index_bg.wasm"; +import * as index_bg from "./index_bg.js"; + +const _wasm_memory = new WebAssembly.Memory({ initial: 512 }); +let importsObject = { + env: { memory: _wasm_memory }, + "./index_bg.js": index_bg, +}; + +export default new WebAssembly.Instance(_wasm, importsObject).exports; diff --git a/fixtures/wasm-app/worker/service-worker-module/index.js b/fixtures/wasm-app/worker/service-worker-module/index.js new file mode 100644 index 0000000..7b7e293 --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker-module/index.js @@ -0,0 +1,5 @@ +import * as worker from "./index_bg.js"; + +addEventListener("fetch", (event) => { + event.respondWith(worker.fetch(event.request)); +}); diff --git a/fixtures/wasm-app/worker/service-worker-module/index_bg.js b/fixtures/wasm-app/worker/service-worker-module/index_bg.js new file mode 100644 index 0000000..4305fb0 --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker-module/index_bg.js @@ -0,0 +1,683 @@ +import wasm from "./export_wasm.js"; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { + return heap[idx]; +} + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let WASM_VECTOR_LEN = 0; + +let cachegetUint8Memory0 = null; +function getUint8Memory0() { + if ( + cachegetUint8Memory0 === null || + cachegetUint8Memory0.buffer !== wasm.memory.buffer + ) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +const lTextEncoder = + typeof TextEncoder === "undefined" + ? (0, module.require)("util").TextEncoder + : TextEncoder; + +let cachedTextEncoder = new lTextEncoder("utf-8"); + +const encodeString = + typeof cachedTextEncoder.encodeInto === "function" + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); + } + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length, + }; + }; + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0() + .subarray(ptr, ptr + buf.length) + .set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, (len = offset + arg.length * 3)); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if ( + cachegetInt32Memory0 === null || + cachegetInt32Memory0.buffer !== wasm.memory.buffer + ) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +const lTextDecoder = + typeof TextDecoder === "undefined" + ? (0, module.require)("util").TextDecoder + : TextDecoder; + +let cachedTextDecoder = new lTextDecoder("utf-8", { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == "number" || type == "boolean" || val == null) { + return `${val}`; + } + if (type == "string") { + return `"${val}"`; + } + if (type == "symbol") { + const description = val.description; + if (description == null) { + return "Symbol"; + } else { + return `Symbol(${description})`; + } + } + if (type == "function") { + const name = val.name; + if (typeof name == "string" && name.length > 0) { + return `Function(${name})`; + } else { + return "Function"; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = "["; + if (length > 0) { + debug += debugString(val[0]); + } + for (let i = 1; i < length; i++) { + debug += ", " + debugString(val[i]); + } + debug += "]"; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == "Object") { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return "Object(" + JSON.stringify(val) + ")"; + } catch (_) { + return "Object"; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + } else { + state.a = a; + } + } + }; + real.original = state; + + return real; +} +function __wbg_adapter_22(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h29cf9a4fd4f08c73( + arg0, + arg1, + addHeapObject(arg2) + ); +} + +/** + * @param {any} req + * @param {any} env + * @param {any} ctx + * @returns {Promise} + */ +export function fetch(req, env, ctx) { + var ret = wasm.fetch( + addHeapObject(req), + addHeapObject(env), + addHeapObject(ctx) + ); + return takeObject(ret); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +function __wbg_adapter_86(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h70365e5614c937ba( + arg0, + arg1, + addHeapObject(arg2), + addHeapObject(arg3) + ); +} + +/** + * Configuration options for Cloudflare's image optimization feature: + * + */ +export const PolishConfig = Object.freeze({ + Off: 0, + 0: "Off", + Lossy: 1, + 1: "Lossy", + Lossless: 2, + 2: "Lossless", +}); +/** + */ +export const RequestRedirect = Object.freeze({ + Error: 0, + 0: "Error", + Follow: 1, + 1: "Follow", + Manual: 2, + 2: "Manual", +}); +/** + * Configuration options for Cloudflare's minification features: + * + */ +export class MinifyConfig { + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_minifyconfig_free(ptr); + } + /** + */ + get js() { + var ret = wasm.__wbg_get_minifyconfig_js(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set js(arg0) { + wasm.__wbg_set_minifyconfig_js(this.ptr, arg0); + } + /** + */ + get html() { + var ret = wasm.__wbg_get_minifyconfig_html(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set html(arg0) { + wasm.__wbg_set_minifyconfig_html(this.ptr, arg0); + } + /** + */ + get css() { + var ret = wasm.__wbg_get_minifyconfig_css(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set css(arg0) { + wasm.__wbg_set_minifyconfig_css(this.ptr, arg0); + } +} + +export function __wbindgen_object_drop_ref(arg0) { + takeObject(arg0); +} + +export function __wbindgen_string_get(arg0, arg1) { + const obj = getObject(arg1); + var ret = typeof obj === "string" ? obj : undefined; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_new_59cb74e423758ede() { + var ret = new Error(); + return addHeapObject(ret); +} + +export function __wbg_stack_558ba5917b466edd(arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_error_4bb6c2a97407129a(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } +} + +export function __wbindgen_string_new(arg0, arg1) { + var ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); +} + +export function __wbindgen_is_undefined(arg0) { + var ret = getObject(arg0) === undefined; + return ret; +} + +export function __wbindgen_number_new(arg0) { + var ret = arg0; + return addHeapObject(ret); +} + +export function __wbindgen_object_clone_ref(arg0) { + var ret = getObject(arg0); + return addHeapObject(ret); +} + +export function __wbindgen_cb_drop(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + var ret = false; + return ret; +} + +export function __wbg_body_b67afdc865ca6d95(arg0) { + var ret = getObject(arg0).body; + return isLikeNone(ret) ? 0 : addHeapObject(ret); +} + +export function __wbg_newwithoptu8arrayandinit_2358601704784951() { + return handleError(function (arg0, arg1) { + var ret = new Response(takeObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_newwithoptstrandinit_cd8a4402e68873df() { + return handleError(function (arg0, arg1, arg2) { + var ret = new Response( + arg0 === 0 ? undefined : getStringFromWasm0(arg0, arg1), + getObject(arg2) + ); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_newwithoptstreamandinit_2cdcade777fddab8() { + return handleError(function (arg0, arg1) { + var ret = new Response(takeObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_latitude_6d0dc7510853aaea(arg0, arg1) { + var ret = getObject(arg1).latitude; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_longitude_b566ab6d05581b27(arg0, arg1) { + var ret = getObject(arg1).longitude; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_region_5b42be38a5fb9fee(arg0, arg1) { + var ret = getObject(arg1).region; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_method_8ec82ee079ce2702(arg0, arg1) { + var ret = getObject(arg1).method; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_url_9b689d511a7995b5(arg0, arg1) { + var ret = getObject(arg1).url; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_headers_62f682054d74e541(arg0) { + var ret = getObject(arg0).headers; + return addHeapObject(ret); +} + +export function __wbg_formData_c5b7ee7b1f027402() { + return handleError(function (arg0) { + var ret = getObject(arg0).formData(); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_cf_619e9c1d3e10de88(arg0) { + var ret = getObject(arg0).cf; + return addHeapObject(ret); +} + +export function __wbg_new_9e449026aa04d852() { + return handleError(function () { + var ret = new Headers(); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_set_ecc7ab7b550ca8b7() { + return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).set( + getStringFromWasm0(arg1, arg2), + getStringFromWasm0(arg3, arg4) + ); + }, arguments); +} + +export function __wbg_log_9c5d40d7de6fd4f7(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); +} + +export function __wbg_instanceof_File_05917b01e27498d9(arg0) { + var ret = getObject(arg0) instanceof File; + return ret; +} + +export function __wbg_get_4139ec5751043532(arg0, arg1, arg2) { + var ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); +} + +export function __wbg_get_4d0f21c2f823742e() { + return handleError(function (arg0, arg1) { + var ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_new_0b83d3df67ecb33e() { + var ret = new Object(); + return addHeapObject(ret); +} + +export function __wbg_instanceof_Error_561efcb1265706d8(arg0) { + var ret = getObject(arg0) instanceof Error; + return ret; +} + +export function __wbg_toString_0ef1ea57b966aed4(arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); +} + +export function __wbg_call_346669c262382ad7() { + return handleError(function (arg0, arg1, arg2) { + var ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_name_9a3ff1e21a0e3304(arg0) { + var ret = getObject(arg0).name; + return addHeapObject(ret); +} + +export function __wbg_new0_fd3a3a290b25cdac() { + var ret = new Date(); + return addHeapObject(ret); +} + +export function __wbg_toString_646e437de608a0a1(arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); +} + +export function __wbg_constructor_9fe544cc0957fdd0(arg0) { + var ret = getObject(arg0).constructor; + return addHeapObject(ret); +} + +export function __wbg_new_b1d61b5687f5e73a(arg0, arg1) { + try { + var state0 = { a: arg0, b: arg1 }; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_86(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + var ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } +} + +export function __wbg_resolve_d23068002f584f22(arg0) { + var ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); +} + +export function __wbg_then_2fcac196782070cc(arg0, arg1) { + var ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); +} + +export function __wbg_then_8c2d62e8ae5978f7(arg0, arg1, arg2) { + var ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); +} + +export function __wbg_buffer_397eaa4d72ee94dd(arg0) { + var ret = getObject(arg0).buffer; + return addHeapObject(ret); +} + +export function __wbg_newwithbyteoffsetandlength_4b9b8c4e3f5adbff( + arg0, + arg1, + arg2 +) { + var ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); +} + +export function __wbg_set_969ad0a60e51d320(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); +} + +export function __wbg_length_1eb8fc608a0d4cdb(arg0) { + var ret = getObject(arg0).length; + return ret; +} + +export function __wbg_newwithlength_929232475839a482(arg0) { + var ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); +} + +export function __wbg_set_82a4e8a85e31ac42() { + return handleError(function (arg0, arg1, arg2) { + var ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments); +} + +export function __wbindgen_debug_string(arg0, arg1) { + var ret = debugString(getObject(arg1)); + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbindgen_throw(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +} + +export function __wbindgen_memory() { + var ret = wasm.memory; + return addHeapObject(ret); +} + +export function __wbindgen_closure_wrapper515(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 100, __wbg_adapter_22); + return addHeapObject(ret); +} diff --git a/fixtures/wasm-app/worker/service-worker/export_wasm.js b/fixtures/wasm-app/worker/service-worker/export_wasm.js new file mode 100644 index 0000000..bd9ed4a --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker/export_wasm.js @@ -0,0 +1,11 @@ +import * as index_bg from "./index_bg.js"; + +const _wasm = MYWASM; + +const _wasm_memory = new WebAssembly.Memory({ initial: 512 }); +let importsObject = { + env: { memory: _wasm_memory }, + "./index_bg.js": index_bg, +}; + +export default new WebAssembly.Instance(_wasm, importsObject).exports; diff --git a/fixtures/wasm-app/worker/service-worker/index.js b/fixtures/wasm-app/worker/service-worker/index.js new file mode 100644 index 0000000..7b7e293 --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker/index.js @@ -0,0 +1,5 @@ +import * as worker from "./index_bg.js"; + +addEventListener("fetch", (event) => { + event.respondWith(worker.fetch(event.request)); +}); diff --git a/fixtures/wasm-app/worker/service-worker/index_bg.js b/fixtures/wasm-app/worker/service-worker/index_bg.js new file mode 100644 index 0000000..4305fb0 --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker/index_bg.js @@ -0,0 +1,683 @@ +import wasm from "./export_wasm.js"; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { + return heap[idx]; +} + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let WASM_VECTOR_LEN = 0; + +let cachegetUint8Memory0 = null; +function getUint8Memory0() { + if ( + cachegetUint8Memory0 === null || + cachegetUint8Memory0.buffer !== wasm.memory.buffer + ) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +const lTextEncoder = + typeof TextEncoder === "undefined" + ? (0, module.require)("util").TextEncoder + : TextEncoder; + +let cachedTextEncoder = new lTextEncoder("utf-8"); + +const encodeString = + typeof cachedTextEncoder.encodeInto === "function" + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); + } + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length, + }; + }; + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0() + .subarray(ptr, ptr + buf.length) + .set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, (len = offset + arg.length * 3)); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if ( + cachegetInt32Memory0 === null || + cachegetInt32Memory0.buffer !== wasm.memory.buffer + ) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +const lTextDecoder = + typeof TextDecoder === "undefined" + ? (0, module.require)("util").TextDecoder + : TextDecoder; + +let cachedTextDecoder = new lTextDecoder("utf-8", { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == "number" || type == "boolean" || val == null) { + return `${val}`; + } + if (type == "string") { + return `"${val}"`; + } + if (type == "symbol") { + const description = val.description; + if (description == null) { + return "Symbol"; + } else { + return `Symbol(${description})`; + } + } + if (type == "function") { + const name = val.name; + if (typeof name == "string" && name.length > 0) { + return `Function(${name})`; + } else { + return "Function"; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = "["; + if (length > 0) { + debug += debugString(val[0]); + } + for (let i = 1; i < length; i++) { + debug += ", " + debugString(val[i]); + } + debug += "]"; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == "Object") { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return "Object(" + JSON.stringify(val) + ")"; + } catch (_) { + return "Object"; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_2.get(state.dtor)(a, state.b); + } else { + state.a = a; + } + } + }; + real.original = state; + + return real; +} +function __wbg_adapter_22(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h29cf9a4fd4f08c73( + arg0, + arg1, + addHeapObject(arg2) + ); +} + +/** + * @param {any} req + * @param {any} env + * @param {any} ctx + * @returns {Promise} + */ +export function fetch(req, env, ctx) { + var ret = wasm.fetch( + addHeapObject(req), + addHeapObject(env), + addHeapObject(ctx) + ); + return takeObject(ret); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +function __wbg_adapter_86(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h70365e5614c937ba( + arg0, + arg1, + addHeapObject(arg2), + addHeapObject(arg3) + ); +} + +/** + * Configuration options for Cloudflare's image optimization feature: + * + */ +export const PolishConfig = Object.freeze({ + Off: 0, + 0: "Off", + Lossy: 1, + 1: "Lossy", + Lossless: 2, + 2: "Lossless", +}); +/** + */ +export const RequestRedirect = Object.freeze({ + Error: 0, + 0: "Error", + Follow: 1, + 1: "Follow", + Manual: 2, + 2: "Manual", +}); +/** + * Configuration options for Cloudflare's minification features: + * + */ +export class MinifyConfig { + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_minifyconfig_free(ptr); + } + /** + */ + get js() { + var ret = wasm.__wbg_get_minifyconfig_js(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set js(arg0) { + wasm.__wbg_set_minifyconfig_js(this.ptr, arg0); + } + /** + */ + get html() { + var ret = wasm.__wbg_get_minifyconfig_html(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set html(arg0) { + wasm.__wbg_set_minifyconfig_html(this.ptr, arg0); + } + /** + */ + get css() { + var ret = wasm.__wbg_get_minifyconfig_css(this.ptr); + return ret !== 0; + } + /** + * @param {boolean} arg0 + */ + set css(arg0) { + wasm.__wbg_set_minifyconfig_css(this.ptr, arg0); + } +} + +export function __wbindgen_object_drop_ref(arg0) { + takeObject(arg0); +} + +export function __wbindgen_string_get(arg0, arg1) { + const obj = getObject(arg1); + var ret = typeof obj === "string" ? obj : undefined; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_new_59cb74e423758ede() { + var ret = new Error(); + return addHeapObject(ret); +} + +export function __wbg_stack_558ba5917b466edd(arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_error_4bb6c2a97407129a(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } +} + +export function __wbindgen_string_new(arg0, arg1) { + var ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); +} + +export function __wbindgen_is_undefined(arg0) { + var ret = getObject(arg0) === undefined; + return ret; +} + +export function __wbindgen_number_new(arg0) { + var ret = arg0; + return addHeapObject(ret); +} + +export function __wbindgen_object_clone_ref(arg0) { + var ret = getObject(arg0); + return addHeapObject(ret); +} + +export function __wbindgen_cb_drop(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + var ret = false; + return ret; +} + +export function __wbg_body_b67afdc865ca6d95(arg0) { + var ret = getObject(arg0).body; + return isLikeNone(ret) ? 0 : addHeapObject(ret); +} + +export function __wbg_newwithoptu8arrayandinit_2358601704784951() { + return handleError(function (arg0, arg1) { + var ret = new Response(takeObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_newwithoptstrandinit_cd8a4402e68873df() { + return handleError(function (arg0, arg1, arg2) { + var ret = new Response( + arg0 === 0 ? undefined : getStringFromWasm0(arg0, arg1), + getObject(arg2) + ); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_newwithoptstreamandinit_2cdcade777fddab8() { + return handleError(function (arg0, arg1) { + var ret = new Response(takeObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_latitude_6d0dc7510853aaea(arg0, arg1) { + var ret = getObject(arg1).latitude; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_longitude_b566ab6d05581b27(arg0, arg1) { + var ret = getObject(arg1).longitude; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_region_5b42be38a5fb9fee(arg0, arg1) { + var ret = getObject(arg1).region; + var ptr0 = isLikeNone(ret) + ? 0 + : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_method_8ec82ee079ce2702(arg0, arg1) { + var ret = getObject(arg1).method; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_url_9b689d511a7995b5(arg0, arg1) { + var ret = getObject(arg1).url; + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbg_headers_62f682054d74e541(arg0) { + var ret = getObject(arg0).headers; + return addHeapObject(ret); +} + +export function __wbg_formData_c5b7ee7b1f027402() { + return handleError(function (arg0) { + var ret = getObject(arg0).formData(); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_cf_619e9c1d3e10de88(arg0) { + var ret = getObject(arg0).cf; + return addHeapObject(ret); +} + +export function __wbg_new_9e449026aa04d852() { + return handleError(function () { + var ret = new Headers(); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_set_ecc7ab7b550ca8b7() { + return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).set( + getStringFromWasm0(arg1, arg2), + getStringFromWasm0(arg3, arg4) + ); + }, arguments); +} + +export function __wbg_log_9c5d40d7de6fd4f7(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); +} + +export function __wbg_instanceof_File_05917b01e27498d9(arg0) { + var ret = getObject(arg0) instanceof File; + return ret; +} + +export function __wbg_get_4139ec5751043532(arg0, arg1, arg2) { + var ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); +} + +export function __wbg_get_4d0f21c2f823742e() { + return handleError(function (arg0, arg1) { + var ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_new_0b83d3df67ecb33e() { + var ret = new Object(); + return addHeapObject(ret); +} + +export function __wbg_instanceof_Error_561efcb1265706d8(arg0) { + var ret = getObject(arg0) instanceof Error; + return ret; +} + +export function __wbg_toString_0ef1ea57b966aed4(arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); +} + +export function __wbg_call_346669c262382ad7() { + return handleError(function (arg0, arg1, arg2) { + var ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments); +} + +export function __wbg_name_9a3ff1e21a0e3304(arg0) { + var ret = getObject(arg0).name; + return addHeapObject(ret); +} + +export function __wbg_new0_fd3a3a290b25cdac() { + var ret = new Date(); + return addHeapObject(ret); +} + +export function __wbg_toString_646e437de608a0a1(arg0) { + var ret = getObject(arg0).toString(); + return addHeapObject(ret); +} + +export function __wbg_constructor_9fe544cc0957fdd0(arg0) { + var ret = getObject(arg0).constructor; + return addHeapObject(ret); +} + +export function __wbg_new_b1d61b5687f5e73a(arg0, arg1) { + try { + var state0 = { a: arg0, b: arg1 }; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wbg_adapter_86(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + var ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = state0.b = 0; + } +} + +export function __wbg_resolve_d23068002f584f22(arg0) { + var ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); +} + +export function __wbg_then_2fcac196782070cc(arg0, arg1) { + var ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); +} + +export function __wbg_then_8c2d62e8ae5978f7(arg0, arg1, arg2) { + var ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); +} + +export function __wbg_buffer_397eaa4d72ee94dd(arg0) { + var ret = getObject(arg0).buffer; + return addHeapObject(ret); +} + +export function __wbg_newwithbyteoffsetandlength_4b9b8c4e3f5adbff( + arg0, + arg1, + arg2 +) { + var ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); +} + +export function __wbg_set_969ad0a60e51d320(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); +} + +export function __wbg_length_1eb8fc608a0d4cdb(arg0) { + var ret = getObject(arg0).length; + return ret; +} + +export function __wbg_newwithlength_929232475839a482(arg0) { + var ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); +} + +export function __wbg_set_82a4e8a85e31ac42() { + return handleError(function (arg0, arg1, arg2) { + var ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments); +} + +export function __wbindgen_debug_string(arg0, arg1) { + var ret = debugString(getObject(arg1)); + var ptr0 = passStringToWasm0( + ret, + wasm.__wbindgen_malloc, + wasm.__wbindgen_realloc + ); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +} + +export function __wbindgen_throw(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +} + +export function __wbindgen_memory() { + var ret = wasm.memory; + return addHeapObject(ret); +} + +export function __wbindgen_closure_wrapper515(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 100, __wbg_adapter_22); + return addHeapObject(ret); +} diff --git a/fixtures/wasm-app/worker/service-worker/wrangler.jsonc b/fixtures/wasm-app/worker/service-worker/wrangler.jsonc new file mode 100644 index 0000000..4119cd3 --- /dev/null +++ b/fixtures/wasm-app/worker/service-worker/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "wasm_modules": { + "MYWASM": "../index_bg.wasm", + }, +} diff --git a/fixtures/wildcard-modules/package.json b/fixtures/wildcard-modules/package.json new file mode 100644 index 0000000..4528977 --- /dev/null +++ b/fixtures/wildcard-modules/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/wildcard-modules", + "private": true, + "scripts": { + "build": "wrangler deploy --dry-run --outdir=dist", + "check:type": "tsc", + "deploy": "wrangler deploy", + "start": "wrangler dev --x-dev-env", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./test/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@fixture/shared": "workspace:*", + "undici": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/wildcard-modules/src/common.cjs b/fixtures/wildcard-modules/src/common.cjs new file mode 100644 index 0000000..19658c1 --- /dev/null +++ b/fixtures/wildcard-modules/src/common.cjs @@ -0,0 +1 @@ +module.exports = "common"; diff --git a/fixtures/wildcard-modules/src/dep.ts b/fixtures/wildcard-modules/src/dep.ts new file mode 100644 index 0000000..cc0be2a --- /dev/null +++ b/fixtures/wildcard-modules/src/dep.ts @@ -0,0 +1 @@ +export default "bundled"; diff --git a/fixtures/wildcard-modules/src/dynamic.js b/fixtures/wildcard-modules/src/dynamic.js new file mode 100644 index 0000000..6c40343 --- /dev/null +++ b/fixtures/wildcard-modules/src/dynamic.js @@ -0,0 +1 @@ +export default "dynamic"; diff --git a/fixtures/wildcard-modules/src/index.ts b/fixtures/wildcard-modules/src/index.ts new file mode 100644 index 0000000..a831998 --- /dev/null +++ b/fixtures/wildcard-modules/src/index.ts @@ -0,0 +1,32 @@ +import common from "./common.cjs"; +import dep from "./dep"; +import text from "./text.txt"; + +export default { + async fetch(request) { + const url = new URL(request.url); + if (url.pathname === "/dep") { + return new Response(dep); + } + if (url.pathname === "/text") { + return new Response(text); + } + if (url.pathname === "/common") { + return new Response(common); + } + if (url.pathname === "/dynamic") { + return new Response((await import("./dynamic.js")).default); + } + if (url.pathname.startsWith("/lang/")) { + // Build the path dynamically to ensure esbuild does a wildcard import + return new Response( + ( + await import( + "./lang/" + url.pathname.substring("/lang/".length) + ".js" + ) + ).default.hello + ); + } + return new Response("Not Found", { status: 404 }); + }, +}; diff --git a/fixtures/wildcard-modules/src/lang/en.js b/fixtures/wildcard-modules/src/lang/en.js new file mode 100644 index 0000000..969f5b9 --- /dev/null +++ b/fixtures/wildcard-modules/src/lang/en.js @@ -0,0 +1 @@ +export default { hello: "hello" }; diff --git a/fixtures/wildcard-modules/src/lang/fr.js b/fixtures/wildcard-modules/src/lang/fr.js new file mode 100644 index 0000000..67e5320 --- /dev/null +++ b/fixtures/wildcard-modules/src/lang/fr.js @@ -0,0 +1 @@ +export default { hello: "bonjour" }; diff --git a/fixtures/wildcard-modules/src/text.d.ts b/fixtures/wildcard-modules/src/text.d.ts new file mode 100644 index 0000000..4695e49 --- /dev/null +++ b/fixtures/wildcard-modules/src/text.d.ts @@ -0,0 +1,4 @@ +declare module "*.txt" { + const value: string; + export default value; +} diff --git a/fixtures/wildcard-modules/src/text.txt b/fixtures/wildcard-modules/src/text.txt new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/fixtures/wildcard-modules/src/text.txt @@ -0,0 +1 @@ +test diff --git a/fixtures/wildcard-modules/test/index.test.ts b/fixtures/wildcard-modules/test/index.test.ts new file mode 100644 index 0000000..a5e2d12 --- /dev/null +++ b/fixtures/wildcard-modules/test/index.test.ts @@ -0,0 +1,207 @@ +import childProcess from "node:child_process"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { setTimeout } from "node:timers/promises"; +import { removeDir } from "@fixture/shared/src/fs-helpers"; +import { fetch } from "undici"; +import { afterAll, assert, beforeAll, describe, test } from "vitest"; +import { + runWranglerDev, + wranglerEntryPath, +} from "../../shared/src/run-wrangler-long-lived"; + +async function getTmpDir() { + return fs.mkdtemp(path.join(os.tmpdir(), "wrangler-modules-")); +} + +type WranglerDev = Awaited>; +function get(worker: WranglerDev, pathname: string) { + const url = `http://${worker.ip}:${worker.port}${pathname}`; + // Disable Miniflare's pretty error page, so we can parse errors as JSON + return fetch(url, { headers: { "MF-Disable-Pretty-Error": "true" } }); +} + +async function retry(closure: () => Promise, max = 30): Promise { + for (let attempt = 1; attempt <= max; attempt++) { + try { + return await closure(); + } catch (e) { + if (attempt === max) throw e; + } + await setTimeout(1_000); + } + assert.fail("Unreachable"); +} + +describe("wildcard imports: dev", () => { + let tmpDir: string; + let worker: WranglerDev; + + beforeAll(async () => { + // Copy over files to a temporary directory as we'll be modifying them + tmpDir = await getTmpDir(); + await fs.cp( + path.resolve(__dirname, "..", "src"), + path.join(tmpDir, "src"), + { recursive: true } + ); + await fs.cp( + path.resolve(__dirname, "..", "wrangler.jsonc"), + path.join(tmpDir, "wrangler.jsonc") + ); + + worker = await runWranglerDev(tmpDir, ["--port=0", "--inspector-port=0"]); + }); + afterAll(async () => { + await worker.stop(); + removeDir(tmpDir, { fireAndForget: true }); + }); + + test("supports bundled modules", async ({ expect }) => { + const res = await get(worker, "/dep"); + expect(await res.text()).toBe("bundled"); + }); + test("supports text modules", async ({ expect }) => { + const res = await get(worker, "/text"); + expect(await res.text()).toBe("test\n"); + }); + test("supports dynamic imports", async ({ expect }) => { + const res = await get(worker, "/dynamic"); + expect(await res.text()).toBe("dynamic"); + }); + test("supports commonjs lazy imports", async ({ expect }) => { + const res = await get(worker, "/common"); + expect(await res.text()).toBe("common"); + }); + test("supports variable dynamic imports", async ({ expect }) => { + const res = await get(worker, "/lang/en"); + expect(await res.text()).toBe("hello"); + }); + + test("watches wildcard modules", async ({ expect }) => { + const srcDir = path.join(tmpDir, "src"); + + // Update dynamically imported file + await fs.writeFile( + path.join(srcDir, "dynamic.js"), + 'export default "new dynamic";' + ); + await retry(async () => { + const res = await get(worker, "/dynamic"); + assert.strictEqual(await res.text(), "new dynamic"); + }); + + // Delete dynamically imported file + await fs.rm(path.join(srcDir, "lang", "en.js")); + const res = await retry(async () => { + const res = await get(worker, "/lang/en"); + assert.strictEqual(res.status, 500); + return res; + }); + const error = (await res.json()) as { message?: string }; + expect(error.message).toBe("Module not found in bundle: ./lang/en.js"); + + // Create new dynamically imported file in new directory + await fs.mkdir(path.join(srcDir, "lang", "en")); + await fs.writeFile( + path.join(srcDir, "lang", "en", "us.js"), + 'export default { hello: "hey" };' + ); + await retry(async () => { + const res = await get(worker, "/lang/en/us"); + assert.strictEqual(await res.text(), "hey"); + }); + + // Update newly created file + await fs.writeFile( + path.join(srcDir, "lang", "en", "us.js"), + 'export default { hello: "bye" };' + ); + await retry(async () => { + const res = await get(worker, "/lang/en/us"); + assert.strictEqual(await res.text(), "bye"); + }); + }); +}); + +function build(cwd: string, outDir: string) { + return childProcess.spawnSync( + process.execPath, + [wranglerEntryPath, "deploy", "--dry-run", `--outdir=${outDir}`], + { cwd } + ); +} + +describe("wildcard imports: deploy", () => { + let tmpDir: string; + beforeAll(async () => { + tmpDir = await getTmpDir(); + }); + afterAll(() => { + removeDir(tmpDir, { fireAndForget: true }); + }); + + test("bundles wildcard modules", async ({ expect }) => { + const outDir = path.join(tmpDir, "out"); + const result = build(path.resolve(__dirname, ".."), outDir); + expect(result.status).toBe(0); + + // Check additional modules marked external, but other dependencies bundled + const bundledEntryPath = path.join(outDir, "index.js"); + const bundledEntry = await fs.readFile(bundledEntryPath, "utf8"); + const imports = [ + `import text from "./4e1243bd22c66e76c2ba9eddc1f91394e57f9f83-text.txt";`, + `// src/common.cjs`, + `// src/lang/en.js`, + `// src/lang/fr.js`, + `// src/dynamic.js`, + `// import("./lang/**/*.js") in src/index.ts`, + ]; + for (const importStatement of imports) { + expect(bundledEntry).toContain(importStatement); + } + + // Check additional modules included in output + expect( + existsSync( + path.join(outDir, "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83-text.txt") + ) + ).toBe(true); + }); + + test("fails with service worker entrypoint", async ({ expect }) => { + const serviceWorkerDir = path.join(tmpDir, "service-worker"); + await fs.mkdir(serviceWorkerDir, { recursive: true }); + await fs.writeFile( + path.join(serviceWorkerDir, "index.js"), + ` + async function handleRequest(e) { + const url = new URL(e.request.url); + if (url.pathname === "/") { + return new Response("Hello World"); + } else if(url.pathname.startsWith("/lang/")) { + const language = url.pathname.substring("/lang/".length); + return new Response((await import(\`./lang/\${language}.js\`)).default.hello); + } + return new Response("Not Found", { status: 404 }); + } + + addEventListener('fetch', (e) => e.respondWith(handleRequest(e)))` + ); + await fs.writeFile( + path.join(serviceWorkerDir, "wrangler.toml"), + [ + 'name="service-worker-test"', + 'main = "index.js"', + 'compatibility_date = "2024-09-09"', + ].join("\n") + ); + + // Try build, and check fails + const serviceWorkerOutDir = path.join(tmpDir, "service-worker-out"); + const result = build(serviceWorkerDir, serviceWorkerOutDir); + expect(result.status).toBe(1); + }); +}); diff --git a/fixtures/wildcard-modules/test/tsconfig.json b/fixtures/wildcard-modules/test/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/wildcard-modules/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/wildcard-modules/tsconfig.json b/fixtures/wildcard-modules/tsconfig.json new file mode 100644 index 0000000..873892f --- /dev/null +++ b/fixtures/wildcard-modules/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "strict": true, + "isolatedModules": true, + "noEmit": true, + "types": ["@cloudflare/workers-types/experimental"], + "allowJs": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} diff --git a/fixtures/wildcard-modules/turbo.json b/fixtures/wildcard-modules/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/fixtures/wildcard-modules/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/fixtures/wildcard-modules/vitest.config.mts b/fixtures/wildcard-modules/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/wildcard-modules/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/wildcard-modules/wrangler.jsonc b/fixtures/wildcard-modules/wrangler.jsonc new file mode 100644 index 0000000..4c9ab9d --- /dev/null +++ b/fixtures/wildcard-modules/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "wildcard-modules", + "main": "src/index.ts", + "compatibility_date": "2024-09-09", +} diff --git a/fixtures/worker-app/.env b/fixtures/worker-app/.env new file mode 100644 index 0000000..1566bb1 --- /dev/null +++ b/fixtures/worker-app/.env @@ -0,0 +1 @@ +FOO=bar \ No newline at end of file diff --git a/fixtures/worker-app/.gitignore b/fixtures/worker-app/.gitignore new file mode 100644 index 0000000..b67a5dd --- /dev/null +++ b/fixtures/worker-app/.gitignore @@ -0,0 +1,2 @@ +dist +!.env \ No newline at end of file diff --git a/fixtures/worker-app/package.json b/fixtures/worker-app/package.json new file mode 100644 index 0000000..0983e13 --- /dev/null +++ b/fixtures/worker-app/package.json @@ -0,0 +1,25 @@ +{ + "name": "@fixture/worker-basic", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "check:type": "tsc", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "@fixture/isomorphic-random": "workspace:^", + "cookie": "^0.6.0" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/worker-app/public/404.html b/fixtures/worker-app/public/404.html new file mode 100644 index 0000000..5ac9921 --- /dev/null +++ b/fixtures/worker-app/public/404.html @@ -0,0 +1,50 @@ + + + + + + + + + +

+ + diff --git a/fixtures/worker-app/public/favicon.ico b/fixtures/worker-app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cc6c23bd37bb4fa58f810c5f9e36041d96b23462 GIT binary patch literal 1150 zcmXw&dr(wW9LJA6klCG0Y`fdtcD^W zZ;*%(!UqcotRgi|$B@@&|J#+okFh*l9>gNc5&sPx{583mU`;2{d&-7lcYdrHJ zV6g+cdI9tI3XjTu1YOoNblCzTvxM=;eZu<4eJ-{zQwSS$UpSd=@030w<3Vp+juRO@ zL9RJ=p9em-bEK6D+>qyj;VU=vxdL=KLJy%QSXABXQmS|xl??P zJ@7Pt0LZ-pWRqv}2_KI>X@^vI zWC%*ZH6Z^7?JS=1#1apWEAk-but)QRjD;RcQk6F;q1-dJti+9%zMe&N0Xe>WTv<9C ziZ1^JLq$V?^(WvUd3Ll&e918{;)}hAEAk@tuxD&#y;!xP#;Yb)dhtrB8}UW6lQD(U zsUO)SRzHfJ#4(kINyxrds67%0w(J347EvjXqa$gNRq_L{KiET#i=)tk@A^ z%{O7F8^u7yuR!StaO`K`gR=@xs1&ihLd47DdF`a%mD=RP>Ti5lRZpH;@%ZvG7poan znQa1wrkfb3{~f5l4pfro!!Cs<)%uWB<3nPVh_p7T>h)H?y0+PmlyAw?AWkNq5L5PC z4YH0~Ft?6FZu$dgya5~?QMswb&yySdSk>UCoPFw>XQZU`)6y7uu&%|A)bD(tJymqx z=F#1EFnD$XX!{dr9Roh;5Ad}oC1|8H`LVjuKW4JYY`smkywxs;(oadCT#(xAcb1KY zYWx0za_$b$c^jy?q4F&@nZI#brkrK0Yn4Lev25s)8DS(GlY z-2?1*>Hl8~a8suYQ-=ati{{A=%GtQ7M`3TDBf1qcA!%b-!vWRt2>8(n4Ezl=PpJI; z3ku(44`5@Df(>1YL>@D>;rm1TXM{fOl+?SuL&Ev{D!)Drj64Pgr|2Fzg8ZF}0c`6J zV(WzfHroR|w5EPz_gs98RbVUYM7E^dYswo8jQ>WlTKwn?V7nXQJNgxDzZ?wn?xo;3 mxlH6@Tl)fR@>#})7R+I#Y&5j;3=uJp_O2D{Jpa$vV*dkKub%Ay literal 0 HcmV?d00001 diff --git a/fixtures/worker-app/public/img/200-wrangler-ferris.gif b/fixtures/worker-app/public/img/200-wrangler-ferris.gif new file mode 100644 index 0000000000000000000000000000000000000000..8853751fac7b39559f140971700360fe249fcdc5 GIT binary patch literal 45598 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 Os#2HA)TVYVAOJf?`|0KY literal 0 HcmV?d00001 diff --git a/fixtures/worker-app/public/img/404-wrangler-ferris.gif b/fixtures/worker-app/public/img/404-wrangler-ferris.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ac1479fcf6f4ded732b8b8ea7bfd9f98b5ca3e2 GIT binary patch literal 31606 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 + + + + + + + + +
+

200 Success

+

Hello World! Welcome to your Workers Site.

+ a happy crab is wearing a cowboy hat and holding a lasso. 200 success. +
+ + diff --git a/fixtures/worker-app/src/dep.js b/fixtures/worker-app/src/dep.js new file mode 100644 index 0000000..2256914 --- /dev/null +++ b/fixtures/worker-app/src/dep.js @@ -0,0 +1,3 @@ +export function now() { + return Date.now(); +} diff --git a/fixtures/worker-app/src/explicit-resource-management.js b/fixtures/worker-app/src/explicit-resource-management.js new file mode 100644 index 0000000..5885674 --- /dev/null +++ b/fixtures/worker-app/src/explicit-resource-management.js @@ -0,0 +1,24 @@ +/** @param {string[]} logs */ +function connect(logs) { + logs.push("Connected"); + return { + send(message) { + logs.push(`Sent ${message}`); + }, + [Symbol.dispose]() { + logs.push("Disconnected synchronously"); + }, + async [Symbol.asyncDispose]() { + logs.push("Disconnected asynchronously"); + }, + }; +} + +/** @param {string[]} logs */ +export async function testExplicitResourceManagement(logs) { + using syncConnect = connect(logs); + await using asyncConnect = connect(logs); + + syncConnect.send("hello"); + asyncConnect.send("goodbye"); +} diff --git a/fixtures/worker-app/src/index.js b/fixtures/worker-app/src/index.js new file mode 100644 index 0000000..2ecac73 --- /dev/null +++ b/fixtures/worker-app/src/index.js @@ -0,0 +1,108 @@ +import { randomBytes } from "@fixture/isomorphic-random"; +import cookie from "cookie"; +import { now } from "./dep"; +import { testExplicitResourceManagement } from "./explicit-resource-management"; +import { logErrors } from "./log"; + +console.log("startup log"); + +console.log("The following error is a fake for testing"); +console.error( + "*** Received structured exception #0xc0000005: access violation; stack: 7ffe71872f57 7ff7834b643b 7ff7834b643b" +); + +/** @param {Uint8Array} array */ +function hexEncode(array) { + return Array.from(array) + .map((x) => x.toString(16).padStart(2, "0")) + .join(""); +} + +export default { + async fetch(request, env) { + console.log("request log"); + + const { pathname, origin, hostname, host } = new URL(request.url); + if (pathname.startsWith("/fav")) + return new Response("Not found", { status: 404 }); + if (pathname === "/env") return Response.json(env.FOO); + if (pathname === "/version_metadata") return Response.json(env.METADATA); + if (pathname === "/random") return new Response(hexEncode(randomBytes(8))); + if (pathname === "/error") throw new Error("Oops!"); + if (pathname === "/redirect") return Response.redirect(`${origin}/foo`); + if (pathname === "/cookie") + return new Response("", { + headers: [ + [ + "Set-Cookie", + cookie.serialize("hello", "world", { + domain: hostname, + }), + ], + [ + "Set-Cookie", + cookie.serialize("hello2", "world2", { + domain: host, + secure: true, + }), + ], + ], + }); + + if (pathname === "/content-encoding") { + return Response.json({ + AcceptEncoding: request.headers.get("Accept-Encoding"), + clientAcceptEncoding: request.cf.clientAcceptEncoding, + }); + } + if (pathname === "/content-encoding/gzip") { + return new Response("x".repeat(100), { + headers: { "Content-Encoding": "gzip" }, + }); + } + + if (pathname === "/explicit-resource-management") { + const logs = []; + await testExplicitResourceManagement(logs); + return Response.json(logs); + } + + if (request.headers.get("X-Test-URL") !== null) { + return new Response(request.url); + } + + console.log("METHOD =", request.method); + console.log("URL = ", request.url); + console.log("HEADERS =", new Map([...request.headers])); + console.log("CF =", request.cf); + + logErrors(); + + await fetch(new URL("https://example.com")); + await fetch( + new Request("https://example.com", { method: "POST", body: "foo" }) + ); + + console.log("end of request"); + return new Response( + `${request.url} ${now()} HOST:${request.headers.get( + "Host" + )} ORIGIN:${request.headers.get("Origin")}` + ); + }, + + /** + * Handle a scheduled event. + * + * If developing using `--local` mode, you can trigger this scheduled event via a CURL. + * E.g. `curl "http://localhost:8787/cdn-cgi/mf/scheduled"`. + * See the Miniflare docs: https://miniflare.dev/core/scheduled. + */ + scheduled(event, env, ctx) { + ctx.waitUntil(Promise.resolve(event.scheduledTime)); + ctx.waitUntil(Promise.resolve(event.cron)); + }, + tail(events) { + console.log("tails", { events }); + }, +}; diff --git a/fixtures/worker-app/src/log.ts b/fixtures/worker-app/src/log.ts new file mode 100644 index 0000000..a2f2ccc --- /dev/null +++ b/fixtures/worker-app/src/log.ts @@ -0,0 +1,17 @@ +// Make sure built JavaScript file doesn't use the same line numbers +interface Output { + thing: 42; +} + +export function logErrors(): Output { + console.log(new Error("logged error one")); + console.log(new Error("logged error two").stack); + console.log({ error: new Error("logged error three") }); + console.log({ nested: { error: new Error("logged error four").stack } }); + + console.log("some normal text to log"); + console.log("text with at in the middle"); + console.log("more text with at in the middle"); + + return { thing: 42 }; +} diff --git a/fixtures/worker-app/tests/build-conditions.test.ts b/fixtures/worker-app/tests/build-conditions.test.ts new file mode 100644 index 0000000..14587af --- /dev/null +++ b/fixtures/worker-app/tests/build-conditions.test.ts @@ -0,0 +1,94 @@ +import { execFileSync } from "child_process"; +import { mkdtempSync, readFileSync } from "fs"; +import { tmpdir } from "os"; +import { join, resolve } from "path"; +import { beforeEach, describe, it } from "vitest"; +import { wranglerEntryPath } from "../../shared/src/run-wrangler-long-lived"; + +const basePath = resolve(__dirname, ".."); + +describe("'wrangler dev' with WRANGLER_BUILD_CONDITIONS", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "c3-wrangler-init--from-dash-")); + }); + + it("should import from the `other` package export if that is in the conditions", async ({ + expect, + }) => { + execFileSync( + "node", + [wranglerEntryPath, "deploy", "--dry-run", `--outdir=${tempDir}`], + { + env: { + ...process.env, + WRANGLER_BUILD_CONDITIONS: "other,node,browser", + }, + cwd: basePath, + } + ); + expect(readFileSync(resolve(tempDir, "index.js"), "utf8")).toContain( + "isomorphic-random-example/src/other.js" + ); + }); + + it("should import from the `default` package export if the conditions are explicitly empty", async ({ + expect, + }) => { + execFileSync( + "node", + [wranglerEntryPath, "deploy", "--dry-run", `--outdir=${tempDir}`], + { + env: { + ...process.env, + WRANGLER_BUILD_CONDITIONS: "", + }, + cwd: basePath, + } + ); + expect(readFileSync(resolve(tempDir, "index.js"), "utf8")).toContain( + "isomorphic-random-example/src/default.js" + ); + }); +}); + +describe("'wrangler build' with WRANGLER_BUILD_PLATFORM", () => { + it("should import from node imports if platform is set to 'node'", ({ + expect, + }) => { + execFileSync( + "node", + [wranglerEntryPath, "deploy", "--dry-run", "--outdir=dist/node"], + { + env: { + ...process.env, + WRANGLER_BUILD_PLATFORM: "node", + }, + cwd: basePath, + } + ); + expect( + readFileSync(resolve(__dirname, "../dist/node/index.js"), "utf8") + ).toContain("isomorphic-random-example/src/node.js"); + }); + + it("should import from node imports if platform is set to 'browser'", ({ + expect, + }) => { + execFileSync( + "node", + [wranglerEntryPath, "deploy", "--dry-run", "--outdir=dist/browser"], + { + env: { + ...process.env, + WRANGLER_BUILD_PLATFORM: "browser", + }, + cwd: basePath, + } + ); + expect( + readFileSync(resolve(__dirname, "../dist/browser/index.js"), "utf8") + ).toContain("../isomorphic-random-example/src/workerd.mjs"); + }); +}); diff --git a/fixtures/worker-app/tests/https.test.ts b/fixtures/worker-app/tests/https.test.ts new file mode 100644 index 0000000..312f265 --- /dev/null +++ b/fixtures/worker-app/tests/https.test.ts @@ -0,0 +1,33 @@ +import { resolve } from "path"; +import { Agent, fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("'wrangler dev' starts HTTPS server", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + "--local-protocol=https", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("allows access over HTTPS", async ({ expect }) => { + const dispatcher = new Agent({ + // `wrangler dev` will generate a self-signed certificate. By default, + // Node will reject these. Therefore, we need to use a custom + // dispatcher that accepts "invalid" certificates. + connect: { rejectUnauthorized: false }, + }); + const response = await fetch(`https://${ip}:${port}/random`, { + dispatcher, + }); + expect(response.ok).toBe(true); + }); +}); diff --git a/fixtures/worker-app/tests/index.test.ts b/fixtures/worker-app/tests/index.test.ts new file mode 100644 index 0000000..f3bd1b5 --- /dev/null +++ b/fixtures/worker-app/tests/index.test.ts @@ -0,0 +1,212 @@ +import { resolve } from "path"; +import { setTimeout } from "timers/promises"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it, vi } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("'wrangler dev' correctly renders pages", () => { + let ip: string, + port: number, + stop: (() => Promise) | undefined, + getOutput: () => string; + + beforeAll(async () => { + ({ ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + [ + "--port=0", + "--inspector-port=0", + "--upstream-protocol=https", + "--host=prod.example.org", + ] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("renders ", async ({ expect }) => { + await vi.waitFor( + async () => { + // Note that the local protocol defaults to http + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain(`https://prod.example.org/`); + }, + { interval: 1000, timeout: 5000 } + ); + + // Wait up to 5s for all request logs to be flushed + for (let i = 0; i < 10; i++) { + if (getOutput().includes("end of request")) break; + await setTimeout(500); + } + + // Ensure `console.log()`s from startup and requests are shown + const output = getOutput(); + expect(output).toContain("startup log"); + expect(output).toContain("request log"); + + if (process.platform === "win32") { + // Check that the Windows warning is shown for the fake access violation error + expect(output).toContain( + "On Windows, this may be caused by an outdated Microsoft Visual C++ Redistributable library." + ); + } + + // check host on request in the Worker is as expected + expect(output).toContain(`host' => 'prod.example.org`); + + // Check logged strings are source mapped + expect(output).toMatch( + /Error: logged error one\n.+at logErrors.+fixtures\/worker-app\/src\/log\.ts:7:14/ + ); + expect(output).toMatch( + /Error: logged error two\n.+at logErrors.+fixtures\/worker-app\/src\/log\.ts:8:14/ + ); + expect(output).toMatch( + /Error: logged error three\n.+at logErrors.+fixtures\/worker-app\/src\/log\.ts:9:23/ + ); + expect(output).toMatch( + /Error: logged error four\\n' \+\n.+at logErrors.+fixtures\/worker-app\/src\/log\.ts:10:33/ + ); + + // Regression test for https://github.com/cloudflare/workers-sdk/issues/4668 + expect(output).toContain("some normal text to log"); + expect(output).toContain("text with at in the middle"); + expect(output).toContain("more text with at in the middle"); + }); + + it("renders pretty error after logging", async ({ expect }) => { + // Regression test for https://github.com/cloudflare/workers-sdk/issues/4715 + const response = await fetch(`http://${ip}:${port}/error`); + const text = await response.text(); + expect(text).toContain("Oops!"); + expect(response.headers.get("Content-Type")).toBe( + "text/html;charset=utf-8" + ); + }); + + it("uses `workerd` condition when bundling", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/random`); + const text = await response.text(); + expect(text).toMatch(/[0-9a-f]{16}/); // 8 hex bytes + }); + + it("passes through URL unchanged", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}//thing?a=1`, { + headers: { "X-Test-URL": "true" }, + }); + const text = await response.text(); + expect(text).toBe(`https://prod.example.org//thing?a=1`); + }); + + it("rewrites the Host and Origin headers appropriately", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/test`, { + // Pass in an Origin header to trigger the rewriting + headers: { Origin: `http://${ip}:${port}` }, + }); + const text = await response.text(); + expect(text).toContain(`HOST:prod.example.org`); + expect(text).toContain(`ORIGIN:https://prod.example.org`); + }); + + it("does not rewrite Origin header if one is not passed by the client", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/test`, {}); + const text = await response.text(); + expect(text).toContain(`HOST:prod.example.org`); + expect(text).toContain(`ORIGIN:null`); + }); + + it("does not rewrite Origin header if it not the same origin as the proxy Worker", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/test`, { + headers: { Origin: `http://foo.com` }, + }); + const text = await response.text(); + expect(text).toContain(`HOST:prod.example.org`); + expect(text).toContain(`ORIGIN:http://foo.com`); + }); + + it("rewrites response headers containing the emulated host", async ({ + expect, + }) => { + // This /redirect request will add a Location header that points to prod.example.com/foo + // But we should rewrite this back to that of the proxy. + const response = await fetch(`http://${ip}:${port}/redirect`, { + redirect: "manual", + }); + expect(response.status).toBe(302); + expect(await response.text()).toEqual(""); + expect(response.headers.get("Location")).toEqual( + `http://${ip}:${port}/foo` + ); + }); + + it("rewrites set-cookie headers to the hostname, not host", async ({ + expect, + }) => { + const response = await fetch(`http://${ip}:${port}/cookie`); + + expect(response.headers.getSetCookie()).toStrictEqual([ + `hello=world; Domain=${ip}`, + `hello2=world2; Domain=${ip}; Secure`, + ]); + }); + + it("has access to version_metadata binding", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/version_metadata`); + + await expect(response.json()).resolves.toMatchObject({ + id: expect.any(String), + tag: expect.any(String), + }); + }); + + it("passes through client content encoding", async ({ expect }) => { + // https://github.com/cloudflare/workers-sdk/issues/5246 + const response = await fetch(`http://${ip}:${port}/content-encoding`, { + headers: { "Accept-Encoding": "hello" }, + }); + expect(await response.json()).toStrictEqual({ + AcceptEncoding: "br, gzip", + clientAcceptEncoding: "hello", + }); + }); + + it("supports encoded responses", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/content-encoding/gzip`, { + headers: { "Accept-Encoding": "gzip" }, + }); + expect(await response.text()).toEqual("x".repeat(100)); + }); + + it("uses explicit resource management", async ({ expect }) => { + const response = await fetch( + `http://${ip}:${port}/explicit-resource-management` + ); + expect(await response.json()).toMatchInlineSnapshot(` + [ + "Connected", + "Connected", + "Sent hello", + "Sent goodbye", + "Disconnected asynchronously", + "Disconnected synchronously", + ] + `); + }); + + it("reads local dev vars from the .env file", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/env`); + const env = await response.text(); + expect(env).toBe(`"bar"`); + }); +}); diff --git a/fixtures/worker-app/tests/tsconfig.json b/fixtures/worker-app/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/worker-app/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/worker-app/tests/undrained-body.test.ts b/fixtures/worker-app/tests/undrained-body.test.ts new file mode 100644 index 0000000..18da5d6 --- /dev/null +++ b/fixtures/worker-app/tests/undrained-body.test.ts @@ -0,0 +1,44 @@ +import { resolve } from "path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("wrangler dev", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + // https://github.com/cloudflare/workers-sdk/issues/5095 + it("should not fail requests if the Worker does not drain the body", async ({ + expect, + }) => { + const COUNT = 30; + const requests: boolean[] = []; + const errors: string[] = []; + + const body = new Uint8Array(2_000); + for (let i = 0; i < COUNT; i++) { + const response = await fetch(`http://${ip}:${port}/random`, { + method: "POST", + body, + }); + requests.push(response.ok); + if (!response.ok) { + errors.push(await response.text()); + } + } + + expect(requests.length).toBe(COUNT); + expect(errors).toEqual([]); + expect(requests).toEqual(Array.from({ length: COUNT }).map((i) => true)); + }); +}); diff --git a/fixtures/worker-app/tsconfig.json b/fixtures/worker-app/tsconfig.json new file mode 100644 index 0000000..7480e11 --- /dev/null +++ b/fixtures/worker-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2020"], + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/worker-app/vitest.config.mts b/fixtures/worker-app/vitest.config.mts new file mode 100644 index 0000000..a2ea8a0 --- /dev/null +++ b/fixtures/worker-app/vitest.config.mts @@ -0,0 +1,17 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + // The three test files in this fixture each spawn their own + // `wrangler dev` against the same working directory, so they share + // the `.wrangler/state` SQLite cache. Running them in parallel hits + // `SQLITE_BUSY` intermittently and causes workerd to abort with + // "The Workers runtime failed to start", leaving tests to fail with + // ECONNREFUSED. Run the files serially to avoid the contention. + fileParallelism: false, + }, + }) +); diff --git a/fixtures/worker-app/wrangler.jsonc b/fixtures/worker-app/wrangler.jsonc new file mode 100644 index 0000000..ca2eeaa --- /dev/null +++ b/fixtures/worker-app/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "name": "worker-app", + "compatibility_date": "2022-03-31", + "main": "src/index.js", + "triggers": { + "crons": ["1 * * * *"], + }, + "version_metadata": { + "binding": "METADATA", + }, +} diff --git a/fixtures/worker-logs/package.json b/fixtures/worker-logs/package.json new file mode 100644 index 0000000..8123c78 --- /dev/null +++ b/fixtures/worker-logs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/worker-logs", + "private": true, + "scripts": { + "check:type": "tsc", + "dev": "pnpm dev.module", + "dev.module": "wrangler dev -c wrangler.module.jsonc", + "dev.service": "wrangler dev -c wrangler.service.jsonc", + "start": "pnpm dev.module", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "typescript": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/worker-logs/src/module.js b/fixtures/worker-logs/src/module.js new file mode 100644 index 0000000..06008fe --- /dev/null +++ b/fixtures/worker-logs/src/module.js @@ -0,0 +1,28 @@ +export default { + async fetch(request, env) { + const response = new Response("Hello"); + + const customMessage = request.headers.get("x-custom-message"); + if (customMessage) { + if (customMessage === "%__VERY_VERY_LONG_MESSAGE_%") { + // We can't simply pass a huge long message as a header thus + // why a placeholder is used here + console.log("<<<<< " + "z".repeat(2 ** 20) + " >>>>>"); + } else { + console.log("<<<<< " + customMessage + " >>>>>"); + } + return response; + } + + console.log("<<<<< console.log() message >>>>>"); + console.warn("<<<<< console.warning() message >>>>>"); + console.error("<<<<< console.error() message >>>>>"); + console.debug("<<<<< console.debug() message >>>>>"); + console.info("<<<<< console.info() message >>>>>"); + + process.stderr.write("<<<<< stderr.write() message >>>>>\n"); + process.stdout.write("<<<<< stdout.write() message >>>>>\n"); + + return response; + }, +}; diff --git a/fixtures/worker-logs/src/service.js b/fixtures/worker-logs/src/service.js new file mode 100644 index 0000000..8fa27db --- /dev/null +++ b/fixtures/worker-logs/src/service.js @@ -0,0 +1,12 @@ +async function handler(request) { + console.log("<<<<< console.log() message >>>>>"); + console.warn("<<<<< console.warning() message >>>>>"); + console.error("<<<<< console.error() message >>>>>"); + console.debug("<<<<< console.debug() message >>>>>"); + console.info("<<<<< console.info() message >>>>>"); + return new Response("Hello"); +} + +addEventListener("fetch", (event) => { + event.respondWith(handler(event.request)); +}); diff --git a/fixtures/worker-logs/tests/index.test.ts b/fixtures/worker-logs/tests/index.test.ts new file mode 100644 index 0000000..3bfc753 --- /dev/null +++ b/fixtures/worker-logs/tests/index.test.ts @@ -0,0 +1,345 @@ +import { stripVTControlCharacters } from "node:util"; +import { resolve } from "path"; +import { describe, onTestFinished, test, vi } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +/** + * Run a Worker, make a request to it, and capture the output logs. + * + * You can call the returned function repeatedly to get the currently captured logs. + * The Worker will be stopped automatically when the test finishes. + * + * @param type The type of worker: "module" or "service". + * @param extraArgs Additional command-line arguments to pass to `wrangler dev`. + * @param customMessage A custom message to include in the `x-custom-message` header. + * @param env Environment variables to set for the worker process + * @returns A function that returns the captured output logs when called + */ +async function getWranglerDevOutput( + type: "module" | "service", + extraArgs: string[] = [], + customMessage?: string, + env = {} +) { + const { ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + [ + `-c=wrangler.${type}.jsonc`, + "--port=0", + "--inspector-port=0", + ...extraArgs, + ], + env + ); + + onTestFinished(() => stop()); + + const request = new Request(`http://${ip}:${port}`); + if (customMessage) { + request.headers.set("x-custom-message", customMessage); + } + + const response = await fetch(request); + await response.text(); + + return () => { + const output = stripVTControlCharacters(getOutput()) + // Windows gets a different marker for ✘, so let's normalize it here + // so that these tests can be platform independent + .replaceAll("✘", "X") + // Let's also normalize Windows newlines + .replaceAll("\r\n", "\n"); + + // Let's filter out lines we're not interested in + const messages = Array.from(output.matchAll(/^.*<<<<<.*>>>>>$/gm)).flat(); + // Let's also sort the logs for more stability of the tests. + // Ideally we would want to test the log's ordering as well but that seems + // to cause flakes in the CI runs + return messages.sort(); + }; +} + +describe("'wrangler dev' correctly displays logs", () => { + describe("module workers", () => { + test("default behavior", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module"); + await vi.waitFor( + () => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< stderr.write() message >>>>>", + "<<<<< stdout.write() message >>>>>", + "X [ERROR] <<<<< console.error() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]), + { timeout: 5000 } + ); + }); + + test("with --log-level=log", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--log-level=log", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< stderr.write() message >>>>>", + "<<<<< stdout.write() message >>>>>", + "X [ERROR] <<<<< console.error() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=info", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--log-level=info", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.info() message >>>>>", + "X [ERROR] <<<<< console.error() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=warn", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--log-level=warn", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "X [ERROR] <<<<< console.error() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=error", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--log-level=error", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "X [ERROR] <<<<< console.error() message >>>>>", + ]) + ); + }); + + test("with --log-level=debug", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--log-level=debug", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< stderr.write() message >>>>>", + "<<<<< stdout.write() message >>>>>", + "X [ERROR] <<<<< console.error() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]) + ); + }); + + test('with WRANGLER_LOG="debug"', async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [], undefined, { + WRANGLER_LOG: "debug", + }); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< stderr.write() message >>>>>", + "<<<<< stdout.write() message >>>>>", + "X [ERROR] <<<<< console.error() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=none", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--log-level=none", + ]); + await vi.waitFor(() => expect(getOutput()).toEqual([])); + }); + + // the workerd structured logs follow this structure: + // {"timestamp":,"level":"","message":""} + // the following tests check for edge case scenario where the following + // structure could not get detected correctly + describe("edge case scenarios", () => { + test("base case", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [], "hello"); + await vi.waitFor(() => + expect(getOutput()).toEqual(["<<<<< hello >>>>>"]) + ); + }); + test("quotes in message", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [], 'hel"lo'); + await vi.waitFor(() => + expect(getOutput()).toEqual(['<<<<< hel"lo >>>>>']) + ); + }); + + test("braces in message", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [], "hel{}lo"); + await vi.waitFor(() => + expect(getOutput()).toEqual(["<<<<< hel{}lo >>>>>"]) + ); + }); + + test("a workerd structured message in the message", async ({ + expect, + }) => { + const getOutput = await getWranglerDevOutput( + "module", + [], + 'This is an example of a Workerd structured log: {"timestamp":1234567890,"level":"log","message":"Hello World!"}' + ); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + '<<<<< This is an example of a Workerd structured log: {"timestamp":1234567890,"level":"log","message":"Hello World!"} >>>>>', + ]) + ); + }); + + test("a very very very long message (that gets split in multiple chunks)", async ({ + expect, + }) => { + const getOutput = await getWranglerDevOutput( + "module", + [], + "%__VERY_VERY_LONG_MESSAGE_%" + ); + await vi.waitFor(() => + expect(getOutput()).toContain( + "<<<<< " + "z".repeat(2 ** 20) + " >>>>>" + ) + ); + }); + }); + }); + + // Note: service workers logs are handled differently from standard logs (and are built on top of + // inspector Runtime.consoleAPICalled events), they don't work as well as logs for module + // workers. Service workers are also deprecated so it's not a huge deal, the following + // tests are only here in place to make sure that the basic logging functionality of + // service workers does work + describe("service workers", () => { + test("default behavior", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service"); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.error() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=log", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service", [ + "--log-level=log", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.error() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=info", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service", [ + "--log-level=info", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.error() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=warn", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service", [ + "--log-level=warn", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.error() message >>>>>", + "<<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=error", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service", [ + "--log-level=error", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual(["<<<<< console.error() message >>>>>"]) + ); + }); + + test("with --log-level=debug", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service", [ + "--log-level=debug", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.error() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "<<<<< console.warning() message >>>>>", + ]) + ); + }); + + test("with --log-level=none", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("service", [ + "--log-level=none", + ]); + await vi.waitFor(() => expect(getOutput()).toEqual([])); + }); + }); + + describe("nodejs compat process v2", () => { + test("default behavior", async ({ expect }) => { + const getOutput = await getWranglerDevOutput("module", [ + "--compatibility-flags=enable_nodejs_process_v2", + "--compatibility-flags=nodejs_compat", + // Those flags are needed to enable the new process.v2 implementation + // See `getProcessOverrides` in `packages/unenv-preset/src/preset.ts` + "--compatibility-flags=fetch_iterable_type_support", + "--compatibility-flags=fetch_iterable_type_support_override_adjustment", + ]); + await vi.waitFor(() => + expect(getOutput()).toEqual([ + "<<<<< console.debug() message >>>>>", + "<<<<< console.info() message >>>>>", + "<<<<< console.log() message >>>>>", + "X [ERROR] <<<<< console.error() message >>>>>", + "stderr: <<<<< stderr.write() message >>>>>", + "stdout: <<<<< stdout.write() message >>>>>", + "▲ [WARNING] <<<<< console.warning() message >>>>>", + ]) + ); + }); + }); +}); diff --git a/fixtures/worker-logs/tsconfig.json b/fixtures/worker-logs/tsconfig.json new file mode 100644 index 0000000..5bccae9 --- /dev/null +++ b/fixtures/worker-logs/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2021", + "esModuleInterop": true, + "module": "preserve", + "lib": ["ES2021"], + "types": ["node"], + "skipLibCheck": true, + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests"] +} diff --git a/fixtures/worker-logs/turbo.json b/fixtures/worker-logs/turbo.json new file mode 100644 index 0000000..789f1de --- /dev/null +++ b/fixtures/worker-logs/turbo.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "test:ci": { + "env": [ + "VITEST", + "NODE_DEBUG", + "MINIFLARE_WORKERD_PATH", + "WRANGLER", + "WRANGLER_IMPORT", + "MINIFLARE_IMPORT", + "TEST_CLOUDFLARE_ACCOUNT_ID", + "TEST_CLOUDFLARE_API_TOKEN", + "WRANGLER_E2E_TEST_FILE" + ] + } + } +} diff --git a/fixtures/worker-logs/vitest.config.mts b/fixtures/worker-logs/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/worker-logs/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/worker-logs/wrangler.module.jsonc b/fixtures/worker-logs/wrangler.module.jsonc new file mode 100644 index 0000000..18d451c --- /dev/null +++ b/fixtures/worker-logs/wrangler.module.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-logs", + "compatibility_date": "2026-01-01", + "main": "src/module.js", + "compatibility_flags": ["nodejs_compat"], +} diff --git a/fixtures/worker-logs/wrangler.service.jsonc b/fixtures/worker-logs/wrangler.service.jsonc new file mode 100644 index 0000000..9507931 --- /dev/null +++ b/fixtures/worker-logs/wrangler.service.jsonc @@ -0,0 +1,5 @@ +{ + "name": "worker-logs", + "compatibility_date": "2022-03-31", + "main": "src/service.js", +} diff --git a/fixtures/worker-ts/CHANGELOG.md b/fixtures/worker-ts/CHANGELOG.md new file mode 100644 index 0000000..912ae7b --- /dev/null +++ b/fixtures/worker-ts/CHANGELOG.md @@ -0,0 +1,7 @@ +# worker-ts + +## 0.0.1 + +### Patch Changes + +- [#3588](https://github.com/cloudflare/workers-sdk/pull/3588) [`64631d8b`](https://github.com/cloudflare/workers-sdk/commit/64631d8b59572f49d65325d8f6fec098c5e912b9) Thanks [@penalosa](https://github.com/penalosa)! - fix: Preserve email handlers when applying middleware to user workers. diff --git a/fixtures/worker-ts/package.json b/fixtures/worker-ts/package.json new file mode 100644 index 0000000..ed385e0 --- /dev/null +++ b/fixtures/worker-ts/package.json @@ -0,0 +1,12 @@ +{ + "name": "@fixture/worker-typescript", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/worker-ts/src/index.ts b/fixtures/worker-ts/src/index.ts new file mode 100644 index 0000000..6a010a4 --- /dev/null +++ b/fixtures/worker-ts/src/index.ts @@ -0,0 +1,34 @@ +/** + * Welcome to Cloudflare Workers! This is your first worker. + * + * - Run `wrangler dev src/index.ts` in your terminal to start a development server + * - Open a browser tab at http://localhost:8787/ to see your worker in action + * - Run `wrangler deploy src/index.ts --name my-worker` to deploy your worker + * + * Learn more at https://developers.cloudflare.com/workers/ + */ +export interface Env { + // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ + // MY_KV_NAMESPACE: KVNamespace; + // + // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ + // MY_DURABLE_OBJECT: DurableObjectNamespace; + // + // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ + // MY_BUCKET: R2Bucket; + // + // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/ + // MY_SERVICE: Fetcher; +} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const url = new URL(request.url); + if (url.pathname === "/error") throw new Error("Hello Error"); + return new Response("Hello World!"); + }, +}; diff --git a/fixtures/worker-ts/tsconfig.json b/fixtures/worker-ts/tsconfig.json new file mode 100644 index 0000000..2431bac --- /dev/null +++ b/fixtures/worker-ts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "types": ["@cloudflare/workers-types/experimental"], + "noEmit": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/fixtures/worker-ts/wrangler.jsonc b/fixtures/worker-ts/wrangler.jsonc new file mode 100644 index 0000000..25cff9c --- /dev/null +++ b/fixtures/worker-ts/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "worker-ts", + "main": "src/index.ts", + "compatibility_date": "2023-05-04", +} diff --git a/fixtures/worker-with-resources/package.json b/fixtures/worker-with-resources/package.json new file mode 100644 index 0000000..180b314 --- /dev/null +++ b/fixtures/worker-with-resources/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fixture/worker-with-resources", + "private": true, + "scripts": { + "cf-typegen": "wrangler types --no-include-runtime && wrangler types ./worker-b/worker-configuration.d.ts -c ./worker-b/wrangler.jsonc --no-include-runtime", + "deploy": "wrangler deploy", + "start": "X_LOCAL_EXPLORER=true wrangler dev", + "start:worker-b": "X_LOCAL_EXPLORER=true wrangler dev -c worker-b/wrangler.jsonc", + "test:ci": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:^", + "@cloudflare/workers-types": "catalog:default", + "miniflare": "workspace:*", + "typescript": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/worker-with-resources/src/index.ts b/fixtures/worker-with-resources/src/index.ts new file mode 100644 index 0000000..125d2ec --- /dev/null +++ b/fixtures/worker-with-resources/src/index.ts @@ -0,0 +1,371 @@ +import { DurableObject, WorkflowEntrypoint } from "cloudflare:workers"; +import type { WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + switch (url.pathname) { + // KV routes + case "/kv/get": { + const keyToGet = url.searchParams.get("key") ?? "default"; + const value = await env.KV.get(keyToGet); + return new Response(value || "null"); + } + case "/kv/put": { + const keyToSet = url.searchParams.get("key") ?? "default"; + const val = url.searchParams.get("value"); + await env.KV.put(keyToSet, val); + return new Response("OK"); + } + case "/kv/delete": { + const keyToDelete = url.searchParams.get("key") ?? "default"; + await env.KV.delete(keyToDelete); + return new Response(`Deleted key ${keyToDelete} from KV`); + } + case "/kv/seed": { + await Promise.all(SEED_DATA.map(([k, v]) => env.KV.put(k, v))); + return new Response(`Seeded ${SEED_DATA.length} KV entries`); + } + + // R2 routes + case "/r2/seed": { + await Promise.all( + R2_SEED_DATA.map(({ key, content, contentType, customMetadata }) => + env.BUCKET.put(key, content, { + httpMetadata: { contentType }, + customMetadata, + }) + ) + ); + return new Response(`Seeded ${R2_SEED_DATA.length} R2 objects`); + } + + // D1 database route + case "/d1": { + await env.DB.exec(` + DROP TABLE IF EXISTS users; + CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'); + `); + return new Response("OK"); + } + // Durable Object SQLite routes + case "/do": { + const id = url.searchParams.get("id") || "default"; + const doId = env.DO.idFromName(id); + const stub = env.DO.get(doId); + return stub.fetch(request); + } + } + return new Response("Hello World!"); + }, +}; + +export class MyDurableObject extends DurableObject { + async fetch(_request: Request): Promise { + this.ctx.storage.sql.exec(` + DROP TABLE IF EXISTS users; + CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'); + `); + return new Response("OK"); + } +} + +interface WorkflowParams { + name: string; +} + +export class MyWorkflow extends WorkflowEntrypoint { + async run(event: WorkflowEvent, step: WorkflowStep) { + const name = event.payload.name ?? "World"; + + const greeting = await step.do("greet", async () => `Hello, ${name}!`); + + await step.sleep("wait", "1 second"); + + const result = await step.do("finalize", async () => { + return `${greeting} Workflow complete.`; + }); + + return result; + } +} + +const SEED_DATA: [string, string][] = [ + ["greeting", "Hello, World!"], + ["counter", "42"], + ["config:theme", "dark"], + ["config:language", "en-US"], + ["config:timezone", "America/New_York"], + ["config:debug", "false"], + ["config:max-retries", "3"], + ["feature:dark-mode", "enabled"], + ["feature:beta-ui", "disabled"], + ["feature:notifications", "enabled"], + ["api:rate-limit", "1000"], + ["api:timeout-ms", "30000"], + ["api:base-url", "https://api.example.com/v1"], + [ + "user:1", + JSON.stringify({ + id: 1, + name: "Alice", + email: "alice@example.com", + role: "admin", + }), + ], + [ + "user:2", + JSON.stringify({ + id: 2, + name: "Bob", + email: "bob@example.com", + role: "user", + }), + ], + [ + "user:3", + JSON.stringify({ + id: 3, + name: "Charlie", + email: "charlie@example.com", + role: "user", + }), + ], + [ + "user:4", + JSON.stringify({ + id: 4, + name: "Diana", + email: "diana@example.com", + role: "moderator", + }), + ], + [ + "user:5", + JSON.stringify({ + id: 5, + name: "Eve", + email: "eve@example.com", + role: "user", + }), + ], + [ + "session:abc123", + JSON.stringify({ userId: 1, expiresAt: "2025-12-31T23:59:59Z" }), + ], + [ + "session:def456", + JSON.stringify({ userId: 2, expiresAt: "2025-06-15T12:00:00Z" }), + ], + [ + "session:ghi789", + JSON.stringify({ userId: 3, expiresAt: "2025-03-01T08:30:00Z" }), + ], + ["cache:homepage", "

Welcome

"], + ["cache:about", "

About Us

"], + ["cache:contact", "

Contact

"], + [ + "product:1", + JSON.stringify({ id: 1, name: "Widget", price: 9.99, stock: 150 }), + ], + [ + "product:2", + JSON.stringify({ id: 2, name: "Gadget", price: 24.99, stock: 75 }), + ], + [ + "product:3", + JSON.stringify({ id: 3, name: "Gizmo", price: 14.99, stock: 200 }), + ], + [ + "product:4", + JSON.stringify({ id: 4, name: "Thingamajig", price: 39.99, stock: 30 }), + ], + [ + "product:5", + JSON.stringify({ id: 5, name: "Doohickey", price: 19.99, stock: 100 }), + ], + ["stats:visits", "123456"], + ["stats:unique-users", "45678"], + ["stats:page-views", "987654"], + ["stats:bounce-rate", "0.35"], + ["stats:avg-session", "4m 32s"], + [ + "log:error:1", + JSON.stringify({ + level: "error", + message: "Connection timeout", + timestamp: "2025-01-15T10:30:00Z", + }), + ], + [ + "log:error:2", + JSON.stringify({ + level: "error", + message: "Invalid token", + timestamp: "2025-01-15T11:45:00Z", + }), + ], + [ + "log:warn:1", + JSON.stringify({ + level: "warn", + message: "Rate limit approaching", + timestamp: "2025-01-15T12:00:00Z", + }), + ], + ["queue:pending", "15"], + ["queue:processing", "3"], + ["queue:completed", "1247"], + ["queue:failed", "12"], + ["special/key/with/slashes", "value with slashes in key"], + ["key with spaces", "value for spaced key"], + ["key.with.dots", "dotted key value"], + ["UPPERCASE_KEY", "uppercase key value"], + ["mixedCase_Key-123", "mixed case key value"], + [ + "long-value-example", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + ], + ["emoji-key-🔑", "value with emoji in key"], + ["empty-value", ""], + ["null-value", null], + ["boolean-true", "true"], + ["boolean-false", "false"], + ["number-integer", "42"], + ["number-float", "3.14159"], + ["number-negative", "-273.15"], + ["large-key-1", "x".repeat(10 * 1024 * 1024)], +]; + +interface R2SeedItem { + key: string; + content: string | ArrayBuffer; + contentType: string; + customMetadata?: Record; +} + +const R2_SEED_DATA: R2SeedItem[] = [ + // Root level files + { + key: "readme.txt", + content: "Welcome to the R2 bucket! This is a sample readme file.", + contentType: "text/plain", + }, + { + key: "config.json", + content: JSON.stringify( + { version: "1.0.0", environment: "development" }, + null, + 2 + ), + contentType: "application/json", + customMetadata: { author: "admin", created: "2025-01-15" }, + }, + + // Images folder + { + key: "images/logo.svg", + content: + '', + contentType: "image/svg+xml", + }, + { + key: "images/banner.svg", + content: + '', + contentType: "image/svg+xml", + }, + { + key: "images/icons/home.svg", + content: + '', + contentType: "image/svg+xml", + }, + { + key: "images/icons/settings.svg", + content: + '', + contentType: "image/svg+xml", + }, + + // Documents folder + { + key: "documents/report.txt", + content: + "Annual Report 2024\n\nThis is a sample annual report with important business metrics.", + contentType: "text/plain", + customMetadata: { department: "finance", year: "2024" }, + }, + { + key: "documents/notes.md", + content: + "# Meeting Notes\n\n## Action Items\n- Review budget\n- Update roadmap\n- Schedule follow-up", + contentType: "text/markdown", + }, + { + key: "documents/data.csv", + content: "id,name,value\n1,Alpha,100\n2,Beta,200\n3,Gamma,300", + contentType: "text/csv", + }, + + // Data folder with nested structure + { + key: "data/users.json", + content: JSON.stringify( + [ + { id: 1, name: "Alice", role: "admin" }, + { id: 2, name: "Bob", role: "user" }, + ], + null, + 2 + ), + contentType: "application/json", + }, + { + key: "data/backup/2024/january.json", + content: JSON.stringify({ month: "January", records: 150 }), + contentType: "application/json", + customMetadata: { backup: "true", period: "2024-01" }, + }, + { + key: "data/backup/2024/february.json", + content: JSON.stringify({ month: "February", records: 175 }), + contentType: "application/json", + customMetadata: { backup: "true", period: "2024-02" }, + }, + + // Logs folder + { + key: "logs/access.log", + content: + "2025-01-15 10:00:00 GET /api/users 200\n2025-01-15 10:01:00 POST /api/login 200\n2025-01-15 10:02:00 GET /api/products 200", + contentType: "text/plain", + }, + { + key: "logs/error.log", + content: + "2025-01-15 09:30:00 ERROR Connection timeout\n2025-01-15 09:45:00 ERROR Invalid token", + contentType: "text/plain", + }, + + // Assets with various content types + { + key: "assets/styles.css", + content: + "body { font-family: sans-serif; }\n.container { max-width: 1200px; margin: 0 auto; }", + contentType: "text/css", + }, + { + key: "assets/script.js", + content: 'console.log("Hello from R2!");\nfunction init() { return true; }', + contentType: "application/javascript", + }, + { + key: "assets/template.html", + content: + "\nTemplate

Hello

", + contentType: "text/html", + }, +]; diff --git a/fixtures/worker-with-resources/tests/index.test.ts b/fixtures/worker-with-resources/tests/index.test.ts new file mode 100644 index 0000000..4930b1f --- /dev/null +++ b/fixtures/worker-with-resources/tests/index.test.ts @@ -0,0 +1,199 @@ +import { resolve } from "path"; +import { CorePaths } from "miniflare"; +import { afterAll, assert, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +const EXPLORER_API_PATH = `${CorePaths.EXPLORER}/api`; + +describe("local explorer", () => { + let ip: string; + let port: number; + let stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + it(`returns local explorer API response for ${EXPLORER_API_PATH}`, async ({ + expect, + }) => { + const response = await fetch( + `http://${ip}:${port}${EXPLORER_API_PATH}/storage/kv/namespaces` + ); + expect(response.headers.get("Content-Type")).toBe("application/json"); + const json = await response.json(); + expect(json).toMatchObject({ + errors: [], + messages: [], + result: [ + { + id: "KV", + title: "KV", + }, + { + id: "some-kv-id", + title: "KV_WITH_ID", + }, + { + id: "worker-b-kv-id", + title: "KV_B", + }, + ], + result_info: { + count: 3, + }, + success: true, + }); + }); + + it("returns worker response for normal requests", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toBe("Hello World!"); + }); + + it(`serves UI index.html at ${CorePaths.EXPLORER}`, async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}${CorePaths.EXPLORER}`); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/html; charset=utf-8" + ); + const text = await response.text(); + expect(text).toContain(""); + expect(text).toContain("Cloudflare Local Explorer"); + }); + + it(`serves UI assets at ${CorePaths.EXPLORER}/assets/*`, async ({ + expect, + }) => { + // First get index.html to find the actual asset paths + const indexResponse = await fetch( + `http://${ip}:${port}${CorePaths.EXPLORER}` + ); + const html = await indexResponse.text(); + + // Extract JS asset path from the HTML + // The HTML looks like: + + + + + + + diff --git a/fixtures/workers-with-assets-spa/public/shadowed-by-asset.txt b/fixtures/workers-with-assets-spa/public/shadowed-by-asset.txt new file mode 100644 index 0000000..1190b77 --- /dev/null +++ b/fixtures/workers-with-assets-spa/public/shadowed-by-asset.txt @@ -0,0 +1 @@ +i'm some text! \ No newline at end of file diff --git a/fixtures/workers-with-assets-spa/src/index.ts b/fixtures/workers-with-assets-spa/src/index.ts new file mode 100644 index 0000000..f1cf871 --- /dev/null +++ b/fixtures/workers-with-assets-spa/src/index.ts @@ -0,0 +1,24 @@ +export default { + async fetch(request: Request): Promise { + const url = new URL(request.url); + if (url.pathname === "/api/math") { + return new Response(`1 + 1 = ${1 + 1}`); + } + if (url.pathname === "/api/json") { + return Response.json({ hello: "world" }); + } + if (url.pathname === "/api/html") { + return new Response("

Hello, world!

", { + headers: { "Content-Type": "text/html" }, + }); + } + if (url.pathname === "/shadowed-by-spa") { + return new Response("nope"); + } + if (url.pathname === "/shadowed-by-asset.txt") { + return new Response("nope"); + } + + return new Response("nope", { status: 404 }); + }, +}; diff --git a/fixtures/workers-with-assets-spa/tests/__image_snapshots__/index-test-ts-workers-assets-spa-renders-the-homepage-in-a-browser-correctly-1-snap.png b/fixtures/workers-with-assets-spa/tests/__image_snapshots__/index-test-ts-workers-assets-spa-renders-the-homepage-in-a-browser-correctly-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..d1b8b3d49f777beb6545048bc1bbb010e9b3e18c GIT binary patch literal 37988 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

H69>AA?L0L7dp=J*i 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 literal 0 HcmV?d00001 diff --git a/fixtures/workers-with-assets-spa/tests/index.test.ts b/fixtures/workers-with-assets-spa/tests/index.test.ts new file mode 100644 index 0000000..a559c06 --- /dev/null +++ b/fixtures/workers-with-assets-spa/tests/index.test.ts @@ -0,0 +1,267 @@ +import { resolve } from "node:path"; +import { toMatchImageSnapshot } from "jest-image-snapshot"; +import { Browser, chromium } from "playwright-chromium"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Workers + Assets + SPA", () => { + let ip: string, + port: number, + stop: (() => Promise) | undefined, + getOutput: () => string; + let browser: Browser | undefined; + + beforeAll(async () => { + ({ ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + )); + + browser = await chromium.launch({ + headless: !process.env.VITE_DEBUG_SERVE, + args: process.env.CI + ? ["--no-sandbox", "--disable-setuid-sandbox"] + : undefined, + }); + }); + + afterAll(async () => { + await stop?.(); + await browser?.close(); + }); + + it("renders the homepage in a browser correctly", async ({ expect }) => { + expect.extend({ toMatchImageSnapshot }); + + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + await page.goto("/"); + if (process.platform === "darwin") { + // different platforms render the page differently (fonts?) + expect(await page.screenshot()).toMatchImageSnapshot({ + failureThreshold: 0.02, + failureThresholdType: "percent", + }); + } + + const mathResultLocator = page.getByText("1 + 1 = 2"); + await mathResultLocator.waitFor({ state: "attached" }); + expect(await mathResultLocator.getAttribute("id")).toBe(`math-result`); + + const jsonResultLocator = page.getByText( + JSON.stringify({ hello: "world" }, null, 2) + ); + await jsonResultLocator.waitFor({ state: "attached" }); + expect(await jsonResultLocator.getAttribute("id")).toBe(`json-result`); + + const htmlResultLocator = page.getByText("Hello, world!"); + await htmlResultLocator.waitFor({ state: "attached" }); + const parentHtmlResultLocator = htmlResultLocator.locator(".."); + expect(await parentHtmlResultLocator.getAttribute("id")).toBe( + "html-result" + ); + }); + + it("navigates soft page navigations correctly", async ({ expect }) => { + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + await page.goto("/"); + + const homepageLink = page.getByText("/ HARD SOFT").getByText("SOFT"); + expect(await homepageLink.getAttribute("href")).toBe("/"); + await homepageLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/`); + const homepageHeader = page.getByRole("heading", { name: "Homepage" }); + await homepageHeader.waitFor({ state: "attached" }); + + const blogLink = page.getByText("/blog HARD SOFT").getByText("SOFT"); + expect(await blogLink.getAttribute("href")).toBe("/blog"); + await blogLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/blog`); + const blogTitleLocator = page.getByRole("heading", { name: "Blog" }); + await blogTitleLocator.waitFor({ state: "attached" }); + + const blogSlugInput = page.getByRole("textbox"); + blogSlugInput.fill("/blog/some-slug-here"); + + const blogSlugLink = page.getByRole("button", { name: "SOFT load" }); + await blogSlugLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/blog/some-slug-here`); + const blogSlugTitleLocator = page.getByRole("heading", { + name: "Blog | some-slug-here", + }); + await blogSlugTitleLocator.waitFor({ state: "attached" }); + + const blogRandomLink = page + .getByText("/blog/random HARD SOFT") + .getByText("SOFT"); + expect(await blogRandomLink.getAttribute("href")).toBe("/blog/random"); + await blogRandomLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/blog/random`); + const blogRandomTitleLocator = page.getByRole("heading", { + name: "Blog | random", + }); + await blogRandomTitleLocator.waitFor({ state: "attached" }); + + const shadowedByAssetLink = page + .getByText("/shadowed-by-asset.txt HARD SOFT") + .getByText("SOFT"); + expect(await shadowedByAssetLink.getAttribute("href")).toBe( + "/shadowed-by-asset.txt" + ); + await shadowedByAssetLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/shadowed-by-asset.txt`); + const shadowedByAssetHeader = page.getByRole("heading", { + name: "404 page!", + }); + await shadowedByAssetHeader.waitFor({ state: "attached" }); + + const shadowedBySpaLink = page + .getByText("/shadowed-by-spa HARD SOFT") + .getByText("SOFT"); + expect(await shadowedBySpaLink.getAttribute("href")).toBe( + "/shadowed-by-spa" + ); + await shadowedBySpaLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/shadowed-by-spa`); + const shadowedBySpaHeader = page.getByRole("heading", { + name: "Shadowed by SPA!", + }); + await shadowedBySpaHeader.waitFor({ state: "attached" }); + + const mathLink = page.getByText("/api/math HARD SOFT").getByText("SOFT"); + expect(await mathLink.getAttribute("href")).toBe("/api/math"); + await mathLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/api/math`); + const mathHeader = page.getByRole("heading", { + name: "404 page!", + }); + await mathHeader.waitFor({ state: "attached" }); + }); + + it("navigates hard navigations correctly", async ({ expect }) => { + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + await page.goto("/"); + + const homepageLink = page.getByText("/ HARD SOFT").getByText("HARD"); + expect(await homepageLink.getAttribute("href")).toBe("/"); + await homepageLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/`); + const homepageHeader = page.getByRole("heading", { name: "Homepage" }); + await homepageHeader.waitFor({ state: "attached" }); + + const blogLink = page.getByText("/blog HARD SOFT").getByText("HARD"); + expect(await blogLink.getAttribute("href")).toBe("/blog"); + await blogLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/blog`); + const blogTitleLocator = page.getByRole("heading", { name: "Blog" }); + await blogTitleLocator.waitFor({ state: "attached" }); + + expect( + getOutput().match( + /GET \/blog 200 OK \(.*\) `Sec-Fetch-Mode: navigate` header present - using `not_found_handling` behavior/ + ) + ).toBeTruthy(); + + const blogSlugInput = page.getByRole("textbox"); + blogSlugInput.fill("/blog/some-slug-here"); + + const blogSlugLink = page.getByRole("link", { name: "HARD load" }); + await blogSlugLink.click(); + expect(page.url()).toBe(`http://${ip}:${port}/blog/some-slug-here`); + const blogSlugTitleLocator = page.getByRole("heading", { + name: "Blog | some-slug-here", + }); + await blogSlugTitleLocator.waitFor({ state: "attached" }); + + const blogRandomLink = page + .getByText("/blog/random HARD SOFT") + .getByText("HARD"); + expect(await blogRandomLink.getAttribute("href")).toBe("/blog/random"); + await blogRandomLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/blog/random`); + const blogRandomTitleLocator = page.getByRole("heading", { + name: "Blog | random", + }); + await blogRandomTitleLocator.waitFor({ state: "attached" }); + + const shadowedByAssetLink = page + .getByText("/shadowed-by-asset.txt HARD SOFT") + .getByText("HARD"); + expect(await shadowedByAssetLink.getAttribute("href")).toBe( + "/shadowed-by-asset.txt" + ); + await shadowedByAssetLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/shadowed-by-asset.txt`); + expect(await page.content()).toContain("i'm some text!"); + + await page.goBack(); + + const shadowedBySpaLink = page + .getByText("/shadowed-by-spa HARD SOFT") + .getByText("HARD"); + expect(await shadowedBySpaLink.getAttribute("href")).toBe( + "/shadowed-by-spa" + ); + await shadowedBySpaLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/shadowed-by-spa`); + const shadowedBySpaHeader = page.getByRole("heading", { + name: "Shadowed by SPA!", + }); + await shadowedBySpaHeader.waitFor({ state: "attached" }); + + const mathLink = page.getByText("/api/math HARD SOFT").getByText("HARD"); + expect(await mathLink.getAttribute("href")).toBe("/api/math"); + await mathLink.click(); + + expect(page.url()).toBe(`http://${ip}:${port}/api/math`); + const mathHeader = page.getByRole("heading", { + name: "404 page!", + }); + await mathHeader.waitFor({ state: "attached" }); + }); + + it("direct fetches don't look like SPA requests", async ({ expect }) => { + const homepageResponse = await fetch(`http://${ip}:${port}/`); + expect(await homepageResponse.text()).toContain("Homepage"); + + const blogResponse = await fetch(`http://${ip}:${port}/blog`); + expect(await blogResponse.text()).toBe("nope"); + + const shadowedByAssetResponse = await fetch( + `http://${ip}:${port}/shadowed-by-asset.txt` + ); + expect(await shadowedByAssetResponse.text()).toBe("i'm some text!"); + + const mathResponse = await fetch(`http://${ip}:${port}/api/math`); + expect(await mathResponse.text()).toBe("1 + 1 = 2"); + }); +}); diff --git a/fixtures/workers-with-assets-spa/tests/tsconfig.json b/fixtures/workers-with-assets-spa/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/workers-with-assets-spa/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/workers-with-assets-spa/tsconfig.json b/fixtures/workers-with-assets-spa/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/workers-with-assets-spa/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/workers-with-assets-spa/vitest.config.mts b/fixtures/workers-with-assets-spa/vitest.config.mts new file mode 100644 index 0000000..472d8fe --- /dev/null +++ b/fixtures/workers-with-assets-spa/vitest.config.mts @@ -0,0 +1,14 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + // The `runWranglerDev` helper will wait up to 50 secs for Wrangler to boot up + // The `chromium.launch` helper will wait up to 30 secs for the browser to boot up. + hookTimeout: 50_000, + testTimeout: 50_000, + }, + }) +); diff --git a/fixtures/workers-with-assets-spa/wrangler.jsonc b/fixtures/workers-with-assets-spa/wrangler.jsonc new file mode 100644 index 0000000..d949bcb --- /dev/null +++ b/fixtures/workers-with-assets-spa/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "name": "worker-with-assets-spa", + "main": "./src/index.ts", + "compatibility_date": "2025-03-11", + "compatibility_flags": ["assets_navigation_prefers_asset_serving"], + "assets": { + "directory": "./public/", + "binding": "ASSETS", + "not_found_handling": "single-page-application", + "run_worker_first": false, + }, + "routes": ["example.com"], +} diff --git a/fixtures/workers-with-assets-static-routing/package.json b/fixtures/workers-with-assets-static-routing/package.json new file mode 100644 index 0000000..965c116 --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/package.json @@ -0,0 +1,23 @@ +{ + "name": "@fixture/workers-with-assets-static-routing", + "private": true, + "scripts": { + "check:type": "tsc", + "dev": "wrangler dev", + "dev:spa": "wrangler dev -c spa.wrangler.jsonc", + "pretest:ci": "pnpm playwright install chromium", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./test/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "@types/node": "catalog:default", + "playwright-chromium": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/workers-with-assets-static-routing/public/static/page.html b/fixtures/workers-with-assets-static-routing/public/static/page.html new file mode 100644 index 0000000..c61edeb --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/public/static/page.html @@ -0,0 +1 @@ +

A normal asset

diff --git a/fixtures/workers-with-assets-static-routing/public/worker/asset.html b/fixtures/workers-with-assets-static-routing/public/worker/asset.html new file mode 100644 index 0000000..1052ed1 --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/public/worker/asset.html @@ -0,0 +1 @@ +

Hello, I'm an asset!

diff --git a/fixtures/workers-with-assets-static-routing/public/worker/worker-runs.html b/fixtures/workers-with-assets-static-routing/public/worker/worker-runs.html new file mode 100644 index 0000000..57c660b --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/public/worker/worker-runs.html @@ -0,0 +1 @@ +

Hello, I'm an asset at /worker/worker-runs.html!

diff --git a/fixtures/workers-with-assets-static-routing/spa-assets/index.html b/fixtures/workers-with-assets-static-routing/spa-assets/index.html new file mode 100644 index 0000000..b696b5e --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/spa-assets/index.html @@ -0,0 +1,14 @@ + + + + I'm an index.html for a SPA + + +

Here I am, at /!

+ + + diff --git a/fixtures/workers-with-assets-static-routing/spa.wrangler.jsonc b/fixtures/workers-with-assets-static-routing/spa.wrangler.jsonc new file mode 100644 index 0000000..f1472a2 --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/spa.wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "name": "workers-with-assets-static-routing", + "main": "src/index.ts", + "compatibility_date": "2025-05-20", + "assets": { + "binding": "ASSETS", + "directory": "./spa-assets", + "not_found_handling": "single-page-application", + "run_worker_first": ["/api/*", "!/api/asset"], + }, +} diff --git a/fixtures/workers-with-assets-static-routing/src/index.ts b/fixtures/workers-with-assets-static-routing/src/index.ts new file mode 100644 index 0000000..5580077 --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/src/index.ts @@ -0,0 +1,31 @@ +export type Env = { + ASSETS: Fetcher; +}; + +export default { + async fetch(request, env, ctx): Promise { + const { pathname } = new URL(request.url); + + // api routes + if (pathname.startsWith("/api/")) { + return Response.json({ some: ["json", "response"] }); + } + + // asset middleware + const assetResp = await env.ASSETS.fetch(request); + if (assetResp.ok) { + let text = await assetResp.text(); + text = text.replace( + "I'm an asset", + "I'm an asset (and was intercepted by the User Worker)" + ); + return new Response(text, { + headers: assetResp.headers, + status: assetResp.status, + }); + } + + // default handling + return new Response("404 from the User Worker", { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/fixtures/workers-with-assets-static-routing/test/index.test.ts b/fixtures/workers-with-assets-static-routing/test/index.test.ts new file mode 100644 index 0000000..91a445c --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/test/index.test.ts @@ -0,0 +1,177 @@ +import { resolve } from "node:path"; +import { Browser, chromium } from "playwright-chromium"; +import { afterAll, beforeAll, describe, it } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("[Workers + Assets] static routing", () => { + describe("static routing behavior", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should serve assets when they exist for a path", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/static/page`); + expect(response.status).toBe(200); + expect(await response.text()).toContain(`

A normal asset

`); + }); + + it("should run the worker when no assets exist for a path", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/`); + expect(response.status).toBe(404); + expect(await response.text()).toContain(`404 from the User Worker`); + }); + + it("should run the worker when a positive run_worker_first rule matches", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/worker/worker-runs`); + expect(response.status).toBe(200); + expect(await response.text()).toContain( + `

Hello, I'm an asset (and was intercepted by the User Worker) at /worker/worker-runs.html!

` + ); + }); + + it("should serve a 404 when a negative run_worker_first rule matches", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/missing-asset`); + expect(response.status).toBe(404); + expect(await response.text()).toEqual(""); + }); + + it("should serve an asset when both a positive and negative (asset) run_worker_first matches", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/worker/asset`); + expect(response.status).toBe(200); + expect(await response.text()).toContain(`

Hello, I'm an asset!

`); + }); + }); + + describe("static routing + SPA behavior", async () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "-c=spa.wrangler.jsonc", + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + describe("browser navigation", () => { + let browser: Browser | undefined; + + beforeAll(async () => { + browser = await chromium.launch({ + headless: !process.env.VITE_DEBUG_SERVE, + args: process.env.CI + ? ["--no-sandbox", "--disable-setuid-sandbox"] + : undefined, + }); + }, 40_000); + + it("renders the root with index.html", async ({ expect }) => { + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + await page.goto("/"); + expect(await page.getByRole("heading").innerText()).toBe( + "Here I am, at /!" + ); + }); + + it("renders another path with index.html", async ({ expect }) => { + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + await page.goto("/some/page"); + expect(await page.getByRole("heading").innerText()).toBe( + "Here I am, at /some/page!" + ); + }); + + it("renders an include path with the User worker", async ({ expect }) => { + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + const response = await page.goto("/api/route"); + expect(response?.headers()).toHaveProperty( + "content-type", + "application/json" + ); + expect(await page.content()).toContain(`{"some":["json","response"]}`); + }); + + it("renders an exclude path with index.html", async ({ expect }) => { + if (!browser) { + throw new Error("Browser couldn't be initialized"); + } + + const page = await browser.newPage({ + baseURL: `http://${ip}:${port}`, + }); + await page.goto("/api/asset"); + expect(await page.getByRole("heading").innerText()).toBe( + "Here I am, at /api/asset!" + ); + }); + }); + + describe("non-browser navigation", () => { + it("renders the root with index.html", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}`); + expect(response.status).toBe(200); + expect(await response.text()).toContain(`I'm an index.html for a SPA`); + }); + + it("renders another path with index.html", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/some/page`); + expect(response.status).toBe(200); + expect(await response.text()).toContain(`I'm an index.html for a SPA`); + }); + + it("renders an include path with the User worker", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/api/route`); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toEqual( + "application/json" + ); + expect(await response.text()).toContain(`{"some":["json","response"]}`); + }); + + it("renders an exclude path with index.html", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/api/asset`); + expect(response.status).toBe(200); + expect(await response.text()).toContain(`I'm an index.html for a SPA`); + }); + }); + }); +}); diff --git a/fixtures/workers-with-assets-static-routing/test/tsconfig.json b/fixtures/workers-with-assets-static-routing/test/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/workers-with-assets-static-routing/tsconfig.json b/fixtures/workers-with-assets-static-routing/tsconfig.json new file mode 100644 index 0000000..6ec3531 --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["test"] +} diff --git a/fixtures/workers-with-assets-static-routing/vitest.config.mts b/fixtures/workers-with-assets-static-routing/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/workers-with-assets-static-routing/wrangler.jsonc b/fixtures/workers-with-assets-static-routing/wrangler.jsonc new file mode 100644 index 0000000..cc23155 --- /dev/null +++ b/fixtures/workers-with-assets-static-routing/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "name": "workers-with-assets-static-routing", + "main": "src/index.ts", + "compatibility_date": "2025-05-20", + "assets": { + "binding": "ASSETS", + "directory": "./public", + "run_worker_first": [ + // The `/oauth/callback` path and anything under `/worker` will be served by the Worker first + "/worker/*", + "/oauth/callback", + // The `/missing-asset` and `worker/asset` paths will not be served by the Worker first + "!/missing-asset", + "!/worker/asset", + ], + }, +} diff --git a/fixtures/workers-with-assets/.gitignore b/fixtures/workers-with-assets/.gitignore new file mode 100644 index 0000000..36c1562 --- /dev/null +++ b/fixtures/workers-with-assets/.gitignore @@ -0,0 +1,2 @@ +./public/.assetsignore +./public/_redirects diff --git a/fixtures/workers-with-assets/README.md b/fixtures/workers-with-assets/README.md new file mode 100644 index 0000000..4a0d8f0 --- /dev/null +++ b/fixtures/workers-with-assets/README.md @@ -0,0 +1,17 @@ +# workers-assets-with-user-worker + +`workers-assets-with-user-worker` is a test fixture that showcases Workers with Assets. This particular fixture sets up a User Worker, assets, and a binding from the user Worker to the assets. + +## dev + +To start a dev session you can run + +``` +wrangler dev +``` + +## Run tests + +``` +npm run test +``` diff --git a/fixtures/workers-with-assets/package.json b/fixtures/workers-with-assets/package.json new file mode 100644 index 0000000..b675ff5 --- /dev/null +++ b/fixtures/workers-with-assets/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fixture/workers-assets", + "private": true, + "scripts": { + "check:type": "tsc", + "dev": "wrangler dev", + "test:ci": "vitest run", + "test:watch": "vitest", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/workers-with-assets/public/.assetsignore b/fixtures/workers-with-assets/public/.assetsignore new file mode 100644 index 0000000..554fc76 --- /dev/null +++ b/fixtures/workers-with-assets/public/.assetsignore @@ -0,0 +1 @@ +ignore-me.txt diff --git a/fixtures/workers-with-assets/public/.dot/index.html b/fixtures/workers-with-assets/public/.dot/index.html new file mode 100644 index 0000000..15f2047 --- /dev/null +++ b/fixtures/workers-with-assets/public/.dot/index.html @@ -0,0 +1 @@ +hi from .dot/index.html diff --git a/fixtures/workers-with-assets/public/.dotfile.html b/fixtures/workers-with-assets/public/.dotfile.html new file mode 100644 index 0000000..23ee20b --- /dev/null +++ b/fixtures/workers-with-assets/public/.dotfile.html @@ -0,0 +1 @@ +hi from .dotfile.html diff --git a/fixtures/workers-with-assets/public/README.md b/fixtures/workers-with-assets/public/README.md new file mode 100644 index 0000000..a78f2f0 --- /dev/null +++ b/fixtures/workers-with-assets/public/README.md @@ -0,0 +1,57 @@ +# Workers with Assets + +Welcome to Workers + Assets YAY! + +Please proceed with much excitement ^.^ + + ................. + .......-++**********:. + ..-+****++**********#: + ...-+***+++====+********##= + ....:=****+======+***********##+ + ....-****+======+*************###+ + .:==+*+======+***************####= + ....-===--=====++****************#####: + ....:====-:::-==+******************#####+. + ..-====:::::::+*******************#####*.. + ..-===-:::::::-==+*****************######=.. + ..-===-::::::-=======***************######*. + ...-===-::::::-==========+************######*:. + ...-===-::::::-==----========+*********#######:.. + .-===-::::::--::::::::::-======+******#######-. . + ..:====::::::-=:::-*####*-:::-=======+**#######-. + .:====-:::::-==:::*#########-::========+**#####- + ..====-:::::-===-::###########*:::======*******+.. + ..=+====:::::=====::-############:::====+*******=... + ....:=++====:::-======-::###########*:::===+*******-.. + ...........:-==+**+=++++=============-:::*#########-::===********:.. + ....--=+************====+++===============:::=*####*=:::-=+*******=.. + .:*************###*=====++++================::::::::::-==********:.. + ..*****#############=======++++==========================+*******+... + ...+****#############*==:::-===++++========================********-. + ...+****###############+=::::=====+++++====================********+... + .=****################*=-:::-======+++++=================+********:.. + ..-****##################===:-=========++++++=============+********:.. + .+***####################===========+***#++++++=========+********=.. + .*###################+-.-=======+****##*==+++++++====+********=... + ...-+#############*=...-++=====+****####+=====++++++*********+... + ....-*######+:.....++++===******####==========+*######**+.. + ...+=.....:::-+++++******#####=========+***######*.. + .......::::::=++******#####+========***********:... + ....::::::...+*****######=======+**********##*. + ..:::::....-*****######+=====+**********#####*. + ...:::::...:******#####*+++++*********#########+. + ..:::::...+*****######+++++++=.=###############:. + .:::::..-*****######-..-=++=..-##########***###.. + ....::::..=****######=....::::..:########******##*.. + ...::::..+***######-.....::::...*##***********###=.. + ..::::...-**####*:......::::...###************###-.. + ..:::......=##*:......:::::...+##*************###: + ...::::...............:::::....+##*************####. + ...::::::::::::::..:::::::......**************###-.. + .::::::::::::::::::::::..... ...:***********###+.... + ..::::.......::::::...... ..:********###*:... + ........ ...=******###=.. + .=***###+.. + ..*####:... + ........ diff --git a/fixtures/workers-with-assets/public/_headers b/fixtures/workers-with-assets/public/_headers new file mode 100644 index 0000000..572e15d --- /dev/null +++ b/fixtures/workers-with-assets/public/_headers @@ -0,0 +1,2 @@ +/ + X-Header: Custom-Value \ No newline at end of file diff --git a/fixtures/workers-with-assets/public/_redirects b/fixtures/workers-with-assets/public/_redirects new file mode 100644 index 0000000..947c638 --- /dev/null +++ b/fixtures/workers-with-assets/public/_redirects @@ -0,0 +1,2 @@ +/foo /bar +/pic /lava-lamps.jpg 200 diff --git a/fixtures/workers-with-assets/public/_worker.js b/fixtures/workers-with-assets/public/_worker.js new file mode 100644 index 0000000..7b5c6d1 --- /dev/null +++ b/fixtures/workers-with-assets/public/_worker.js @@ -0,0 +1 @@ +bang; diff --git a/fixtures/workers-with-assets/public/about/%5Bboop%5D.html b/fixtures/workers-with-assets/public/about/%5Bboop%5D.html new file mode 100644 index 0000000..7a0e369 --- /dev/null +++ b/fixtures/workers-with-assets/public/about/%5Bboop%5D.html @@ -0,0 +1 @@ +

%5Bboop%5D.html

diff --git a/fixtures/workers-with-assets/public/about/%5Bwomp%5D.html b/fixtures/workers-with-assets/public/about/%5Bwomp%5D.html new file mode 100644 index 0000000..de76167 --- /dev/null +++ b/fixtures/workers-with-assets/public/about/%5Bwomp%5D.html @@ -0,0 +1 @@ +

womp

diff --git a/fixtures/workers-with-assets/public/about/[boop].html b/fixtures/workers-with-assets/public/about/[boop].html new file mode 100644 index 0000000..37af005 --- /dev/null +++ b/fixtures/workers-with-assets/public/about/[boop].html @@ -0,0 +1 @@ +

[boop].html

diff --git a/fixtures/workers-with-assets/public/about/[fünky].txt b/fixtures/workers-with-assets/public/about/[fünky].txt new file mode 100644 index 0000000..dbb8484 --- /dev/null +++ b/fixtures/workers-with-assets/public/about/[fünky].txt @@ -0,0 +1 @@ +This should work. \ No newline at end of file diff --git a/fixtures/workers-with-assets/public/about/index.html b/fixtures/workers-with-assets/public/about/index.html new file mode 100644 index 0000000..14f9dff --- /dev/null +++ b/fixtures/workers-with-assets/public/about/index.html @@ -0,0 +1 @@ +

Learn more about Workers with Assets soon!

diff --git a/fixtures/workers-with-assets/public/bar.html b/fixtures/workers-with-assets/public/bar.html new file mode 100644 index 0000000..5e46a91 --- /dev/null +++ b/fixtures/workers-with-assets/public/bar.html @@ -0,0 +1 @@ +

Bar

diff --git a/fixtures/workers-with-assets/public/binding.html b/fixtures/workers-with-assets/public/binding.html new file mode 100644 index 0000000..8edcac2 --- /dev/null +++ b/fixtures/workers-with-assets/public/binding.html @@ -0,0 +1 @@ +

✨This is from a user Worker binding✨

diff --git a/fixtures/workers-with-assets/public/foo.html b/fixtures/workers-with-assets/public/foo.html new file mode 100644 index 0000000..88dadda --- /dev/null +++ b/fixtures/workers-with-assets/public/foo.html @@ -0,0 +1 @@ +

Foo

diff --git a/fixtures/workers-with-assets/public/ignore-me.txt b/fixtures/workers-with-assets/public/ignore-me.txt new file mode 100644 index 0000000..4e2646a --- /dev/null +++ b/fixtures/workers-with-assets/public/ignore-me.txt @@ -0,0 +1 @@ +THIS IS A SECRET \ No newline at end of file diff --git a/fixtures/workers-with-assets/public/index.html b/fixtures/workers-with-assets/public/index.html new file mode 100644 index 0000000..b7351e6 --- /dev/null +++ b/fixtures/workers-with-assets/public/index.html @@ -0,0 +1 @@ +

Hello Workers + Assets World 🚀!

diff --git a/fixtures/workers-with-assets/public/lava-lamps.jpg b/fixtures/workers-with-assets/public/lava-lamps.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fc66cad30f7dd24ed50942ceca9c0003039c8ef4 GIT binary patch literal 337047 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
h;RU%9F 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 ZgE3@i| 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

QD|oI%-0o<{lTI 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℘9urPe(f>)beI+Li0 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%0D@}uHy@a?>M<13&U&2|q-rp?B1YW9#y@{Q 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 +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + if (url.pathname === "/assets-binding") { + return await env.ASSETS.fetch(new URL("binding.html", request.url)); + } + + if (url.pathname === "/named-entrypoint") { + const res = await env.NAMED.sayHello(); + return new Response(res); + } + // 404s from the Asset Worker will return this: + return new Response( + "There were no assets at this route! Hello from the user Worker instead!" + + `\n${new Date()}` + ); + }, +}; + +export class NamedEntrypoint extends WorkerEntrypoint { + async sayHello() { + return "hello from a named entrypoint"; + } +} diff --git a/fixtures/workers-with-assets/tests/index.test.ts b/fixtures/workers-with-assets/tests/index.test.ts new file mode 100644 index 0000000..ff2b68f --- /dev/null +++ b/fixtures/workers-with-assets/tests/index.test.ts @@ -0,0 +1,299 @@ +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it, onTestFinished } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("[Workers + Assets] dynamic site", () => { + let ip: string, port: number, stop: (() => Promise) | undefined; + + beforeAll(async () => { + ({ ip, port, stop } = await runWranglerDev(resolve(__dirname, ".."), [ + "--port=0", + "--inspector-port=0", + ])); + }); + + afterAll(async () => { + await stop?.(); + }); + + it("should respond with static asset content", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/index.html`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain(`

Hello Workers + Assets World 🚀!

`); + + response = await fetch(`http://${ip}:${port}/about/index.html`); + text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain(`

Learn more about Workers with Assets soon!

`); + }); + + it("should fallback to the user Worker if there are no assets at a given path ", async ({ + expect, + }) => { + // Requests should hit the Asset Worker *first*, then try the user Worker + const response = await fetch(`http://${ip}:${port}/no-assets-here`); + const text = await response.text(); + expect(text).toContain( + "There were no assets at this route! Hello from the user Worker instead!" + ); + }); + + // html_handling defaults to 'auto-trailing-slash' + it("should `/` resolve to `/index.html` ", async ({ expect }) => { + const response = await fetch(`http://${ip}:${port}/`); + const text = await response.text(); + expect(text).toContain("

Hello Workers + Assets World 🚀!

"); + }); + + it("should handle content types correctly on asset routes", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/index.html`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/html; charset=utf-8" + ); + + response = await fetch(`http://${ip}:${port}/README.md`); + text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/markdown; charset=utf-8" + ); + expect(text).toContain(`Welcome to Workers + Assets YAY!`); + + response = await fetch(`http://${ip}:${port}/yay.txt`); + text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/plain; charset=utf-8" + ); + expect(text).toContain(`.----------------.`); + + response = await fetch(`http://${ip}:${port}/lava-lamps.jpg`); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe("image/jpeg"); + + response = await fetch(`http://${ip}:${port}/totallyinvalidextension.greg`); + expect(response.status).toBe(200); + expect(response.headers.has("Content-Type")).toBeFalsy(); + }); + + it("should return 405 for non-GET or HEAD requests on routes where assets exist", async ({ + expect, + }) => { + // these should return the error and NOT be forwarded onto the user Worker + // POST etc. request -> RW -> AW -> check manifest --405--> RW --405--> eyeball + + // methods as per https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods + // excl. TRACE and CONNECT which are not supported + + let response = await fetch(`http://${ip}:${port}/index.html`, { + method: "POST", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/index.html`, { + method: "PUT", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/index.html`, { + method: "DELETE", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/index.html`, { + method: "OPTIONS", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + + response = await fetch(`http://${ip}:${port}/index.html`, { + method: "PATCH", + }); + expect(response.status).toBe(405); + expect(response.statusText).toBe("Method Not Allowed"); + }); + + it("should work with encoded path names", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/about/[fünky].txt`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(response.url).toBe( + `http://${ip}:${port}/about/%5Bf%C3%BCnky%5D.txt` + ); + expect(text).toContain(`This should work.`); + + response = await fetch(`http://${ip}:${port}/about/[boop]`); + text = await response.text(); + expect(response.status).toBe(200); + expect(response.url).toBe(`http://${ip}:${port}/about/%5Bboop%5D`); + expect(text).toContain(`[boop].html`); + + response = await fetch(`http://${ip}:${port}/about/%5Bboop%5D`); + text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain(`[boop].html`); + + response = await fetch(`http://${ip}:${port}/about/%255Bboop%255D`); + text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain(`%5Bboop%5D.html`); + }); + + it("should forward all request types to the user Worker if there are *not* assets on that route", async ({ + expect, + }) => { + // Unlike above, if the AW does NOT find assets on a route, non-GET request should return 404s + // This is because all requests are first sent to the AW and only then to the UW + // and we don't want to 405 on a valid POST request intended for the UW + // POST etc. request -> RW -> AW -> checks manifest --404--> RW -> UW -> response + + let response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "GET", + }); + expect(response.status).toBe(200); + + response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "HEAD", + }); + expect(response.status).toBe(200); + + response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "POST", + }); + expect(response.status).toBe(200); + + response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "PUT", + }); + expect(response.status).toBe(200); + + response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "DELETE", + }); + expect(response.status).toBe(200); + + response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "OPTIONS", + }); + expect(response.status).toBe(200); + + response = await fetch(`http://${ip}:${port}/no-assets-here`, { + method: "PATCH", + }); + expect(response.status).toBe(200); + }); + + it("should be able to use an ASSETS binding", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/assets-binding`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Type")).toBe( + "text/html; charset=utf-8" + ); + expect(text).toContain("

✨This is from a user Worker binding✨

"); + }); + + it("should be able to use a binding to a named entrypoint", async ({ + expect, + }) => { + let response = await fetch(`http://${ip}:${port}/named-entrypoint`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(text).toContain("hello from a named entrypoint"); + }); + + it("should apply custom redirects", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/foo`, { + redirect: "manual", + }); + expect(response.status).toBe(302); + expect(response.headers.get("Location")).toBe("/bar"); + + response = await fetch(`http://${ip}:${port}/pic`); + expect(response.status).toBe(200); + expect(await response.arrayBuffer()).toEqual( + readFileSync(join(__dirname, "../public/lava-lamps.jpg")).buffer + ); + }); + + it("should apply custom headers", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/`); + expect(response.status).toBe(200); + expect(response.headers.get("X-Header")).toBe("Custom-Value"); + }); + + it("should apply .assetsignore", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/.assetsignore`); + expect(await response.text()).not.toContain("ignore-me.txt"); + + response = await fetch(`http://${ip}:${port}/ignore-me.txt`); + expect(await response.text()).not.toContain("SECRET"); + + response = await fetch(`http://${ip}:${port}/_headers`); + expect(await response.text()).not.toContain("X-Header"); + }); + + it.todo("should warn of _worker.js", async () => { + // let response = await fetch(`http://${ip}:${port}/_worker.js`); + // expect(await response.text()).not.toContain("bang"); + }); + + it("should work with files which start with .", async ({ expect }) => { + let response = await fetch(`http://${ip}:${port}/.dot`); + let text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(` + "hi from .dot/index.html + " + `); + + response = await fetch(`http://${ip}:${port}/.dotfile.html`); + text = await response.text(); + expect(response.status).toBe(200); + expect(text).toMatchInlineSnapshot(` + "hi from .dotfile.html + " + `); + }); +}); + +describe("[Workers + Assets] logging", () => { + it("should log _headers and _redirects parsing", async ({ expect }) => { + const { ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + ); + + // Await a request to ensure the logs have been written + await fetch(`http://${ip}:${port}/`); + + expect(getOutput()).toContain( + `[wrangler:info] ✨ Parsed 2 valid redirect rules.` + ); + expect(getOutput()).toContain( + `[wrangler:info] ✨ Parsed 1 valid header rule.` + ); + onTestFinished(() => stop()); + }); + + it("should not log _headers and _redirects parsing when log level set to none", async ({ + expect, + }) => { + const { ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0", "--log-level=none"] + ); + expect(getOutput()).toMatchInlineSnapshot(`""`); + onTestFinished(() => stop()); + }); +}); diff --git a/fixtures/workers-with-assets/tests/tsconfig.json b/fixtures/workers-with-assets/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/workers-with-assets/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/workers-with-assets/tsconfig.json b/fixtures/workers-with-assets/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/workers-with-assets/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/workers-with-assets/vitest.config.mts b/fixtures/workers-with-assets/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/workers-with-assets/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/workers-with-assets/wrangler.jsonc b/fixtures/workers-with-assets/wrangler.jsonc new file mode 100644 index 0000000..eca8be6 --- /dev/null +++ b/fixtures/workers-with-assets/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "name": "worker-with-assets", + "main": "src/index.ts", + "compatibility_date": "2024-12-12", + "assets": { + "directory": "./public", + "binding": "ASSETS", + "run_worker_first": false, + }, + "services": [ + { + "binding": "NAMED", + "service": "worker-with-assets", + "entrypoint": "NamedEntrypoint", + }, + ], +} diff --git a/fixtures/workflow-multiple/package.json b/fixtures/workflow-multiple/package.json new file mode 100644 index 0000000..2d7d6e5 --- /dev/null +++ b/fixtures/workflow-multiple/package.json @@ -0,0 +1,16 @@ +{ + "name": "@fixture/workflow-multiple", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/workflow-multiple/src/index.ts b/fixtures/workflow-multiple/src/index.ts new file mode 100644 index 0000000..083ed12 --- /dev/null +++ b/fixtures/workflow-multiple/src/index.ts @@ -0,0 +1,139 @@ +import { + WorkerEntrypoint, + WorkflowEntrypoint, + WorkflowEvent, + WorkflowStep, +} from "cloudflare:workers"; + +type Params = { + name: string; +}; + +export class Demo extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const { timestamp, payload } = event; + + await step.sleep("Wait", "1 second"); + + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.sleep("Wait", "1 second"); + + const result2 = await step.do("Second step", async function () { + return { + output: "workflow1", + }; + }); + + return "i'm workflow1"; + } +} + +export class Demo2 extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const { timestamp, payload } = event; + + await step.sleep("Wait", "1 second"); + + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.sleep("Wait", "1 second"); + + const result2 = await step.do("Second step", async function () { + return { + output: "workflow2", + }; + }); + + return "i'm workflow2"; + } +} + +export class Demo3 extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.waitForEvent("wait for signal", { + type: "continue", + }); + + const result2 = await step.do("Second step", async function () { + return { + output: "workflow3", + }; + }); + + return "i'm workflow3"; + } +} + +type Env = { + WORKFLOW: Workflow; + WORKFLOW2: Workflow; + WORKFLOW3: Workflow; +}; + +export default class extends WorkerEntrypoint { + async fetch(req: Request) { + const url = new URL(req.url); + const id = url.searchParams.get("id"); + const workflowName = url.searchParams.get("workflowName"); + + if (url.pathname === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + + let workflowToUse: Workflow; + if (workflowName === "3") { + workflowToUse = this.env.WORKFLOW3; + } else if (workflowName === "2") { + workflowToUse = this.env.WORKFLOW2; + } else { + workflowToUse = this.env.WORKFLOW; + } + + let handle: WorkflowInstance; + if (url.pathname === "/create") { + if (id === null) { + handle = await workflowToUse.create(); + } else { + handle = await workflowToUse.create({ id }); + } + } else if (url.pathname === "/pause") { + handle = await workflowToUse.get(id); + await handle.pause(); + } else if (url.pathname === "/resume") { + handle = await workflowToUse.get(id); + await handle.resume(); + } else if (url.pathname === "/restart") { + handle = await workflowToUse.get(id); + await handle.restart(); + } else if (url.pathname === "/terminate") { + handle = await workflowToUse.get(id); + await handle.terminate(); + } else if (url.pathname === "/sendEvent") { + handle = await workflowToUse.get(id); + await handle.sendEvent({ + type: "continue", + payload: await req.json(), + }); + return Response.json({ ok: true }); + } else { + handle = await workflowToUse.get(id); + } + + return Response.json({ status: await handle.status(), id: handle.id }); + } +} diff --git a/fixtures/workflow-multiple/tests/index.test.ts b/fixtures/workflow-multiple/tests/index.test.ts new file mode 100644 index 0000000..410d17d --- /dev/null +++ b/fixtures/workflow-multiple/tests/index.test.ts @@ -0,0 +1,244 @@ +import { randomUUID } from "crypto"; +import { rm } from "fs/promises"; +import { resolve } from "path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it, test, vi } from "vitest"; +import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived"; + +describe("Workflows", () => { + let ip: string, + port: number, + stop: (() => Promise) | undefined, + getOutput: () => string; + + beforeAll(async () => { + // delete previous run contents because of persistence + await rm(resolve(__dirname, "..") + "/.wrangler", { + force: true, + recursive: true, + }); + ({ ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + async function fetchJson(url: string, body?: unknown, method?: string) { + const response = await fetch(url, { + headers: { + "MF-Disable-Pretty-Error": "1", + }, + method: method ?? "GET", + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + + try { + return JSON.parse(text); + } catch (err) { + throw new Error(`Couldn't parse JSON:\n\n${text}`); + } + } + + it("creates two instances with same id in two different workflows", async ({ + expect, + }) => { + // Create both workflow instances + // Note: We don't assert the intermediate "running" status because the workflow + // may complete before we can observe it, causing flaky tests on fast CI machines + await Promise.all([ + fetchJson(`http://${ip}:${port}/create?workflowName=1&id=test`), + fetchJson(`http://${ip}:${port}/create?workflowName=2&id=test`), + ]); + + // Wait for both workflows to complete with their final outputs + await Promise.all([ + vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=1&id=test`) + ).resolves.toStrictEqual({ + id: "test", + status: { + status: "complete", + __LOCAL_DEV_STEP_OUTPUTS: [ + { output: "First step result" }, + { output: "workflow1" }, + ], + output: "i'm workflow1", + }, + }); + }, + { timeout: 10000 } + ), + vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=2&id=test`) + ).resolves.toStrictEqual({ + id: "test", + status: { + status: "complete", + __LOCAL_DEV_STEP_OUTPUTS: [ + { output: "First step result" }, + { output: "workflow2" }, + ], + output: "i'm workflow2", + }, + }); + }, + { timeout: 10000 } + ), + ]); + }); + + describe("instance lifecycle methods (workflow3)", () => { + test("pause and resume a workflow", async ({ expect }) => { + const id = randomUUID(); + + await fetchJson(`http://${ip}:${port}/create?workflowName=3&id=${id}`); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { + status: { + __LOCAL_DEV_STEP_OUTPUTS: { output: string }[]; + }; + }; + expect(result.status.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 5000 } + ); + + // Pause the instance + await fetchJson(`http://${ip}:${port}/pause?workflowName=3&id=${id}`); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { status: { status: string } }; + expect(result.status.status).toBe("paused"); + }, + { timeout: 5000 } + ); + + // Resume the instance + await fetchJson(`http://${ip}:${port}/resume?workflowName=3&id=${id}`); + + await fetchJson( + `http://${ip}:${port}/sendEvent?workflowName=3&id=${id}`, + { done: true }, + "POST" + ); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { status: { status: string; output: string } }; + expect(result.status.status).toBe("complete"); + expect(result.status.output).toBe("i'm workflow3"); + }, + { timeout: 5000 } + ); + }); + + test("terminate a running workflow", async ({ expect }) => { + const id = randomUUID(); + + await fetchJson(`http://${ip}:${port}/create?workflowName=3&id=${id}`); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { + status: { + __LOCAL_DEV_STEP_OUTPUTS: { output: string }[]; + }; + }; + expect(result.status.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 5000 } + ); + + // Terminate + await fetchJson(`http://${ip}:${port}/terminate?workflowName=3&id=${id}`); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { status: { status: string } }; + expect(result.status.status).toBe("terminated"); + }, + { timeout: 5000 } + ); + }); + + test("restart a running workflow", async ({ expect }) => { + const id = randomUUID(); + + await fetchJson(`http://${ip}:${port}/create?workflowName=3&id=${id}`); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { + status: { + __LOCAL_DEV_STEP_OUTPUTS: { output: string }[]; + }; + }; + expect(result.status.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 5000 } + ); + + // Restart the instance + await fetchJson(`http://${ip}:${port}/restart?workflowName=3&id=${id}`); + + // After restart, wait for it to be running again + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { status: { status: string } }; + expect(result.status.status).toBe("running"); + }, + { timeout: 5000 } + ); + + // Send event to complete the restarted workflow + await fetchJson( + `http://${ip}:${port}/sendEvent?workflowName=3&id=${id}`, + { done: true }, + "POST" + ); + + await vi.waitFor( + async () => { + const result = (await fetchJson( + `http://${ip}:${port}/status?workflowName=3&id=${id}` + )) as { status: { status: string; output: string } }; + expect(result.status.status).toBe("complete"); + expect(result.status.output).toBe("i'm workflow3"); + }, + { timeout: 5000 } + ); + }); + }); +}); diff --git a/fixtures/workflow-multiple/tests/tsconfig.json b/fixtures/workflow-multiple/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/workflow-multiple/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/workflow-multiple/tsconfig.json b/fixtures/workflow-multiple/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/workflow-multiple/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/workflow-multiple/vitest.config.mts b/fixtures/workflow-multiple/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/workflow-multiple/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/workflow-multiple/wrangler.jsonc b/fixtures/workflow-multiple/wrangler.jsonc new file mode 100644 index 0000000..08061b6 --- /dev/null +++ b/fixtures/workflow-multiple/wrangler.jsonc @@ -0,0 +1,22 @@ +{ + "name": "my-workflow-demo", + "main": "src/index.ts", + "compatibility_date": "2024-10-22", + "workflows": [ + { + "binding": "WORKFLOW", + "name": "my-workflow", + "class_name": "Demo", + }, + { + "binding": "WORKFLOW2", + "name": "my-workflow-2", + "class_name": "Demo2", + }, + { + "binding": "WORKFLOW3", + "name": "my-workflow-3", + "class_name": "Demo3", + }, + ], +} diff --git a/fixtures/workflow/package.json b/fixtures/workflow/package.json new file mode 100644 index 0000000..316ad79 --- /dev/null +++ b/fixtures/workflow/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fixture/workflow-single", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-types": "catalog:default", + "typescript": "catalog:default", + "undici": "catalog:default", + "vitest": "catalog:default", + "wrangler": "workspace:*" + } +} diff --git a/fixtures/workflow/src/index.ts b/fixtures/workflow/src/index.ts new file mode 100644 index 0000000..ccc6435 --- /dev/null +++ b/fixtures/workflow/src/index.ts @@ -0,0 +1,175 @@ +import { + WorkerEntrypoint, + WorkflowBackoff, + WorkflowEntrypoint, + WorkflowEvent, + WorkflowStep, +} from "cloudflare:workers"; +import { NonRetryableError } from "cloudflare:workflows"; + +type Params = { + name: string; +}; + +export class Demo extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const { timestamp, payload } = event; + + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.sleep("Wait", "1 second"); + + const result2 = await step.do("Second step", async function () { + return { + output: "Second step result", + }; + }); + + return payload ?? "no-payload"; + } +} + +export class Demo2 extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const { timestamp, payload } = event; + + const result = await step.do("First step", async function () { + return { + output: "First step result", + }; + }); + + await step.waitForEvent("event-1 provider", { + type: "event-1", + }); + + const result2 = await step.do("Second step", async function () { + return { + output: "Second step result", + }; + }); + + return payload ?? "no-payload"; + } +} + +export class Demo3 extends WorkflowEntrypoint<{}, Params> { + async run( + event: WorkflowEvent, + step: WorkflowStep + ) { + let runs = -1; + try { + await step.do( + "First (and only step) step", + { + retries: { + limit: 3, + delay: 100, + }, + }, + async function () { + runs++; + if (event.payload.doRetry) { + throw new Error(event.payload.errorMessage); + } else { + throw new NonRetryableError(event.payload.errorMessage); + } + } + ); + } catch {} + + return `The step was retried ${runs} time${runs === 1 ? "" : "s"}`; + } +} + +type Env = { + WORKFLOW: Workflow; + WORKFLOW2: Workflow; + WORKFLOW3: Workflow<{ doRetry: boolean; errorMessage: string }>; +}; +export default class extends WorkerEntrypoint { + async fetch(req: Request) { + const url = new URL(req.url); + const id = url.searchParams.get("workflowName"); + const doRetry = url.searchParams.get("doRetry"); + const errorMessage = url.searchParams.get("errorMessage"); + + if (url.pathname === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + + let handle: WorkflowInstance; + if (url.pathname === "/createBatch") { + // creates two instances + const batch = await this.env.WORKFLOW.createBatch([ + { id: "batch-1", params: "1" }, + { id: "batch-2", params: "2" }, + ]); + return Response.json(batch.map((instance) => instance.id)); + } else if (url.pathname === "/create") { + if (id === null) { + handle = await this.env.WORKFLOW.create(); + } else { + handle = await this.env.WORKFLOW.create({ id }); + } + } else if (url.pathname === "/createDemo2") { + if (id === null) { + handle = await this.env.WORKFLOW2.create(); + } else { + handle = await this.env.WORKFLOW2.create({ id }); + } + } else if (url.pathname === "/createDemo3") { + if (id === null) { + handle = await this.env.WORKFLOW3.create(); + } else { + handle = await this.env.WORKFLOW3.create({ + id, + params: { + doRetry: doRetry === "false" ? false : true, + errorMessage: errorMessage ?? "", + }, + }); + } + } else if (url.pathname === "/sendEvent") { + handle = await this.env.WORKFLOW2.get(id); + + await handle.sendEvent({ + type: "event-1", + payload: await req.json(), + }); + } else if (url.pathname === "/pause") { + handle = await this.env.WORKFLOW2.get(id); + await handle.pause(); + } else if (url.pathname === "/resume") { + handle = await this.env.WORKFLOW2.get(id); + await handle.resume(); + } else if (url.pathname === "/restart") { + handle = await this.env.WORKFLOW2.get(id); + await handle.restart(); + } else if (url.pathname === "/terminate") { + handle = await this.env.WORKFLOW2.get(id); + await handle.terminate(); + } else if (url.pathname === "/get2") { + handle = await this.env.WORKFLOW2.get(id); + } else if (url.pathname === "/get3") { + handle = await this.env.WORKFLOW3.get(id); + } else if (url.pathname === "/createWithRedirect") { + console.log("create with redirect called"); + handle = await this.env.WORKFLOW.create(); + + return Response.redirect( + new URL(`/status?workflowName=${handle.id}`, url).toString(), + 302 + ); + } else if (url.pathname === "/status") { + handle = await this.env.WORKFLOW.get(id); + } + + return Response.json(await handle.status()); + } +} diff --git a/fixtures/workflow/tests/index.test.ts b/fixtures/workflow/tests/index.test.ts new file mode 100644 index 0000000..d3337fc --- /dev/null +++ b/fixtures/workflow/tests/index.test.ts @@ -0,0 +1,699 @@ +import { randomUUID } from "crypto"; +import { rm } from "fs/promises"; +import { fork } from "node:child_process"; +import { resolve } from "path"; +import { fetch } from "undici"; +import { afterAll, beforeAll, describe, it, test, vi } from "vitest"; +import { + runWranglerDev, + wranglerEntryPath, +} from "../../shared/src/run-wrangler-long-lived"; + +describe("Workflows", () => { + let ip: string, + port: number, + stop: (() => Promise) | undefined, + getOutput: () => string; + + beforeAll(async () => { + // delete previous run contents because of persistence + await rm(resolve(__dirname, "..") + "/.wrangler", { + force: true, + recursive: true, + }); + ({ ip, port, stop, getOutput } = await runWranglerDev( + resolve(__dirname, ".."), + ["--port=0", "--inspector-port=0"] + )); + }); + + afterAll(async () => { + await stop?.(); + }); + + async function fetchJson(url: string, body?: unknown, method?: string) { + const response = await fetch(url, { + headers: { + "MF-Disable-Pretty-Error": "1", + }, + method: method ?? "GET", + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + + try { + return JSON.parse(text); + } catch (err) { + throw new Error(`Couldn't parse JSON:\n\n${text}`); + } + } + + it("creates a workflow with id", async ({ expect }) => { + await expect(fetchJson(`http://${ip}:${port}/create?workflowName=test`)) + .resolves.toMatchInlineSnapshot(` + { + "__LOCAL_DEV_STEP_OUTPUTS": [ + { + "output": "First step result", + }, + ], + "output": null, + "status": "running", + } + `); + + await vi.waitFor( + async () => { + await expect(fetchJson(`http://${ip}:${port}/status?workflowName=test`)) + .resolves.toMatchInlineSnapshot(` + { + "__LOCAL_DEV_STEP_OUTPUTS": [ + { + "output": "First step result", + }, + ], + "output": null, + "status": "running", + } + `); + }, + { timeout: 5000 } + ); + }); + + it("creates a workflow without id", async ({ expect }) => { + await expect(fetchJson(`http://${ip}:${port}/create`)).resolves + .toMatchInlineSnapshot(` + { + "__LOCAL_DEV_STEP_OUTPUTS": [ + { + "output": "First step result", + }, + ], + "output": null, + "status": "running", + } + `); + }); + + it("fails getting a workflow without creating it first", async ({ + expect, + }) => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=anotherTest`) + ).resolves.toMatchObject({ + message: "instance.not_found", + name: "Error", + }); + }); + + test("batchCreate should create multiple instances and run them separately", async ({ + expect, + }) => { + await expect(fetchJson(`http://${ip}:${port}/createBatch`)).resolves + .toMatchInlineSnapshot(` + [ + "batch-1", + "batch-2", + ] + `); + + await Promise.all([ + vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=batch-1`) + ).resolves.toStrictEqual({ + status: "complete", + __LOCAL_DEV_STEP_OUTPUTS: [ + { output: "First step result" }, + { output: "Second step result" }, + ], + output: "1", + }); + }, + { timeout: 5000 } + ), + vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/status?workflowName=batch-2`) + ).resolves.toStrictEqual({ + status: "complete", + __LOCAL_DEV_STEP_OUTPUTS: [ + { output: "First step result" }, + { output: "Second step result" }, + ], + output: "2", + }); + }, + { timeout: 5000 } + ), + ]); + }); + + describe("retrying a step", () => { + test("should retry a step if a generic Error (with a generic error message) is thrown", async ({ + expect, + }) => { + const name = randomUUID(); + await fetchJson( + `http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=true&errorMessage=generic_error_message` + ); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get3?workflowName=${name}` + ); + + expect(result["output"]).toEqual("The step was retried 3 times"); + }, + { timeout: 1500 } + ); + }); + + test("should retry a step if a generic Error (with an empty error message) is thrown", async ({ + expect, + }) => { + const name = randomUUID(); + await fetchJson( + `http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=true&errorMessage=` + ); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get3?workflowName=${name}` + ); + + expect(result["output"]).toEqual("The step was retried 3 times"); + }, + { timeout: 1500 } + ); + }); + + test("should not retry a step if a NonRetryableError (with a generic error message) is thrown", async ({ + expect, + }) => { + const name = randomUUID(); + await fetchJson( + `http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=false&errorMessage=generic_error_message` + ); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get3?workflowName=${name}` + ); + + expect(result["output"]).toEqual("The step was retried 0 times"); + }, + { timeout: 1500 } + ); + }); + + test("should not retry a step if a NonRetryableError (with an empty error message) is thrown", async ({ + expect, + }) => { + const name = randomUUID(); + await fetchJson( + `http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=false&errorMessage=` + ); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get3?workflowName=${name}` + ); + + expect(result["output"]).toEqual("The step was retried 0 times"); + }, + { timeout: 1500 } + ); + }); + }); + + test("waitForEvent should work", async ({ expect }) => { + await fetchJson(`http://${ip}:${port}/createDemo2?workflowName=something`); + + await fetchJson( + `http://${ip}:${port}/sendEvent?workflowName=something`, + { event: true }, + "POST" + ); + + await vi.waitFor( + async () => { + await expect( + fetchJson(`http://${ip}:${port}/get2?workflowName=something`) + ).resolves.toMatchInlineSnapshot(` + { + "__LOCAL_DEV_STEP_OUTPUTS": [ + { + "output": "First step result", + }, + { + "event": true, + }, + { + "output": "Second step result", + }, + ], + "output": {}, + "status": "complete", + } + `); + }, + { timeout: 5000 } + ); + }); + + describe("instance lifecycle methods", () => { + test("pause and resume a workflow", async ({ expect }) => { + const name = randomUUID(); + + await fetchJson(`http://${ip}:${port}/createDemo2?workflowName=${name}`); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 1500 } + ); + + // Pause the instance + await fetchJson(`http://${ip}:${port}/pause?workflowName=${name}`); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.status).toBe("paused"); + }, + { timeout: 1500 } + ); + + // Resume the instance + await fetchJson(`http://${ip}:${port}/resume?workflowName=${name}`); + + await fetchJson( + `http://${ip}:${port}/sendEvent?workflowName=${name}`, + { event: true }, + "POST" + ); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.status).toBe("complete"); + }, + { timeout: 5000 } + ); + }); + + test("terminate a running workflow", async ({ expect }) => { + const name = randomUUID(); + + await fetchJson(`http://${ip}:${port}/createDemo2?workflowName=${name}`); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 1500 } + ); + + // Terminate the instance + await fetchJson(`http://${ip}:${port}/terminate?workflowName=${name}`); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.status).toBe("terminated"); + }, + { timeout: 1500 } + ); + }); + + test("restart a running workflow", async ({ expect }) => { + const name = randomUUID(); + + await fetchJson(`http://${ip}:${port}/createDemo2?workflowName=${name}`); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 5000 } + ); + + // Restart the instance + await fetchJson(`http://${ip}:${port}/restart?workflowName=${name}`); + + // After restart, wait for it to be running again + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.status).toBe("running"); + }, + { timeout: 1500 } + ); + + // Send event to complete the restarted workflow + await fetchJson( + `http://${ip}:${port}/sendEvent?workflowName=${name}`, + { event: true }, + "POST" + ); + + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=${name}` + ); + expect(result.status).toBe("complete"); + }, + { timeout: 5000 } + ); + }); + }); + + it("should create an instance after immediate redirect", async ({ + expect, + }) => { + await expect(fetchJson(`http://${ip}:${port}/createWithRedirect`)).resolves + .toMatchInlineSnapshot(` + { + "__LOCAL_DEV_STEP_OUTPUTS": [ + { + "output": "First step result", + }, + ], + "output": null, + "status": "running", + } + `); + }); + + // ========================================================================= + // Local CLI commands (wrangler workflows ... --local --port=) + // ========================================================================= + + /** + * Run a short-lived wrangler CLI command and return its combined output. + * Merges stdout + stderr into a single `output` string (wrangler routes + * log messages across both streams depending on the logger method). + */ + function runWranglerCommand( + args: string[] + ): Promise<{ output: string; exitCode: number | null }> { + return new Promise((resolve) => { + const chunks: Buffer[] = []; + + const child = fork(wranglerEntryPath, args, { + stdio: ["ignore", "pipe", "pipe", "ipc"], + cwd: __dirname, + }); + + child.stdout?.on("data", (chunk) => chunks.push(chunk)); + child.stderr?.on("data", (chunk) => chunks.push(chunk)); + + child.on("exit", (exitCode) => { + resolve({ + output: Buffer.concat(chunks).toString(), + exitCode, + }); + }); + }); + } + + describe("local CLI commands", () => { + it("workflows list --local", async ({ expect }) => { + const result = await runWranglerCommand([ + "workflows", + "list", + "--local", + `--port=${port}`, + ]); + + expect(result.exitCode).toBe(0); + expect(result.output).toContain( + "Showing 3 workflows from local dev session:" + ); + expect(result.output).toContain("my-workflow"); + expect(result.output).toContain("my-workflow2"); + expect(result.output).toContain("my-workflow3"); + expect(result.output).toContain("my-workflow-demo"); + expect(result.output).toContain("Demo"); + expect(result.output).toContain("Demo2"); + expect(result.output).toContain("Demo3"); + }); + + it("workflows describe --local", async ({ expect }) => { + const result = await runWranglerCommand([ + "workflows", + "describe", + "my-workflow", + "--local", + `--port=${port}`, + ]); + + expect(result.exitCode).toBe(0); + expect(result.output).toContain("my-workflow"); + expect(result.output).toContain("my-workflow-demo"); + expect(result.output).toContain("Demo"); + expect(result.output).toContain("Instance Status Counts:"); + }); + + it("workflows trigger --local and instances list --local", async ({ + expect, + }) => { + const result = await runWranglerCommand([ + "workflows", + "trigger", + "my-workflow", + "--local", + `--port=${port}`, + ]); + + expect(result.exitCode).toBe(0); + expect(result.output).toContain("has been triggered successfully"); + + // Wait for the instance to register + await vi.waitFor( + async () => { + const listResult = await runWranglerCommand([ + "workflows", + "instances", + "list", + "my-workflow", + "--local", + `--port=${port}`, + ]); + + expect(listResult.exitCode).toBe(0); + expect(listResult.output).toContain("from page 1:"); + }, + { timeout: 5000 } + ); + }); + + it("workflows instances describe --local", async ({ expect }) => { + // Trigger an instance first + const triggerResult = await runWranglerCommand([ + "workflows", + "trigger", + "my-workflow", + "--local", + `--port=${port}`, + ]); + expect(triggerResult.exitCode).toBe(0); + + // Describe latest + await vi.waitFor( + async () => { + const result = await runWranglerCommand([ + "workflows", + "instances", + "describe", + "my-workflow", + "latest", + "--local", + `--port=${port}`, + ]); + + expect(result.exitCode).toBe(0); + expect(result.output).toContain("Describing latest instance:"); + expect(result.output).toContain("my-workflow"); + expect(result.output).toContain("Steps:"); + }, + { timeout: 5000 } + ); + }); + + it("workflows instances pause and resume --local", async ({ expect }) => { + // Create a Demo2 instance (has waitForEvent so it stays running) + await fetchJson( + `http://${ip}:${port}/createDemo2?workflowName=pause-resume-test` + ); + + // Wait for it to be running + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=pause-resume-test` + ); + expect(result.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 5000 } + ); + + // Pause via CLI + const pauseResult = await runWranglerCommand([ + "workflows", + "instances", + "pause", + "my-workflow2", + "pause-resume-test", + "--local", + `--port=${port}`, + ]); + expect(pauseResult.exitCode).toBe(0); + expect(pauseResult.output).toContain( + 'The instance "pause-resume-test" from my-workflow2 was paused successfully' + ); + + // Verify paused via binding + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=pause-resume-test` + ); + expect(result.status).toBe("paused"); + }, + { timeout: 5000 } + ); + + // Resume via CLI + const resumeResult = await runWranglerCommand([ + "workflows", + "instances", + "resume", + "my-workflow2", + "pause-resume-test", + "--local", + `--port=${port}`, + ]); + expect(resumeResult.exitCode).toBe(0); + expect(resumeResult.output).toContain( + 'The instance "pause-resume-test" from my-workflow2 was resumed successfully' + ); + }); + + it("workflows instances terminate --local", async ({ expect }) => { + // Create a Demo2 instance (has waitForEvent so it stays running) + await fetchJson( + `http://${ip}:${port}/createDemo2?workflowName=terminate-test` + ); + + // Wait for it to be running + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=terminate-test` + ); + expect(result.__LOCAL_DEV_STEP_OUTPUTS).toContainEqual({ + output: "First step result", + }); + }, + { timeout: 5000 } + ); + + // Terminate via CLI + const terminateResult = await runWranglerCommand([ + "workflows", + "instances", + "terminate", + "my-workflow2", + "terminate-test", + "--local", + `--port=${port}`, + ]); + expect(terminateResult.exitCode).toBe(0); + expect(terminateResult.output).toContain( + 'The instance "terminate-test" from my-workflow2 was terminated successfully' + ); + + // Verify terminated via binding + await vi.waitFor( + async () => { + const result = await fetchJson( + `http://${ip}:${port}/get2?workflowName=terminate-test` + ); + expect(result.status).toBe("terminated"); + }, + { timeout: 5000 } + ); + }); + + it("workflows delete --local", async ({ expect }) => { + const result = await runWranglerCommand([ + "workflows", + "delete", + "my-workflow3", + "--local", + `--port=${port}`, + ]); + + expect(result.exitCode).toBe(0); + expect(result.output).toContain( + 'Workflow "my-workflow3" instances removed successfully from local dev session.' + ); + }); + }); + + it("should persist instances across lifetimes", async ({ expect }) => { + await fetchJson(`http://${ip}:${port}/create?workflowName=something`); + + await stop?.(); + + const { port: portChild2, stop: stopChild2Process } = await runWranglerDev( + resolve(__dirname, ".."), + [`--port=0`, `--inspector-port=0`] + ); + + try { + const result = await fetchJson( + `http://${ip}:${portChild2}/status?workflowName=something` + ); + expect(result).not.toBeUndefined(); + } finally { + await stopChild2Process?.(); + } + }); +}); diff --git a/fixtures/workflow/tests/tsconfig.json b/fixtures/workflow/tests/tsconfig.json new file mode 100644 index 0000000..be0dabd --- /dev/null +++ b/fixtures/workflow/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"] +} diff --git a/fixtures/workflow/tsconfig.json b/fixtures/workflow/tsconfig.json new file mode 100644 index 0000000..6112e71 --- /dev/null +++ b/fixtures/workflow/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "preserve", + "lib": ["ES2020"], + "types": ["@cloudflare/workers-types"], + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"], + "exclude": ["tests"] +} diff --git a/fixtures/workflow/vitest.config.mts b/fixtures/workflow/vitest.config.mts new file mode 100644 index 0000000..846cddc --- /dev/null +++ b/fixtures/workflow/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: {}, + }) +); diff --git a/fixtures/workflow/wrangler.jsonc b/fixtures/workflow/wrangler.jsonc new file mode 100644 index 0000000..bee45e9 --- /dev/null +++ b/fixtures/workflow/wrangler.jsonc @@ -0,0 +1,22 @@ +{ + "name": "my-workflow-demo", + "main": "src/index.ts", + "compatibility_date": "2024-10-22", + "workflows": [ + { + "binding": "WORKFLOW", + "name": "my-workflow", + "class_name": "Demo", + }, + { + "binding": "WORKFLOW2", + "name": "my-workflow2", + "class_name": "Demo2", + }, + { + "binding": "WORKFLOW3", + "name": "my-workflow3", + "class_name": "Demo3", + }, + ], +} diff --git a/lint-turbo.mjs b/lint-turbo.mjs new file mode 100644 index 0000000..c53f6df --- /dev/null +++ b/lint-turbo.mjs @@ -0,0 +1,63 @@ +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" +); +const paths = listResult.toString().trim().split("\n"); +const vitePluginPlaygroundDir = path.join( + "packages", + "vite-plugin-cloudflare", + "playground" +); + +for (const p of paths) { + if (!path.isAbsolute(p)) continue; + + const pkg = readJson(path.join(p, "package.json")); + const relativePath = path.relative(process.cwd(), p); + + // Ensure playground packages don't have a "build" script (use "build:default" instead) + if (relativePath.startsWith(`${vitePluginPlaygroundDir}${path.sep}`)) { + assert( + !pkg.scripts?.build, + `Vite plugin playground package "${pkg.name}" should not have a "build" script. Use "build:default" instead.` + ); + } + + // Ensure all packages with a build script have a turbo build output configured + if (pkg.scripts?.build) { + console.log(pkg.name, "has build script. Checking turbo.json"); + let turboConfig; + try { + turboConfig = readJsonc(path.join(p, "turbo.json")); + } catch { + console.log("Failed to read turbo.json for", pkg.name); + process.exit(1); + } + const buildOutputs = turboConfig.tasks.build.outputs; + assert(buildOutputs.length > 0); + } + + // Ensure all packages with a vitest config file have a "test:ci" script + if ( + existsSync(path.join(p, "vitest.config.ts")) || + existsSync(path.join(p, "vitest.config.mts")) + ) { + assert( + pkg.scripts["test:ci"], + `Package "${p}" is missing a "test:ci" script in package.json` + ); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..26bc18e --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "@cloudflare/workers-sdk", + "version": "0.0.0", + "private": true, + "description": "Monorepo for wrangler and associated packages", + "homepage": "https://github.com/cloudflare/workers-sdk#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "MIT OR Apache-2.0", + "author": "wrangler@cloudflare.com", + "scripts": { + "build": "dotenv -- turbo build", + "check": "pnpm check:fixtures && pnpm check:private-packages && pnpm check:package-deps && pnpm check:catalog && pnpm check:pinned-deps && pnpm check:deployments && pnpm check:workflows && node lint-turbo.mjs && dotenv -- turbo check:lint check:type check:format type:tests", + "check:catalog": "node -r esbuild-register tools/deployments/validate-catalog-usage.ts", + "check:deployments": "node -r esbuild-register tools/deployments/deploy-non-npm-packages.ts check", + "check:fixtures": "node -r esbuild-register tools/deployments/validate-fixtures.ts", + "check:format": "oxfmt --check", + "check:lint": "oxlint --deny-warnings --type-aware", + "check:package-deps": "pnpm build && node -r esbuild-register tools/deployments/validate-package-dependencies.ts", + "check:pinned-deps": "node -r esbuild-register tools/deployments/validate-pinned-dependencies.ts", + "check:private-packages": "node -r esbuild-register tools/deployments/validate-private-packages.ts", + "check:type": "dotenv -- turbo check:type type:tests", + "check:workflows": "node -r esbuild-register tools/github-workflow-helpers/validate-action-pinning.ts", + "dev": "dotenv -- turbo dev", + "fix": "oxlint --fix --type-aware && pnpm run prettify", + "prettify": "oxfmt", + "test": "dotenv -- turbo test", + "test:ci": "dotenv -- turbo test:ci", + "test:e2e": "dotenv -- turbo test:e2e", + "test:e2e:c3": "dotenv -- node -r esbuild-register packages/create-cloudflare/scripts/e2e/run-tests.ts", + "test:e2e:wrangler": "dotenv -- node -r esbuild-register tools/e2e/runIndividualE2EFiles.ts", + "test:watch": "turbo test:watch", + "type:tests": "dotenv -- turbo type:tests" + }, + "dependencies": { + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.29.7", + "@changesets/parse": "^0.4.1", + "@manypkg/cli": "^0.23.0", + "@types/node": "catalog:default", + "@vue/compiler-sfc": "^3.3.4", + "cross-env": "^7.0.3", + "dotenv-cli": "^7.3.0", + "esbuild": "catalog:default", + "esbuild-register": "^3.5.0", + "eslint-plugin-turbo": "^2.8.17", + "jsonc-parser": "catalog:default", + "oxfmt": "^0.41.0", + "oxlint": "^1.56.0", + "oxlint-tsgolint": "^0.17.0", + "tree-kill": "catalog:default", + "turbo": "^2.7.2", + "typescript": "catalog:default", + "vitest": "catalog:default" + }, + "engines": { + "node": ">=22.0.0", + "pnpm": "^10.33.0" + }, + "volta": { + "node": "22.22.1", + "pnpm": "10.33.0" + }, + "packageManager": "pnpm@10.33.0" +} diff --git a/packages/autoconfig/CHANGELOG.md b/packages/autoconfig/CHANGELOG.md new file mode 100644 index 0000000..405811f --- /dev/null +++ b/packages/autoconfig/CHANGELOG.md @@ -0,0 +1,51 @@ +# @cloudflare/autoconfig + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`0283a1f`](https://github.com/cloudflare/workers-sdk/commit/0283a1fcdc635244f731010422e513e8b4ab0be3)]: + - @cloudflare/workers-utils@0.26.0 + - @cloudflare/cli-shared-helpers@0.1.13 + +## 0.1.3 + +### Patch Changes + +- [#14548](https://github.com/cloudflare/workers-sdk/pull/14548) [`383e679`](https://github.com/cloudflare/workers-sdk/commit/383e679b52e39dcb71cbbb66909218c008d9aac4) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Fix Vite version detection for vite+ projects during autoconfiguration + + vite+ installs `@voidzero-dev/vite-plus-core` under the `vite` npm alias, so the resolved `node_modules/vite/package.json` reports the wrapper's own version (e.g. `0.2.2`) rather than the underlying Vite version it bundles. This caused autoconfiguration to fail with an error claiming the Vite version was too old to be configured automatically. Autoconfiguration now detects the correct underlying Vite version for these projects. + +- Updated dependencies [[`aad35b7`](https://github.com/cloudflare/workers-sdk/commit/aad35b79d07df1bb764a4a5912d6b4328a34474b), [`1ac96a1`](https://github.com/cloudflare/workers-sdk/commit/1ac96a14b7fb022acada114ab8793fe8a4ba79a5), [`1ca8d8f`](https://github.com/cloudflare/workers-sdk/commit/1ca8d8f0bbd012a1d65cabadf7b6987b252775e9)]: + - @cloudflare/workers-utils@0.25.1 + - @cloudflare/cli-shared-helpers@0.1.12 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`aa5d580`](https://github.com/cloudflare/workers-sdk/commit/aa5d5801450b7e4417bfdbd477f86de3a4bc6933)]: + - @cloudflare/workers-utils@0.25.0 + - @cloudflare/cli-shared-helpers@0.1.11 + +## 0.1.1 + +### Patch Changes + +- [#14368](https://github.com/cloudflare/workers-sdk/pull/14368) [`a55b29a`](https://github.com/cloudflare/workers-sdk/commit/a55b29a4ba8a24f4d539538e2bf38e6a7e5b8e52) Thanks [@penalosa](https://github.com/penalosa)! - Add repository URL to `@cloudflare/autoconfig` + +## 0.1.0 + +### Minor Changes + +- [#14365](https://github.com/cloudflare/workers-sdk/pull/14365) [`f224ce2`](https://github.com/cloudflare/workers-sdk/commit/f224ce2009b6844a606eb53a71fb114434e8a7a0) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Add experimental package for framework autoconfig detection and configuration + +- [#14365](https://github.com/cloudflare/workers-sdk/pull/14365) [`f224ce2`](https://github.com/cloudflare/workers-sdk/commit/f224ce2009b6844a606eb53a71fb114434e8a7a0) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Add support for React Router >= 8.0.0 + + React Router v8 enables `viteEnvironmentApi` and `middleware` by default, so autoconfig no longer adds `future` flags to `react-router.config.ts` for v8+ projects and uses the middleware code pattern unconditionally. + +### Patch Changes + +- Updated dependencies [[`cfd6205`](https://github.com/cloudflare/workers-sdk/commit/cfd6205fe86f6afd74b5881f09524c93c83b8359), [`cfd6205`](https://github.com/cloudflare/workers-sdk/commit/cfd6205fe86f6afd74b5881f09524c93c83b8359)]: + - @cloudflare/workers-utils@0.24.0 + - @cloudflare/cli-shared-helpers@0.1.10 diff --git a/packages/autoconfig/package.json b/packages/autoconfig/package.json new file mode 100644 index 0000000..276b31e --- /dev/null +++ b/packages/autoconfig/package.json @@ -0,0 +1,57 @@ +{ + "name": "@cloudflare/autoconfig", + "version": "0.1.4", + "description": "Framework autoconfig detection and configuration for Cloudflare Workers", + "license": "MIT OR Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/workers-sdk.git", + "directory": "packages/autoconfig" + }, + "files": [ + "dist" + ], + "type": "module", + "sideEffects": false, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsup", + "check:type": "tsc -p ./tsconfig.json", + "dev": "tsup --watch", + "test": "vitest", + "test:ci": "vitest run" + }, + "dependencies": { + "@cloudflare/cli-shared-helpers": "workspace:*", + "@cloudflare/workers-utils": "workspace:*" + }, + "devDependencies": { + "@cloudflare/codemod": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@netlify/build-info": "^10.5.1", + "@types/esprima": "^4.0.3", + "@types/node": "catalog:default", + "chalk": "catalog:default", + "empathic": "^2.0.0", + "esprima": "4.0.1", + "recast": "0.23.11", + "semiver": "^1.1.0", + "ts-dedent": "^2.2.0", + "tsup": "8.3.0", + "typescript": "catalog:default", + "vitest": "catalog:default" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "prerelease": true + } +} diff --git a/packages/autoconfig/scripts/deps.ts b/packages/autoconfig/scripts/deps.ts new file mode 100644 index 0000000..6d7361e --- /dev/null +++ b/packages/autoconfig/scripts/deps.ts @@ -0,0 +1,12 @@ +/** + * Dependencies that _are not_ bundled along with @cloudflare/autoconfig. + * + * These must be explicitly documented with a reason why they cannot be bundled. + * This list is validated by `tools/deployments/validate-package-dependencies.ts`. + */ +export const EXTERNAL_DEPENDENCIES = [ + // Published workspace packages that consumers must install alongside autoconfig. + // They are kept external to share a single copy with wrangler and other SDK tools. + "@cloudflare/cli-shared-helpers", + "@cloudflare/workers-utils", +]; diff --git a/packages/autoconfig/src/context.ts b/packages/autoconfig/src/context.ts new file mode 100644 index 0000000..8c70cd9 --- /dev/null +++ b/packages/autoconfig/src/context.ts @@ -0,0 +1,107 @@ +/** + * Logger interface for autoconfig output. + * Callers provide their own implementation (e.g., wrapping `console` or a custom logger). + */ +export interface AutoConfigLogger { + /** Logs informational output. */ + log(...args: unknown[]): void; + /** Logs an informational message. */ + info(...args: unknown[]): void; + /** Logs a warning message. */ + warn(...args: unknown[]): void; + /** Logs a debug-level message (may be suppressed in production). */ + debug(...args: unknown[]): void; + /** Logs an error message. */ + error(...args: unknown[]): void; +} + +/** + * Dialog interface for interactive prompts. + * Callers provide their own implementation (e.g., using `prompts`, `inquirer`, or a custom UI). + */ +export interface AutoConfigDialogs { + /** + * Asks a yes/no confirmation question. + * + * @param text - The question to display + * @param options - Optional defaults and fallback behavior + * @returns `true` if confirmed, `false` otherwise + */ + confirm( + text: string, + options?: { defaultValue?: boolean; fallbackValue?: boolean } + ): Promise; + + /** + * Prompts the user for a text input. + * + * @param text - The prompt message + * @param options - Optional default value and validation function + * @returns The user-provided string + */ + prompt( + text: string, + options?: { + defaultValue?: string; + validate?: ( + value: string + ) => boolean | string | Promise; + } + ): Promise; + + /** + * Presents a selection list to the user. + * + * @param text - The prompt message + * @param options - Available choices and optional default selection + * @returns The selected value + */ + select( + text: string, + options: { + choices: Array<{ + title: string; + value: string; + description?: string; + }>; + defaultOption?: number; + } + ): Promise; +} + +/** + * Context object that provides external dependencies to the autoconfig system. + * + * Callers must provide implementations for `logger` and `dialogs`. + * All other fields are optional and allow callers to customize behavior + * (e.g., error reporting, command execution, CI detection). + */ +export interface AutoConfigContext { + /** Logger used for all autoconfig output. */ + logger: AutoConfigLogger; + /** Dialogs used for interactive prompts. */ + dialogs: AutoConfigDialogs; + /** + * Runs a shell command in the given directory. + * + * @param command - The shell command string to execute + * @param cwd - The working directory for the command + * @param label - A short label for logging (e.g., "[build]") + * @returns A promise that resolves when the command completes + */ + runCommand: (command: string, cwd: string, label: string) => Promise; + /** + * Returns `true` if running in a non-interactive or CI environment. + * Defaults to `() => false` if not provided. + * + * @returns Whether the current environment is non-interactive + */ + isNonInteractiveOrCI?: () => boolean; + /** + * Returns a cache folder path used for detecting cached project state, + * or `undefined` if not available. + * + * @returns The cache folder path, or `undefined` + */ + getCacheFolder?: () => string | undefined; +} diff --git a/packages/autoconfig/src/details/framework-detection.ts b/packages/autoconfig/src/details/framework-detection.ts new file mode 100644 index 0000000..3a64c2f --- /dev/null +++ b/packages/autoconfig/src/details/framework-detection.ts @@ -0,0 +1,338 @@ +import { existsSync, statSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { + BunPackageManager, + FatalError, + NpmPackageManager, + PnpmPackageManager, + UserError, + YarnPackageManager, +} from "@cloudflare/workers-utils"; +import { Project } from "@netlify/build-info"; +import { NodeFS } from "@netlify/build-info/node"; +import chalk from "chalk"; +import dedent from "ts-dedent"; +import { isKnownFramework } from "../frameworks"; +import { staticFramework } from "../frameworks/all-frameworks"; +import type { AutoConfigContext } from "../context"; +import type { PackageManager } from "@cloudflare/workers-utils"; +import type { Config, TelemetryMessage } from "@cloudflare/workers-utils"; +import type { Settings } from "@netlify/build-info"; + +/** + * Detects the framework used by the project at the given path. + * + * Uses `@netlify/build-info` to analyze build settings and identify the + * framework, then maps the detected package manager to wrangler's own type. + * + * If the project is identified as a Cloudflare Pages project the function + * returns early with a synthetic "Cloudflare Pages" framework entry. + * + * @param projectPath Path to the project root + * @param wranglerConfig Optional parsed wrangler config for the project + * @returns An object containing: + * - `detectedFramework`: The matched framework together with its build + * command and output directory. + * - `packageManager`: The package manager detected in the project. + * - `isWorkspaceRoot`: `true` when the project path is the root of a + * monorepo workspace (only present when relevant). + * @throws {UserError} When called from a workspace root that does not itself + * contain the targeted project path. + * @throws {MultipleFrameworksCIError} (via `findDetectedFramework`) in CI / + * non-interactive environments when multiple known frameworks are detected + * and no clear winner can be determined. + */ +export async function detectFramework( + projectPath: string, + context: AutoConfigContext, + wranglerConfig?: Config +): Promise<{ + detectedFramework: DetectedFramework; + packageManager: PackageManager; + isWorkspaceRoot?: boolean; +}> { + const fs = new NodeFS(); + + fs.logger = context.logger; + + const project = new Project(fs, projectPath, projectPath) + .setEnvironment(process.env) + .setNodeVersion(process.version); + + const buildSettings = await project.getBuildSettings(); + + const isWorkspaceRoot = !!project.workspace?.isRoot; + + if (isWorkspaceRoot) { + const resolvedProjectPath = resolve(projectPath); + + const workspaceRootIncludesProject = project.workspace?.packages.some( + (pkg) => resolve(pkg.path) === resolvedProjectPath + ); + + if (!workspaceRootIncludesProject) { + throw new UserError( + "The Cloudflare application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.", + { + telemetryMessage: "autoconfig detection workspace root unsupported", + } + ); + } + } + + // Convert the package manager detected by @netlify/build-info to our PackageManager type. + // This is populated after getBuildSettings() runs, which triggers the full detection chain. + const packageManager = convertDetectedPackageManager(project.packageManager); + + const lockFileExists = packageManager.lockFiles.some((lockFile) => + existsSync(join(projectPath, lockFile)) + ); + + const maybeDetectedFramework = maybeFindDetectedFramework( + buildSettings, + context + ); + + if ( + await isPagesProject( + projectPath, + context, + wranglerConfig, + maybeDetectedFramework + ) + ) { + return { + detectedFramework: { + framework: { + name: "Cloudflare Pages", + id: "cloudflare-pages", + }, + dist: wranglerConfig?.pages_build_output_dir, + }, + packageManager, + }; + } + + const detectedFramework = maybeDetectedFramework ?? { + framework: { + id: staticFramework.id, + name: staticFramework.name, + }, + }; + + if ( + !lockFileExists && + detectedFramework.framework.id !== staticFramework.id + ) { + context.logger.warn( + "No lock file has been detected in the current working directory." + + " This might indicate that the project is part of a workspace. Auto-configuration of " + + `projects inside workspaces is limited. See ${chalk.hex("#3B818D")( + "https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/#workspaces" + )}` + ); + } + + return { + detectedFramework, + packageManager, + isWorkspaceRoot, + }; +} + +/** + * Converts the package manager detected by @netlify/build-info to our PackageManager type. + * Falls back to npm if no package manager was detected. + * + * @param pkgManager The package manager detected by @netlify/build-info (from project.packageManager) + * @returns A PackageManager object compatible with wrangler's package manager utilities + */ +function convertDetectedPackageManager( + pkgManager: { name: string } | null +): PackageManager { + if (!pkgManager) { + return NpmPackageManager; + } + + switch (pkgManager?.name) { + case "pnpm": + return PnpmPackageManager; + case "yarn": + return YarnPackageManager; + case "bun": + return BunPackageManager; + case "npm": + default: + return NpmPackageManager; + } +} + +class MultipleFrameworksCIError extends FatalError { + constructor( + frameworks: string[], + telemetryOptions: TelemetryMessage = { + telemetryMessage: "autoconfig detection multiple frameworks", + } + ) { + super( + dedent`Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found: ${frameworks.join( + ", " + )}. + + To fix this issue either: + - check your project's configuration to make sure that the target framework + is the only configured one and try again + - run \`wrangler setup\` locally to get an interactive user experience where + you can specify what framework you want to target + + `, + { ...telemetryOptions, code: 1 } + ); + } +} + +function throwMultipleFrameworksNonInteractiveError( + settings: Settings[] +): never { + throw new MultipleFrameworksCIError( + settings.map((b) => b.name), + { + telemetryMessage: "autoconfig detection multiple frameworks", + } + ); +} + +type DetectedFramework = { + framework: { + name: string; + id: string; + }; + buildCommand?: string | undefined; + dist?: string; +}; + +async function isPagesProject( + projectPath: string, + context: AutoConfigContext, + wranglerConfig: Config | undefined, + detectedFramework?: DetectedFramework | undefined +): Promise { + if (wranglerConfig?.pages_build_output_dir) { + // The `pages_build_output_dir` is set only for Pages projects + return true; + } + + const cacheFolder = context.getCacheFolder?.(); + if (cacheFolder) { + const pagesConfigCache = join(cacheFolder, "pages.json"); + if (existsSync(pagesConfigCache)) { + // If there is a cached pages.json we can safely assume that the project + // is a Pages one + return true; + } + } + + if (detectedFramework === undefined) { + const functionsPath = join(projectPath, "functions"); + if (existsSync(functionsPath)) { + const functionsStat = statSync(functionsPath); + if (functionsStat.isDirectory()) { + const pagesConfirmed = await context.dialogs.confirm( + "We have identified a `functions` directory in this project, which might indicate you have an active Cloudflare Pages deployment. Is this correct?", + { + defaultValue: true, + // In CI we do want to fallback to `false` so that we can proceed with the autoconfig flow + fallbackValue: false, + } + ); + return pagesConfirmed; + } + } + } + + return false; +} + +/** + * Selects the most appropriate framework from a list of detected framework settings. + * + * When there is a good level of confidence that the selected framework is correct or + * the process is running locally (where the user can choose a different framework or + * abort the process) the framework is returned. If there is no such confidence and the + * process is running in non interactive mode (where the user doesn't have the option to + * change the detected framework) an error is instead thrown. + * + * @param settings The array of framework settings + * @returns The selected framework settings, or `undefined` if none provided + * @throws {MultipleFrameworksCIError} In CI environments when multiple known frameworks + * are detected and no clear winner can be determined + */ +function maybeFindDetectedFramework( + settings: Settings[], + context: AutoConfigContext +): DetectedFramework | undefined { + if (settings.length === 0) { + return undefined; + } + + if (settings.length === 1) { + return settings[0]; + } + + const settingsForOnlyKnownFrameworks = settings.filter(({ framework }) => + isKnownFramework(framework.id) + ); + + if (settingsForOnlyKnownFrameworks.length === 0) { + if (context.isNonInteractiveOrCI?.() ?? false) { + // If we're in a non interactive session (e.g. CI) let's throw to be on the safe side + throwMultipleFrameworksNonInteractiveError(settings); + } + // Locally we can just return the first one since the user can anyways choose a different + // framework or abort the process anyways + return settings[0]; + } + + if (settingsForOnlyKnownFrameworks.length === 1) { + // If there is a single known framework it's quite safe to assume that that's the + // one we care about + return settingsForOnlyKnownFrameworks[0]; + } + + if (settingsForOnlyKnownFrameworks.length === 2) { + const settingsForOnlyKnownFrameworksIds = new Set( + settingsForOnlyKnownFrameworks.map(({ framework }) => framework.id) + ); + + // Some frameworks (e.g. Vite, Hono) can serve as auxiliary tooling for a primary + // framework (e.g. Vite with React, Hono with Waku). When exactly two frameworks + // are detected and one is auxiliary, we discard it and return the primary one. + const idsOfAuxiliaryFrameworks = ["vite", "hono"]; + + for (const auxiliaryFrameworkId of idsOfAuxiliaryFrameworks) { + if (settingsForOnlyKnownFrameworksIds.has(auxiliaryFrameworkId)) { + const nonAuxiliaryFrameworkSettings = + settingsForOnlyKnownFrameworks.find( + ({ framework }) => framework.id !== auxiliaryFrameworkId + ); + + // Note: here nonAuxiliaryFrameworkSettings should always be defined, it could be undefined only if the + // same framework is actually detected twice (which shouldn't be possible). + if (nonAuxiliaryFrameworkSettings) { + return nonAuxiliaryFrameworkSettings; + } + } + } + } + + // If we've detected multiple frameworks, and we're in a non interactive session (e.g. CI) let's stay on the safe side and error + // (otherwise we just pick the first one as the user is always able to choose a different framework or terminate the process anyways) + if (context.isNonInteractiveOrCI?.() ?? false) { + throw new MultipleFrameworksCIError( + settingsForOnlyKnownFrameworks.map((b) => b.name), + { telemetryMessage: "autoconfig detection multiple frameworks" } + ); + } + + return settingsForOnlyKnownFrameworks[0]; +} diff --git a/packages/autoconfig/src/details/index.ts b/packages/autoconfig/src/details/index.ts new file mode 100644 index 0000000..3ba589d --- /dev/null +++ b/packages/autoconfig/src/details/index.ts @@ -0,0 +1,349 @@ +import assert from "node:assert"; +import { statSync } from "node:fs"; +import { readdir, stat } from "node:fs/promises"; +import { join, relative, resolve } from "node:path"; +import { brandColor } from "@cloudflare/cli-shared-helpers/colors"; +import { + checkWorkerNameValidity, + getWorkerName, + NpmPackageManager, + parsePackageJSON, + readFileSync, +} from "@cloudflare/workers-utils"; +import { AutoConfigDetectionError } from "../errors"; +import { getFrameworkClassInstance } from "../frameworks"; +import { + allFrameworksInfos, + staticFramework, +} from "../frameworks/all-frameworks"; +import { detectFramework } from "./framework-detection"; +import type { AutoConfigContext } from "../context"; +import type { + AutoConfigDetails, + AutoConfigDetailsForNonConfiguredProject, +} from "../types"; +import type { PackageManager } from "@cloudflare/workers-utils"; +import type { Config, PackageJSON } from "@cloudflare/workers-utils"; + +/** + * Asserts that the current project being targeted for autoconfig is not already configured. + * + * @param details The details detected for the project. + */ +export function assertNonConfigured( + details: AutoConfigDetails +): asserts details is AutoConfigDetailsForNonConfiguredProject { + assert( + details.configured === false, + "Error: expected the current project not to be already configured" + ); +} + +async function hasIndexHtml(dir: string): Promise { + const children = await readdir(dir); + for (const child of children) { + const stats = await stat(join(dir, child)); + if (stats.isFile() && child === "index.html") { + return true; + } + } + return false; +} + +/** + * If we haven't detected a framework being used, or the project is a Pages one, we need to "guess" what output dir the + * user is intending to use. This is best-effort, and so will not be accurate all the time. The heuristic we use is the + * first child directory with an `index.html` file present. + */ +async function findAssetsDir(from: string): Promise { + if (await hasIndexHtml(from)) { + return "."; + } + const children = await readdir(from); + for (const child of children) { + const path = join(from, child); + const stats = await stat(path); + if (stats.isDirectory() && (await hasIndexHtml(path))) { + return relative(from, path); + } + } + return undefined; +} + +type DetectedFramework = { + framework: { + name: string; + id: string; + }; + buildCommand?: string | undefined; + dist?: string; +}; + +/** + * Detects project details needed for autoconfig: framework, package manager, + * output directory, worker name, and whether the project is already configured. + * + * @param options - Detection options including project path, wrangler config, and context. + * @returns The detected project details. + */ +export async function getDetailsForAutoConfig({ + projectPath = process.cwd(), + wranglerConfig, + context, +}: { + /** The path to the project, defaults to cwd. */ + projectPath?: string; + /** The parsed wrangler configuration for the project (if any). */ + wranglerConfig?: Config; + /** The autoconfig context providing logger, dialogs, and other dependencies. */ + context: AutoConfigContext; +}): Promise { + const { logger } = context; + + logger.debug(`Running autoconfig detection in ${projectPath}...`); + + if ( + // If a real Wrangler config has been found the project is already configured for Workers + wranglerConfig?.configPath && + // Unless `pages_build_output_dir` is set, since that indicates that the project is a Pages one instead + !wranglerConfig.pages_build_output_dir + ) { + return { + configured: true, + projectPath, + workerName: getWorkerName(wranglerConfig.name, projectPath), + // Fall back to npm when already configured since we don't need to run package manager commands + packageManager: NpmPackageManager, + }; + } + + const { detectedFramework, packageManager, isWorkspaceRoot } = + await detectFramework(projectPath, context, wranglerConfig); + + const framework = getFrameworkClassInstance(detectedFramework.framework.id); + const packageJsonPath = resolve(projectPath, "package.json"); + + let packageJson: PackageJSON | undefined; + + try { + packageJson = parsePackageJSON( + readFileSync(packageJsonPath), + packageJsonPath + ); + } catch { + logger.debug("No package.json found when running autoconfig"); + } + + const configured = framework.isConfigured(projectPath) ?? false; + + const outputDir = + detectedFramework?.dist ?? (await findAssetsDir(projectPath)); + + const baseDetails = { + projectPath, + framework, + packageJson, + packageManager, + ...(detectedFramework + ? { + buildCommand: getProjectBuildCommand( + detectedFramework, + packageManager + ), + } + : {}), + workerName: getWorkerName(packageJson?.name, projectPath), + }; + + if (configured) { + return { + ...baseDetails, + configured: true, + isWorkspaceRoot, + }; + } + + if (!outputDir) { + const errorMessage = + framework.id === "static" || framework.id === "cloudflare-pages" + ? "Could not detect a directory containing static files (e.g. html, css and js) for the project" + : "Failed to detect an output directory for the project"; + + throw new AutoConfigDetectionError(errorMessage, { + telemetryMessage: "autoconfig details output directory missing", + frameworkId: framework.id, + configured, + }); + } + + return { + ...baseDetails, + outputDir, + configured: false, + isWorkspaceRoot, + }; +} + +/** + * Given a detected framework this function gets a `build` command for the target project that can be run in the terminal + * (such as `npm run build` or `npx astro build`). If no build command is detected `undefined` is returned instead. + * + * @param detectedFramework The detected framework (or settings) for the project + * @param packageManager The package manager to use for command prefixes + * @returns A runnable command for the build process if detected, undefined otherwise + */ +function getProjectBuildCommand( + detectedFramework: DetectedFramework, + packageManager: PackageManager +): string | undefined { + if (!detectedFramework.buildCommand) { + return undefined; + } + + const { type, dlx, npx } = packageManager; + + for (const packageManagerCommandPrefix of [type, dlx.join(" "), npx]) { + if ( + detectedFramework.buildCommand.startsWith(packageManagerCommandPrefix) + ) { + // The build command already is something like `npm run build` or similar + return detectedFramework.buildCommand; + } + } + + // The command is something like `astro build` so we need to prefix it with `npx` and equivalents + return `${npx} ${detectedFramework.buildCommand}`; +} + +/** + * Displays the detected autoconfig details to the user via the context logger. + * + * @param autoConfigDetails - The detected project details to display. + * @param context - The autoconfig context providing the logger. + * @param displayOptions - Optional display customization. + */ +export function displayAutoConfigDetails( + autoConfigDetails: AutoConfigDetails, + context: AutoConfigContext, + displayOptions?: { heading?: string } +): void { + const { logger } = context; + logger.log(""); + + logger.log(displayOptions?.heading ?? "Detected Project Settings:"); + + logger.log(brandColor(" - Worker Name:"), autoConfigDetails.workerName); + if (autoConfigDetails.framework) { + logger.log(brandColor(" - Framework:"), autoConfigDetails.framework.name); + } + if (autoConfigDetails.buildCommand) { + logger.log(brandColor(" - Build Command:"), autoConfigDetails.buildCommand); + } + if (autoConfigDetails.outputDir) { + logger.log(brandColor(" - Output Directory:"), autoConfigDetails.outputDir); + } + + logger.log(""); +} + +/** + * Prompts the user to confirm or modify the detected autoconfig details. + * + * @param autoConfigDetails - The detected project details. + * @param context - The autoconfig context providing dialogs. + * @returns The (possibly updated) autoconfig details. + */ +export async function confirmAutoConfigDetails( + autoConfigDetails: AutoConfigDetails, + context: AutoConfigContext +): Promise { + const { dialogs } = context; + const modifySettings = await dialogs.confirm( + "Do you want to modify these settings?", + { defaultValue: false, fallbackValue: false } + ); + + if (!modifySettings) { + return autoConfigDetails; + } + + // Just spreading the object to shallow clone it to avoid some potential side effects + const { ...updatedAutoConfigDetails } = autoConfigDetails; + + const workerName = await dialogs.prompt( + "What do you want to name your Worker?", + { + defaultValue: autoConfigDetails.workerName ?? "", + validate: (value: string) => { + const validity = checkWorkerNameValidity(value); + if (validity.valid) { + return true; + } + return validity.cause; + }, + } + ); + + updatedAutoConfigDetails.workerName = workerName; + + const frameworkId = await dialogs.select( + "What framework is your application using?", + { + choices: allFrameworksInfos.map((f) => ({ + title: f.name, + value: f.id, + description: + f.id === staticFramework.id + ? "No framework at all, or a static framework such as Vite, React or Gatsby." + : `The ${f.name} JavaScript framework`, + })), + defaultOption: allFrameworksInfos.findIndex((framework) => { + if (!autoConfigDetails?.framework) { + // If there is no framework already detected let's default to the static one + // (note: there should always be a framework at this point) + return framework.id === staticFramework.id; + } + return autoConfigDetails.framework.id === framework.id; + }), + } + ); + + updatedAutoConfigDetails.framework = getFrameworkClassInstance(frameworkId); + + const outputDir = await dialogs.prompt( + "What directory contains your applications' output/asset files?", + { + defaultValue: autoConfigDetails.outputDir ?? "", + validate: async (value) => { + if (!value) { + return "Please provide a valid directory path"; + } + const valueStats = statSync(resolve(value), { throwIfNoEntry: false }); + if (!valueStats) { + // If the path doesn't point to anything that's fine since the directory will likely be + // generated by the build command anyways + return true; + } + if (valueStats?.isFile()) { + return "Please select a directory"; + } + return true; + }, + } + ); + + updatedAutoConfigDetails.outputDir = outputDir; + + if (autoConfigDetails.buildCommand || autoConfigDetails.packageJson) { + const buildCommand = await dialogs.prompt( + "What is your application's build command?", + { + defaultValue: autoConfigDetails.buildCommand ?? "", + } + ); + + updatedAutoConfigDetails.buildCommand = buildCommand; + } + + return updatedAutoConfigDetails; +} diff --git a/packages/autoconfig/src/errors.ts b/packages/autoconfig/src/errors.ts new file mode 100644 index 0000000..2bbb4cb --- /dev/null +++ b/packages/autoconfig/src/errors.ts @@ -0,0 +1,39 @@ +import { FatalError, UserError } from "@cloudflare/workers-utils"; +import type { TelemetryMessage } from "@cloudflare/workers-utils"; + +/** + * Base class for errors where something in a autoconfig frameworks' configuration goes + * something wrong. These are not reported to Sentry. + */ +export class AutoConfigFrameworkConfigurationError extends UserError {} + +/** + * Error thrown when autoconfig detection fails. + * Carries detection metadata (`frameworkId`, `configured`) so that callers can + * extract it for telemetry without the autoconfig library needing to know about + * the telemetry system. + */ +export class AutoConfigDetectionError extends FatalError { + /** The detected framework identifier (if detection got far enough to determine it). */ + readonly frameworkId: string | undefined; + /** Whether the project was already configured at the time of the error. */ + readonly configured: boolean; + + /** + * @param message - The human-readable error message. + * @param options - Error options including telemetry message, optional code, and detection metadata. + */ + constructor( + message: string, + options: TelemetryMessage & { + code?: number; + frameworkId?: string; + configured: boolean; + } + ) { + super(message, options); + Object.setPrototypeOf(this, new.target.prototype); + this.frameworkId = options.frameworkId; + this.configured = options.configured; + } +} diff --git a/packages/autoconfig/src/frameworks/all-frameworks.ts b/packages/autoconfig/src/frameworks/all-frameworks.ts new file mode 100644 index 0000000..7770d9f --- /dev/null +++ b/packages/autoconfig/src/frameworks/all-frameworks.ts @@ -0,0 +1,276 @@ +import assert from "node:assert"; +import { Analog } from "./analog"; +import { Angular } from "./angular"; +import { Astro } from "./astro"; +import { NextJs } from "./next"; +import { Nuxt } from "./nuxt"; +import { Qwik } from "./qwik"; +import { ReactRouter } from "./react-router"; +import { SolidStart } from "./solid-start"; +import { Static } from "./static"; +import { SvelteKit } from "./sveltekit"; +import { TanstackStart } from "./tanstack"; +import { Vike } from "./vike"; +import { Vite } from "./vite"; +import { Waku } from "./waku"; +import type { AutoConfigFrameworkPackageInfo, FrameworkInfo } from "."; + +/** + * Information about all the known frameworks, including frameworks that we know about but we don't support. + * + * The "static" framework is not included in this list. + */ +export const allKnownFrameworks = [ + { + id: "analog", + name: "Analog", + class: Analog, + frameworkPackageInfo: { + name: "@analogjs/platform", + // Analog didn't work well before 2.0.0 with Cloudflare + // See: https://github.com/cloudflare/workers-sdk/issues/11470 + minimumVersion: "2.0.0", + maximumKnownMajorVersion: "2", + }, + supported: true, + }, + { + id: "angular", + name: "Angular", + class: Angular, + frameworkPackageInfo: { + name: "@angular/core", + // Angular 19 introduced ssr.experimentalPlatform and AngularAppEngine + // which are required for Cloudflare Workers support + // See: https://github.com/angular/angular-cli/releases/tag/19.0.0 + // Angular 22 renamed experimentalPlatform to platform + // See: https://github.com/angular/angular-cli/commit/af2c7e9444fba81d3b1fd2d37dc4412f8305b5ed + minimumVersion: "19.0.0", + maximumKnownMajorVersion: "22", + }, + supported: true, + }, + { + id: "astro", + name: "Astro", + class: Astro, + frameworkPackageInfo: { + name: "astro", + // Version 4 was the earliest version that we manually tested + // in https://github.com/cloudflare/workers-sdk/pull/12938 + // earlier versions might also be supported but we haven't checked them + minimumVersion: "4.0.0", + maximumKnownMajorVersion: "6", + }, + supported: true, + }, + { + id: "hono", + name: "Hono", + supported: false, + }, + { + id: "next", + name: "Next.js", + class: NextJs, + frameworkPackageInfo: { + name: "next", + // 14.2.35 is the earliest version of Next.js officially supported by open-next + // see: https://github.com/cloudflare/workers-sdk/pull/11704#discussion_r2634519440 + minimumVersion: "14.2.35", + maximumKnownMajorVersion: "16", + }, + supported: true, + }, + { + id: "nuxt", + name: "Nuxt", + class: Nuxt, + frameworkPackageInfo: { + name: "nuxt", + // 3.21.0 is the first Nuxt version with Nitro 2.11+ which supports + // cloudflare.deployConfig and cloudflare.nodeCompat options + // See: https://github.com/nuxt/nuxt/releases/tag/v3.21.0 + // See: https://github.com/nitrojs/nitro/releases/tag/v2.11.0 + minimumVersion: "3.21.0", + maximumKnownMajorVersion: "4", + }, + supported: true, + }, + { + id: "qwik", + name: "Qwik", + class: Qwik, + frameworkPackageInfo: { + name: "@builder.io/qwik", + // 1.1.0 added the `platform` option in the qwikCity() Vite plugin + // which is required for getPlatformProxy integration + // See: https://github.com/QwikDev/qwik/pull/3604 + minimumVersion: "1.1.0", + maximumKnownMajorVersion: "1", + }, + supported: true, + }, + { + id: "react-router", + name: "React Router", + class: ReactRouter, + frameworkPackageInfo: { + name: "react-router", + // React Router v7 introduced framework mode with Vite integration and + // react-router.config.ts which are required for Cloudflare Workers support + // See: https://remix.run/blog/react-router-v7 + minimumVersion: "7.0.0", + maximumKnownMajorVersion: "8", + }, + supported: true, + }, + { + id: "solid-start", + name: "Solid Start", + class: SolidStart, + frameworkPackageInfo: { + name: "@solidjs/start", + // 1.0.0 is the first stable release with Nitro/Cloudflare Workers support + // See: https://github.com/solidjs/solid-start/releases/tag/v1.0.0 + minimumVersion: "1.0.0", + maximumKnownMajorVersion: "2", + }, + supported: true, + }, + { + id: "svelte-kit", + name: "SvelteKit", + class: SvelteKit, + frameworkPackageInfo: { + name: "@sveltejs/kit", + // 2.20.3 is required by @sveltejs/adapter-cloudflare@7.0.0 which first + // added Workers Static Assets support (cfTarget:workers option) + // See: https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fadapter-cloudflare%407.0.0 + minimumVersion: "2.20.3", + maximumKnownMajorVersion: "2", + }, + supported: true, + }, + { + id: "tanstack-start", + name: "TanStack Start", + class: TanstackStart, + frameworkPackageInfo: { + name: "@tanstack/react-start", + // 1.132.0 is the first Release Candidate for TanStack Start that supports Cloudflare + // See: https://github.com/TanStack/router/releases/tag/v1.132.0 + minimumVersion: "1.132.0", + maximumKnownMajorVersion: "1", + }, + supported: true, + }, + { + id: "vite", + name: "Vite", + class: Vite, + frameworkPackageInfo: { + name: "vite", + // Vite 6 introduced the Environment API which @cloudflare/vite-plugin requires + // See: https://vite.dev/blog/announcing-vite6#experimental-environment-api + // (6.1.0 is the minimum version supported by the vite plugin: + // https://github.com/cloudflare/workers-sdk/blob/b9b7e9d9fe/packages/vite-plugin-cloudflare/package.json#L80 + // we anyways allow for `6.0.x` versions since we bump them to `^6.1.0` in the autoconfig process) + minimumVersion: "6.0.0", + maximumKnownMajorVersion: "8", + }, + supported: true, + }, + { + id: "vike", + name: "Vike", + class: Vike, + frameworkPackageInfo: { + name: "vike", + minimumVersion: "0.0.0", + maximumKnownMajorVersion: "0", + }, + supported: true, + }, + { + id: "waku", + name: "Waku", + class: Waku, + frameworkPackageInfo: { + name: "waku", + // Autoconfig could support Waku before 1.0.0-alpha.4, but different autoconfig logic + // would need to be implemented for such versions, so we just decided to only support + // version 1.0.0-alpha.4 and up + // See: https://github.com/cloudflare/workers-sdk/pull/12657 + minimumVersion: "1.0.0-alpha.4", + maximumKnownMajorVersion: "1", + }, + supported: true, + }, + { + id: "cloudflare-pages", + name: "Cloudflare Pages", + // Autoconfiguring a Pages project into a Workers one is not yet supported + supported: false, + }, + { + id: "hydrogen", + name: "Hydrogen", + supported: false, + }, +] as const satisfies FrameworkInfo[]; + +/** + * Type specific for the "static" framework. + * + * It is supported by autoconfig but, unlike all other frameworks, it doesn't have a package associated to it + */ +type StaticFrameworkInfo = Omit< + Extract, + "frameworkPackageInfo" +>; + +export const staticFramework = { + id: "static", + name: "Static", + class: Static, + supported: true, +} as const satisfies StaticFrameworkInfo; + +/** Information for all the possible frameworks. This includes the "static" framework */ +export const allFrameworksInfos = [ + staticFramework, + ...allKnownFrameworks, +] as const satisfies (FrameworkInfo | StaticFrameworkInfo)[]; + +/** + * Gets the package information for a given framework, erroring if the framework + * could not be determined or is not supported. + * + * Returns `undefined` for the "static" framework, which has no associated package. + * + * @param frameworkId The id of the target framework + * @returns The framework's target package info, or undefined if the framework is "static" + */ +export function getFrameworkPackageInfo( + frameworkId: FrameworkInfo["id"] +): AutoConfigFrameworkPackageInfo | undefined { + if (frameworkId === staticFramework.id) { + // The "static" framework does not have an associated package + return undefined; + } + const targetedFramework = allKnownFrameworks.find( + (framework) => framework.id === frameworkId + ); + assert( + targetedFramework, + `Could not determine framework package info for ${JSON.stringify( + frameworkId + )}` + ); + assert( + targetedFramework.supported, + `Framework unexpectedly not supported ${JSON.stringify(frameworkId)}` + ); + return targetedFramework.frameworkPackageInfo; +} diff --git a/packages/autoconfig/src/frameworks/analog.ts b/packages/autoconfig/src/frameworks/analog.ts new file mode 100644 index 0000000..19faef8 --- /dev/null +++ b/packages/autoconfig/src/frameworks/analog.ts @@ -0,0 +1,90 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue } from "@cloudflare/cli-shared-helpers/colors"; +import { mergeObjectProperties, transformFile } from "@cloudflare/codemod"; +import { getTodaysCompatDate } from "@cloudflare/workers-utils"; +import * as recast from "recast"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class Analog extends Framework { + async configure({ + dryRun, + projectPath, + }: ConfigurationOptions): Promise { + if (!dryRun) { + await updateViteConfig(projectPath); + } + + return { + wranglerConfig: { + main: "./dist/analog/server/index.mjs", + assets: { + binding: "ASSETS", + directory: "./dist/analog/public", + }, + }, + }; + } +} + +async function updateViteConfig(projectPath: string) { + const viteConfigTsPAth = join(projectPath, "vite.config.ts"); + const viteConfigJsPath = join(projectPath, "vite.config.js"); + + let viteConfigPath: string; + + if (existsSync(viteConfigTsPAth)) { + viteConfigPath = viteConfigTsPAth; + } else if (existsSync(viteConfigJsPath)) { + viteConfigPath = viteConfigJsPath; + } else { + throw new Error("Could not find Vite config file to modify"); + } + + const compatDate = getTodaysCompatDate(); + + updateStatus(`Updating configuration in ${blue(viteConfigPath)}`); + + transformFile(viteConfigPath, { + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name !== "analog") { + return this.traverse(n); + } + + const b = recast.types.builders; + const presetDef = [ + b.objectProperty( + b.identifier("nitro"), + b.objectExpression([ + // preset: "cloudflare_module" + b.objectProperty( + b.identifier("preset"), + b.stringLiteral("cloudflare_module") + ), + b.objectProperty( + b.identifier("compatibilityDate"), + b.stringLiteral(compatDate) + ), + ]) + ), + ]; + + if (n.node.arguments.length === 0) { + n.node.arguments.push(b.objectExpression(presetDef)); + } else { + mergeObjectProperties( + n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, + presetDef + ); + } + + return false; + }, + }); +} diff --git a/packages/autoconfig/src/frameworks/angular.ts b/packages/autoconfig/src/frameworks/angular.ts new file mode 100644 index 0000000..fdebfb1 --- /dev/null +++ b/packages/autoconfig/src/frameworks/angular.ts @@ -0,0 +1,155 @@ +import assert from "node:assert"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { parseJSONC } from "@cloudflare/workers-utils"; +import semiver from "semiver"; +import dedent from "ts-dedent"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; +import type { PackageManager } from "@cloudflare/workers-utils"; + +export class Angular extends Framework { + async configure({ + workerName, + outputDir, + dryRun, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + const angularJson = parseJSONC( + await readFile(resolve("angular.json"), "utf8") + ) as AngularJson; + + if (hasSsr(angularJson, workerName)) { + this.configurationDescription = "Configuring project for Angular"; + if (!dryRun) { + await updateAngularJson(workerName, angularJson, this.frameworkVersion); + await overrideServerFile(); + await installAdditionalDependencies(packageManager, isWorkspaceRoot); + } + return { + wranglerConfig: { + main: "./dist/server/server.mjs", + assets: { + binding: "ASSETS", + directory: `${outputDir}browser`, + }, + }, + }; + } else { + this.configurationDescription = + "Configuring Angular SPA project (assets only)"; + return { + wranglerConfig: { + assets: { + directory: outputDir, + }, + }, + }; + } + } +} + +/** + * Checks whether the given Angular project has SSR configured. + * SSR is considered enabled when `architect.build.options.ssr` exists and is truthy. + */ +function hasSsr(angularJson: AngularJson, projectName: string): boolean { + const ssr = angularJson.projects[projectName]?.architect?.build?.options?.ssr; + return !!ssr; +} + +async function updateAngularJson( + projectName: string, + angularJson: AngularJson, + frameworkVersion: string +) { + const s = spinner(); + s.start(`Updating angular.json config`); + + // Update builder + const architectSection = angularJson.projects[projectName].architect; + architectSection.build.options.outputPath = "dist"; + architectSection.build.options.outputMode = "server"; + // ssr is guaranteed to be truthy here (checked by hasSsr before calling this). + // `ssr: true` is a valid Angular shorthand for SSR with defaults — normalise it + // to an object before setting properties on it. + assert(architectSection.build.options.ssr); + if (typeof architectSection.build.options.ssr === "boolean") { + architectSection.build.options.ssr = {}; + } + const platformKey = + semiver(frameworkVersion, "22.0.0") >= 0 + ? "platform" + : "experimentalPlatform"; + architectSection.build.options.ssr[platformKey] = "neutral"; + + await writeFile( + resolve("angular.json"), + JSON.stringify(angularJson, null, 2) + ); + + s.stop(`${brandColor(`updated`)} ${dim(`\`angular.json\``)}`); +} + +async function overrideServerFile() { + await writeFile( + resolve("src/server.ts"), + dedent` + import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; + + const angularApp = new AngularAppEngine({ + // It is safe to set allow \`localhost\`, so that SSR can run in local development, + // as, in production, Cloudflare will ensure that \`localhost\` is not the host. + allowedHosts: ['localhost'], + }); + + /** + * This is a request handler used by the Angular CLI (dev-server and during build). + */ + export const reqHandler = createRequestHandler(async (req) => { + const res = await angularApp.handle(req); + + return res ?? new Response('Page not found.', { status: 404 }); + }); + + export default { fetch: reqHandler }; + ` + ); +} + +async function installAdditionalDependencies( + packageManager: PackageManager, + isWorkspaceRoot: boolean +) { + await installPackages(packageManager.type, ["xhr2"], { + dev: true, + startText: "Installing additional dependencies", + doneText: `${brandColor("installed")}`, + isWorkspaceRoot, + }); +} + +type AngularJson = { + projects: Record< + string, + { + architect: { + build: { + options: { + outputPath: string; + outputMode: string; + ssr?: Record | boolean | null; + assets: string[]; + }; + }; + }; + } + >; +}; diff --git a/packages/autoconfig/src/frameworks/astro.ts b/packages/autoconfig/src/frameworks/astro.ts new file mode 100644 index 0000000..cbc986f --- /dev/null +++ b/packages/autoconfig/src/frameworks/astro.ts @@ -0,0 +1,305 @@ +import { + existsSync, + readFileSync as fsReadFileSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue, brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { runCommand } from "@cloudflare/cli-shared-helpers/command"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { mergeObjectProperties, transformFile } from "@cloudflare/codemod"; +import { parseJSONC } from "@cloudflare/workers-utils"; +import * as recast from "recast"; +import semiver from "semiver"; +import { Framework } from "./framework-class"; +import type { AutoConfigContext } from "../context"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; +import type { PackageManager } from "@cloudflare/workers-utils"; + +export class Astro extends Framework { + async configure({ + outputDir, + dryRun, + packageManager, + projectPath, + isWorkspaceRoot, + context, + }: ConfigurationOptions): Promise { + const astroVersion = this.frameworkVersion; + + const { npx } = packageManager; + if (!dryRun) { + if (semiver(astroVersion, "6.0.0") >= 0) { + // For Astro 6.0.0+ use the native `astro add cloudflare` command + await runCommand([npx, "astro", "add", "cloudflare", "-y"], { + silent: true, + startText: "Installing adapter", + doneText: `${brandColor("installed")} ${dim( + `via \`${npx} astro add cloudflare\`` + )}`, + }); + } else { + // For older versions of Astro we need to apply manual configuration since `astro add cloudflare` + // tries to install the latest version of the adapter causing conflicts + + // Note: here the Astro version can only be 5 or 4 because of the minimum version validation + const astroMajorVersion = semiver(astroVersion, "5.0.0") >= 0 ? 5 : 4; + await configureAstroLegacy( + projectPath, + isWorkspaceRoot, + packageManager, + astroMajorVersion, + context + ); + } + + writeFileSync("public/.assetsignore", "_worker.js\n_routes.json"); + } + + if (semiver(astroVersion, "6.0.0") < 0) { + // Before version 6 Astro required a wrangler config file + return { + wranglerConfig: { + main: `${outputDir}/_worker.js/index.js`, + compatibility_flags: ["global_fetch_strictly_public"], + assets: { + binding: "ASSETS", + directory: outputDir, + }, + }, + }; + } + + // From version 6 Astro doesn't need a wrangler config file but generates a redirected config on build + return { + wranglerConfig: null, + }; + } + + configurationDescription = + 'Configuring project for Astro with "astro add cloudflare"'; +} + +/** + * Finds the Astro config file in the project directory. + * Checks for astro.config.mjs, astro.config.ts, and astro.config.js in that order. + * + * @param projectPath The path of the project + * @returns The path to the Astro config file + * @throws Error if no config file is found + */ +function findAstroConfigFile(projectPath: string): string { + const extensions = ["mjs", "mts", "ts", "js"]; + for (const ext of extensions) { + const configPath = join(projectPath, `astro.config.${ext}`); + if (existsSync(configPath)) { + return configPath; + } + } + throw new Error( + "Could not find Astro config file (astro.config.mjs, astro.config.mts, astro.config.ts, or astro.config.js)" + ); +} + +/** + * Updates the Astro config file to add the Cloudflare adapter. + * This replicates the logic from `astro add cloudflare` for Astro versions < 6.0.0. + * + * @param projectPath The path of the project + * @param astroMajorVersion The major version of Astro (4 or 5) to determine the config options + */ +function updateAstroConfig( + projectPath: string, + astroMajorVersion: 4 | 5 +): void { + const configPath = findAstroConfigFile(projectPath); + + updateStatus(`Updating configuration in ${blue(configPath)}`); + + // Track the adapter identifier name (either from existing import or the one we add) + let adapterIdentifier = "cloudflare"; + + transformFile(configPath, { + // First pass: check for existing cloudflare import and add if missing + visitProgram(path) { + const body = path.node.body; + const b = recast.types.builders; + + // Check if cloudflare import already exists and capture the local identifier + let hasCloudflareImport = false; + for (const node of body) { + if ( + node.type === "ImportDeclaration" && + node.source.value === "@astrojs/cloudflare" + ) { + hasCloudflareImport = true; + // Find the default import specifier and capture its local name + for (const specifier of node.specifiers ?? []) { + if ( + specifier.type === "ImportDefaultSpecifier" && + specifier.local + ) { + // specifier.local is an Identifier node with a `name` property + const local = specifier.local as { name: string }; + adapterIdentifier = local.name; + break; + } + } + break; + } + } + + // Add the import if it doesn't exist + if (!hasCloudflareImport) { + const importDeclaration = b.importDeclaration( + [b.importDefaultSpecifier(b.identifier("cloudflare"))], + b.literal("@astrojs/cloudflare") + ); + + // Find the last import statement and insert after it + let lastImportIndex = -1; + for (let i = 0; i < body.length; i++) { + if (body[i].type === "ImportDeclaration") { + lastImportIndex = i; + } + } + + if (lastImportIndex >= 0) { + body.splice(lastImportIndex + 1, 0, importDeclaration); + } else { + // No imports found, add at the beginning + body.unshift(importDeclaration); + } + } + + this.traverse(path); + }, + + // Second pass: add adapter (and output for Astro 4) to defineConfig + visitCallExpression(path) { + const callee = path.node.callee; + + // Check if this is a defineConfig call + if (callee.type !== "Identifier" || callee.name !== "defineConfig") { + return this.traverse(path); + } + + const b = recast.types.builders; + + // Create the adapter property using the captured identifier name + const adapterProp = b.objectProperty( + b.identifier("adapter"), + b.callExpression(b.identifier(adapterIdentifier), []) + ); + + const propsToAdd: recast.types.namedTypes.ObjectProperty[] = []; + + if (astroMajorVersion === 4) { + // Astro 4 requires explicit output: "hybrid" for SSR + const outputProp = b.objectProperty( + b.identifier("output"), + b.literal("hybrid") + ); + propsToAdd.push(outputProp); + } + propsToAdd.push(adapterProp); + + // Get or create the config object argument + if (path.node.arguments.length === 0) { + path.node.arguments.push(b.objectExpression(propsToAdd)); + } else { + const configArg = path.node + .arguments[0] as recast.types.namedTypes.ObjectExpression; + if (configArg.type === "ObjectExpression") { + mergeObjectProperties(configArg, propsToAdd); + } + } + + return false; + }, + }); +} + +/** + * Updates the tsconfig.json to include worker-configuration.d.ts. + * This replicates part of the `astro add cloudflare` behavior. + * + * @param projectPath The path of the project + * @param context The autoconfig context providing logger and other dependencies + */ +function updateTsConfig(projectPath: string, context: AutoConfigContext) { + const tsconfigPath = join(projectPath, "tsconfig.json"); + if (!existsSync(tsconfigPath)) { + return; + } + + try { + const content = fsReadFileSync(tsconfigPath, "utf-8"); + const tsconfig = parseJSONC(content, tsconfigPath) as Record< + string, + unknown + >; + + const includeEntry = "./worker-configuration.d.ts"; + + if (!tsconfig.include) { + // If `include` is not defined, the tsconfig likely inherits it from a parent config (e.g., "extends": "astro/tsconfigs/base"). + // Adding an `include` field here would override the parent's includes, breaking type-checking. + // Instead, warn the user to add it manually. + context.logger.warn( + `Could not find an existing \`include\` field in tsconfig.json. You may need to manually add ${JSON.stringify( + includeEntry + )} to your tsconfig.json \`include\` array.` + ); + return; + } + + if (!(tsconfig.include as string[]).includes(includeEntry)) { + (tsconfig.include as string[]).push(includeEntry); + writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, "\t")); + updateStatus( + `Updated ${blue("tsconfig.json")} to include ${blue(includeEntry)}` + ); + } + } catch { + context.logger.warn( + `Could not update tsconfig.json to include worker-configuration.d.ts. You may need to add it manually.` + ); + } +} + +/** + * Configures an Astro project for Cloudflare deployment when running Astro < 6.0.0. + * This replicates the core logic from `astro add cloudflare` command since that command + * is not available or behaves differently in older Astro versions. + * + * @param projectPath The path of the project + * @param isWorkspaceRoot Whether the project is at the root of a workspace (affects package installation flags) + * @param packageManager The package manager to use for installing dependencies + * @param astroMajorVersion The major version of Astro (4 or 5) to determine the correct adapter version + */ +async function configureAstroLegacy( + projectPath: string, + isWorkspaceRoot: boolean, + packageManager: PackageManager, + astroMajorVersion: 4 | 5, + context: AutoConfigContext +): Promise { + const astroCloudflarePackageVersion = astroMajorVersion === 5 ? 12 : 11; + + await installPackages( + packageManager.type, + [`@astrojs/cloudflare@${astroCloudflarePackageVersion}`], + { + startText: `Installing @astrojs/cloudflare adapter (version ${astroCloudflarePackageVersion})`, + doneText: `${brandColor("installed")} ${dim("@astrojs/cloudflare")}`, + isWorkspaceRoot, + } + ); + updateAstroConfig(projectPath, astroMajorVersion); + updateTsConfig(projectPath, context); +} diff --git a/packages/autoconfig/src/frameworks/framework-class.ts b/packages/autoconfig/src/frameworks/framework-class.ts new file mode 100644 index 0000000..93f5152 --- /dev/null +++ b/packages/autoconfig/src/frameworks/framework-class.ts @@ -0,0 +1,119 @@ +import assert from "node:assert"; +import semiver from "semiver"; +import { AutoConfigFrameworkConfigurationError } from "../errors"; +import { getInstalledPackageVersion } from "./utils/packages"; +import type { AutoConfigFrameworkPackageInfo, FrameworkInfo } from "."; +import type { AutoConfigContext } from "../context"; +import type { PackageManager } from "@cloudflare/workers-utils"; +import type { RawConfig } from "@cloudflare/workers-utils"; + +export abstract class Framework { + readonly id: FrameworkInfo["id"]; + readonly name: FrameworkInfo["name"]; + + #frameworkVersion: string | undefined; + get frameworkVersion(): string { + assert( + this.#frameworkVersion, + `The version for ${JSON.stringify(this.name)} is unexpectedly missing` + ); + return this.#frameworkVersion; + } + + constructor(frameworkInfo: Pick) { + this.id = frameworkInfo.id; + this.name = frameworkInfo.name; + } + + isConfigured(_projectPath: string): boolean { + return false; + } + + abstract configure( + options: ConfigurationOptions + ): Promise | ConfigurationResults; + + configurationDescription?: string; + + /** + * Validates the installed framework version against the supported range and + * stores it for later access via the `frameworkVersion` getter. + * Warns via the context logger if the version exceeds `maximumKnownMajorVersion`. + * + * @param projectPath - Path to the project root used to resolve the installed version. + * @param frameworkPackageInfo - Package metadata including name and version bounds. + * @param context - The autoconfig context providing logger and other dependencies. + * @throws {AssertionError} If the installed version cannot be determined. + * @throws {AutoConfigFrameworkConfigurationError} If the version is below `minimumVersion`. + */ + validateFrameworkVersion( + projectPath: string, + frameworkPackageInfo: AutoConfigFrameworkPackageInfo, + context: AutoConfigContext + ) { + const frameworkVersion = getInstalledPackageVersion( + frameworkPackageInfo.name, + projectPath + ); + + assert( + frameworkVersion, + `Unable to detect the version of the \`${frameworkPackageInfo.name}\` package` + ); + + if (semiver(frameworkVersion, frameworkPackageInfo.minimumVersion) < 0) { + throw new AutoConfigFrameworkConfigurationError( + `The version of ${this.name} used in the project (${JSON.stringify( + frameworkVersion + )}) cannot be automatically configured. Please update the ${ + this.name + } version to at least ${JSON.stringify( + frameworkPackageInfo.minimumVersion + )} and try again.`, + { telemetryMessage: "autoconfig framework version unsupported" } + ); + } + + if ( + semiver(frameworkVersion, frameworkPackageInfo.maximumKnownMajorVersion) > + 0 + ) { + context.logger.warn( + `The version of ${this.name} used in the project (${JSON.stringify( + frameworkVersion + )}) is not officially supported, and may fail to correctly configure. Please report any issues to https://github.com/cloudflare/workers-sdk/issues` + ); + } + + this.#frameworkVersion = frameworkVersion; + } +} + +export type ConfigurationOptions = { + outputDir: string; + projectPath: string; + workerName: string; + dryRun: boolean; + packageManager: PackageManager; + isWorkspaceRoot: boolean; + context: AutoConfigContext; +}; + +export type PackageJsonScriptsOverrides = { + preview?: string; // default is `npm run build && wrangler dev` + deploy?: string; // default is `npm run build && wrangler deploy` + typegen?: string; // default is `wrangler types` +}; + +export type ConfigurationResults = { + /** The wrangler configuration that the framework's `configure()` hook should generate. `null` if autoconfig should not create the wrangler file (in case an external tool already does that) */ + wranglerConfig: RawConfig | null; + // Scripts to override in the package.json. Most frameworks should not need to do this, as their default detected build command will be sufficient + packageJsonScriptsOverrides?: PackageJsonScriptsOverrides; + // Build command to override the standard one (`npm run build` or framework's build command) + buildCommandOverride?: string; + // Deploy command to override the standard one (`npx wrangler deploy`) + deployCommandOverride?: string; + // Version command to override the standard one (`npx wrangler versions upload`) + versionCommandOverride?: string; +}; diff --git a/packages/autoconfig/src/frameworks/index.ts b/packages/autoconfig/src/frameworks/index.ts new file mode 100644 index 0000000..9e90c2a --- /dev/null +++ b/packages/autoconfig/src/frameworks/index.ts @@ -0,0 +1,83 @@ +import assert from "node:assert"; +import { allFrameworksInfos, staticFramework } from "./all-frameworks"; +import { NoOpFramework } from "./no-op"; +import type { Framework } from "./framework-class"; + +export type { Framework, PackageJsonScriptsOverrides } from "./framework-class"; + +/** Set of the ids of all the possible frameworks, including the "static" framework */ +const allKnownFrameworksIds = new Set( + allFrameworksInfos.map(({ id }) => id) +); + +/** + * Identifies whether a given id maps to a known framework's id + * + * @param frameworkId The target id to check + * @returns true if the id is that of a known framework, false otherwise + */ +export function isKnownFramework(frameworkId: string): boolean { + return allKnownFrameworksIds.has(frameworkId); +} + +/** + * Gets a class instance for a framework based on its id + * + * @param frameworkId The target framework's id + * @returns The class for the framework, defaulting to the static framework is the id is not recognized + */ +export function getFrameworkClassInstance( + frameworkId: FrameworkInfo["id"] +): Framework { + const targetedFramework = allFrameworksInfos.find( + (framework) => framework.id === frameworkId + ); + const framework = targetedFramework ?? staticFramework; + const targetClass = + framework.supported === false ? NoOpFramework : framework.class; + + return new targetClass({ id: framework.id, name: framework.name }); +} + +/** + * Checks whether a framework is supported by autoconfig. + * + * @param frameworkId The target framework's id + * @returns a boolean indicating wether the framework is supported + */ +export function isFrameworkSupported( + frameworkId: FrameworkInfo["id"] +): boolean { + const targetedFramework = allFrameworksInfos.find( + (framework) => framework.id === frameworkId + ); + assert( + targetedFramework, + `Unexpected unknown framework id: ${JSON.stringify(frameworkId)}` + ); + return targetedFramework.supported; +} + +export type FrameworkInfo = { + id: string; + name: string; +} & ( + | { supported: false } + | { + supported: true; + class: typeof Framework; + frameworkPackageInfo: AutoConfigFrameworkPackageInfo; + } +); + +/** + * AutoConfig information for a package that defines a framework. + */ +export type AutoConfigFrameworkPackageInfo = { + /** The package name (e.g. "astro" for the Astro framework and "@solidjs/start" for the SolidStart framework) */ + name: string; + /** The minimum version (if any) of the package/framework that autoconfig supports */ + minimumVersion: string; + /** The latest major version of the package/framework that autoconfig supports */ + maximumKnownMajorVersion: string; +}; diff --git a/packages/autoconfig/src/frameworks/next.ts b/packages/autoconfig/src/frameworks/next.ts new file mode 100644 index 0000000..70af217 --- /dev/null +++ b/packages/autoconfig/src/frameworks/next.ts @@ -0,0 +1,48 @@ +import { runCommand } from "@cloudflare/cli-shared-helpers/command"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class NextJs extends Framework { + async configure({ + dryRun, + projectPath, + packageManager, + }: ConfigurationOptions): Promise { + const { npx, dlx } = packageManager; + + if (!dryRun) { + await runCommand( + [ + ...dlx, + "@opennextjs/cloudflare", + "migrate", + // Note: we force-install so that even if an incompatible version of + // Next.js is used this installation still succeeds, moving users + // (hopefully) in right direction (instead of failing at this step) + "--force-install", + ], + { + cwd: projectPath, + } + ); + } + + return { + // `@opennextjs/cloudflare migrate` creates the wrangler config file + wranglerConfig: {}, + packageJsonScriptsOverrides: { + preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview", + deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy", + }, + buildCommandOverride: `${npx} opennextjs-cloudflare build`, + deployCommandOverride: `${npx} opennextjs-cloudflare deploy`, + versionCommandOverride: `${npx} opennextjs-cloudflare upload`, + }; + } + + configurationDescription = + "Configuring project for Next.js with OpenNext by running `@opennextjs/cloudflare migrate`"; +} diff --git a/packages/autoconfig/src/frameworks/no-op.ts b/packages/autoconfig/src/frameworks/no-op.ts new file mode 100644 index 0000000..4746451 --- /dev/null +++ b/packages/autoconfig/src/frameworks/no-op.ts @@ -0,0 +1,10 @@ +import { Framework } from "./framework-class"; +import type { ConfigurationResults } from "./framework-class"; + +export class NoOpFramework extends Framework { + async configure(): Promise { + return { + wranglerConfig: {}, + }; + } +} diff --git a/packages/autoconfig/src/frameworks/nuxt.ts b/packages/autoconfig/src/frameworks/nuxt.ts new file mode 100644 index 0000000..4720ea3 --- /dev/null +++ b/packages/autoconfig/src/frameworks/nuxt.ts @@ -0,0 +1,89 @@ +import path from "node:path"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { mergeObjectProperties, transformFile } from "@cloudflare/codemod"; +import * as recast from "recast"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +const updateNuxtConfig = (projectPath: string) => { + const configFile = path.join(projectPath, "nuxt.config.ts"); + + const b = recast.types.builders; + + const presetDef = b.objectProperty( + b.identifier("nitro"), + b.objectExpression([ + b.objectProperty( + b.identifier("preset"), + b.stringLiteral("cloudflare_module") + ), + b.objectProperty( + b.identifier("cloudflare"), + b.objectExpression([ + b.objectProperty( + b.identifier("deployConfig"), + b.booleanLiteral(true) + ), + b.objectProperty(b.identifier("nodeCompat"), b.booleanLiteral(true)), + ]) + ), + ]) + ); + + const moduleDef = b.objectProperty( + b.identifier("modules"), + b.arrayExpression([b.stringLiteral("nitro-cloudflare-dev")]) + ); + + transformFile(configFile, { + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name === "defineNuxtConfig") { + mergeObjectProperties( + n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, + [presetDef, moduleDef] + ); + } + + return this.traverse(n); + }, + }); +}; + +export class Nuxt extends Framework { + async configure({ + dryRun, + projectPath, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + if (!dryRun) { + await installPackages(packageManager.type, ["nitro-cloudflare-dev"], { + dev: true, + startText: "Installing the Cloudflare dev module", + doneText: `${brandColor(`installed`)} ${dim("nitro-cloudflare-dev")}`, + isWorkspaceRoot, + }); + updateNuxtConfig(projectPath); + } + + return { + wranglerConfig: { + main: "./.output/server/index.mjs", + assets: { + binding: "ASSETS", + directory: "./.output/public/", + }, + observability: { + enabled: true, + }, + }, + }; + } + + configurationDescription = "Configuring project for Nuxt"; +} diff --git a/packages/autoconfig/src/frameworks/qwik.ts b/packages/autoconfig/src/frameworks/qwik.ts new file mode 100644 index 0000000..698ec69 --- /dev/null +++ b/packages/autoconfig/src/frameworks/qwik.ts @@ -0,0 +1,135 @@ +import { endSection } from "@cloudflare/cli-shared-helpers"; +import { brandColor } from "@cloudflare/cli-shared-helpers/colors"; +import { + quoteShellArgs, + runCommand, +} from "@cloudflare/cli-shared-helpers/command"; +import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; +import { transformFile } from "@cloudflare/codemod"; +import * as recast from "recast"; +import * as typescriptParser from "recast/parsers/typescript"; +import { usesTypescript } from "../uses-typescript"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; +import type { Program } from "esprima"; + +export class Qwik extends Framework { + async configure({ + projectPath, + dryRun, + packageManager, + }: ConfigurationOptions): Promise { + if (!dryRun) { + // Add the workers integration + const cmd = [ + // For some reason `pnpx qwik add` fails for qwik so we use `pnpm qwik add` instead. + packageManager.type === "pnpm" + ? packageManager.type + : packageManager.npx, + "qwik", + "add", + "cloudflare-workers", + "--skipConfirmation=true", + ]; + endSection(`Running ${quoteShellArgs(cmd)}`); + await runCommand(cmd); + + addBindingsProxy(projectPath); + } + return { + wranglerConfig: { + main: "./dist/_worker.js", + compatibility_flags: ["global_fetch_strictly_public"], + assets: { + binding: "ASSET", + directory: "./dist", + }, + }, + packageJsonScriptsOverrides: { + preview: `${packageManager.type} run build && wrangler dev`, + deploy: `${packageManager.type} run build && wrangler deploy`, + }, + }; + } + + configurationDescription = + 'Configuring project for Qwik with "qwik add cloudflare-workers"'; +} + +function addBindingsProxy(projectPath: string) { + // Qwik only has a typescript template atm. + // This check is an extra precaution + if (!usesTypescript(projectPath)) { + return; + } + + const s = spinner(); + s.start("Updating `vite.config.ts`"); + + const b = recast.types.builders; + + const getPlatformProxyTsAstNodes = ( + recast.parse( + ` + let platform = {}; + + if (process.env.NODE_ENV === 'development') { + const { getPlatformProxy } = await import('wrangler'); + platform = await getPlatformProxy(); + } + `, + { parser: typescriptParser } + ).program as Program + ).body; + + transformFile("vite.config.ts", { + // Insert the env declaration after the last import (but before the rest of the body) + visitProgram: function (n) { + const lastImportIndex = n.node.body.findLastIndex( + (t) => t.type === "ImportDeclaration" + ); + const lastImport = n.get("body", lastImportIndex); + lastImport.insertAfter(...getPlatformProxyTsAstNodes); + + return this.traverse(n); + }, + // Pass the `platform` object from the declaration to the `qwikCity` plugin + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name !== "qwikCity") { + return this.traverse(n); + } + + // The config object passed to `qwikCity` + const configArgument = n.node.arguments[0] as + | recast.types.namedTypes.ObjectExpression + | undefined; + + const platformPropery = b.objectProperty.from({ + key: b.identifier("platform"), + value: b.identifier("platform"), + shorthand: true, + }); + + if (!configArgument) { + n.node.arguments = [b.objectExpression([platformPropery])]; + + return false; + } + + if (configArgument.type !== "ObjectExpression") { + throw new Error("Failed to update `vite.config.ts`"); + } + + // Add the `platform` object to the object + configArgument.properties.push(platformPropery); + + return false; + }, + }); + + s.stop(`${brandColor("updated")} \`vite.config.ts\``); +} diff --git a/packages/autoconfig/src/frameworks/react-router.ts b/packages/autoconfig/src/frameworks/react-router.ts new file mode 100644 index 0000000..449e6e5 --- /dev/null +++ b/packages/autoconfig/src/frameworks/react-router.ts @@ -0,0 +1,524 @@ +import assert from "node:assert"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { parseFile, transformFile } from "@cloudflare/codemod"; +import * as recast from "recast"; +import semiver from "semiver"; +import dedent from "ts-dedent"; +import { Framework } from "./framework-class"; +import { transformViteConfig } from "./utils/vite-config"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { AutoConfigContext } from "../context"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; +import type { Program } from "esprima"; + +const b = recast.types.builders; + +/** + * Resolves the path to the React Router config file (`react-router.config.ts` or `.js`) + * in the given project directory. + * + * @param projectPath - Absolute path to the project root. + * @returns The resolved config file path, or `null` if neither `.ts` nor `.js` variant exists. + */ +function getReactRouterConfigPath(projectPath: string): string | null { + const filePathTS = path.join(projectPath, "react-router.config.ts"); + const filePathJS = path.join(projectPath, "react-router.config.js"); + + if (existsSync(filePathTS)) { + return filePathTS; + } + + if (existsSync(filePathJS)) { + return filePathJS; + } + + return null; +} + +/** + * Checks whether the middleware pattern should be used for the given React Router project. + * + * For React Router >= 8.0.0, middleware is enabled by default + * (ref: https://github.com/remix-run/react-router/pull/15078), so this always returns `true`. + * + * For earlier versions, it parses the user's `react-router.config.ts` (or `.js`) to check + * whether `future.v8_middleware` is explicitly set to `true`. + * + * This determines which code pattern to generate: + * - With middleware: simplified fetch handler using `cloudflare:workers` env pattern + * - Without middleware: traditional `AppLoadContext` pattern with `env`/`ctx` params + * + * Parses the config file read-only (via `parseFile`) without writing anything to disk. + * Handles both TS `satisfies`/`as` expressions and plain object exports. + * + * @param projectPath - Absolute path to the project root containing the React Router config file. + * @param frameworkVersion - React Router semver version string. When provided and + * >= 8.0.0, returns `true` without parsing the config file (middleware is the default in v8). + * @returns `true` if middleware is enabled (either by default in v8+ or via `future.v8_middleware` + * in the config), `false` otherwise (including when the config file is missing or has no + * `future` block). + */ +export function hasV8MiddlewareFlag( + projectPath: string, + frameworkVersion?: string +): boolean { + // React Router >= 8.0.0 has middleware enabled by default + if (frameworkVersion && semiver(frameworkVersion, "8.0.0") >= 0) { + return true; + } + + const filePath = getReactRouterConfigPath(projectPath); + if (!filePath) { + return false; + } + + let ast: Program | null = null; + try { + ast = parseFile(filePath); + } catch {} + + if (!ast) { + return false; + } + + let found = false; + recast.visit(ast, { + visitExportDefaultDeclaration(n) { + let node: recast.types.namedTypes.ObjectExpression | null = null; + if ( + (n.node.declaration.type === "TSAsExpression" || + n.node.declaration.type === "TSSatisfiesExpression") && + n.node.declaration.expression.type === "ObjectExpression" + ) { + node = n.node.declaration.expression; + } else if (n.node.declaration.type === "ObjectExpression") { + node = n.node.declaration; + } + + if (node) { + const futureProp = node.properties.find( + (p) => + p.type === "ObjectProperty" && + p.key.type === "Identifier" && + p.key.name === "future" && + p.value.type === "ObjectExpression" + ); + if ( + futureProp?.type === "ObjectProperty" && + futureProp.value.type === "ObjectExpression" + ) { + found = futureProp.value.properties.some( + (p) => + p.type === "ObjectProperty" && + p.key.type === "Identifier" && + p.key.name === "v8_middleware" && + p.value.type === "BooleanLiteral" && + p.value.value === true + ); + } + } + return false; + }, + }); + + return found; +} + +/** + * Transforms the user's `react-router.config.ts` (or `.js`) to ensure the + * `future.unstable_viteEnvironmentApi` or `future.v8_viteEnvironmentApi` flag + * is set to `true`. If a `future` block already exists, the flag is added or + * updated in place; otherwise a new `future` block is created. + * + * Supports TS `as` and `satisfies` expressions wrapping the default export object. + * + * @param projectPath - Absolute path to the project root containing the React Router config file. + * @param viteEnvironmentKey - The config property name to set, either + * `"unstable_viteEnvironmentApi"` (React Router < 7.10.0) or `"v8_viteEnvironmentApi"`. + * @throws {Error} If no `react-router.config.ts` or `.js` file exists in the project. + * @throws {Error} If the default export cannot be parsed as an object expression. + */ +function transformReactRouterConfig( + projectPath: string, + viteEnvironmentKey: ReturnType +) { + const filePath = getReactRouterConfigPath(projectPath); + if (!filePath) { + throw new Error("Could not find React Router config file to modify"); + } + + transformFile(filePath, { + /** + * Visit an export default declaration of the form: + * + * export default { + * ... + * } + * + * and add or modify the `future` property to look like: + * + * future: { + * unstable_viteEnvironmentApi: true // v8_viteEnvironment depending on the React Router version + * } + * + * For some extra complexity, this also supports TS `as` and `satisfies` expressions + */ + visitExportDefaultDeclaration(n) { + let node: recast.types.namedTypes.ObjectExpression; + if ( + (n.node.declaration.type === "TSAsExpression" || + n.node.declaration.type === "TSSatisfiesExpression") && + n.node.declaration.expression.type === "ObjectExpression" + ) { + node = n.node.declaration.expression; + } else if (n.node.declaration.type === "ObjectExpression") { + node = n.node.declaration; + } else { + throw new Error( + `Could not parse React Router config file. Please add the following snippet manually:\n future: {\n ${viteEnvironmentKey}: true,\n }` + ); + } + + assert(node.type === "ObjectExpression"); + + // Is there an existing `future` key? If there is, we should modify it rather than creating a new one + const futureKey = node.properties.findIndex( + (p) => + p.type === "ObjectProperty" && + p.key.type === "Identifier" && + p.key.name === "future" && + p.value.type === "ObjectExpression" + ); + if (futureKey !== -1) { + const future = node.properties[futureKey]; + assert( + future.type === "ObjectProperty" && + future.value.type === "ObjectExpression" + ); + + // Does the `future` key already have a property called `unstable_viteEnvironmentApi`? + const viteEnvironment = future.value.properties.findIndex( + (p) => + p.type === "ObjectProperty" && + p.key.type === "Identifier" && + p.key.name === viteEnvironmentKey && + p.value.type === "BooleanLiteral" + ); + + // If there's already a unstable_viteEnvironmentApi key, set the value to true + if (viteEnvironment !== -1) { + const prop = future.value.properties[viteEnvironment]; + assert( + prop.type === "ObjectProperty" && + prop.value.type === "BooleanLiteral" + ); + prop.value.value = true; + } else { + const prop = b.objectProperty( + b.identifier(viteEnvironmentKey), + b.booleanLiteral(true) + ); + future.value.properties.push(prop); + } + } else { + node.properties.push( + b.objectProperty( + b.identifier("future"), + b.objectExpression([ + b.objectProperty( + b.identifier(viteEnvironmentKey), + b.booleanLiteral(true) + ), + ]) + ) + ); + } + + return false; + }, + }); +} + +/** + * Returns the correct future flag property name for enabling the Vite Environment API + * based on the installed React Router version. + * + * @param reactRouterVersion - The installed React Router semver version string (e.g. `"7.16.0"`). + * When empty, defaults to the stable `"v8_viteEnvironmentApi"` name. + * @returns `"unstable_viteEnvironmentApi"` for versions before 7.10.0, + * or `"v8_viteEnvironmentApi"` for 7.10.0 and later. + */ +function viteEnvApiConfigPropertyName(reactRouterVersion: string) { + if (!reactRouterVersion) { + return "v8_viteEnvironmentApi"; + } + + // version less than 7.10.0 + if (semiver(reactRouterVersion, "7.10.0") === -1) { + return "unstable_viteEnvironmentApi"; + } else { + return "v8_viteEnvironmentApi"; + } +} + +/** + * Determines whether the installed React Router version still requires the explicit + * `future` flag in `react-router.config.ts` for enabling the vite environment API. + * + * React Router >= 8.0.0 enables these features by default + * (ref: https://github.com/remix-run/react-router/pull/15077), so no future flags + * are needed (they also can't be included as they would cause errors at build time). + * + * @param reactRouterVersion - The installed React Router semver version string (e.g. `"7.16.0"`). + * @returns `true` if the future flag needs to be set (versions < 8.0.0), `false` otherwise. + */ +function needsViteEnvAPIFutureFlag(reactRouterVersion: string): boolean { + if (!reactRouterVersion) { + return false; + } + + return semiver(reactRouterVersion, "8.0.0") < 0; +} + +/** + * Writes the `workers/app.ts` Worker entry point. When `v8_middleware` is enabled, + * generates a simplified fetch handler that delegates directly to the request handler. + * Otherwise generates the traditional pattern with `AppLoadContext` module augmentation + * and explicit `env`/`ctx` forwarding. + * + * @param useMiddlewarePattern - Whether `v8_middleware` is enabled in the user's config. + */ +function writeAppTs(useMiddlewarePattern: boolean) { + if (useMiddlewarePattern) { + writeFileSync( + "workers/app.ts", + dedent /* javascript */ ` + import { createRequestHandler } from "react-router"; + + const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE, + ); + + export default { + async fetch(request) { + return requestHandler(request); + }, + } satisfies ExportedHandler; + ` + ); + return; + } + + writeFileSync( + "workers/app.ts", + dedent /* javascript */ ` + import { createRequestHandler } from "react-router"; + + declare module "react-router" { + export interface AppLoadContext { + cloudflare: { + env: Env; + ctx: ExecutionContext; + }; + } + } + + const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE + ); + + export default { + async fetch(request, env, ctx) { + return requestHandler(request, { + cloudflare: { env, ctx }, + }); + }, + } satisfies ExportedHandler; + ` + ); +} + +/** + * Writes `app/entry.server.tsx` if it does not already exist. When `v8_middleware` + * is enabled, the generated file omits the `AppLoadContext` import and `_loadContext` + * parameter. Otherwise uses the traditional signature that includes both. + * + * If the file already exists on disk it is left untouched and a warning is logged. + * + * @param useMiddlewarePattern - Whether `v8_middleware` is enabled in the user's config. + * @param context - The autoconfig context providing logger and other dependencies. + */ +function writeEntryServerTsx( + useMiddlewarePattern: boolean, + context: AutoConfigContext +) { + if (existsSync("app/entry.server.tsx")) { + context.logger.warn( + "The file `app/entry.server.tsx` already exists on disk, and so we're not modifying it. This may lead to deployment failures if `app/entry.server.tsx` is not set up correctly." + ); + return; + } + + if (useMiddlewarePattern) { + writeFileSync( + `app/entry.server.tsx`, + dedent /* javascript */ ` + import type { EntryContext } from "react-router"; + import { ServerRouter } from "react-router"; + import { isbot } from "isbot"; + import { renderToReadableStream } from "react-dom/server"; + + export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + ) { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); + } + ` + ); + return; + } + + writeFileSync( + `app/entry.server.tsx`, + dedent /* javascript */ ` + import type { AppLoadContext, EntryContext } from "react-router"; + import { ServerRouter } from "react-router"; + import { isbot } from "isbot"; + import { renderToReadableStream } from "react-dom/server"; + + export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + _loadContext: AppLoadContext + ) { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); + } + ` + ); +} + +export class ReactRouter extends Framework { + async configure({ + dryRun, + projectPath, + packageManager, + isWorkspaceRoot, + context, + }: ConfigurationOptions): Promise { + const useMiddlewarePattern = hasV8MiddlewareFlag( + projectPath, + this.frameworkVersion + ); + if (!dryRun) { + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + projectPath, + isWorkspaceRoot, + }); + + mkdirSync("workers"); + + writeAppTs(useMiddlewarePattern); + + await installPackages(packageManager.type, ["isbot"], { + dev: true, + startText: "Installing the isbot package", + doneText: `${brandColor(`installed`)} ${dim("isbot")}`, + isWorkspaceRoot, + }); + + writeEntryServerTsx(useMiddlewarePattern, context); + + transformViteConfig(projectPath, { + viteEnvironmentName: "ssr", + incompatibleVitePlugins: ["netlifyReactRouter"], + }); + + if (needsViteEnvAPIFutureFlag(this.frameworkVersion)) { + const viteEnvironmentKey = viteEnvApiConfigPropertyName( + this.frameworkVersion + ); + transformReactRouterConfig(projectPath, viteEnvironmentKey); + } + } + + return { + wranglerConfig: { + main: "./workers/app.ts", + }, + }; + } + + configurationDescription = "Configuring project for React Router"; +} diff --git a/packages/autoconfig/src/frameworks/solid-start.ts b/packages/autoconfig/src/frameworks/solid-start.ts new file mode 100644 index 0000000..5e12292 --- /dev/null +++ b/packages/autoconfig/src/frameworks/solid-start.ts @@ -0,0 +1,127 @@ +import { updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue } from "@cloudflare/cli-shared-helpers/colors"; +import { mergeObjectProperties, transformFile } from "@cloudflare/codemod"; +import { getTodaysCompatDate } from "@cloudflare/workers-utils"; +import * as recast from "recast"; +import semiver from "semiver"; +import { usesTypescript } from "../uses-typescript"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class SolidStart extends Framework { + async configure({ + projectPath, + dryRun, + }: ConfigurationOptions): Promise { + if (!dryRun) { + const solidStartVersion = this.frameworkVersion; + + if (semiver(solidStartVersion, "2.0.0-alpha") < 0) { + updateAppConfigFile(projectPath); + } else { + updateViteConfigFile(projectPath); + } + } + + return { + wranglerConfig: { + main: "./.output/server/index.mjs", + assets: { + binding: "ASSETS", + directory: "./.output/public", + }, + }, + }; + } +} + +/** + * This functions updates the `vite.config.(js|ts)` files used by SolidStart applications + * to use the `cloudflare-module` preset to target Cloudflare Workers. + * + * Note: SolidStart projects prior to version `2.0.0-alpha` used to have an `app.config.(js|ts)` file instead + * + * @param projectPath The path of the project + */ +function updateViteConfigFile(projectPath: string): void { + const filePath = `vite.config.${usesTypescript(projectPath) ? "ts" : "js"}`; + + transformFile(filePath, { + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name !== "nitro") { + return this.traverse(n); + } + + const b = recast.types.builders; + const presetProp = b.objectProperty( + b.identifier("preset"), + b.stringLiteral("cloudflare-module") + ); + + if (n.node.arguments.length === 0) { + n.node.arguments.push(b.objectExpression([presetProp])); + } else { + mergeObjectProperties( + n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, + [presetProp] + ); + } + + return false; + }, + }); +} + +/** + * SolidStart apps used to have an `app.config.(js|ts)` before version `2.0.0-alpha` + * (afterwards this has been replaced by `vite.config.(js|ts)`). + * Reference: https://github.com/solidjs/templates/commit/c4cd73e08bdc + * + * This functions updates the `app.config.(js|ts)` to use the `cloudflare_module` preset + * to target Cloudflare Workers. + * + * @param projectPath The path of the project + */ +function updateAppConfigFile(projectPath: string): void { + const filePath = `app.config.${usesTypescript(projectPath) ? "ts" : "js"}`; + + const compatDate = getTodaysCompatDate(); + + updateStatus(`Updating configuration in ${blue(filePath)}`); + + transformFile(filePath, { + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name !== "defineConfig") { + return this.traverse(n); + } + + const b = recast.types.builders; + mergeObjectProperties( + n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, + [ + b.objectProperty( + b.identifier("server"), + b.objectExpression([ + // preset: "cloudflare_module" + b.objectProperty( + b.identifier("preset"), + b.stringLiteral("cloudflare_module") + ), + b.objectProperty( + b.identifier("compatibilityDate"), + b.stringLiteral(compatDate) + ), + ]) + ), + ] + ); + + return false; + }, + }); +} diff --git a/packages/autoconfig/src/frameworks/static.ts b/packages/autoconfig/src/frameworks/static.ts new file mode 100644 index 0000000..0d1d796 --- /dev/null +++ b/packages/autoconfig/src/frameworks/static.ts @@ -0,0 +1,17 @@ +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class Static extends Framework { + configure({ outputDir }: ConfigurationOptions): ConfigurationResults { + return { + wranglerConfig: { + assets: { + directory: outputDir, + }, + }, + }; + } +} diff --git a/packages/autoconfig/src/frameworks/sveltekit.ts b/packages/autoconfig/src/frameworks/sveltekit.ts new file mode 100644 index 0000000..d37eeda --- /dev/null +++ b/packages/autoconfig/src/frameworks/sveltekit.ts @@ -0,0 +1,58 @@ +import { writeFileSync } from "node:fs"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { runCommand } from "@cloudflare/cli-shared-helpers/command"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { Framework } from "./framework-class"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class SvelteKit extends Framework { + async configure({ + dryRun, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + const { dlx } = packageManager; + if (!dryRun) { + await runCommand( + [ + ...dlx, + "sv", + "add", + "--no-install", + "--no-git-check", + "sveltekit-adapter=adapter:cloudflare+cfTarget:workers", + ], + { + silent: true, + startText: "Installing adapter", + doneText: `${brandColor("installed")} ${dim( + `via \`${dlx.join( + " " + )} sv add sveltekit-adapter=adapter:cloudflare+cfTarget:workers\`` + )}`, + } + ); + writeFileSync("static/.assetsignore", "_worker.js\n_routes.json"); + + await installPackages(packageManager.type, [], { + startText: "Installing packages", + doneText: `${brandColor("installed")}`, + isWorkspaceRoot, + }); + } + return { + wranglerConfig: { + main: ".svelte-kit/cloudflare/_worker.js", + assets: { + binding: "ASSETS", + directory: ".svelte-kit/cloudflare", + }, + }, + }; + } + + configurationDescription = 'Configuring project for SvelteKit with "sv add"'; +} diff --git a/packages/autoconfig/src/frameworks/tanstack.ts b/packages/autoconfig/src/frameworks/tanstack.ts new file mode 100644 index 0000000..5b8f2ba --- /dev/null +++ b/packages/autoconfig/src/frameworks/tanstack.ts @@ -0,0 +1,34 @@ +import { Framework } from "./framework-class"; +import { transformViteConfig } from "./utils/vite-config"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class TanstackStart extends Framework { + async configure({ + dryRun, + projectPath, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + if (!dryRun) { + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + isWorkspaceRoot, + projectPath, + }); + + transformViteConfig(projectPath, { viteEnvironmentName: "ssr" }); + } + + return { + wranglerConfig: { + main: "@tanstack/react-start/server-entry", + }, + }; + } + + configurationDescription = "Configuring project for Tanstack Start"; +} diff --git a/packages/autoconfig/src/frameworks/utils/packages.ts b/packages/autoconfig/src/frameworks/utils/packages.ts new file mode 100644 index 0000000..7d55577 --- /dev/null +++ b/packages/autoconfig/src/frameworks/utils/packages.ts @@ -0,0 +1,4 @@ +export { + isPackageInstalled, + getInstalledPackageVersion, +} from "@cloudflare/workers-utils"; diff --git a/packages/autoconfig/src/frameworks/utils/vite-config.ts b/packages/autoconfig/src/frameworks/utils/vite-config.ts new file mode 100644 index 0000000..42867b1 --- /dev/null +++ b/packages/autoconfig/src/frameworks/utils/vite-config.ts @@ -0,0 +1,323 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { transformFile } from "@cloudflare/codemod"; +import { UserError } from "@cloudflare/workers-utils"; +import * as recast from "recast"; +import dedent from "ts-dedent"; +import type { AutoConfigContext } from "../../context"; +import type { types } from "recast"; + +const b = recast.types.builders; +const t = recast.types.namedTypes; + +/** + * Extracts the ObjectExpression from the first argument of defineConfig(). + * + * Handles: + * - defineConfig({ ... }) + * - defineConfig(() => ({ ... })) + * - defineConfig(({ isSsrBuild }) => ({ ... })) + * - defineConfig(() => { return { ... }; }) + * - defineConfig(function() { return { ... }; }) + */ +function extractConfigObject( + node: recast.types.ASTNode +): types.namedTypes.ObjectExpression | null { + // Direct object: defineConfig({ ... }) + if (t.ObjectExpression.check(node)) { + return node; + } + + // Arrow function: defineConfig(() => ({ ... })) or defineConfig(() => { return { ... }; }) + if (t.ArrowFunctionExpression.check(node)) { + if (t.ObjectExpression.check(node.body)) { + return node.body; + } + if (t.BlockStatement.check(node.body)) { + return extractFromBlockStatement(node.body); + } + } + + // Function expression: defineConfig(function() { return { ... }; }) + if (t.FunctionExpression.check(node)) { + return extractFromBlockStatement(node.body); + } + + return null; +} + +function extractFromBlockStatement( + block: types.namedTypes.BlockStatement +): types.namedTypes.ObjectExpression | null { + const returnStmt = block.body.find((s) => t.ReturnStatement.check(s)); + if ( + returnStmt && + t.ReturnStatement.check(returnStmt) && + returnStmt.argument && + t.ObjectExpression.check(returnStmt.argument) + ) { + return returnStmt.argument; + } + return null; +} + +export function checkIfViteConfigUsesCloudflarePlugin( + projectPath: string, + context?: AutoConfigContext +): boolean { + const filePath = getViteConfigPath(projectPath); + + let importsCloudflarePlugin = false; + let usesCloudflarePlugin = false; + + transformFile(filePath, { + visitProgram(n) { + if ( + n.node.body.some( + (s) => + s.type === "ImportDeclaration" && + s.source.value === "@cloudflare/vite-plugin" + ) + ) { + importsCloudflarePlugin = true; + return this.traverse(n); + } + + this.traverse(n); + }, + visitCallExpression: function (n) { + const callee = n.node.callee as types.namedTypes.Identifier; + if (callee.name !== "defineConfig") { + return this.traverse(n); + } + + const configObject = extractConfigObject(n.node.arguments[0]); + if (!configObject) { + context?.logger.debug( + `Vite config uses an unsupported expression type. Skipping Cloudflare plugin check.` + ); + return this.traverse(n); + } + + const pluginsProp = configObject.properties.find((prop) => + isPluginsProp(prop) + ); + if (!pluginsProp || !t.ArrayExpression.check(pluginsProp.value)) { + context?.logger.debug( + `Vite config does not have a valid plugins array. Skipping Cloudflare plugin check.` + ); + return this.traverse(n); + } + + if ( + pluginsProp.value.elements.some( + (el) => + el?.type === "CallExpression" && + el.callee.type === "Identifier" && + el.callee.name === "cloudflare" + ) + ) { + usesCloudflarePlugin = true; + return this.traverse(n); + } + + this.traverse(n); + }, + }); + + return importsCloudflarePlugin && usesCloudflarePlugin; +} + +/** + * Returns whether a Vite config file (`vite.config.ts` or `vite.config.js`) + * exists in the given project directory. + */ +export function hasViteConfig(projectPath: string): boolean { + return ( + existsSync(path.join(projectPath, "vite.config.ts")) || + existsSync(path.join(projectPath, "vite.config.js")) + ); +} + +function getViteConfigPath(projectPath: string): string { + const filePathTS = path.join(projectPath, `vite.config.ts`); + const filePathJS = path.join(projectPath, `vite.config.js`); + + let filePath: string; + + if (existsSync(filePathTS)) { + filePath = filePathTS; + } else if (existsSync(filePathJS)) { + filePath = filePathJS; + } else { + throw new Error("Could not find Vite config file to modify"); + } + + return filePath; +} + +/** + * Name of vite plugins that we know are incompatible with the Cloudflare one + */ +const knownIncompatiblePlugins = ["nitro", "nitroV2Plugin", "netlify"]; + +export function transformViteConfig( + projectPath: string, + options: { + viteEnvironmentName?: string; + incompatibleVitePlugins?: string[]; + } = {} +) { + const filePath = getViteConfigPath(projectPath); + + transformFile(filePath, { + visitProgram(n) { + // Add an import of the @cloudflare/vite-plugin + // ``` + // import {cloudflare} from "@cloudflare/vite-plugin"; + // ``` + const lastImportIndex = n.node.body.findLastIndex( + (statement) => statement.type === "ImportDeclaration" + ); + const lastImport = n.get("body", lastImportIndex); + const importAst = b.importDeclaration( + [b.importSpecifier(b.identifier("cloudflare"))], + b.stringLiteral("@cloudflare/vite-plugin") + ); + + // Only import if not already imported + if ( + !n.node.body.some( + (s) => + s.type === "ImportDeclaration" && + s.source.value === "@cloudflare/vite-plugin" + ) + ) { + lastImport.insertAfter(importAst); + } + + return this.traverse(n); + }, + visitCallExpression: function (n) { + // Add the imported plugin to the config + // ``` + // defineConfig({ + // plugins: [cloudflare({ viteEnvironment: { name: 'ssr' } })], + // }); + // ``` + // Also handles function-based configs like: + // ``` + // defineConfig(({ isSsrBuild }) => ({ + // plugins: [...] + // })); + // ``` + const callee = n.node.callee as types.namedTypes.Identifier; + if (callee.name !== "defineConfig") { + return this.traverse(n); + } + + const configObject = extractConfigObject(n.node.arguments[0]); + if (!configObject) { + const argType = n.node.arguments[0]?.type ?? "unknown"; + throw new UserError( + dedent` + Cannot modify Vite config: could not extract a config object (found ${argType}). + + The Cloudflare plugin can only be automatically added to Vite configs that use: + - A simple object: defineConfig({ plugins: [...] }) + - An arrow function returning an object: defineConfig(() => ({ plugins: [...] })) + + If your config uses a more complex pattern, please manually add the plugin: + + import { cloudflare } from "@cloudflare/vite-plugin"; + + export default defineConfig({ + plugins: [cloudflare()] + }); + `, + { + telemetryMessage: "autoconfig vite config object unsupported", + } + ); + } + + const pluginsProp = configObject.properties.find((prop) => + isPluginsProp(prop) + ); + if (!pluginsProp || !t.ArrayExpression.check(pluginsProp.value)) { + throw new UserError( + dedent` + Cannot modify Vite config: could not find a valid plugins array. + + Please ensure your Vite config has a plugins array: + + export default defineConfig({ + plugins: [] + }); + `, + { + telemetryMessage: "autoconfig vite plugins array missing", + } + ); + } + + // Only add the Cloudflare plugin if it's not already present + if ( + !pluginsProp.value.elements.some( + (el) => + el?.type === "CallExpression" && + el.callee.type === "Identifier" && + el.callee.name === "cloudflare" + ) + ) { + pluginsProp.value.elements.push( + b.callExpression(b.identifier("cloudflare"), [ + ...(options.viteEnvironmentName + ? [ + b.objectExpression([ + b.objectProperty( + b.identifier("viteEnvironment"), + b.objectExpression([ + b.objectProperty( + b.identifier("name"), + b.stringLiteral(options.viteEnvironmentName) + ), + ]) + ), + ]), + ] + : []), + ]) + ); + } + + const incompatibleVitePlugins = [ + ...knownIncompatiblePlugins, + ...(options.incompatibleVitePlugins ?? []), + ]; + + // Remove incompatible plugins + pluginsProp.value.elements = pluginsProp.value.elements.filter((el) => { + if ( + el?.type === "CallExpression" && + el.callee.type === "Identifier" && + incompatibleVitePlugins.includes(el.callee.name) + ) { + return false; + } + return true; + }); + return false; + }, + }); +} + +function isPluginsProp( + prop: unknown +): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { + return ( + (t.Property.check(prop) || t.ObjectProperty.check(prop)) && + t.Identifier.check(prop.key) && + prop.key.name === "plugins" + ); +} diff --git a/packages/autoconfig/src/frameworks/utils/vite-plugin.ts b/packages/autoconfig/src/frameworks/utils/vite-plugin.ts new file mode 100644 index 0000000..c6eabd0 --- /dev/null +++ b/packages/autoconfig/src/frameworks/utils/vite-plugin.ts @@ -0,0 +1,51 @@ +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import semiver from "semiver"; +import { getInstalledPackageVersion } from "./packages"; +import type { PackageManager } from "@cloudflare/workers-utils"; + +/** + * Installs the `@cloudflare/vite-plugin` package as a dev dependency + * + * If the project has Vite >= 6.0.0 but < 6.1.0 installed, it will first + * be updated to `^6.1.0` to ensure compatibility with the plugin. + * + * @param packageManager the type of package manager to use for installation + * @param projectPath the path of the project (used to check the installed Vite version) + * @param isWorkspaceRoot whether the current project is a workspace root + */ +export async function installCloudflareVitePlugin({ + packageManager, + projectPath, + isWorkspaceRoot, +}: { + packageManager: PackageManager["type"]; + projectPath: string; + isWorkspaceRoot: boolean; +}): Promise { + const viteVersion = getInstalledPackageVersion("vite", projectPath); + + if ( + viteVersion && + semiver(viteVersion, "6.0.0") >= 0 && + semiver(viteVersion, "6.1.0") < 0 + ) { + // If the vite version is between 6.0.0 and 6.1.0 lets bump it to + // the latest version of 6.x, in this way it will be compatible + // with the vite plugin (likely without causing any inconvenience) + await installPackages(packageManager, ["vite@^6.1.0"], { + dev: true, + startText: + "Updating the version of vite to be compatible with the Cloudflare Vite Plugin", + doneText: `${brandColor(`updated`)} ${dim("Vite")}`, + isWorkspaceRoot, + }); + } + + await installPackages(packageManager, ["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, + isWorkspaceRoot, + }); +} diff --git a/packages/autoconfig/src/frameworks/vike.ts b/packages/autoconfig/src/frameworks/vike.ts new file mode 100644 index 0000000..eb9ae0a --- /dev/null +++ b/packages/autoconfig/src/frameworks/vike.ts @@ -0,0 +1,293 @@ +import assert from "node:assert"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { brandColor } from "@cloudflare/cli-shared-helpers/colors"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { transformFile } from "@cloudflare/codemod"; +import * as recast from "recast"; +import { Framework } from "./framework-class"; +import { isPackageInstalled } from "./utils/packages"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; +import type { types } from "recast"; + +const b = recast.types.builders; +const t = recast.types.namedTypes; + +export class Vike extends Framework { + async configure({ + projectPath, + dryRun, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + const vikeServerIsInstalled = isPackageInstalled( + "vike-server", + projectPath + ); + if (vikeServerIsInstalled) { + // Note: if vike-server is used we just error, this is the simplest solution, alternatively we could + // migrate the project as described in https://vike.dev/migration/vike-photon, but that is probably + // not currently worth the effort (but something we might consider if/when needed) + throw new Error( + 'Aborting since the project is using the deprecated "vike-server" package, please remove the package from your project and try again.' + ); + } + + if (!dryRun) { + // note: the following installation steps follow the guide in: https://vike.dev/cloudflare#get-started + + await installPackages( + packageManager.type, + ["vike-photon", "@photonjs/cloudflare"], + { + startText: "Installing vike-photon and @photonjs/cloudflare", + doneText: `${brandColor(`installed`)} photon packages`, + isWorkspaceRoot, + } + ); + + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + isWorkspaceRoot, + projectPath, + }); + + addVikePhotonToConfigFile(projectPath); + } + + return { + wranglerConfig: { + main: "virtual:photon:cloudflare:server-entry", + }, + packageJsonScriptsOverrides: { + preview: "vike build && vike preview", + deploy: "vike build && wrangler deploy", + }, + }; + } +} + +/** + * Modifies a vike config file present at `pages/+config.(js|ts)` to import and use `vikePhoton` + * + * @param projectPath The project's path + */ +function addVikePhotonToConfigFile(projectPath: string) { + const filePath = getConfigPath(projectPath); + let programNode: types.namedTypes.Program | undefined; + + transformFile(filePath, { + visitProgram(n) { + programNode = n.node; + addVikePhotonImportToConfigFile(n); + return this.traverse(n); + }, + visitExportDefaultDeclaration(n) { + addVikePhotonToVikeConfigExportObject(n, programNode); + return this.traverse(n); + }, + }); +} + +/** Name of the vike config property that needs to include vikePhoton */ +const vikeConfigExtendsPropName = "extends"; + +/** + * Given a recast visitor node path for the vike config export declaration it adds vikePhoton to the config extends property + * + * @param n node path for the vike config file's default export + */ +function addVikePhotonToVikeConfigExportObject( + n: Parameters>[0], + programNode?: types.namedTypes.Program +) { + const configObject = getVikeConfigObjectExpression(n, programNode); + + let configTargetProp = configObject.properties.find((prop) => + isExtendsProp(prop) + ); + if (!configTargetProp) { + configTargetProp = b.objectProperty( + b.identifier(vikeConfigExtendsPropName), + b.arrayExpression([]) + ); + configObject.properties.push(configTargetProp); + } + + assert(configTargetProp && t.ArrayExpression.check(configTargetProp.value)); + + // Determine the local import name for "vike-photon/config" (defaults to "vikePhoton"). + // This handles cases where the user already imported it under a different name. + const vikePhotonLocalName = + getVikePhotonImportLocalName(programNode) ?? "vikePhoton"; + + // Only add vikePhoton if it's not already present + if ( + !configTargetProp.value.elements.some( + (el) => el?.type === "Identifier" && el.name === vikePhotonLocalName + ) + ) { + configTargetProp.value.elements.push(b.identifier(vikePhotonLocalName)); + } +} + +/** + * Finds the local name used for the default import from "vike-photon/config". + * e.g. `import vikePhoton from "vike-photon/config"` returns "vikePhoton", + * `import photon from "vike-photon/config"` returns "photon". + */ +function getVikePhotonImportLocalName( + programNode?: types.namedTypes.Program +): string | undefined { + if (!programNode) { + return undefined; + } + for (const stmt of programNode.body) { + if ( + t.ImportDeclaration.check(stmt) && + stmt.source.value === "vike-photon/config" + ) { + const defaultSpec = stmt.specifiers?.find((s) => + t.ImportDefaultSpecifier.check(s) + ); + if (defaultSpec && t.Identifier.check(defaultSpec.local)) { + return defaultSpec.local.name; + } + } + } + return undefined; +} + +/** + * Given a recast visitor node path for the vike config export declaration it returns the object expression associated to it + * + * @param n node path for the vike config file's default export + */ +function getVikeConfigObjectExpression( + n: Parameters>[0], + programNode?: types.namedTypes.Program +): types.namedTypes.ObjectExpression { + if (n.node.declaration.type === "ObjectExpression") { + // The export is a simple object expression + return n.node.declaration; + } + + if ( + (n.node.declaration.type === "TSAsExpression" || + n.node.declaration.type === "TSSatisfiesExpression") && + n.node.declaration.expression.type === "ObjectExpression" + ) { + // The export is an `as Config` or `satisfies Config` expression, so we go a level + // deeper to get the object expression + return n.node.declaration.expression; + } + + if (n.node.declaration.type === "Identifier" && programNode) { + // The export is a variable reference, e.g., `const config: Config = { ... }; export default config;` + const objectExpression = resolveIdentifierToObjectExpression( + n.node.declaration.name, + programNode + ); + if (objectExpression) { + return objectExpression; + } + } + + throw new Error("Could not determine Vike default object export"); +} + +/** + * Resolves a variable name to its ObjectExpression initializer by searching the program body. + * Handles both plain object initializers and those wrapped in TSAsExpression/TSSatisfiesExpression. + */ +function resolveIdentifierToObjectExpression( + varName: string, + programNode: types.namedTypes.Program +): types.namedTypes.ObjectExpression | undefined { + for (const stmt of programNode.body) { + if (t.VariableDeclaration.check(stmt)) { + for (const declarator of stmt.declarations) { + if ( + t.VariableDeclarator.check(declarator) && + t.Identifier.check(declarator.id) && + declarator.id.name === varName && + declarator.init + ) { + if (t.ObjectExpression.check(declarator.init)) { + return declarator.init; + } + if ( + (t.TSAsExpression.check(declarator.init) || + t.TSSatisfiesExpression.check(declarator.init)) && + t.ObjectExpression.check(declarator.init.expression) + ) { + return declarator.init.expression; + } + } + } + } + } + return undefined; +} + +/** + * Given a recast visitor node path for the vike config file it adds the following vikePhoton import to the file (if not already present): + * ``` + * import vikePhoton from "vike-photon/config"; + * ``` + * @param n node path for the vike config file + */ +function addVikePhotonImportToConfigFile( + n: Parameters>[0] +): void { + const lastImportIndex = n.node.body.findLastIndex( + (statement) => statement.type === "ImportDeclaration" + ); + const lastImport = n.get("body", lastImportIndex); + const importAst = b.importDeclaration( + [b.importDefaultSpecifier(b.identifier("vikePhoton"))], + b.stringLiteral("vike-photon/config") + ); + + // Only import if not already imported + if ( + !n.node.body.some( + (s) => + s.type === "ImportDeclaration" && + s.source.value === "vike-photon/config" + ) + ) { + lastImport.insertAfter(importAst); + } +} + +function getConfigPath(projectPath: string): string { + const filePathTS = path.join(projectPath, "pages", "+config.ts"); + const filePathJS = path.join(projectPath, "pages", "+config.js"); + + let filePath: string; + + if (existsSync(filePathTS)) { + filePath = filePathTS; + } else if (existsSync(filePathJS)) { + filePath = filePathJS; + } else { + throw new Error("Could not find config file to modify"); + } + + return filePath; +} + +function isExtendsProp( + prop: unknown +): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { + return ( + (t.Property.check(prop) || t.ObjectProperty.check(prop)) && + t.Identifier.check(prop.key) && + prop.key.name === vikeConfigExtendsPropName + ); +} diff --git a/packages/autoconfig/src/frameworks/vite.ts b/packages/autoconfig/src/frameworks/vite.ts new file mode 100644 index 0000000..a8d7a83 --- /dev/null +++ b/packages/autoconfig/src/frameworks/vite.ts @@ -0,0 +1,73 @@ +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { usesTypescript } from "../uses-typescript"; +import { Framework } from "./framework-class"; +import { + checkIfViteConfigUsesCloudflarePlugin, + hasViteConfig, + transformViteConfig, +} from "./utils/vite-config"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; + +export class Vite extends Framework { + isConfigured(projectPath: string): boolean { + if (!hasViteConfig(projectPath)) { + return false; + } + return checkIfViteConfigUsesCloudflarePlugin(projectPath); + } + + async configure({ + dryRun, + projectPath, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + if (!dryRun) { + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + isWorkspaceRoot, + projectPath, + }); + + if (hasViteConfig(projectPath)) { + transformViteConfig(projectPath); + } else { + createViteConfig(projectPath); + } + } + + return { + wranglerConfig: { + assets: { + not_found_handling: "single-page-application", + }, + }, + }; + } +} + +/** + * Creates a minimal `vite.config` file with the Cloudflare plugin already + * configured. Used when a Vite project has no config file (e.g. projects + * scaffolded with `npm create vite@latest --template vanilla`). + * + * The file extension (`.ts` or `.js`) is chosen based on whether the project + * has a `tsconfig.json`. + */ +function createViteConfig(projectPath: string): void { + const ext = usesTypescript(projectPath) ? "ts" : "js"; + const filePath = join(projectPath, `vite.config.${ext}`); + const content = `import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ +\tplugins: [cloudflare()], +}); +`; + writeFileSync(filePath, content); +} diff --git a/packages/autoconfig/src/frameworks/waku.ts b/packages/autoconfig/src/frameworks/waku.ts new file mode 100644 index 0000000..9eac04a --- /dev/null +++ b/packages/autoconfig/src/frameworks/waku.ts @@ -0,0 +1,210 @@ +import assert from "node:assert"; +import { existsSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue, brandColor } from "@cloudflare/cli-shared-helpers/colors"; +import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; +import { transformFile } from "@cloudflare/codemod"; +import * as recast from "recast"; +import dedent from "ts-dedent"; +import { Framework } from "./framework-class"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "./framework-class"; +import type { types } from "recast"; + +const b = recast.types.builders; +const t = recast.types.namedTypes; + +export class Waku extends Framework { + async configure({ + dryRun, + projectPath, + packageManager, + isWorkspaceRoot, + }: ConfigurationOptions): Promise { + if (!dryRun) { + await installPackages(packageManager.type, ["hono"], { + dev: true, + startText: "Installing hono dependency", + doneText: `${brandColor("installed")}`, + isWorkspaceRoot, + }); + + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + projectPath, + isWorkspaceRoot, + }); + + await createWakuServerFile(projectPath); + await updateWakuConfig(projectPath); + } + + return { + wranglerConfig: { + main: "./src/waku.server", + assets: { + binding: "ASSETS", + directory: "./dist/public", + html_handling: "drop-trailing-slash", + }, + }, + }; + } +} + +/** + * Created a waku.server.tsx file that uses the Cloudflare adapter + * + * @param projectPath Path to the project + */ +async function createWakuServerFile(projectPath: string) { + await writeFile( + `${projectPath}/src/waku.server.tsx`, + dedent` + import { fsRouter } from 'waku'; + import adapter from 'waku/adapters/cloudflare'; + + export default adapter( + fsRouter(import.meta.glob('./**/*.{tsx,ts}', { base: './pages' })), + { + handlers: { + // Define additional Cloudflare Workers handlers here + // https://developers.cloudflare.com/workers/runtime-apis/handlers/ + // async queue( + // batch: MessageBatch, + // _env: Env, + // _ctx: ExecutionContext, + // ): Promise { + // for (const message of batch.messages) { + // console.log('Received', message); + // } + // }, + }, + }, + ); + ` + ); +} + +/** + * Updated the waku.config.ts file to import and use the Cloudflare Vite plugin + * + * @param projectPath Path to the project + */ +async function updateWakuConfig(projectPath: string) { + const wakuConfigPath = join(projectPath, "waku.config.ts"); + + if (!existsSync(wakuConfigPath)) { + throw new Error("Could not find Waku config file to modify"); + } + + updateStatus(`Updating Waku configuration in ${blue(wakuConfigPath)}`); + + transformFile(wakuConfigPath, { + visitProgram(n) { + // Add an import of the @cloudflare/vite-plugin + // ``` + // import { cloudflare } from '@cloudflare/vite-plugin'; + // ``` + const lastImportIndex = n.node.body.findLastIndex( + (statement) => statement.type === "ImportDeclaration" + ); + const lastImport = n.get("body", lastImportIndex); + + // Only import if not already imported + if ( + !n.node.body.some( + (s) => + s.type === "ImportDeclaration" && + s.source.value === "@cloudflare/vite-plugin" + ) + ) { + const importAst = b.importDeclaration( + [b.importSpecifier(b.identifier("cloudflare"))], + b.stringLiteral("@cloudflare/vite-plugin") + ); + lastImport.insertAfter(importAst); + } + + return this.traverse(n); + }, + visitCallExpression: function (n) { + const callee = n.node.callee as types.namedTypes.Identifier; + if (callee.name !== "defineConfig") { + return this.traverse(n); + } + + const config = n.node.arguments[0]; + assert(t.ObjectExpression.check(config)); + const viteConfig = config.properties.find((prop) => + isViteProp(prop) + )?.value; + assert(t.ObjectExpression.check(viteConfig)); + const pluginsProp = viteConfig.properties.find((prop) => + isPluginsProp(prop) + ); + assert(pluginsProp && t.ArrayExpression.check(pluginsProp.value)); + + // Only add the Cloudflare loader plugin if it's not already present + if ( + !pluginsProp.value.elements.some( + (el) => + el?.type === "CallExpression" && + el.callee.type === "Identifier" && + el.callee.name === "cloudflare" + ) + ) { + pluginsProp.value.elements.push( + b.callExpression(b.identifier("cloudflare"), [ + b.objectExpression([ + b.objectProperty( + b.identifier("viteEnvironment"), + b.objectExpression([ + b.objectProperty( + b.identifier("name"), + b.stringLiteral("rsc") + ), + b.objectProperty( + b.identifier("childEnvironments"), + b.arrayExpression([b.stringLiteral("ssr")]) + ), + ]) + ), + b.objectProperty( + b.identifier("inspectorPort"), + b.booleanLiteral(false) + ), + ]), + ]) + ); + } + + this.traverse(n); + }, + }); +} + +function isViteProp( + prop: unknown +): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { + return ( + (t.Property.check(prop) || t.ObjectProperty.check(prop)) && + t.Identifier.check(prop.key) && + prop.key.name === "vite" + ); +} + +function isPluginsProp( + prop: unknown +): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { + return ( + (t.Property.check(prop) || t.ObjectProperty.check(prop)) && + t.Identifier.check(prop.key) && + prop.key.name === "plugins" + ); +} diff --git a/packages/autoconfig/src/index.ts b/packages/autoconfig/src/index.ts new file mode 100644 index 0000000..a2f9df1 --- /dev/null +++ b/packages/autoconfig/src/index.ts @@ -0,0 +1,39 @@ +export type { + AutoConfigContext, + AutoConfigLogger, + AutoConfigDialogs, +} from "./context"; + +export { getDetailsForAutoConfig } from "./details"; +export { runAutoConfig, buildOperationsSummary } from "./run"; + +export { Framework } from "./frameworks/framework-class"; +export type { + ConfigurationOptions, + ConfigurationResults, + PackageJsonScriptsOverrides, +} from "./frameworks/framework-class"; + +export { isFrameworkSupported } from "./frameworks"; + +export type { + FrameworkInfo, + AutoConfigFrameworkPackageInfo, +} from "./frameworks"; + +export { displayAutoConfigDetails, confirmAutoConfigDetails } from "./details"; + +export type { + AutoConfigDetails, + AutoConfigDetailsForConfiguredProject, + AutoConfigDetailsForNonConfiguredProject, + AutoConfigOptions, + AutoConfigSummary, +} from "./types"; + +export { + AutoConfigDetectionError, + AutoConfigFrameworkConfigurationError, +} from "./errors"; + +export { getInstalledPackageVersion } from "@cloudflare/workers-utils"; diff --git a/packages/autoconfig/src/run.ts b/packages/autoconfig/src/run.ts new file mode 100644 index 0000000..58b045b --- /dev/null +++ b/packages/autoconfig/src/run.ts @@ -0,0 +1,448 @@ +import assert from "node:assert"; +import { existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { + maybeAppendWranglerToGitIgnoreLikeFile, + maybeAppendWranglerToGitIgnore, +} from "@cloudflare/cli-shared-helpers/gitignore"; +import { installWrangler } from "@cloudflare/cli-shared-helpers/packages"; +import { + FatalError, + getTodaysCompatDate, + parseJSONC, +} from "@cloudflare/workers-utils"; +import { + assertNonConfigured, + confirmAutoConfigDetails, + displayAutoConfigDetails, +} from "./details"; +import { + isFrameworkSupported, + isKnownFramework, + type PackageJsonScriptsOverrides, +} from "./frameworks"; +import { getFrameworkPackageInfo } from "./frameworks/all-frameworks"; +import { Static } from "./frameworks/static"; +import { usesTypescript } from "./uses-typescript"; +import type { AutoConfigContext } from "./context"; +import type { + AutoConfigDetails, + AutoConfigDetailsForNonConfiguredProject, + AutoConfigOptions, + AutoConfigSummary, +} from "./types"; +import type { PackageJSON, RawConfig } from "@cloudflare/workers-utils"; + +/** + * Runs the full autoconfig flow: displays detected settings, confirms with the user, + * validates the framework version, runs framework configuration, writes wrangler config, + * updates package.json scripts, and optionally runs the build command. + * + * @param autoConfigDetails - The detected project details from `getDetailsForAutoConfig()`. + * @param autoConfigOptions - Options controlling dry-run, confirmations, build, and context. + * @returns A summary of all operations performed. + */ +export async function runAutoConfig( + autoConfigDetails: AutoConfigDetails, + autoConfigOptions: AutoConfigOptions +): Promise { + const { context } = autoConfigOptions; + const { logger } = context; + const dryRun = autoConfigOptions.dryRun === true; + const runBuild = !dryRun && (autoConfigOptions.runBuild ?? true); + const skipConfirmations = + dryRun || autoConfigOptions.skipConfirmations === true; + const enableWranglerInstallation = + autoConfigOptions.enableWranglerInstallation ?? true; + + assertNonConfigured(autoConfigDetails); + + displayAutoConfigDetails(autoConfigDetails, context); + + const updatedAutoConfigDetails = skipConfirmations + ? autoConfigDetails + : await confirmAutoConfigDetails(autoConfigDetails, context); + + if (autoConfigDetails !== updatedAutoConfigDetails) { + displayAutoConfigDetails(updatedAutoConfigDetails, context, { + heading: "Updated Project Settings:", + }); + } + + autoConfigDetails = updatedAutoConfigDetails; + assertNonConfigured(autoConfigDetails); + + if (isKnownFramework(autoConfigDetails.framework.id)) { + const frameworkIsSupported = isFrameworkSupported( + autoConfigDetails.framework.id + ); + if (!frameworkIsSupported) { + throw new FatalError( + autoConfigDetails.framework.id === "cloudflare-pages" + ? `The target project seems to be using Cloudflare Pages. Automatically migrating from a Pages project to Workers is not yet supported.` + : `The detected framework ("${autoConfigDetails.framework.name}") cannot be automatically configured.`, + { telemetryMessage: "autoconfig run framework unsupported" } + ); + } + } + + assert( + autoConfigDetails.outputDir, + "The Output Directory is unexpectedly missing" + ); + + const compatibilityDate = getTodaysCompatDate(); + + const wranglerConfig: RawConfig = { + $schema: "node_modules/wrangler/config-schema.json", + name: autoConfigDetails.workerName, + compatibility_date: compatibilityDate, + observability: { + enabled: true, + }, + } satisfies RawConfig; + + const { packageManager } = autoConfigDetails; + + const isWorkspaceRoot = autoConfigDetails.isWorkspaceRoot ?? false; + + const frameworkPackageInfo = getFrameworkPackageInfo( + autoConfigDetails.framework.id + ); + if (frameworkPackageInfo) { + autoConfigDetails.framework.validateFrameworkVersion( + autoConfigDetails.projectPath, + frameworkPackageInfo, + context + ); + } + + const dryRunConfigurationResults = + await autoConfigDetails.framework.configure({ + outputDir: autoConfigDetails.outputDir, + projectPath: autoConfigDetails.projectPath, + workerName: autoConfigDetails.workerName, + isWorkspaceRoot, + dryRun: true, + packageManager, + context, + }); + + const { npx } = packageManager; + + const autoConfigSummary = await buildOperationsSummary( + { ...autoConfigDetails, outputDir: autoConfigDetails.outputDir }, + dryRunConfigurationResults.wranglerConfig === null + ? null + : ensureNodejsCompatIsInConfig({ + ...wranglerConfig, + ...dryRunConfigurationResults.wranglerConfig, + }), + { + build: + dryRunConfigurationResults.buildCommandOverride ?? + autoConfigDetails.buildCommand, + deploy: + dryRunConfigurationResults.deployCommandOverride ?? + `${npx} wrangler deploy`, + version: + dryRunConfigurationResults?.versionCommandOverride ?? + `${npx} wrangler versions upload`, + }, + context, + dryRunConfigurationResults.packageJsonScriptsOverrides + ); + + if ( + !( + skipConfirmations || + (await context.dialogs.confirm("Proceed with setup?")) + ) + ) { + throw new FatalError("Setup cancelled", { + telemetryMessage: "autoconfig run setup cancelled", + }); + } + + if (dryRun) { + logger.log( + `✋ ${"Autoconfig process run in dry-run mode, existing now."}` + ); + logger.log(""); + + return autoConfigSummary; + } + + logger.debug( + `Running autoconfig with:\n${JSON.stringify(autoConfigDetails, null, 2)}...` + ); + + if (autoConfigSummary.wranglerInstall && enableWranglerInstallation) { + await installWrangler(packageManager.type, isWorkspaceRoot); + } + + const configurationResults = await autoConfigDetails.framework.configure({ + outputDir: autoConfigDetails.outputDir, + projectPath: autoConfigDetails.projectPath, + workerName: autoConfigDetails.workerName, + isWorkspaceRoot, + dryRun: false, + packageManager, + context, + }); + + if (autoConfigDetails.packageJson) { + const packageJsonPath = resolve( + autoConfigDetails.projectPath, + "package.json" + ); + const existingPackageJson = JSON.parse( + await readFile(packageJsonPath, "utf8") + ) as PackageJSON; + + await writeFile( + packageJsonPath, + JSON.stringify( + { + ...existingPackageJson, + scripts: { + ...existingPackageJson.scripts, + ...autoConfigSummary.scripts, + }, + } satisfies PackageJSON, + null, + 2 + ) + "\n" + ); + } + + if (configurationResults.wranglerConfig !== null) { + await saveWranglerJsonc( + autoConfigDetails.projectPath, + ensureNodejsCompatIsInConfig({ + ...wranglerConfig, + ...configurationResults.wranglerConfig, + }) + ); + } + + maybeAppendWranglerToGitIgnore(autoConfigDetails.projectPath); + + // If we're uploading the project path as the output directory, make sure we don't accidentally upload any sensitive Wrangler files + if (autoConfigDetails.outputDir === autoConfigDetails.projectPath) { + maybeAppendWranglerToGitIgnoreLikeFile( + `${autoConfigDetails.projectPath}/.assetsignore` + ); + } + + const buildCommand = + configurationResults.buildCommandOverride ?? autoConfigDetails.buildCommand; + + if (buildCommand && runBuild) { + await context.runCommand( + buildCommand, + autoConfigDetails.projectPath, + "[build]" + ); + } + + return autoConfigSummary; +} + +/** + * Given a wrangler config object this function makes sure that the `nodejs_compat` flag is present + * in its `compatibility_flags` setting. + * + * Just to be sure the function also filters out any compatibility flag already present starting with `nodejs_` (e.g. `nodejs_als`) + * + * @param wranglerConfig The target wrangler config object + * @returns A copy of the config object where the `compatibility_flags` settings is assured to contain `nodejs_compat` + */ +function ensureNodejsCompatIsInConfig(wranglerConfig: RawConfig): RawConfig { + if (wranglerConfig.compatibility_flags?.includes("nodejs_compat")) { + return wranglerConfig; + } + + return { + ...wranglerConfig, + compatibility_flags: [ + ...(wranglerConfig.compatibility_flags?.filter( + (flag) => !flag.startsWith("nodejs_") + ) ?? []), + "nodejs_compat", + ], + }; +} + +/** + * Saves the a wrangler.jsonc file for the current project potentially combining new values to the potential + * pre-existing wrangler config file generated by the framework's CLI + * + * @param projectPath The project's path + * @param baseWranglerConfig The wrangler config to use + */ +async function saveWranglerJsonc( + projectPath: string, + wranglerConfig: RawConfig +): Promise { + let existingWranglerConfig: RawConfig = {}; + + const wranglerConfigPath = getDirWranglerJsonConfigPath(projectPath); + if (wranglerConfigPath) { + const existingContent = await readFile(wranglerConfigPath, "utf8"); + existingWranglerConfig = parseJSONC( + existingContent, + wranglerConfigPath + ) as RawConfig; + } + + await writeFile( + resolve(projectPath, "wrangler.jsonc"), + JSON.stringify( + { + ...existingWranglerConfig, + ...wranglerConfig, + }, + null, + 2 + ) + "\n" + ); +} + +/** + * Builds a summary of all operations that autoconfig will (or did) perform, + * including package installation, package.json script updates, wrangler config + * creation, and framework-specific configuration. + * + * @param autoConfigDetails - The detected project details. + * @param wranglerConfigToWrite - The wrangler config object to write, or `null` if not applicable. + * @param projectCommands - The build, deploy, and version commands for the project. + * @param context - The autoconfig context providing logger and other dependencies. + * @param packageJsonScriptsOverrides - Optional overrides for package.json script entries. + * @returns A summary object describing all planned operations. + */ +export async function buildOperationsSummary( + autoConfigDetails: AutoConfigDetailsForNonConfiguredProject & { + outputDir: NonNullable; + }, + wranglerConfigToWrite: RawConfig | null, + projectCommands: { + build?: string; + deploy: string; + version?: string; + }, + context: AutoConfigContext, + packageJsonScriptsOverrides?: PackageJsonScriptsOverrides +): Promise { + const { logger } = context; + logger.log(""); + + const summary: AutoConfigSummary = { + wranglerInstall: false, + scripts: {}, + ...(wranglerConfigToWrite !== null + ? { + wranglerConfig: wranglerConfigToWrite, + } + : {}), + outputDir: autoConfigDetails.outputDir, + frameworkId: autoConfigDetails.framework.id, + buildCommand: projectCommands.build, + deployCommand: projectCommands.deploy, + versionCommand: projectCommands.version, + }; + + if (autoConfigDetails.packageJson) { + // If there is a package.json file we will want to install wrangler + summary.wranglerInstall = true; + + logger.log("📦 Install packages:"); + logger.log(` - wrangler (devDependency)`); + logger.log(""); + + summary.scripts = { + deploy: + packageJsonScriptsOverrides?.deploy ?? + (autoConfigDetails.buildCommand + ? `${autoConfigDetails.buildCommand} && wrangler deploy` + : `wrangler deploy`), + preview: + packageJsonScriptsOverrides?.preview ?? + (autoConfigDetails.buildCommand + ? `${autoConfigDetails.buildCommand} && wrangler dev` + : `wrangler dev`), + }; + + const containsServerSideCode = + // If there is an entrypoint then we know that there is server side code + !!wranglerConfigToWrite?.main; + + if ( + // If there is no server side code, then there is no need to add the cf-typegen script + containsServerSideCode && + usesTypescript(autoConfigDetails.projectPath) && + !("cf-typegen" in (autoConfigDetails.packageJson.scripts ?? {})) + ) { + summary.scripts["cf-typegen"] = + packageJsonScriptsOverrides?.typegen ?? "wrangler types"; + } + + logger.log("📝 Update package.json scripts:"); + for (const [name, script] of Object.entries(summary.scripts)) { + logger.log(` - "${name}": "${script}"`); + } + logger.log(""); + } + + if (wranglerConfigToWrite) { + const wranglerConfigPath = resolve( + autoConfigDetails.projectPath, + "wrangler.jsonc" + ); + const configExists = existsSync(wranglerConfigPath); + logger.log( + configExists ? "📄 Update wrangler.jsonc:" : "📄 Create wrangler.jsonc:" + ); + logger.log( + " " + + JSON.stringify(wranglerConfigToWrite, null, 2).replace(/\n/g, "\n ") + ); + logger.log(""); + } + + if ( + autoConfigDetails.framework && + !(autoConfigDetails.framework instanceof Static) && + !autoConfigDetails.framework.isConfigured(autoConfigDetails.projectPath) + ) { + summary.frameworkConfiguration = + autoConfigDetails.framework.configurationDescription ?? + `Configuring project for ${autoConfigDetails.framework.name}`; + + logger.log(`🛠️ ${summary.frameworkConfiguration}`); + logger.log(""); + } + + return summary; +} + +/** + * Gets the path to the wrangler config file, in jsonc or json format, if present in a target directory. + * + * @param dir The target directory + * @returns The path to the wrangler config file if present, `undefined` otherwise + */ +function getDirWranglerJsonConfigPath(dir: string): string | undefined { + const filePathJsonC = resolve(dir, "wrangler.jsonc"); + if (existsSync(filePathJsonC)) { + return filePathJsonC; + } + + const filePathJson = resolve(dir, "wrangler.json"); + if (existsSync(filePathJson)) { + return filePathJson; + } + + return undefined; +} diff --git a/packages/autoconfig/src/types.ts b/packages/autoconfig/src/types.ts new file mode 100644 index 0000000..3cd97d0 --- /dev/null +++ b/packages/autoconfig/src/types.ts @@ -0,0 +1,80 @@ +import type { AutoConfigContext } from "./context"; +import type { Framework } from "./frameworks/framework-class"; +import type { PackageManager } from "@cloudflare/workers-utils"; +import type { PackageJSON, RawConfig } from "@cloudflare/workers-utils"; + +/** Makes the specified keys of T optional. */ +type Optional = Omit & Partial>; + +type AutoConfigDetailsBase = { + /** The name of the worker */ + workerName: string; + /** The path to the project (defaults to cwd) */ + projectPath: string; + /** The content of the project's package.json file (if any) */ + packageJson?: PackageJSON; + /** Whether the project is already configured (no autoconfig required) */ + configured: boolean; + /** Details about the detected framework. It can be a JS framework or 'Static' if no actual JS framework is used. */ + framework: Framework; + /** The build command used to build the project (if any) */ + buildCommand?: string; + /** The output directory (if no framework is used, points to the raw asset files) */ + outputDir: string; + /** The detected package manager for the project */ + packageManager: PackageManager; + /** Whether the current path is at the root of a workspace */ + isWorkspaceRoot?: boolean; +}; + +export type AutoConfigDetailsForConfiguredProject = Optional< + AutoConfigDetailsBase, + // If AutoConfig detects that the project is already configured it's unnecessary to check + // what framework is being used nor the output directory, so in this case thee fields are optional + "framework" | "outputDir" +> & { + configured: true; +}; + +export type AutoConfigDetailsForNonConfiguredProject = AutoConfigDetailsBase & { + configured: false; +}; + +export type AutoConfigDetails = + | AutoConfigDetailsForConfiguredProject + | AutoConfigDetailsForNonConfiguredProject; + +export type AutoConfigOptions = { + /** The autoconfig context providing logger, dialogs, and other dependencies. */ + context: AutoConfigContext; + /** Whether to run autoconfig without actually applying any filesystem modification (default: false) */ + dryRun?: boolean; + /** + * Whether the build command should be run (default: true) + * + * Note: When `dryRun` is `true` the build command is never run. + */ + runBuild?: boolean; + /** + * Whether the confirmation prompts should be skipped (default: false) + * + * Note: When `dryRun` is `true` the the confirmation prompts are always skipped. + */ + skipConfirmations?: boolean; + /** + * Whether to install Wrangler during autoconfig + */ + enableWranglerInstallation?: boolean; +}; + +export type AutoConfigSummary = { + scripts: Record; + wranglerInstall: boolean; + wranglerConfig?: RawConfig; + frameworkConfiguration?: string; + outputDir: string; + frameworkId?: string; + buildCommand?: string; + deployCommand?: string; + versionCommand?: string; +}; diff --git a/packages/autoconfig/src/uses-typescript.ts b/packages/autoconfig/src/uses-typescript.ts new file mode 100644 index 0000000..17b6feb --- /dev/null +++ b/packages/autoconfig/src/uses-typescript.ts @@ -0,0 +1,6 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +export function usesTypescript(projectPath: string) { + return existsSync(join(projectPath, `tsconfig.json`)); +} diff --git a/packages/autoconfig/tests/details/confirm-auto-config-details.test.ts b/packages/autoconfig/tests/details/confirm-auto-config-details.test.ts new file mode 100644 index 0000000..f3550ef --- /dev/null +++ b/packages/autoconfig/tests/details/confirm-auto-config-details.test.ts @@ -0,0 +1,248 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { describe, test, vi } from "vitest"; +import { confirmAutoConfigDetails } from "../../src/details"; +import { Astro } from "../../src/frameworks/astro"; +import { Static } from "../../src/frameworks/static"; +import { createMockContext } from "../helpers/mock-context"; + +describe("autoconfig details - confirmAutoConfigDetails()", () => { + describe("interactive mode", () => { + test("no modifications applied", async ({ expect }) => { + const noModifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "worker-name", + buildCommand: "npm run build", + projectPath: "", + configured: false, + framework: new Static({ id: "static", name: "Static" }), + outputDir: "./public", + packageManager: NpmPackageManager, + }, + noModifyContext + ); + + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "configured": false, + "framework": Static { + "configurationDescription": undefined, + "id": "static", + "name": "Static", + }, + "outputDir": "./public", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "worker-name", + } + `); + }); + + test("settings can be updated in a plain static site without a framework nor a build script", async ({ + expect, + }) => { + const modifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi + .fn() + .mockResolvedValueOnce("new-name") + .mockResolvedValueOnce("./_public_") + .mockResolvedValueOnce("npm run app:build"), + select: vi.fn().mockResolvedValue("static"), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "my-worker", + buildCommand: "npm run build", + outputDir: "", + projectPath: "", + configured: false, + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + modifyContext + ); + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run app:build", + "configured": false, + "framework": Static { + "configurationDescription": undefined, + "id": "static", + "name": "Static", + }, + "outputDir": "./_public_", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "new-name", + } + `); + }); + + test("settings can be updated in a static app using a framework", async ({ + expect, + }) => { + const modifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi + .fn() + .mockResolvedValueOnce("my-astro-worker") + .mockResolvedValueOnce("") + .mockResolvedValueOnce("npm run build"), + select: vi.fn().mockResolvedValue("astro"), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "my-astro-site", + buildCommand: "astro build", + framework: new Astro({ id: "astro", name: "Astro" }), + outputDir: "", + projectPath: "", + configured: false, + packageManager: NpmPackageManager, + }, + modifyContext + ); + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "configured": false, + "framework": Astro { + "configurationDescription": "Configuring project for Astro with "astro add cloudflare"", + "id": "astro", + "name": "Astro", + }, + "outputDir": "", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "my-astro-worker", + } + `); + }); + + test("framework can be changed from a detected framework to another", async ({ + expect, + }) => { + const modifyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi + .fn() + .mockResolvedValueOnce("my-nuxt-worker") + .mockResolvedValueOnce("./dist") + .mockResolvedValueOnce("npm run build"), + select: vi.fn().mockResolvedValue("nuxt"), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "my-astro-site", + buildCommand: "astro build", + framework: new Astro({ id: "astro", name: "Astro" }), + outputDir: "", + projectPath: "", + configured: false, + packageManager: NpmPackageManager, + }, + modifyContext + ); + + expect(updatedAutoConfigDetails.framework?.id).toBe("nuxt"); + expect(updatedAutoConfigDetails.framework?.name).toBe("Nuxt"); + }); + }); + + describe("non-interactive mode", () => { + test("no modifications are applied in non-interactive", async ({ + expect, + }) => { + const nonInteractiveContext = createMockContext({ + isNonInteractiveOrCI: () => true, + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + + const updatedAutoConfigDetails = await confirmAutoConfigDetails( + { + workerName: "worker-name", + buildCommand: "npm run build", + projectPath: "", + configured: false, + framework: new Static({ id: "static", name: "Static" }), + outputDir: "./public", + packageManager: NpmPackageManager, + }, + nonInteractiveContext + ); + + expect(updatedAutoConfigDetails).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "configured": false, + "framework": Static { + "configurationDescription": undefined, + "id": "static", + "name": "Static", + }, + "outputDir": "./public", + "packageManager": { + "dlx": [ + "npx", + ], + "lockFiles": [ + "package-lock.json", + ], + "npx": "npx", + "type": "npm", + }, + "projectPath": "", + "workerName": "worker-name", + } + `); + }); + }); +}); diff --git a/packages/autoconfig/tests/details/display-auto-config-details.test.ts b/packages/autoconfig/tests/details/display-auto-config-details.test.ts new file mode 100644 index 0000000..14dbff1 --- /dev/null +++ b/packages/autoconfig/tests/details/display-auto-config-details.test.ts @@ -0,0 +1,96 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { displayAutoConfigDetails } from "../../src/details"; +import { Static } from "../../src/frameworks/static"; +import { createMockContext } from "../helpers/mock-context"; +import type { Framework } from "../../src/frameworks"; + +describe("autoconfig details - displayAutoConfigDetails()", () => { + const std = mockConsoleMethods(); + const context = createMockContext(); + + it("should cleanly handle a case in which only the worker name has been detected", ({ + expect, + }) => { + displayAutoConfigDetails( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-project", + framework: new Static({ id: "static", name: "Static" }), + outputDir: "./public", + packageManager: NpmPackageManager, + }, + context + ); + expect(std.out).toMatchInlineSnapshot( + ` + " + Detected Project Settings: + - Worker Name: my-project + - Framework: Static + - Output Directory: ./public + " + ` + ); + }); + + it("should display all the project settings provided by the details object", ({ + expect, + }) => { + displayAutoConfigDetails( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-astro-app", + framework: { + name: "Astro", + id: "astro", + isConfigured: () => false, + configure: () => + ({ + wranglerConfig: {}, + }) satisfies ReturnType, + } as unknown as Framework, + buildCommand: "astro build", + outputDir: "dist", + packageManager: NpmPackageManager, + }, + context + ); + expect(std.out).toMatchInlineSnapshot(` + " + Detected Project Settings: + - Worker Name: my-astro-app + - Framework: Astro + - Build Command: astro build + - Output Directory: dist + " + `); + }); + + it("should omit the framework and build command entries when they are not part of the details object", ({ + expect, + }) => { + displayAutoConfigDetails( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-site", + outputDir: "dist", + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + context + ); + expect(std.out).toMatchInlineSnapshot(` + " + Detected Project Settings: + - Worker Name: my-site + - Framework: Static + - Output Directory: dist + " + `); + }); +}); diff --git a/packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts new file mode 100644 index 0000000..6f964c0 --- /dev/null +++ b/packages/autoconfig/tests/details/framework-detection/basic-framework-detection.test.ts @@ -0,0 +1,49 @@ +import { writeFile } from "node:fs/promises"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + +describe("detectFramework() / basic framework detection", () => { + runInTempDir(); + const context = createMockContext(); + + it("defaults to the static framework when no framework is detected", async ({ + expect, + }) => { + await writeFile( + "package-lock.json", + JSON.stringify({ lockfileVersion: 3 }) + ); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework.framework.id).toBe("static"); + }); + + it("detects astro when astro is in dependencies", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework?.framework.id).toBe("astro"); + expect(result.detectedFramework?.framework.name).toBe("Astro"); + }); + + it("includes buildCommand in detectedFramework when available", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework?.buildCommand).toBeDefined(); + expect(result.detectedFramework?.buildCommand).toContain("astro build"); + }); +}); diff --git a/packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts b/packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts new file mode 100644 index 0000000..15167c9 --- /dev/null +++ b/packages/autoconfig/tests/details/framework-detection/lock-file-warning.test.ts @@ -0,0 +1,49 @@ +import { + mockConsoleMethods, + runInTempDir, + seed, +} from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; +const noLockFileWarning = + "No lock file has been detected in the current working directory. This might indicate that the project is part of a workspace."; + +describe("detectFramework() / lock file warning", () => { + runInTempDir(); + const std = mockConsoleMethods(); + const context = createMockContext(); + + it("warns when no lock file is detected", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + }); + + await detectFramework(process.cwd(), context); + + expect(std.warn).toContain(noLockFileWarning); + }); + + it("does not warn for static projects without a lock file", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({}), + }); + + await detectFramework(process.cwd(), context); + + expect(std.warn).not.toContain(noLockFileWarning); + }); + + it("does not warn when a lock file exists", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + await detectFramework(process.cwd(), context); + + expect(std.warn).not.toContain(noLockFileWarning); + }); +}); diff --git a/packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts b/packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts new file mode 100644 index 0000000..3da13b8 --- /dev/null +++ b/packages/autoconfig/tests/details/framework-detection/multiple-frameworks-detected.test.ts @@ -0,0 +1,193 @@ +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { afterEach, describe, it, vi } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + +describe("detectFramework() / multiple frameworks detected", () => { + runInTempDir(); + const context = createMockContext(); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe("non-CI environment", () => { + it("returns the known framework when multiple are detected but only one is known", async ({ + expect, + }) => { + // gatsby is not in allKnownFrameworks, only astro is + await seed({ + "package.json": JSON.stringify({ + dependencies: { astro: "5", gatsby: "5" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework?.framework.id).toBe("astro"); + }); + + it("filters out Vite and returns the other known framework", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { next: "14", vite: "5" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework?.framework.id).toBe("next"); + }); + + it("returns Waku (not Hono) when both Waku and Hono are detected", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { waku: "0.21", hono: "4" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework?.framework.id).toBe("waku"); + }); + + it("returns Hydrogen (not React Router) when both Hydrogen and React Router are detected", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { + "@shopify/hydrogen": "2024", + "@react-router/dev": "7", + "react-router": "7", + }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.detectedFramework?.framework.id).toBe("hydrogen"); + }); + + it("returns first framework without throwing when multiple unknown frameworks are detected", async ({ + expect, + }) => { + // Both gatsby and gridsome are unknown to wrangler + await seed({ + "package.json": JSON.stringify({ + dependencies: { gatsby: "5", gridsome: "1" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + // Should not throw even with multiple unknowns in non-CI mode + await expect( + detectFramework(process.cwd(), context) + ).resolves.toBeDefined(); + }); + }); + + describe("CI environment", () => { + const ciContext = createMockContext({ + isNonInteractiveOrCI: () => true, + }); + + it("throws MultipleFrameworksCIError when multiple known frameworks are detected", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { astro: "5", nuxt: "3" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + await expect( + detectFramework(process.cwd(), ciContext) + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [Error: Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found: Astro, Nuxt. + + To fix this issue either: + - check your project's configuration to make sure that the target framework + is the only configured one and try again + - run \`wrangler setup\` locally to get an interactive user experience where + you can specify what framework you want to target + ] + ` + ); + }); + + it("throws MultipleFrameworksCIError when multiple unknown frameworks are detected", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { gatsby: "5", gridsome: "1" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + await expect(detectFramework(process.cwd(), ciContext)).rejects.toThrow( + /Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found/ + ); + }); + + it("does not throw when Hydrogen and React Router are detected (Hydrogen is selected)", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { + "@shopify/hydrogen": "2024", + "@react-router/dev": "7", + "react-router": "7", + }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), ciContext); + + expect(result.detectedFramework?.framework.id).toBe("hydrogen"); + }); + + it("does not throw when Vite and another known framework are detected (Vite is filtered out)", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { astro: "5", vite: "5" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), ciContext); + + expect(result.detectedFramework?.framework.id).toBe("astro"); + }); + + it("does not throw when Hono and another known framework are detected (Hono is filtered out)", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + dependencies: { "@tanstack/react-start": "1.132.0", hono: "4" }, + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), ciContext); + + expect(result.detectedFramework?.framework.id).toBe("tanstack-start"); + }); + }); +}); diff --git a/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts new file mode 100644 index 0000000..22887c4 --- /dev/null +++ b/packages/autoconfig/tests/details/framework-detection/package-manager-detection.test.ts @@ -0,0 +1,71 @@ +import { + BunPackageManager, + NpmPackageManager, + PnpmPackageManager, + YarnPackageManager, +} from "@cloudflare/workers-utils"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + +describe("detectFramework() / package manager detection", () => { + runInTempDir(); + const context = createMockContext(); + + it("detects npm when package-lock.json is present", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.packageManager).toStrictEqual(NpmPackageManager); + }); + + it("detects pnpm when pnpm-lock.yaml is present", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "pnpm-lock.yaml": "lockfileVersion: '6.0'\n", + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.packageManager).toStrictEqual(PnpmPackageManager); + }); + + it("detects yarn when yarn.lock is present", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "yarn.lock": "# yarn lockfile v1\n", + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.packageManager).toStrictEqual(YarnPackageManager); + }); + + it("detects bun when bun.lock is present", async ({ expect }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "bun.lock": "", + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.packageManager).toStrictEqual(BunPackageManager); + }); + + it("falls back to npm when no package manager lock file is present", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.packageManager).toStrictEqual(NpmPackageManager); + }); +}); diff --git a/packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts b/packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts new file mode 100644 index 0000000..188072c --- /dev/null +++ b/packages/autoconfig/tests/details/framework-detection/pages-project-detection.test.ts @@ -0,0 +1,110 @@ +import { join } from "node:path"; +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it, vi } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; +import type { Config } from "@cloudflare/workers-utils"; + +describe("detectFramework() / Pages project detection", () => { + runInTempDir(); + const context = createMockContext(); + + it("returns Cloudflare Pages framework when pages_build_output_dir is set in wrangler config", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({}), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context, { + pages_build_output_dir: "./dist", + } as Config); + + expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages"); + expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages"); + expect(result.detectedFramework?.dist).toBe("./dist"); + }); + + it("returns Cloudflare Pages framework when the pages cache file exists", async ({ + expect, + }) => { + const cacheFolder = join(process.cwd(), ".cache"); + await seed({ + "package.json": JSON.stringify({}), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + [join(cacheFolder, "pages.json")]: JSON.stringify({ + account_id: "test-account", + }), + }); + + const cacheContext = createMockContext({ + getCacheFolder: () => cacheFolder, + }); + + const result = await detectFramework(process.cwd(), cacheContext); + + expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages"); + expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages"); + }); + + it("returns Cloudflare Pages when a functions/ directory exists, no framework is detected, and the user confirms", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({}), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + "functions/hello.js": `export function onRequest() { return new Response("hi"); }`, + }); + + const confirmContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + + const result = await detectFramework(process.cwd(), confirmContext); + + expect(result.detectedFramework?.framework.id).toBe("cloudflare-pages"); + expect(result.detectedFramework?.framework.name).toBe("Cloudflare Pages"); + }); + + it("does not return Cloudflare Pages when the user denies the functions/ directory prompt", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({}), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + "functions/hello.js": `export function onRequest() { return new Response("hi"); }`, + }); + + const denyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + + const result = await detectFramework(process.cwd(), denyContext); + + expect(result.detectedFramework?.framework.id).not.toBe("cloudflare-pages"); + }); + + it("does not return Cloudflare Pages when a functions/ directory exists alongside a detected framework", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ dependencies: { astro: "5" } }), + "functions/hello.js": `export function onRequest() { return new Response("hi"); }`, + }); + + const result = await detectFramework(process.cwd(), context); + + // Astro is detected, so Pages detection via functions/ is skipped + expect(result.detectedFramework?.framework.id).not.toBe("cloudflare-pages"); + expect(result.detectedFramework?.framework.id).toBe("astro"); + }); +}); diff --git a/packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts b/packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts new file mode 100644 index 0000000..7da594d --- /dev/null +++ b/packages/autoconfig/tests/details/framework-detection/workspace-root-handling.test.ts @@ -0,0 +1,59 @@ +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { detectFramework } from "../../../src/details/framework-detection"; +import { createMockContext } from "../../helpers/mock-context"; + +describe("detectFramework() / workspace root handling", () => { + runInTempDir(); + const context = createMockContext(); + + it("sets isWorkspaceRoot to false for regular (non-monorepo) projects", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ name: "my-app" }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.isWorkspaceRoot).toBe(false); + }); + + it("sets isWorkspaceRoot to true when the workspace root is itself a workspace package", async ({ + expect, + }) => { + await seed({ + "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n - '.'\n", + "package.json": JSON.stringify({ + name: "my-workspace", + workspaces: ["packages/*", "."], + }), + "packages/my-app/package.json": JSON.stringify({ name: "my-app" }), + }); + + const result = await detectFramework(process.cwd(), context); + + expect(result.isWorkspaceRoot).toBe(true); + }); + + it("throws UserError when run from a workspace root that does not include the root as a package", async ({ + expect, + }) => { + await seed({ + "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n", + "package.json": JSON.stringify({ + name: "my-workspace", + workspaces: ["packages/*"], + }), + "packages/my-app/package.json": JSON.stringify({ name: "my-app" }), + "packages/my-app/index.html": "

Hello World

", + }); + + await expect( + detectFramework(process.cwd(), context) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: The Cloudflare application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]` + ); + }); +}); diff --git a/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts b/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts new file mode 100644 index 0000000..9d1190f --- /dev/null +++ b/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts @@ -0,0 +1,562 @@ +import { randomUUID } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { + mockConsoleMethods, + runInTempDir, + seed, +} from "@cloudflare/workers-utils/test-helpers"; +import { afterEach, describe, it, vi } from "vitest"; +import * as details from "../../src/details"; +import { createMockContext } from "../helpers/mock-context"; +import type { Config } from "@cloudflare/workers-utils"; + +describe("autoconfig details - getDetailsForAutoConfig()", () => { + runInTempDir(); + const std = mockConsoleMethods(); + const context = createMockContext(); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("should set configured: true if a configPath exists", async ({ + expect, + }) => { + await expect( + details.getDetailsForAutoConfig({ + wranglerConfig: { configPath: "/tmp" } as Config, + context, + }) + ).resolves.toMatchObject({ configured: true }); + }); + + // Check that Astro is detected. We don't want to duplicate the tests of @netlify/build-info + // by exhaustively checking every possible combination + it.for(["npm", "pnpm"] as const)( + "should perform basic framework detection (using %s)", + async (pm, { expect }) => { + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + astro: "5", + }, + }) + ); + + // Create the appropriate lockfile so @netlify/build-info detects the package manager + if (pm === "pnpm") { + await writeFile("pnpm-lock.yaml", "lockfileVersion: 6.0"); + } else { + await writeFile( + "package-lock.json", + JSON.stringify({ lockfileVersion: 3 }) + ); + } + + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ + buildCommand: pm === "pnpm" ? "pnpm astro build" : "npx astro build", + configured: false, + outputDir: "dist", + packageJson: { + dependencies: { + astro: "5", + }, + }, + }); + } + ); + + it("should select the known framework when multiple frameworks are detected but only one is known", async ({ + expect, + }) => { + // Gatsby is not in allKnownFrameworks, so only Astro should be considered + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + astro: "5", + gatsby: "5", + }, + }) + ); + + const result = await details.getDetailsForAutoConfig({ context }); + + // Should select Astro since it's the only known framework + expect(result.framework?.id).toBe("astro"); + expect(result.framework?.name).toBe("Astro"); + }); + + it("should bail when run in the root of a workspace", async ({ expect }) => { + await seed({ + "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n", + "package.json": JSON.stringify({ + name: "my-workspace", + workspaces: ["packages/*"], + }), + "packages/my-app/package.json": JSON.stringify({ name: "my-app" }), + "packages/my-app/index.html": "

Hello World

", + }); + + await expect( + details.getDetailsForAutoConfig({ context }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: The Cloudflare application detection logic has been run in the root of a workspace instead of targeting a specific project. Change your working directory to one of the applications in the workspace and try again.]` + ); + }); + + it("should not bail when run in the root of a workspace if the root is included as a workspace package", async ({ + expect, + }) => { + await seed({ + "pnpm-workspace.yaml": "packages:\n - 'packages/*'\n - '.'\n", + "package.json": JSON.stringify({ + name: "my-workspace", + workspaces: ["packages/*", "."], + }), + "index.html": "

Hello World

", + "packages/my-app/package.json": JSON.stringify({ name: "my-app" }), + "packages/my-app/index.html": "

Hello World

", + }); + + const result = await details.getDetailsForAutoConfig({ context }); + + expect(result.isWorkspaceRoot).toBe(true); + expect(result.framework?.id).toBe("static"); + }); + + it("should set isWorkspaceRoot to false for non-workspace projects", async ({ + expect, + }) => { + await seed({ + "package.json": JSON.stringify({ + name: "my-app", + }), + "package-lock.json": JSON.stringify({ lockfileVersion: 3 }), + "index.html": "

Hello World

", + }); + + const result = await details.getDetailsForAutoConfig({ context }); + + expect(result.isWorkspaceRoot).toBe(false); + }); + + it("should warn when no lock file is detected (project may be inside a workspace)", async ({ + expect, + }) => { + // Create a project without a lock file - simulating a project inside a workspace + // where the lock file is at the workspace root + await seed({ + "package.json": JSON.stringify({ + name: "my-app", + dependencies: { astro: "6" }, + }), + "index.html": "

Hello World

", + }); + + await details.getDetailsForAutoConfig({ context }); + + expect(std.warn).toContain( + "No lock file has been detected in the current working directory." + ); + expect(std.warn).toContain("project is part of a workspace"); + }); + + it("should use npm build instead of framework build if present", async ({ + expect, + }) => { + await writeFile( + "package.json", + JSON.stringify({ + scripts: { + build: "echo build", + }, + dependencies: { + astro: "5", + }, + }) + ); + + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ + buildCommand: "npm run build", + }); + }); + + it("an error should be thrown if no output dir can be detected", async ({ + expect, + }) => { + await expect( + details.getDetailsForAutoConfig({ context }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Could not detect a directory containing static files (e.g. html, css and js) for the project]` + ); + }); + + it("outputDir should be set to cwd if an index.html file exists", async ({ + expect, + }) => { + await writeFile("index.html", `

Hello World

`); + + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ + outputDir: ".", + }); + }); + + it("outputDir should find first child directory with an index.html file", async ({ + expect, + }) => { + await seed({ + "public/index.html": `

Hello World

`, + "random/index.html": `

Hello World

`, + }); + + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ + outputDir: "public", + }); + }); + + it("outputDir should prioritize the project directory over its child directories", async ({ + expect, + }) => { + await seed({ + "index.html": `

Hello World

`, + "public/index.html": `

Hello World

`, + }); + + await expect( + details.getDetailsForAutoConfig({ context }) + ).resolves.toMatchObject({ + outputDir: ".", + }); + }); + + const workerNamesToTest = [ + { rawName: "my-project-1", normalizedName: "my-project-1" }, + { + rawName: "--my-other-project%1_", + normalizedName: "my-other-project-1", + }, + { + rawName: "x".repeat(100), + normalizedName: "x".repeat(63), + }, + ]; + + it.for(workerNamesToTest)( + "should use the directory name as the worker name for a plain static site, normalizing it if needed (%s)", + async ( + { rawName: dirname, normalizedName: expectedWorkerName }, + { expect } + ) => { + await seed({ + [`./${dirname}/index.html`]: "

Hello World

", + }); + await expect( + details.getDetailsForAutoConfig({ + projectPath: `./${dirname}`, + context, + }) + ).resolves.toMatchObject({ + workerName: expectedWorkerName, + }); + } + ); + + it.for(workerNamesToTest)( + "should use the project name from the package.json file when available as the worker name, normalizing it if needed (%s)", + async ( + { rawName: projectName, normalizedName: expectedWorkerName }, + { expect } + ) => { + const dirname = `project-${randomUUID()}`; + await seed({ + [`./${dirname}/package.json`]: JSON.stringify({ name: projectName }), + [`./${dirname}/index.html`]: "

Hello World

", + }); + await expect( + details.getDetailsForAutoConfig({ + projectPath: `./${dirname}`, + context, + }) + ).resolves.toMatchObject({ + workerName: expectedWorkerName, + }); + } + ); + + it("WRANGLER_CI_OVERRIDE_NAME, when set should override the worker name", async ({ + expect, + }) => { + vi.stubEnv("WRANGLER_CI_OVERRIDE_NAME", "overridden-worker-name"); + + await seed({ + "./my-project/index.html": "

Hello World

", + }); + await expect( + details.getDetailsForAutoConfig({ + projectPath: `./my-project`, + context, + }) + ).resolves.toMatchObject({ + workerName: "overridden-worker-name", + }); + }); + + describe("Pages project detection", () => { + it("should detect Pages project when pages_build_output_dir is set in wrangler config", async ({ + expect, + }) => { + await seed({ + "public/index.html": `

Hello World

`, + }); + + const result = await details.getDetailsForAutoConfig({ + wranglerConfig: { + configPath: "/tmp/wrangler.toml", + pages_build_output_dir: "./dist", + } as Config, + context, + }); + + expect(result.configured).toBe(false); + expect(result.framework?.id).toBe("cloudflare-pages"); + expect(result.framework?.name).toBe("Cloudflare Pages"); + }); + + it("should detect Pages project when pages.json cache file exists", async ({ + expect, + }) => { + const cacheFolder = join(process.cwd(), ".cache"); + await seed({ + "public/index.html": `

Hello World

`, + [join(cacheFolder, "pages.json")]: JSON.stringify({ + account_id: "test-account", + }), + }); + + const cacheContext = createMockContext({ + getCacheFolder: () => cacheFolder, + }); + + const result = await details.getDetailsForAutoConfig({ + context: cacheContext, + }); + + expect(result.framework?.id).toBe("cloudflare-pages"); + expect(result.framework?.name).toBe("Cloudflare Pages"); + }); + + it("should detect Pages project when functions directory exists, no framework is detected and the user confirms that it is", async ({ + expect, + }) => { + await seed({ + "public/index.html": `

Hello World

`, + "functions/hello.js": ` + export function onRequest(context) { + return new Response("Hello, world!"); + } + `, + }); + + const confirmContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + + const result = await details.getDetailsForAutoConfig({ + context: confirmContext, + }); + + expect(result.framework?.id).toBe("cloudflare-pages"); + expect(result.framework?.name).toBe("Cloudflare Pages"); + }); + + it("should not detect Pages project when the user denies that, even if the functions directory exists and no framework is detected", async ({ + expect, + }) => { + await seed({ + "public/index.html": `

Hello World

`, + "functions/hello.js": ` + export function onRequest(context) { + return new Response("Hello, world!"); + } + `, + }); + + const denyContext = createMockContext({ + dialogs: { + confirm: vi.fn().mockResolvedValue(false), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + }); + + const result = await details.getDetailsForAutoConfig({ + context: denyContext, + }); + + expect(result.framework?.id).toBe("static"); + expect(result.framework?.name).toBe("Static"); + }); + + it("should not detect Pages project when functions directory exists but a framework is detected", async ({ + expect, + }) => { + await seed({ + "functions/hello.js": + "export const myFun = () => { console.log('Hello!'); };", + "package.json": JSON.stringify({ + dependencies: { + astro: "5", + }, + }), + }); + + const result = await details.getDetailsForAutoConfig({ context }); + + // Should detect Astro, not Pages + expect(result.framework?.id).toBe("astro"); + }); + }); + + describe("multiple frameworks detected", () => { + describe("local environment (non-CI)", () => { + it("should return a single framework when multiple frameworks are detected", async ({ + expect, + }) => { + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + astro: "5", + "@angular/core": "18", + }, + }) + ); + + const result = await details.getDetailsForAutoConfig({ context }); + + // Should return a framework (either astro or angular) + expect(result.framework).toBeDefined(); + expect(["astro", "angular"]).toContain(result.framework?.id); + }); + }); + + describe("CI environment", () => { + const ciContext = createMockContext({ + isNonInteractiveOrCI: () => true, + }); + + it("should throw MultipleFrameworksCIError when multiple known frameworks are detected in CI", async ({ + expect, + }) => { + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + astro: "5", + nuxt: "3", + }, + }) + ); + + await expect( + details.getDetailsForAutoConfig({ context: ciContext }) + ).rejects.toThrow( + /Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found/ + ); + }); + + it("should NOT throw when Vite and another known framework are detected in CI (Vite is filtered out)", async ({ + expect, + }) => { + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + astro: "5", + vite: "5", + }, + }) + ); + + const result = await details.getDetailsForAutoConfig({ + context: ciContext, + }); + expect(result.framework?.id).toBe("astro"); + }); + + it("should throw MultipleFrameworksCIError when multiple unknown frameworks are detected in CI", async ({ + expect, + }) => { + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + gatsby: "5", + gridsome: "1", + }, + }) + ); + + await expect( + details.getDetailsForAutoConfig({ context: ciContext }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [Error: Cloudflare's tooling was unable to automatically configure your project, since multiple frameworks were found: Gatsby, Gridsome. + + To fix this issue either: + - check your project's configuration to make sure that the target framework + is the only configured one and try again + - run \`wrangler setup\` locally to get an interactive user experience where + you can specify what framework you want to target + ] + ` + ); + }); + }); + + it("should return non-Vite framework when Vite and another known framework are detected", async ({ + expect, + }) => { + await writeFile( + "package.json", + JSON.stringify({ + dependencies: { + next: "14", + vite: "5", + }, + }) + ); + + const result = await details.getDetailsForAutoConfig({ context }); + expect(result.framework?.id).toBe("next"); + }); + + it("should fallback to static framework when no frameworks detected", async ({ + expect, + }) => { + await seed({ + "index.html": "

Hello World

", + "package.json": JSON.stringify({}), + }); + + const result = await details.getDetailsForAutoConfig({ context }); + + expect(result.framework?.id).toBe("static"); + }); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/angular.test.ts b/packages/autoconfig/tests/frameworks/angular.test.ts new file mode 100644 index 0000000..b5aa1cf --- /dev/null +++ b/packages/autoconfig/tests/frameworks/angular.test.ts @@ -0,0 +1,445 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { Angular } from "../../src/frameworks/angular"; +import { createMockContext } from "../helpers/mock-context"; +import type { AutoConfigFrameworkPackageInfo } from "../../src/frameworks"; +const context = createMockContext(); + +const BASE_OPTIONS = { + projectPath: process.cwd(), + workerName: "my-angular-app", + outputDir: "dist/my-angular-app/", + dryRun: false, + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + context, +}; + +const ANGULAR_PACKAGE_INFO: AutoConfigFrameworkPackageInfo = { + name: "@angular/core", + minimumVersion: "19.0.0", + maximumKnownMajorVersion: "22", +}; + +async function mockAngularCoreVersion(version: string) { + const pkgDir = resolve("node_modules/@angular/core"); + await mkdir(pkgDir, { recursive: true }); + await writeFile( + resolve(pkgDir, "package.json"), + JSON.stringify({ name: "@angular/core", version }) + ); +} + +describe("Angular framework configure()", () => { + runInTempDir(); + + let installSpy: ReturnType; + + beforeEach(() => { + installSpy = vi + .spyOn(cliPackages, "installPackages") + .mockImplementation(async () => {}); + }); + + describe("non-SSR (SPA) project", () => { + beforeEach(async () => { + /** Minimal angular.json for a non-SSR (SPA) project — no `ssr` field */ + const NON_SSR_ANGULAR_JSON = { + projects: { + "my-angular-app": { + architect: { + build: { + options: { + browser: "src/main.ts", + tsConfig: "tsconfig.app.json", + assets: [{ glob: "**/*", input: "public" }], + styles: ["src/styles.css"], + }, + }, + }, + }, + }, + }; + + await writeFile( + resolve("angular.json"), + JSON.stringify(NON_SSR_ANGULAR_JSON, null, 2) + ); + await mkdir(resolve("src"), { recursive: true }); + }); + + it("returns assets-only wranglerConfig", async ({ expect }) => { + const framework = new Angular({ id: "angular", name: "Angular" }); + const result = await framework.configure(BASE_OPTIONS); + + expect(result.wranglerConfig).toEqual({ + assets: { + directory: "dist/my-angular-app/", + }, + }); + expect(result.wranglerConfig).not.toHaveProperty("main"); + }); + + it("sets configurationDescription for SPA", async ({ expect }) => { + const framework = new Angular({ id: "angular", name: "Angular" }); + await framework.configure(BASE_OPTIONS); + + expect(framework.configurationDescription).toBe( + "Configuring Angular SPA project (assets only)" + ); + }); + + it("does not install additional dependencies", async ({ expect }) => { + const framework = new Angular({ id: "angular", name: "Angular" }); + await framework.configure(BASE_OPTIONS); + + expect(installSpy).not.toHaveBeenCalled(); + }); + + it("does not create src/server.ts", async ({ expect }) => { + const { existsSync } = await import("node:fs"); + const framework = new Angular({ id: "angular", name: "Angular" }); + await framework.configure(BASE_OPTIONS); + + expect(existsSync(resolve("src/server.ts"))).toBe(false); + }); + + it("does not modify angular.json", async ({ expect }) => { + const { readFileSync } = await import("node:fs"); + const framework = new Angular({ id: "angular", name: "Angular" }); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + // outputMode and outputPath should not have been set + expect( + angularJson.projects["my-angular-app"].architect.build.options + .outputMode + ).toBeUndefined(); + expect( + angularJson.projects["my-angular-app"].architect.build.options + .outputPath + ).toBeUndefined(); + }); + + it("skips side effects in dryRun mode", async ({ expect }) => { + const framework = new Angular({ id: "angular", name: "Angular" }); + const result = await framework.configure({ + ...BASE_OPTIONS, + dryRun: true, + }); + + expect(result.wranglerConfig).toEqual({ + assets: { directory: "dist/my-angular-app/" }, + }); + expect(installSpy).not.toHaveBeenCalled(); + }); + }); + + describe("project with ssr: false", () => { + beforeEach(async () => { + /** angular.json for a project with SSR explicitly set to false */ + const SSR_FALSE_ANGULAR_JSON = { + projects: { + "my-angular-app": { + architect: { + build: { + options: { + browser: "src/main.ts", + tsConfig: "tsconfig.app.json", + assets: [], + styles: [], + ssr: false, + }, + }, + }, + }, + }, + }; + + await writeFile( + resolve("angular.json"), + JSON.stringify(SSR_FALSE_ANGULAR_JSON, null, 2) + ); + await mkdir(resolve("src"), { recursive: true }); + }); + + it("returns assets-only wranglerConfig when ssr is false", async ({ + expect, + }) => { + const framework = new Angular({ id: "angular", name: "Angular" }); + const result = await framework.configure(BASE_OPTIONS); + + expect(result.wranglerConfig).toEqual({ + assets: { directory: "dist/my-angular-app/" }, + }); + expect(result.wranglerConfig).not.toHaveProperty("main"); + }); + + it("sets SPA configurationDescription when ssr is false", async ({ + expect, + }) => { + const framework = new Angular({ id: "angular", name: "Angular" }); + await framework.configure(BASE_OPTIONS); + + expect(framework.configurationDescription).toBe( + "Configuring Angular SPA project (assets only)" + ); + }); + }); + + describe("project with ssr: true (boolean shorthand)", () => { + beforeEach(async () => { + /** angular.json for a project with SSR enabled via the `true` boolean shorthand */ + const SSR_TRUE_ANGULAR_JSON = { + projects: { + "my-angular-app": { + architect: { + build: { + options: { + browser: "src/main.ts", + tsConfig: "tsconfig.app.json", + assets: [], + styles: [], + ssr: true, + }, + }, + }, + }, + }, + }; + + await writeFile( + resolve("angular.json"), + JSON.stringify(SSR_TRUE_ANGULAR_JSON, null, 2) + ); + await mkdir(resolve("src"), { recursive: true }); + }); + + it("returns SSR wranglerConfig without crashing", async ({ expect }) => { + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + const result = await framework.configure(BASE_OPTIONS); + + expect(result.wranglerConfig).toEqual({ + main: "./dist/server/server.mjs", + assets: { + binding: "ASSETS", + directory: "dist/my-angular-app/browser", + }, + }); + }); + + it("sets experimentalPlatform in angular.json when ssr was true (Angular <22)", async ({ + expect, + }) => { + const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + const options = + angularJson.projects["my-angular-app"].architect.build.options; + // The boolean `true` should have been promoted to an object + expect(typeof options.ssr).toBe("object"); + expect(options.ssr.experimentalPlatform).toBe("neutral"); + }); + + it("sets platform in angular.json when ssr was true (Angular >=22)", async ({ + expect, + }) => { + const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("22.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + const options = + angularJson.projects["my-angular-app"].architect.build.options; + expect(typeof options.ssr).toBe("object"); + expect(options.ssr.platform).toBe("neutral"); + }); + }); + + describe("SSR project", () => { + beforeEach(async () => { + /** angular.json for a project with SSR enabled (object value) */ + const SSR_ANGULAR_JSON = { + projects: { + "my-angular-app": { + architect: { + build: { + options: { + browser: "src/main.ts", + tsConfig: "tsconfig.app.json", + assets: [], + styles: [], + ssr: { + entry: "src/server.ts", + }, + }, + }, + }, + }, + }, + }; + + await writeFile( + resolve("angular.json"), + JSON.stringify(SSR_ANGULAR_JSON, null, 2) + ); + await mkdir(resolve("src"), { recursive: true }); + }); + + it("returns SSR wranglerConfig with main and assets", async ({ + expect, + }) => { + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + const result = await framework.configure(BASE_OPTIONS); + + expect(result.wranglerConfig).toEqual({ + main: "./dist/server/server.mjs", + assets: { + binding: "ASSETS", + directory: "dist/my-angular-app/browser", + }, + }); + }); + + it("sets SSR configurationDescription", async ({ expect }) => { + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + expect(framework.configurationDescription).toBe( + "Configuring project for Angular" + ); + }); + + it("sets experimentalPlatform in angular.json (Angular <22)", async ({ + expect, + }) => { + const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + const options = + angularJson.projects["my-angular-app"].architect.build.options; + expect(options.outputMode).toBe("server"); + expect(options.outputPath).toBe("dist"); + expect(options.ssr.experimentalPlatform).toBe("neutral"); + }); + + it("sets platform in angular.json (Angular >=22)", async ({ expect }) => { + const { readFileSync } = await import("node:fs"); + await mockAngularCoreVersion("22.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + const angularJson = JSON.parse( + readFileSync(resolve("angular.json"), "utf8") + ); + const options = + angularJson.projects["my-angular-app"].architect.build.options; + expect(options.outputMode).toBe("server"); + expect(options.outputPath).toBe("dist"); + expect(options.ssr.platform).toBe("neutral"); + }); + + it("creates src/server.ts", async ({ expect }) => { + const { existsSync } = await import("node:fs"); + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + expect(existsSync(resolve("src/server.ts"))).toBe(true); + }); + + it("installs additional dependencies", async ({ expect }) => { + await mockAngularCoreVersion("21.0.0"); + const framework = new Angular({ id: "angular", name: "Angular" }); + framework.validateFrameworkVersion( + process.cwd(), + ANGULAR_PACKAGE_INFO, + context + ); + await framework.configure(BASE_OPTIONS); + + expect(installSpy).toHaveBeenCalledWith( + NpmPackageManager.type, + ["xhr2"], + expect.objectContaining({ dev: true }) + ); + }); + + it("skips side effects in dryRun mode", async ({ expect }) => { + const { existsSync } = await import("node:fs"); + const framework = new Angular({ id: "angular", name: "Angular" }); + const result = await framework.configure({ + ...BASE_OPTIONS, + dryRun: true, + }); + + // Config is still returned + expect(result.wranglerConfig).toHaveProperty("main"); + // But side effects are skipped + expect(existsSync(resolve("src/server.ts"))).toBe(false); + expect(installSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/config-future-no-middleware.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-future-no-middleware.ts new file mode 100644 index 0000000..844e507 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-future-no-middleware.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_splitRouteModules: true, + }, +} satisfies Config; diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-and-split.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-and-split.ts new file mode 100644 index 0000000..d4298c7 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-and-split.ts @@ -0,0 +1,8 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_middleware: true, + v8_splitRouteModules: true, + }, +} satisfies Config; diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-false.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-false.ts new file mode 100644 index 0000000..4696f3a --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-false.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_middleware: false, + }, +} satisfies Config; diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-true.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-true.ts new file mode 100644 index 0000000..dfc17a9 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-middleware-true.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_middleware: true, + }, +} satisfies Config; diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/config-no-future.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-no-future.ts new file mode 100644 index 0000000..f6df076 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-no-future.ts @@ -0,0 +1,4 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, +} satisfies Config; diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/config-plain-object-middleware.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-plain-object-middleware.ts new file mode 100644 index 0000000..44a6ef4 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/config-plain-object-middleware.ts @@ -0,0 +1,6 @@ +export default { + ssr: true, + future: { + v8_middleware: true, + }, +}; diff --git a/packages/autoconfig/tests/frameworks/fixtures/react-router/vite-config-basic.ts b/packages/autoconfig/tests/frameworks/fixtures/react-router/vite-config-basic.ts new file mode 100644 index 0000000..9e2760e --- /dev/null +++ b/packages/autoconfig/tests/frameworks/fixtures/react-router/vite-config-basic.ts @@ -0,0 +1,2 @@ +import { defineConfig } from "vite"; +export default defineConfig({ plugins: [] }); diff --git a/packages/autoconfig/tests/frameworks/get-framework-class.test.ts b/packages/autoconfig/tests/frameworks/get-framework-class.test.ts new file mode 100644 index 0000000..773fdcc --- /dev/null +++ b/packages/autoconfig/tests/frameworks/get-framework-class.test.ts @@ -0,0 +1,37 @@ +import { describe, it } from "vitest"; +import { getFrameworkClassInstance } from "../../src/frameworks"; +import { NextJs } from "../../src/frameworks/next"; +import { NoOpFramework } from "../../src/frameworks/no-op"; +import { Static } from "../../src/frameworks/static"; + +describe("getFrameworkClassInstance()", () => { + it("should return a Static framework when frameworkId is unknown", ({ + expect, + }) => { + const framework = getFrameworkClassInstance("unknown-framework"); + + expect(framework).toBeInstanceOf(Static); + expect(framework.id).toBe("static"); + expect(framework.name).toBe("Static"); + }); + + it("should return a target framework when frameworkId is known and supported", ({ + expect, + }) => { + const framework = getFrameworkClassInstance("next"); + + expect(framework).toBeInstanceOf(NextJs); + expect(framework.id).toBe("next"); + expect(framework.name).toBe("Next.js"); + }); + + it("should return a NoOpFramework for an unsupported framework (hono)", ({ + expect, + }) => { + const framework = getFrameworkClassInstance("hono"); + + expect(framework).toBeInstanceOf(NoOpFramework); + expect(framework.id).toBe("hono"); + expect(framework.name).toBe("Hono"); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/is-framework-supported.test.ts b/packages/autoconfig/tests/frameworks/is-framework-supported.test.ts new file mode 100644 index 0000000..6747cb9 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/is-framework-supported.test.ts @@ -0,0 +1,20 @@ +import { describe, it } from "vitest"; +import { isFrameworkSupported } from "../../src/frameworks"; + +describe("isFrameworkSupported()", () => { + it("should return true for a supported framework id", ({ expect }) => { + expect(isFrameworkSupported("astro")).toBe(true); + }); + + it("should return false for a known but unsupported framework id", ({ + expect, + }) => { + expect(isFrameworkSupported("hono")).toBe(false); + }); + + it("should throw for an unknown framework id", ({ expect }) => { + expect(() => isFrameworkSupported("unknown-framework")).toThrow( + 'Unexpected unknown framework id: "unknown-framework"' + ); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/is-known-framework.test.ts b/packages/autoconfig/tests/frameworks/is-known-framework.test.ts new file mode 100644 index 0000000..97ffa2d --- /dev/null +++ b/packages/autoconfig/tests/frameworks/is-known-framework.test.ts @@ -0,0 +1,22 @@ +import { describe, it } from "vitest"; +import { isKnownFramework } from "../../src/frameworks"; + +describe("isKnownFramework()", () => { + it("should return true for a known supported framework id", ({ expect }) => { + expect(isKnownFramework("astro")).toBe(true); + }); + + it("should return true for a known but unsupported framework id", ({ + expect, + }) => { + expect(isKnownFramework("hono")).toBe(true); + }); + + it('should return true for the "static" framework id', ({ expect }) => { + expect(isKnownFramework("static")).toBe(true); + }); + + it("should return false for an unknown framework id", ({ expect }) => { + expect(isKnownFramework("unknown-framework")).toBe(false); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/react-router.test.ts b/packages/autoconfig/tests/frameworks/react-router.test.ts new file mode 100644 index 0000000..6471727 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/react-router.test.ts @@ -0,0 +1,472 @@ +import { existsSync, readFileSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { + hasV8MiddlewareFlag, + ReactRouter, +} from "../../src/frameworks/react-router"; +import * as packagesUtils from "../../src/frameworks/utils/packages"; +import { createMockContext } from "../helpers/mock-context"; + +function fixture(name: string): string { + return readFileSync(join(__dirname, "fixtures/react-router", name), "utf-8"); +} + +vi.mock("../../src/frameworks/utils/vite-config", () => ({ + transformViteConfig: vi.fn(), +})); + +vi.mock("../../src/frameworks/utils/vite-plugin", () => ({ + installCloudflareVitePlugin: vi.fn(), +})); + +vi.mock("../../src/frameworks/utils/packages", () => ({ + getInstalledPackageVersion: vi.fn(), + isPackageInstalled: vi.fn(() => true), +})); + +const context = createMockContext(); + +function getBaseOptions() { + return { + projectPath: process.cwd(), + outputDir: "build/", + workerName: "my-react-router-app", + dryRun: false, + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + context, + }; +} + +function createFramework(version: string): ReactRouter { + vi.mocked(packagesUtils.getInstalledPackageVersion).mockReturnValue(version); + + const framework = new ReactRouter({ + id: "react-router", + name: "React Router", + }); + + framework.validateFrameworkVersion( + ".", + { + name: "react-router", + minimumVersion: "7.0.0", + maximumKnownMajorVersion: "8", + }, + context + ); + + return framework; +} + +describe("hasV8MiddlewareFlag()", () => { + runInTempDir(); + + it("returns false when no config file exists", ({ expect }) => { + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("returns false when config has no future block", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("returns false when future block does not contain v8_middleware", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-future-no-middleware.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("returns true when v8_middleware is set to true", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(true); + }); + + it("returns false when v8_middleware is set to false", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-false.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("handles plain object export without satisfies", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-plain-object-middleware.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(true); + }); + + it("returns false when config file has syntax errors", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + "export default { this is not valid syntax" + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("detects v8_middleware inside a `satisfies Config` expression", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-and-split.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(true); + }); +}); + +describe("React Router framework configure()", () => { + runInTempDir(); + + beforeEach(async () => { + vi.spyOn(cliPackages, "installPackages").mockImplementation(async () => {}); + + await mkdir(resolve("app"), { recursive: true }); + await writeFile(resolve("vite.config.ts"), fixture("vite-config-basic.ts")); + }); + + describe("workers/app.ts generation — without v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("creates workers/app.ts with AppLoadContext augmentation", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).toContain("AppLoadContext"); + expect(content).toContain("declare module"); + expect(content).toContain("cloudflare: { env, ctx }"); + expect(content).toContain("async fetch(request, env, ctx)"); + expect(content).toContain("satisfies ExportedHandler"); + }); + }); + + describe("workers/app.ts generation — with v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + }); + + it("creates workers/app.ts without AppLoadContext augmentation", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).not.toContain("AppLoadContext"); + expect(content).not.toContain("declare module"); + expect(content).toContain( + 'import { createRequestHandler } from "react-router"' + ); + expect(content).toContain("requestHandler(request)"); + expect(content).toContain("satisfies ExportedHandler"); + }); + + it("creates workers/app.ts with a simple fetch handler", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).toContain("async fetch(request)"); + expect(content).not.toContain("env, ctx"); + expect(content).not.toContain("cloudflare: { env, ctx }"); + }); + }); + + describe("entry.server.tsx generation — without v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("creates entry.server.tsx with AppLoadContext", async ({ expect }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).toContain("AppLoadContext"); + expect(content).toContain("_loadContext: AppLoadContext"); + expect(content).toContain( + 'import type { AppLoadContext, EntryContext } from "react-router"' + ); + }); + }); + + describe("entry.server.tsx generation — with v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + }); + + it("creates entry.server.tsx without AppLoadContext", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).not.toContain("AppLoadContext"); + expect(content).not.toContain("_loadContext"); + expect(content).toContain( + 'import type { EntryContext } from "react-router"' + ); + expect(content).toContain("ServerRouter"); + expect(content).toContain("renderToReadableStream"); + }); + }); + + describe("entry.server.tsx — existing file not overwritten", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + }); + + it("does not overwrite existing entry.server.tsx", async ({ expect }) => { + const existingContent = "// existing entry server"; + await mkdir(resolve("app"), { recursive: true }); + await writeFile(resolve("app/entry.server.tsx"), existingContent); + + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).toBe(existingContent); + }); + }); + + describe("react-router.config.ts transformation", () => { + it("adds v8_viteEnvironmentApi for React Router >= 7.10.0", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + expect(content).toContain("v8_viteEnvironmentApi: true"); + // Should NOT add other v8 flags + expect(content).not.toContain("v8_middleware"); + expect(content).not.toContain("v8_splitRouteModules"); + expect(content).not.toContain("v8_passThroughRequests"); + expect(content).not.toContain("v8_trailingSlashAwareDataRequests"); + }); + + it("uses unstable_viteEnvironmentApi for React Router < 7.10.0", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + + const framework = createFramework("7.9.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + expect(content).toContain("unstable_viteEnvironmentApi: true"); + expect(content).not.toContain("v8_viteEnvironmentApi"); + }); + + it("preserves existing future flags when adding viteEnvironmentApi", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + // Existing flag preserved + expect(content).toContain("v8_middleware: true"); + // New flag added + expect(content).toContain("v8_viteEnvironmentApi: true"); + }); + }); + + describe("React Router >= 8.0.0", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("does not add future flags to react-router.config.ts", async ({ + expect, + }) => { + const original = readFileSync(resolve("react-router.config.ts"), "utf-8"); + + const framework = createFramework("8.0.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + expect(content).not.toContain("v8_viteEnvironmentApi"); + expect(content).not.toContain("unstable_viteEnvironmentApi"); + expect(content).toBe(original); + }); + + it("does not add future flags for React Router 8.x", async ({ expect }) => { + const original = readFileSync(resolve("react-router.config.ts"), "utf-8"); + + const framework = createFramework("8.2.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + expect(content).not.toContain("v8_viteEnvironmentApi"); + expect(content).not.toContain("unstable_viteEnvironmentApi"); + expect(content).toBe(original); + }); + + it("uses middleware pattern by default for workers/app.ts", async ({ + expect, + }) => { + const framework = createFramework("8.0.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).not.toContain("AppLoadContext"); + expect(content).not.toContain("declare module"); + expect(content).toContain("async fetch(request)"); + expect(content).not.toContain("env, ctx"); + expect(content).toContain("requestHandler(request)"); + }); + + it("uses middleware pattern by default for entry.server.tsx", async ({ + expect, + }) => { + const framework = createFramework("8.0.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).not.toContain("AppLoadContext"); + expect(content).not.toContain("_loadContext"); + expect(content).toContain( + 'import type { EntryContext } from "react-router"' + ); + }); + }); + + describe("hasV8MiddlewareFlag with version >= 8.0.0", () => { + it("returns true without config file when version >= 8.0.0", ({ + expect, + }) => { + expect(hasV8MiddlewareFlag(process.cwd(), "8.0.0")).toBe(true); + }); + + it("returns true regardless of config content when version >= 8.0.0", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd(), "8.0.0")).toBe(true); + }); + + it("still parses config for versions < 8.0.0", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd(), "7.16.0")).toBe(false); + }); + }); + + describe("dry run", () => { + it("does not create files in dry run mode", async ({ expect }) => { + const framework = createFramework("7.16.0"); + const result = await framework.configure({ + ...getBaseOptions(), + dryRun: true, + }); + + expect(result.wranglerConfig).toEqual({ + main: "./workers/app.ts", + }); + expect(existsSync(resolve("workers/app.ts"))).toBe(false); + expect(existsSync(resolve("app/entry.server.tsx"))).toBe(false); + }); + }); + + describe("wrangler config", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("returns wrangler config with main pointing to workers/app.ts", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + const result = await framework.configure(getBaseOptions()); + + expect(result.wranglerConfig).toEqual({ + main: "./workers/app.ts", + }); + }); + }); + + describe("package installation", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("installs isbot as a dev dependency", async ({ expect }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + expect(cliPackages.installPackages).toHaveBeenCalledWith( + NpmPackageManager.type, + ["isbot"], + expect.objectContaining({ dev: true }) + ); + }); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts b/packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts new file mode 100644 index 0000000..225bbf6 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/utils/vite-plugin.test.ts @@ -0,0 +1,288 @@ +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { beforeEach, describe, it, vi } from "vitest"; +import { getInstalledPackageVersion } from "../../../src/frameworks/utils/packages"; +import { installCloudflareVitePlugin } from "../../../src/frameworks/utils/vite-plugin"; +import type { MockInstance } from "vitest"; + +vi.mock("../../../src/frameworks/utils/packages", () => ({ + getInstalledPackageVersion: vi.fn(), +})); + +describe("installCloudflareVitePlugin", () => { + let installSpy: MockInstance; + + beforeEach(() => { + installSpy = vi + .spyOn(cliPackages, "installPackages") + .mockImplementation(async () => {}); + }); + + describe("when Vite is not installed/detected", () => { + beforeEach(() => { + vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined); + }); + + it("installs only @cloudflare/vite-plugin", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledWith( + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + + it("does not attempt to upgrade Vite", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + }); + + describe("when Vite version is in [6.0.0, 6.1.0) range", () => { + it("upgrades Vite before installing the plugin for version 6.0.0", async ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(2); + expect(installSpy).toHaveBeenNthCalledWith( + 1, + "npm", + ["vite@^6.1.0"], + expect.objectContaining({ dev: true }) + ); + expect(installSpy).toHaveBeenNthCalledWith( + 2, + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + + it("upgrades Vite before installing the plugin for version 6.0.5", async ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.0.5"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(2); + expect(installSpy).toHaveBeenNthCalledWith( + 1, + "npm", + ["vite@^6.1.0"], + expect.objectContaining({ dev: true }) + ); + expect(installSpy).toHaveBeenNthCalledWith( + 2, + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + }); + + describe("when Vite version is >= 6.1.0", () => { + it("does not upgrade Vite for version 6.1.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.1.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledWith( + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + + it("does not upgrade Vite for version 6.2.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.2.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + + it("does not upgrade Vite for version 7.0.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("7.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + }); + + describe("when Vite version is < 6.0.0", () => { + it("does not upgrade Vite for version 5.4.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("5.4.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + + it("does not upgrade Vite for version 4.0.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("4.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledWith( + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + }); + + describe("parameter forwarding", () => { + beforeEach(() => { + vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined); + }); + + it("forwards isWorkspaceRoot to installPackages", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: true, + }); + + expect(installSpy).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ isWorkspaceRoot: true }) + ); + }); + + it("forwards isWorkspaceRoot when upgrading Vite", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: true, + }); + + // Both the Vite upgrade and plugin install should have isWorkspaceRoot: true + expect(installSpy).toHaveBeenNthCalledWith( + 1, + expect.anything(), + ["vite@^6.1.0"], + expect.objectContaining({ isWorkspaceRoot: true }) + ); + expect(installSpy).toHaveBeenNthCalledWith( + 2, + expect.anything(), + ["@cloudflare/vite-plugin"], + expect.objectContaining({ isWorkspaceRoot: true }) + ); + }); + + it("forwards packageManager to installPackages", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "pnpm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledWith( + "pnpm", + expect.anything(), + expect.anything() + ); + }); + + it("forwards different package managers correctly", async ({ expect }) => { + for (const pm of ["npm", "yarn", "pnpm", "bun"] as const) { + installSpy.mockClear(); + + await installCloudflareVitePlugin({ + packageManager: pm, + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledWith( + pm, + expect.anything(), + expect.anything() + ); + } + }); + + it("passes projectPath to getInstalledPackageVersion", async ({ + expect, + }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/custom/path", + isWorkspaceRoot: false, + }); + + expect(getInstalledPackageVersion).toHaveBeenCalledWith( + "vite", + "/custom/path" + ); + }); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts new file mode 100644 index 0000000..440aec9 --- /dev/null +++ b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts @@ -0,0 +1,149 @@ +import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it, vi } from "vitest"; +import { AutoConfigFrameworkConfigurationError } from "../../src/errors"; +import { Framework } from "../../src/frameworks/framework-class"; +import { getInstalledPackageVersion } from "../../src/frameworks/utils/packages"; +import { createMockContext } from "../helpers/mock-context"; +import type { AutoConfigFrameworkPackageInfo } from "../../src/frameworks"; +import type { + ConfigurationOptions, + ConfigurationResults, +} from "../../src/frameworks/framework-class"; + +vi.mock("../../src/frameworks/utils/packages"); + +/** Minimal concrete subclass so we can instantiate the abstract Framework */ +class TestFramework extends Framework { + configure(_options: ConfigurationOptions): ConfigurationResults { + return { wranglerConfig: null }; + } +} + +const PACKAGE_INFO: AutoConfigFrameworkPackageInfo = { + name: "some-pkg", + minimumVersion: "2.0.0", + maximumKnownMajorVersion: "4", +}; + +describe("Framework.validateFrameworkVersion()", () => { + const std = mockConsoleMethods(); + const context = createMockContext(); + + it("throws an AssertionError when the package version cannot be determined", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).toThrow("Unable to detect the version of the `some-pkg` package"); + }); + + it("throws AutoConfigFrameworkConfigurationError when installed version is below minimum", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("1.0.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).toThrow(AutoConfigFrameworkConfigurationError); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).toThrowErrorMatchingInlineSnapshot( + `[Error: The version of Test used in the project ("1.0.0") cannot be automatically configured. Please update the Test version to at least "2.0.0" and try again.]` + ); + }); + + it("does not throw and sets frameworkVersion when installed version equals minimumVersion", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("2.0.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).not.toThrow(); + expect(framework.frameworkVersion).toBe("2.0.0"); + expect(std.warn).toBe(""); + }); + + it("does not throw and sets frameworkVersion when installed version is within known range", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("3.0.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).not.toThrow(); + expect(framework.frameworkVersion).toBe("3.0.0"); + expect(std.warn).toBe(""); + }); + + it("does not throw and does not warn when installed version equals maximumKnownMajorVersion", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("4.0.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).not.toThrow(); + expect(framework.frameworkVersion).toBe("4.0.0"); + expect(std.warn).toBe(""); + }); + + it("does not throw and does not warn when installed version is a minor/patch update within the known major", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("4.5.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).not.toThrow(); + expect(framework.frameworkVersion).toBe("4.5.0"); + expect(std.warn).toBe(""); + }); + + it("does not throw nor warn when the installed version is an update within the known major", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("4.3.2"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).not.toThrow(); + expect(framework.frameworkVersion).toBe("4.3.2"); + expect(std.warn).toBe(""); + }); + + it("does not throw but warns when installed version exceeds maximumKnownMajorVersion", ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("5.0.0"); + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => + framework.validateFrameworkVersion("/project", PACKAGE_INFO, context) + ).not.toThrow(); + expect(framework.frameworkVersion).toBe("5.0.0"); + expect(std.warn).toContain('"5.0.0"'); + expect(std.warn).toContain("Test"); + expect(std.warn).toContain("is not officially supported"); + }); + + it("throws an AssertionError when frameworkVersion getter is accessed before validateFrameworkVersion is called", ({ + expect, + }) => { + const framework = new TestFramework({ id: "test", name: "Test" }); + + expect(() => framework.frameworkVersion).toThrow( + 'The version for "Test" is unexpectedly missing' + ); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/vike.test.ts b/packages/autoconfig/tests/frameworks/vike.test.ts new file mode 100644 index 0000000..2cdaf3f --- /dev/null +++ b/packages/autoconfig/tests/frameworks/vike.test.ts @@ -0,0 +1,326 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { Vike } from "../../src/frameworks/vike"; +import { createMockContext } from "../helpers/mock-context"; + +vi.mock("../../src/frameworks/utils/packages", () => ({ + isPackageInstalled: () => false, +})); + +vi.mock("../../src/frameworks/utils/vite-plugin", () => ({ + installCloudflareVitePlugin: async () => {}, +})); + +const context = createMockContext(); + +function getBaseOptions() { + return { + projectPath: process.cwd(), + workerName: "my-vike-app", + outputDir: "", + dryRun: false, + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + context, + }; +} + +describe("Vike framework configure()", () => { + runInTempDir(); + + beforeEach(() => { + vi.spyOn(cliPackages, "installPackages").mockImplementation(async () => {}); + }); + + describe("config file transformation", () => { + it("handles the old format: `export default { ... } satisfies Config`", async ({ + expect, + }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import type { Config } from "vike/types"; +import vikeReact from "vike-react/config"; + +// Default config (can be overridden by pages) +// https://vike.dev/config + +export default { + // https://vike.dev/head-tags + title: "My Vike App", + description: "Demo showcasing Vike", + + extends: [vikeReact], +} satisfies Config; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikeReact, vikePhoton]"); + }); + + it("handles the new format: `const config: Config = { ... }; export default config;`", async ({ + expect, + }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import type { Config } from "vike/types"; +import vikeReact from "vike-react/config"; + +// Default config (can be overridden by pages) +// https://vike.dev/config + +const config: Config = { + // https://vike.dev/head-tags + title: "My Vike App", + description: "Demo showcasing Vike", + + extends: [vikeReact], +}; + +export default config; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikeReact, vikePhoton]"); + }); + + it("handles plain format: `export default { ... }`", async ({ expect }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import vikeReact from "vike-react/config"; + +export default { + title: "My Vike App", + extends: [vikeReact], +}; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikeReact, vikePhoton]"); + }); + + it("handles `export default { ... } as Config` format", async ({ + expect, + }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import type { Config } from "vike/types"; +import vikeReact from "vike-react/config"; + +export default { + title: "My Vike App", + extends: [vikeReact], +} as Config; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikeReact, vikePhoton]"); + }); + + it("creates extends array if not present", async ({ expect }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import vikeReact from "vike-react/config"; + +export default { + title: "My Vike App", +}; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikePhoton]"); + }); + + it("creates extends array on variable-referenced config if not present", async ({ + expect, + }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import type { Config } from "vike/types"; + +const config: Config = { + title: "My Vike App", +}; + +export default config; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikePhoton]"); + }); + + it("does not duplicate import or extends entry on repeated calls", async ({ + expect, + }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import vikeReact from "vike-react/config"; +import vikePhoton from "vike-photon/config"; + +export default { + title: "My Vike App", + extends: [vikeReact, vikePhoton], +}; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + + const importCount = ( + result.match(/import vikePhoton from "vike-photon\/config"/g) || [] + ).length; + expect(importCount).toBe(1); + + // Verify the extends array wasn't duplicated either + expect(result).toContain("extends: [vikeReact, vikePhoton]"); + }); + + it("does not duplicate extends entry when vike-photon is imported under a different name", async ({ + expect, + }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import vikeReact from "vike-react/config"; +import photon from "vike-photon/config"; + +export default { + title: "My Vike App", + extends: [vikeReact, photon], +}; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + + // Should not add a second import + const importCount = (result.match(/from "vike-photon\/config"/g) || []) + .length; + expect(importCount).toBe(1); + + // Should not duplicate the extends entry under any name + expect(result).toContain("extends: [vikeReact, photon]"); + expect(result).not.toContain("vikePhoton"); + }); + + it("uses .js config file as fallback", async ({ expect }) => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.js"), + `import vikeReact from "vike-react/config"; + +export default { + title: "My Vike App", + extends: [vikeReact], +}; +` + ); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure(getBaseOptions()); + + const result = await readFile(resolve("pages/+config.js"), "utf8"); + expect(result).toContain('import vikePhoton from "vike-photon/config";'); + expect(result).toContain("extends: [vikeReact, vikePhoton]"); + }); + }); + + describe("configure() return value", () => { + beforeEach(async () => { + await mkdir(resolve("pages"), { recursive: true }); + await writeFile( + resolve("pages/+config.ts"), + `import vikeReact from "vike-react/config"; + +export default { + extends: [vikeReact], +}; +` + ); + }); + + it("returns correct wranglerConfig", async ({ expect }) => { + const framework = new Vike({ id: "vike", name: "Vike" }); + const result = await framework.configure(getBaseOptions()); + + expect(result.wranglerConfig).toEqual({ + main: "virtual:photon:cloudflare:server-entry", + }); + }); + + it("returns correct packageJsonScriptsOverrides", async ({ expect }) => { + const framework = new Vike({ id: "vike", name: "Vike" }); + const result = await framework.configure(getBaseOptions()); + + expect(result.packageJsonScriptsOverrides).toEqual({ + preview: "vike build && vike preview", + deploy: "vike build && wrangler deploy", + }); + }); + }); + + describe("dryRun mode", () => { + it("does not modify config file in dryRun mode", async ({ expect }) => { + await mkdir(resolve("pages"), { recursive: true }); + const originalContent = `import vikeReact from "vike-react/config"; + +export default { + extends: [vikeReact], +}; +`; + await writeFile(resolve("pages/+config.ts"), originalContent); + + const framework = new Vike({ id: "vike", name: "Vike" }); + await framework.configure({ ...getBaseOptions(), dryRun: true }); + + const result = await readFile(resolve("pages/+config.ts"), "utf8"); + expect(result).toBe(originalContent); + }); + }); +}); diff --git a/packages/autoconfig/tests/frameworks/vite.test.ts b/packages/autoconfig/tests/frameworks/vite.test.ts new file mode 100644 index 0000000..68757ca --- /dev/null +++ b/packages/autoconfig/tests/frameworks/vite.test.ts @@ -0,0 +1,111 @@ +import { existsSync, readFileSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { Vite } from "../../src/frameworks/vite"; +import { createMockContext } from "../helpers/mock-context"; + +const context = createMockContext(); + +const BASE_OPTIONS = { + projectPath: ".", + workerName: "my-vite-app", + outputDir: "dist", + dryRun: false, + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + context, +}; + +describe("Vite framework", () => { + runInTempDir(); + + beforeEach(() => { + vi.spyOn(cliPackages, "installPackages").mockImplementation(async () => {}); + }); + + describe("isConfigured()", () => { + it("returns false when no vite config file exists", ({ expect }) => { + const framework = new Vite({ id: "vite", name: "Vite" }); + expect(framework.isConfigured(".")).toBe(false); + }); + }); + + describe("configure()", () => { + it("creates a vite config with the cloudflare plugin when no config file exists", async ({ + expect, + }) => { + const framework = new Vite({ id: "vite", name: "Vite" }); + const result = await framework.configure(BASE_OPTIONS); + + expect(existsSync("vite.config.js")).toBe(true); + const content = readFileSync("vite.config.js", "utf-8"); + expect(content).toContain( + 'import { cloudflare } from "@cloudflare/vite-plugin"' + ); + expect(content).toContain("plugins: [cloudflare()]"); + + expect(result.wranglerConfig).toEqual({ + assets: { + not_found_handling: "single-page-application", + }, + }); + }); + + it("uses .ts extension when the project has a tsconfig.json", async ({ + expect, + }) => { + await writeFile("tsconfig.json", "{}"); + + const framework = new Vite({ id: "vite", name: "Vite" }); + await framework.configure(BASE_OPTIONS); + + expect(existsSync("vite.config.ts")).toBe(true); + expect(existsSync("vite.config.js")).toBe(false); + }); + + it("transforms existing vite config instead of creating a new one", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()] +}); +` + ); + + const framework = new Vite({ id: "vite", name: "Vite" }); + await framework.configure(BASE_OPTIONS); + + const content = readFileSync("vite.config.ts", "utf-8"); + expect(content).toContain("cloudflare()"); + // Existing plugins should be preserved + expect(content).toContain("react()"); + }); + + it("does not create or modify files in dry-run mode", async ({ + expect, + }) => { + const framework = new Vite({ id: "vite", name: "Vite" }); + const result = await framework.configure({ + ...BASE_OPTIONS, + dryRun: true, + }); + + expect(existsSync("vite.config.ts")).toBe(false); + expect(existsSync("vite.config.js")).toBe(false); + expect(result.wranglerConfig).toEqual({ + assets: { + not_found_handling: "single-page-application", + }, + }); + }); + }); +}); diff --git a/packages/autoconfig/tests/get-installed-package-version.test.ts b/packages/autoconfig/tests/get-installed-package-version.test.ts new file mode 100644 index 0000000..533374d --- /dev/null +++ b/packages/autoconfig/tests/get-installed-package-version.test.ts @@ -0,0 +1,81 @@ +import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; +import { describe, test } from "vitest"; +import { getInstalledPackageVersion } from "../src/frameworks/utils/packages"; + +describe("getInstalledPackageVersion()", () => { + runInTempDir(); + test("happy path", async ({ expect }) => { + await seed({ + "node_modules/react-router/package.json": JSON.stringify({ + name: "react-router", + version: "1.2.3", + main: "index.js", + }), + "node_modules/react-router/index.js": "console.log(1)", + }); + expect(getInstalledPackageVersion("react-router", process.cwd())).toBe( + "1.2.3" + ); + }); + + test("no node_modules", async ({ expect }) => { + expect( + getInstalledPackageVersion("react-router", process.cwd()) + ).toBeUndefined(); + }); + + test("aliased package returns the bundled version of the requested package", async ({ + expect, + }) => { + // vite+ installs `@voidzero-dev/vite-plus-core` under the `vite` alias, so + // the resolved package.json has a different `name` and its own `version`. + await seed({ + "node_modules/vite/package.json": JSON.stringify({ + name: "@voidzero-dev/vite-plus-core", + version: "0.2.2", + bundledVersions: { + vite: "8.1.2", + rolldown: "1.0.0", + tsdown: "0.15.0", + }, + main: "index.js", + }), + "node_modules/vite/index.js": "console.log(1)", + }); + expect(getInstalledPackageVersion("vite", process.cwd())).toBe("8.1.2"); + }); + + test("aliased package without a matching bundled version falls back to its own version", async ({ + expect, + }) => { + await seed({ + "node_modules/vite/package.json": JSON.stringify({ + name: "@voidzero-dev/vite-plus-core", + version: "0.2.2", + bundledVersions: { + rolldown: "1.0.0", + }, + main: "index.js", + }), + "node_modules/vite/index.js": "console.log(1)", + }); + expect(getInstalledPackageVersion("vite", process.cwd())).toBe("0.2.2"); + }); + + test("package whose name matches ignores bundledVersions and returns its own version", async ({ + expect, + }) => { + await seed({ + "node_modules/vite/package.json": JSON.stringify({ + name: "vite", + version: "6.3.0", + bundledVersions: { + vite: "8.1.2", + }, + main: "index.js", + }), + "node_modules/vite/index.js": "console.log(1)", + }); + expect(getInstalledPackageVersion("vite", process.cwd())).toBe("6.3.0"); + }); +}); diff --git a/packages/autoconfig/tests/helpers/mock-context.ts b/packages/autoconfig/tests/helpers/mock-context.ts new file mode 100644 index 0000000..98d8d3f --- /dev/null +++ b/packages/autoconfig/tests/helpers/mock-context.ts @@ -0,0 +1,34 @@ +import { vi } from "vitest"; +import type { AutoConfigContext } from "../../src/context"; + +/** + * Creates a mock `AutoConfigContext` suitable for testing. + * All dialog methods default to returning sensible values. + * The logger delegates to `console` so that `mockConsoleMethods()` captures + * the output and tests can assert on `std.out`, `std.warn`, etc. + * + * @param overrides - Partial overrides to customize the context. + * @returns A fully mocked `AutoConfigContext`. + */ +export function createMockContext( + overrides?: Partial +): AutoConfigContext { + return { + logger: { + log: vi.fn((...args: unknown[]) => console.log(...args)), + info: vi.fn((...args: unknown[]) => console.info(...args)), + warn: vi.fn((...args: unknown[]) => console.warn(...args)), + debug: vi.fn((...args: unknown[]) => console.debug(...args)), + error: vi.fn((...args: unknown[]) => console.error(...args)), + }, + dialogs: { + confirm: vi.fn().mockResolvedValue(true), + prompt: vi.fn().mockResolvedValue(""), + select: vi.fn().mockResolvedValue(""), + }, + runCommand: vi.fn(), + isNonInteractiveOrCI: () => false, + getCacheFolder: () => undefined, + ...overrides, + }; +} diff --git a/packages/autoconfig/tests/run-summary.test.ts b/packages/autoconfig/tests/run-summary.test.ts new file mode 100644 index 0000000..8df4ca3 --- /dev/null +++ b/packages/autoconfig/tests/run-summary.test.ts @@ -0,0 +1,250 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers"; +import { dedent } from "ts-dedent"; +import { describe, test } from "vitest"; +import { Astro } from "../src/frameworks/astro"; +import { Static } from "../src/frameworks/static"; +import { buildOperationsSummary } from "../src/run"; +import { createMockContext } from "./helpers/mock-context"; +import type { RawConfig } from "@cloudflare/workers-utils"; + +const testRawConfig: RawConfig = { + $schema: "node_modules/wrangler/config-schema.json", + name: "worker-name", + compatibility_date: "2025-01-01", + observability: { + enabled: true, + }, +}; + +describe("autoconfig run - buildOperationsSummary()", () => { + const std = mockConsoleMethods(); + const context = createMockContext(); + + describe("interactive mode", () => { + test("presents a summary for a simple project where only a wrangler.jsonc file needs to be created", async ({ + expect, + }) => { + const summary = await buildOperationsSummary( + { + workerName: "worker-name", + projectPath: "", + configured: false, + outputDir: "public", + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + testRawConfig, + { + build: "npm run build", + deploy: "npx wrangler deploy", + version: "npx wrangler versions upload", + }, + context + ); + + expect(std.out).toMatchInlineSnapshot(` + " + 📄 Create wrangler.jsonc: + { + "$schema": "node_modules/wrangler/config-schema.json", + "name": "worker-name", + "compatibility_date": "2025-01-01", + "observability": { + "enabled": true + } + } + " + `); + + expect(summary).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "deployCommand": "npx wrangler deploy", + "frameworkId": "static", + "outputDir": "public", + "scripts": {}, + "versionCommand": "npx wrangler versions upload", + "wranglerConfig": { + "$schema": "node_modules/wrangler/config-schema.json", + "compatibility_date": "2025-01-01", + "name": "worker-name", + "observability": { + "enabled": true, + }, + }, + "wranglerInstall": false, + } + `); + }); + + test("shows that wrangler will be added as a devDependency when not already installed as such", async ({ + expect, + }) => { + const summary = await buildOperationsSummary( + { + workerName: "worker-name", + projectPath: "", + packageJson: { + name: "my-project", + devDependencies: {}, + }, + configured: false, + outputDir: "dist", + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + testRawConfig, + { + build: "npm run build", + deploy: "npx wrangler deploy", + version: "npx wrangler versions upload", + }, + context + ); + + expect(std.out).toContain( + dedent` + 📦 Install packages: + - wrangler (devDependency) + ` + ); + + expect(summary).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "deployCommand": "npx wrangler deploy", + "frameworkId": "static", + "outputDir": "dist", + "scripts": { + "deploy": "wrangler deploy", + "preview": "wrangler dev", + }, + "versionCommand": "npx wrangler versions upload", + "wranglerConfig": { + "$schema": "node_modules/wrangler/config-schema.json", + "compatibility_date": "2025-01-01", + "name": "worker-name", + "observability": { + "enabled": true, + }, + }, + "wranglerInstall": true, + } + `); + }); + + test("when a package.json is present wrangler@latest will be unconditionally installed (even if already present)", async ({ + expect, + }) => { + const summary = await buildOperationsSummary( + { + workerName: "worker-name", + projectPath: "", + packageJson: { + name: "my-project", + devDependencies: { + wrangler: "^4.0.0", + }, + }, + configured: false, + outputDir: "out", + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + testRawConfig, + { + build: "npm run build", + deploy: "npx wrangler deploy", + version: "npx wrangler versions upload", + }, + context + ); + + expect(std.out).toContain( + dedent` + 📦 Install packages: + - wrangler (devDependency) + ` + ); + + expect(summary).toMatchInlineSnapshot(` + { + "buildCommand": "npm run build", + "deployCommand": "npx wrangler deploy", + "frameworkId": "static", + "outputDir": "out", + "scripts": { + "deploy": "wrangler deploy", + "preview": "wrangler dev", + }, + "versionCommand": "npx wrangler versions upload", + "wranglerConfig": { + "$schema": "node_modules/wrangler/config-schema.json", + "compatibility_date": "2025-01-01", + "name": "worker-name", + "observability": { + "enabled": true, + }, + }, + "wranglerInstall": true, + } + `); + }); + + test("shows that when needed a framework specific configuration will be run", async ({ + expect, + }) => { + const summary = await buildOperationsSummary( + { + workerName: "worker-name", + projectPath: "", + framework: new Astro({ id: "astro", name: "Astro" }), + configured: false, + outputDir: "dist", + packageManager: NpmPackageManager, + }, + testRawConfig, + { + build: "npm run build", + deploy: "npx wrangler deploy", + }, + context + ); + + expect(std.out).toContain( + '🛠️ Configuring project for Astro with "astro add cloudflare"' + ); + + expect(summary.frameworkConfiguration).toBe( + 'Configuring project for Astro with "astro add cloudflare"' + ); + + expect(summary.frameworkId).toBe("astro"); + }); + + test("doesn't show the framework specific configuration step for the Static framework", async ({ + expect, + }) => { + const summary = await buildOperationsSummary( + { + workerName: "worker-name", + projectPath: "", + framework: new Static({ id: "static", name: "Static" }), + configured: false, + outputDir: "public", + packageManager: NpmPackageManager, + }, + testRawConfig, + { + build: "npm run build", + deploy: "npx wrangler deploy", + }, + context + ); + + expect(std.out).not.toContain("🛠️ Configuring project for"); + expect(summary.frameworkConfiguration).toBeUndefined(); + }); + }); +}); diff --git a/packages/autoconfig/tests/tsconfig.json b/packages/autoconfig/tests/tsconfig.json new file mode 100644 index 0000000..ca97bed --- /dev/null +++ b/packages/autoconfig/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "preserve", + "types": ["node"] + }, + "include": ["../*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"] +} diff --git a/packages/autoconfig/tests/vite-config.test.ts b/packages/autoconfig/tests/vite-config.test.ts new file mode 100644 index 0000000..84ff3b9 --- /dev/null +++ b/packages/autoconfig/tests/vite-config.test.ts @@ -0,0 +1,304 @@ +import { readFileSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { + mockConsoleMethods, + runInTempDir, +} from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { + checkIfViteConfigUsesCloudflarePlugin, + transformViteConfig, +} from "../src/frameworks/utils/vite-config"; +import { createMockContext } from "./helpers/mock-context"; + +describe("vite-config utils", () => { + runInTempDir(); + const std = mockConsoleMethods(); + const context = createMockContext(); + + describe("checkIfViteConfigUsesCloudflarePlugin", () => { + it("should detect cloudflare plugin in function-based defineConfig", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; +import { cloudflare } from '@cloudflare/vite-plugin'; + +export default defineConfig(() => ({ + plugins: [cloudflare()] +})); +` + ); + + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); + expect(result).toBe(true); + }); + + it("should return false for function-based defineConfig without cloudflare plugin", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + plugins: [] +})); +` + ); + + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); + expect(result).toBe(false); + }); + + it("should handle vite config without plugins array", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { port: 3000 } +}); +` + ); + + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); + expect(result).toBe(false); + expect(std.debug).toContain( + "Vite config does not have a valid plugins array" + ); + }); + + it("should detect cloudflare plugin correctly", async ({ expect }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; +import { cloudflare } from '@cloudflare/vite-plugin'; + +export default defineConfig({ + plugins: [cloudflare()] +}); +` + ); + + const result = checkIfViteConfigUsesCloudflarePlugin(".", context); + expect(result).toBe(true); + }); + }); + + describe("transformViteConfig", () => { + it("should successfully transform function-based defineConfig with arrow expression body", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + plugins: [] +})); +` + ); + + expect(() => transformViteConfig(".")).not.toThrow(); + const result = readFileSync("vite.config.ts", "utf-8"); + expect(result).toContain( + 'import { cloudflare } from "@cloudflare/vite-plugin"' + ); + expect(result).toContain("cloudflare()"); + }); + + it("should successfully transform function-based defineConfig with destructured params", async ({ + expect, + }) => { + // This is the exact pattern from the React Router node-postgres template + await writeFile( + "vite.config.ts", + ` +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ isSsrBuild }) => ({ + build: { + rollupOptions: isSsrBuild + ? { + input: "./server/app.ts", + } + : undefined, + }, + plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], +})); +` + ); + + expect(() => transformViteConfig(".")).not.toThrow(); + const result = readFileSync("vite.config.ts", "utf-8"); + expect(result).toContain( + 'import { cloudflare } from "@cloudflare/vite-plugin"' + ); + expect(result).toContain("cloudflare()"); + // The existing plugins should be preserved + expect(result).toContain("tailwindcss()"); + expect(result).toContain("reactRouter()"); + expect(result).toContain("tsconfigPaths()"); + // The function structure should be preserved + expect(result).toContain("isSsrBuild"); + }); + + it("should successfully transform function-based defineConfig with block body", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(() => { + return { + plugins: [] + }; +}); +` + ); + + expect(() => transformViteConfig(".")).not.toThrow(); + const result = readFileSync("vite.config.ts", "utf-8"); + expect(result).toContain( + 'import { cloudflare } from "@cloudflare/vite-plugin"' + ); + expect(result).toContain("cloudflare()"); + }); + + it("should successfully transform function expression defineConfig", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(function() { + return { + plugins: [] + }; +}); +` + ); + + expect(() => transformViteConfig(".")).not.toThrow(); + const result = readFileSync("vite.config.ts", "utf-8"); + expect(result).toContain( + 'import { cloudflare } from "@cloudflare/vite-plugin"' + ); + expect(result).toContain("cloudflare()"); + }); + + it("should pass viteEnvironmentName option with function-based config", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + plugins: [] +})); +` + ); + + expect(() => + transformViteConfig(".", { viteEnvironmentName: "ssr" }) + ).not.toThrow(); + const result = readFileSync("vite.config.ts", "utf-8"); + expect(result).toContain("viteEnvironment"); + expect(result).toContain('"ssr"'); + }); + + it("should remove incompatible plugins from function-based config", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + plugins: [nitro(), someOther()] +})); +` + ); + + expect(() => transformViteConfig(".")).not.toThrow(); + const result = readFileSync("vite.config.ts", "utf-8"); + expect(result).not.toContain("nitro()"); + expect(result).toContain("someOther()"); + expect(result).toContain("cloudflare()"); + }); + + it("should throw UserError when plugins array is missing", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { port: 3000 } +}); +` + ); + + expect(() => transformViteConfig(".")).toThrow( + /Cannot modify Vite config: could not find a valid plugins array/ + ); + }); + + it("should throw UserError when plugins array is missing in function-based config", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + server: { port: 3000 } +})); +` + ); + + expect(() => transformViteConfig(".")).toThrow( + /Cannot modify Vite config: could not find a valid plugins array/ + ); + }); + + it("should successfully transform valid vite config", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + ` +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [] +}); +` + ); + + expect(() => transformViteConfig(".")).not.toThrow(); + }); + }); +}); diff --git a/packages/autoconfig/tsconfig.json b/packages/autoconfig/tsconfig.json new file mode 100644 index 0000000..533933f --- /dev/null +++ b/packages/autoconfig/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["dist", "node_modules", "**/__tests__", "**/*.test.ts", "tests"] +} diff --git a/packages/autoconfig/tsup.config.ts b/packages/autoconfig/tsup.config.ts new file mode 100644 index 0000000..9a4c974 --- /dev/null +++ b/packages/autoconfig/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; +import { EXTERNAL_DEPENDENCIES } from "./scripts/deps"; + +export default defineConfig(() => [ + { + treeshake: true, + keepNames: true, + entry: ["src/index.ts"], + platform: "node", + format: "esm", + dts: true, + outDir: "dist", + tsconfig: "tsconfig.json", + metafile: true, + sourcemap: process.env.SOURCEMAPS !== "false", + define: { + "process.env.NODE_ENV": `'${"production"}'`, + }, + external: EXTERNAL_DEPENDENCIES, + }, +]); diff --git a/packages/autoconfig/turbo.json b/packages/autoconfig/turbo.json new file mode 100644 index 0000000..691f611 --- /dev/null +++ b/packages/autoconfig/turbo.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "!**/tests/**"], + "outputs": ["dist/**"], + "env": ["SOURCEMAPS"], + "passThroughEnv": ["PWD"] + }, + "test:ci": { + "dependsOn": ["build"], + "env": ["LC_ALL", "TZ"] + } + } +} diff --git a/packages/autoconfig/vitest.config.mts b/packages/autoconfig/vitest.config.mts new file mode 100644 index 0000000..407d414 --- /dev/null +++ b/packages/autoconfig/vitest.config.mts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 15_000, + pool: "forks", + include: ["**/tests/**/*.test.ts"], + reporters: ["default"], + unstubEnvs: true, + mockReset: true, + }, +}); diff --git a/packages/chrome-devtools-patches/CHANGELOG.md b/packages/chrome-devtools-patches/CHANGELOG.md new file mode 100644 index 0000000..2dfddb4 --- /dev/null +++ b/packages/chrome-devtools-patches/CHANGELOG.md @@ -0,0 +1,50 @@ +# @cloudflare/chrome-devtools-patches + +## 0.1.5 + +### Patch Changes + +- [#12928](https://github.com/cloudflare/workers-sdk/pull/12928) [`81ee98e`](https://github.com/cloudflare/workers-sdk/commit/81ee98e6a0c6be879757289ef6e34e1559d6ee2a) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Migrate chrome-devtools-patches deployment from Cloudflare Pages to Workers + Assets + + The DevTools frontend is now deployed as a Cloudflare Workers + Assets project instead of a Cloudflare Pages project. This uses `wrangler deploy` for production deployments and `wrangler versions upload` for PR preview deployments. + + The inspector proxy origin allowlists in both wrangler and miniflare have been updated to accept connections from the new `workers.dev` domain patterns, while retaining the legacy `pages.dev` patterns for backward compatibility. + +## 0.1.4 + +### Patch Changes + +- [#9649](https://github.com/cloudflare/workers-sdk/pull/9649) [`ec9b417`](https://github.com/cloudflare/workers-sdk/commit/ec9b417f8ed711e7b5044410e83d781f123a6a62) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - patch release to trigger a test release + +## 0.1.3 + +### Patch Changes + +- [#9551](https://github.com/cloudflare/workers-sdk/pull/9551) [`b0b59e0`](https://github.com/cloudflare/workers-sdk/commit/b0b59e071393e13e3770fd59fb0cb26136e88272) Thanks [@penalosa](https://github.com/penalosa)! - Update Devtools Patches + +## 0.1.2 + +### Patch Changes + +- [#7693](https://github.com/cloudflare/workers-sdk/pull/7693) [`65a3e35`](https://github.com/cloudflare/workers-sdk/commit/65a3e3590aff2f287c669172856512d6b29bd37f) Thanks [@emily-shen](https://github.com/emily-shen)! - chore: rebases patches on latest devtools head + +## 0.1.1 + +### Patch Changes + +- [#7258](https://github.com/cloudflare/workers-sdk/pull/7258) [`f1f508e`](https://github.com/cloudflare/workers-sdk/commit/f1f508ec1acefd7b409b77ae1070029953cca061) Thanks [@andyjessop](https://github.com/andyjessop)! - change package name from @cloudflare/wrangler-devtools to @cloudflare/chrome-devtools-patches + +## 0.1.0 + +### Minor Changes + +- [#7137](https://github.com/cloudflare/workers-sdk/pull/7137) [`1b195bd`](https://github.com/cloudflare/workers-sdk/commit/1b195bd09aef282a8a205d341579cdb7e3755d89) Thanks [@andyjessop](https://github.com/andyjessop)! - feat: update devtools patches for release + - rebases patches on top of latest devtools head + - removes CPU profiling tab + - adds performance tab + +## 0.0.1 + +### Patch Changes + +- [#4738](https://github.com/cloudflare/workers-sdk/pull/4738) [`a9f36ef`](https://github.com/cloudflare/workers-sdk/commit/a9f36ef01c6080352934cfd31c4987c5e197f8b5) Thanks [@neilsh](https://github.com/neilsh)! - fix: Fix typo in CSP diff --git a/packages/chrome-devtools-patches/Makefile b/packages/chrome-devtools-patches/Makefile new file mode 100644 index 0000000..95d3741 --- /dev/null +++ b/packages/chrome-devtools-patches/Makefile @@ -0,0 +1,38 @@ +ROOT = $(realpath .) +PATH_WITH_DEPOT = $(PATH):$(ROOT)/depot/ +# The upstream devtools commit upon which our patches are based +HEAD = b66ecf5bef3ae6c769dbc6d904ce1c54d3771227 +PATCHES = $(shell ls ${PWD}/patches/*.patch) +depot: + git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git depot + +devtools-frontend: depot + git clone https://chromium.googlesource.com/devtools/devtools-frontend + # Ensure other depot_tools can be called from gclient config + PATH="$(PATH_WITH_DEPOT)" $(ROOT)/depot/gclient config https://chromium.googlesource.com/devtools/devtools-frontend --unmanaged + git -C devtools-frontend checkout $(HEAD) + git -C devtools-frontend config user.email "workers-devprod@cloudflare.com" + git -C devtools-frontend config user.name "Workers DevProd" + git -C devtools-frontend am $(PATCHES) + +devtools-frontend/out/Default/gen/front_end: devtools-frontend + cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" $(ROOT)/depot/gclient sync + cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" $(ROOT)/depot/gn gen out/Default + cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" $(ROOT)/depot/autoninja -C out/Default + +publish: cleanup devtools-frontend/out/Default/gen/front_end + npx wrangler deploy + +publish-preview: cleanup devtools-frontend/out/Default/gen/front_end + npx wrangler versions upload + +cleanup: + rm -rf devtools-frontend .gclient* .cipd node_modules depot + +test: + git -C devtools-frontend am $(PATCHES) + +dev: + cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" npm run install-deps + cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" gn gen out/Default --args="devtools_skip_typecheck=true" + cd devtools-frontend && PATH="$(PATH_WITH_DEPOT)" npm run watch diff --git a/packages/chrome-devtools-patches/README.md b/packages/chrome-devtools-patches/README.md new file mode 100644 index 0000000..6f12095 --- /dev/null +++ b/packages/chrome-devtools-patches/README.md @@ -0,0 +1,90 @@ +# Workers Devtools + +This package contains a Workers specific version of Chrome Devtools that is used by the Wrangler dev command and other applications. It is a customized fork of Chrome DevTools specifically tailored for debugging Cloudflare Workers. This package provides Worker-specific functionality through carefully maintained patches on top of Chrome DevTools. + +## Overview + +This package is used across multiple Cloudflare products: + +- Workers Playground (`workers-playground`) +- Quick Editor (`@cloudflare/quick-edit`) +- Wrangler CLI via the `InspectorProxy` + +## Features + +Our customized DevTools implementation provides: + +- Source code viewing and live updates +- Network request inspection +- Worker-specific UI optimizations + +## Development + +We maintain this fork by applying patches on top of Chrome DevTools. These patches need to be periodically rebased as Chrome DevTools evolves. + +**Key Development Tasks:** + +- Generating patches from our customizations +- Rebasing patches onto new Chrome DevTools versions +- Testing functionality across all integration points + +## Updating DevTools + +We perform quarterly updates to stay current with upstream Chrome DevTools. The update process involves: + +1. Cloning the devtools-frontend repo +2. Applying our existing patches +3. Rebasing onto the latest Chrome DevTools +4. Regenerating patches +5. Thorough testing across all integration points + +**For detailed instructions on updating DevTools, please refer to our internal documentation on keeping devtools up-to-date.** + +## Testing + +Two methods are available for testing updates: + +**Local Development:** + +- Build and serve DevTools locally +- Test against local Playground instance +- Make targeted fixes as needed + +**Preview Builds:** + +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. + +Once the preview is built, you will see a comment on the PR with a link to the live URL. + +## Acceptance Criteria + +Our DevTools implementation must maintain full functionality across: + +- Console operations (logging, errors, filters) +- Source code viewing and debugging +- Network request inspection +- Worker-specific UI customizations + +## Contributing + +When making changes: + +- Keep patches minimal and targeted +- Prefer CSS-based UI modifications +- Test thoroughly across all integration points +- Document any new patches or modifications + +## Deployment + +This package is deployed as a Cloudflare Workers + Assets project. The static DevTools frontend is served directly from Workers Assets, configured via `wrangler.jsonc`. + +Deployments are managed by GitHub Actions: + +- deploy-previews.yml: + - Runs on any PR that has the `preview:chrome-devtools-patches` label. + - Uploads a preview version (without activating it in production) via `wrangler versions upload`. + - The preview URL is posted as a comment on the PR. +- changesets.yml: + - Runs when a "Version Packages" PR, containing a changeset that touches this package, is merged to `main`. + - Deploys this package to production via `wrangler deploy`. + - Production is accessible via the custom domain [https://devtools.devprod.cloudflare.dev/]. diff --git a/packages/chrome-devtools-patches/package.json b/packages/chrome-devtools-patches/package.json new file mode 100644 index 0000000..ee54c86 --- /dev/null +++ b/packages/chrome-devtools-patches/package.json @@ -0,0 +1,26 @@ +{ + "name": "@cloudflare/chrome-devtools-patches", + "version": "0.1.5", + "private": true, + "description": "Chrome Devtools hosted for easy use with Workers tooling and applications (Wrangler, Playground, Quick Editor).", + "homepage": "https://github.com/cloudflare/workers-sdk#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "BSD-3-Clause", + "author": "workers-devprod@cloudflare.com", + "scripts": { + "deploy": "CLOUDFLARE_ACCOUNT_ID=e35fd947284363a46fd7061634477114 make publish", + "deploy:preview": "CLOUDFLARE_ACCOUNT_ID=e35fd947284363a46fd7061634477114 make publish-preview", + "testenv": "make testenv" + }, + "devDependencies": { + "wrangler": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "deploy": true + } +} diff --git a/packages/chrome-devtools-patches/patches/0001-Expand-Browser-support-make-it-work-in-Firefox-Safar.patch b/packages/chrome-devtools-patches/patches/0001-Expand-Browser-support-make-it-work-in-Firefox-Safar.patch new file mode 100644 index 0000000..14e0ecc --- /dev/null +++ b/packages/chrome-devtools-patches/patches/0001-Expand-Browser-support-make-it-work-in-Firefox-Safar.patch @@ -0,0 +1,128 @@ +From 261ba678389218354cb8f9dce78483cf4f09716f Mon Sep 17 00:00:00 2001 +From: Workers DevProd +Date: Fri, 25 Oct 2024 14:15:43 +0100 +Subject: [PATCH 1/8] Expand Browser support (make it work in Firefox & Safari) + +* Basic support for text colour in dark mode in browsers + that don't implement :host-context + +* A few firefox-specific patches (with detailed inline comments) + +* A (vendored) custom elements polyfill for Safari + +Acceptance criteria: +If updating the commit of devtools upon which these patches are based, make sure this patch: +* Displays console.log() output with sufficient contrast in dark mode +* Allows devtools to load in Safari (loading with _any_ UI is sufficent to check whether this has worked) +* Supports viewing a list of requests in the network panel in Firefox (without the patches here this goes very obviously wrong) +--- + front_end/core/dom_extension/DOMExtension.ts | 2 +- + front_end/entrypoint_template.html | 38 ++++++++++++++++++- + front_end/entrypoints/js_app/js_app.ts | 3 ++ + .../legacy/components/data_grid/DataGrid.ts | 5 +++ + 4 files changed, 46 insertions(+), 2 deletions(-) + +diff --git a/front_end/core/dom_extension/DOMExtension.ts b/front_end/core/dom_extension/DOMExtension.ts +index 202c680a30..a639437998 100644 +--- a/front_end/core/dom_extension/DOMExtension.ts ++++ b/front_end/core/dom_extension/DOMExtension.ts +@@ -133,7 +133,7 @@ Node.prototype.getComponentSelection = function(): Selection|null { + while (parent && parent.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { + parent = parent.parentNode; + } +- return parent instanceof ShadowRoot ? parent.getSelection() : this.window().getSelection(); ++ return parent instanceof ShadowRoot ? (parent?.getSelection?.() ?? this.window().getSelection()) : this.window().getSelection(); + }; + + Node.prototype.hasSelection = function(): boolean { +diff --git a/front_end/entrypoint_template.html b/front_end/entrypoint_template.html +index 64d55d4c14..07972be81f 100644 +--- a/front_end/entrypoint_template.html ++++ b/front_end/entrypoint_template.html +@@ -13,9 +13,45 @@ + background-color: rgb(41 42 45); + } + } ++ ++ .platform-mac { ++ --monospace-font-size: 11px; ++ --monospace-font-family: menlo, monospace; ++ --source-code-font-size: 11px; ++ --source-code-font-family: menlo, monospace; ++ } ++ ++ .platform-windows { ++ --monospace-font-size: 12px; ++ --monospace-font-family: consolas, lucida console, courier new, monospace; ++ --source-code-font-size: 12px; ++ --source-code-font-family: consolas, lucida console, courier new, monospace; ++ } ++ ++ .platform-linux { ++ --monospace-font-size: 11px; ++ --monospace-font-family: dejavu sans mono, monospace; ++ --source-code-font-size: 11px; ++ --source-code-font-family: dejavu sans mono, monospace; ++ } ++ ++ .-theme-with-dark-background .platform-linux, ++ .-theme-with-dark-background .platform-mac { ++ --override-text-color: rgb(189 198 207); ++ } ++ ++ .cm-editor + .cm-editor { ++ display: 'none', ++ } + +- ++ + ++ + + + +diff --git a/front_end/entrypoints/js_app/js_app.ts b/front_end/entrypoints/js_app/js_app.ts +index a7f4827d68..2dbb4eca42 100644 +--- a/front_end/entrypoints/js_app/js_app.ts ++++ b/front_end/entrypoints/js_app/js_app.ts +@@ -44,6 +44,9 @@ async function loadSourcesModule(): Promise { + } + return loadedSourcesModule; + } ++ ++// This is not supported in Firefox: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView ++Element.prototype.scrollIntoViewIfNeeded = Element.prototype.scrollIntoView; + export class JsMainImpl implements Common.Runnable.Runnable { + static instance(opts: {forceNew: boolean|null} = {forceNew: null}): JsMainImpl { + const {forceNew} = opts; +diff --git a/front_end/ui/legacy/components/data_grid/DataGrid.ts b/front_end/ui/legacy/components/data_grid/DataGrid.ts +index 70820e38a1..5c9e25c522 100644 +--- a/front_end/ui/legacy/components/data_grid/DataGrid.ts ++++ b/front_end/ui/legacy/components/data_grid/DataGrid.ts +@@ -223,6 +223,8 @@ export class DataGridImpl extends Common.ObjectWrapper.ObjectWrapper *under* the ++ (this.dataTableBody as HTMLElement).style.height = 'calc(100% - 27px)'; + this.topFillerRow = this.dataTableBody.createChild('tr', 'data-grid-filler-row revealed'); + UI.ARIAUtils.setHidden(this.topFillerRow, true); + this.bottomFillerRow = this.dataTableBody.createChild('tr', 'data-grid-filler-row revealed'); +@@ -535,6 +537,9 @@ export class DataGridImpl extends Common.ObjectWrapper.ObjectWrapper +Date: Fri, 25 Oct 2024 16:06:06 +0100 +Subject: [PATCH 2/8] Setup Cloudflare devtools target type + +--- + front_end/core/sdk/Target.ts | 5 +++++ + front_end/entrypoints/js_app/js_app.ts | 2 +- + 2 files changed, 6 insertions(+), 1 deletion(-) + +diff --git a/front_end/core/sdk/Target.ts b/front_end/core/sdk/Target.ts +index 2437d6aac1..e9a3318268 100644 +--- a/front_end/core/sdk/Target.ts ++++ b/front_end/core/sdk/Target.ts +@@ -98,6 +98,10 @@ export class Target extends ProtocolClient.InspectorBackend.TargetBase { + break; + case Type.NODE_WORKER: + this.#capabilitiesMask = Capability.JS | Capability.NETWORK | Capability.TARGET; ++ break; ++ case Type.CLOUDFLARE: ++ this.#capabilitiesMask = Capability.JS | Capability.NETWORK; ++ break; + } + this.#typeInternal = type; + this.#parentTargetInternal = parentTarget; +@@ -298,6 +302,7 @@ export enum Type { + AUCTION_WORKLET = 'auction-worklet', + WORKLET = 'worklet', + TAB = 'tab', ++ CLOUDFLARE = 'cloudflare', + NODE_WORKER = 'node-worker', + } + +diff --git a/front_end/entrypoints/js_app/js_app.ts b/front_end/entrypoints/js_app/js_app.ts +index 2dbb4eca42..814bc1fc70 100644 +--- a/front_end/entrypoints/js_app/js_app.ts ++++ b/front_end/entrypoints/js_app/js_app.ts +@@ -61,7 +61,7 @@ export class JsMainImpl implements Common.Runnable.Runnable { + Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConnectToNodeJSDirectly); + void SDK.Connections.initMainConnection(async () => { + const target = SDK.TargetManager.TargetManager.instance().createTarget( +- 'main', i18nString(UIStrings.main), SDK.Target.Type.NODE, null); ++ 'main', i18nString(UIStrings.main), SDK.Target.Type.CLOUDFLARE, null); + void target.runtimeAgent().invoke_runIfWaitingForDebugger(); + }, Components.TargetDetachedDialog.TargetDetachedDialog.connectionLost); + } +-- +2.39.5 (Apple Git-154) + diff --git a/packages/chrome-devtools-patches/patches/0003-Add-ping-to-improve-connection-stability.-Without-th.patch b/packages/chrome-devtools-patches/patches/0003-Add-ping-to-improve-connection-stability.-Without-th.patch new file mode 100644 index 0000000..22f8e74 --- /dev/null +++ b/packages/chrome-devtools-patches/patches/0003-Add-ping-to-improve-connection-stability.-Without-th.patch @@ -0,0 +1,45 @@ +From bcba740e8dd373acf74a8d7f82d93107e2bf4a08 Mon Sep 17 00:00:00 2001 +From: Workers DevProd +Date: Fri, 25 Oct 2024 15:04:17 +0100 +Subject: [PATCH 3/8] Add ping to improve connection stability. Without this, + we see frequent "Devtools disconnected" messages in the dash and Workers + Playground + +--- + front_end/core/protocol_client/InspectorBackend.ts | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/front_end/core/protocol_client/InspectorBackend.ts b/front_end/core/protocol_client/InspectorBackend.ts +index 32948c2dbd..27dfa1abe3 100644 +--- a/front_end/core/protocol_client/InspectorBackend.ts ++++ b/front_end/core/protocol_client/InspectorBackend.ts +@@ -256,6 +256,7 @@ export class SessionRouter { + proxyConnection: ((Connection | undefined)|null), + }>(); + #pendingScripts: Array<() => void> = []; ++ #pingInterval: ReturnType; + + constructor(connection: Connection) { + this.#connectionInternal = connection; +@@ -266,11 +267,18 @@ export class SessionRouter { + this.#connectionInternal.setOnMessage(this.onMessage.bind(this)); + + this.#connectionInternal.setOnDisconnect(reason => { ++ clearInterval(this.#pingInterval); + const session = this.#sessions.get(''); + if (session) { + session.target.dispose(reason); + } + }); ++ this.#pingInterval = setInterval(() => { ++ this.#connectionInternal.sendRawMessage(JSON.stringify({ ++ method: 'Runtime.getIsolateId', ++ id: this.nextMessageId(), ++ })); ++ }, 10_000); + } + + registerSession(target: TargetBase, sessionId: string, proxyConnection?: Connection|null): void { +-- +2.39.5 (Apple Git-154) + diff --git a/packages/chrome-devtools-patches/patches/0004-Support-viewing-source-files-over-the-network.-This-.patch b/packages/chrome-devtools-patches/patches/0004-Support-viewing-source-files-over-the-network.-This-.patch new file mode 100644 index 0000000..3abadff --- /dev/null +++ b/packages/chrome-devtools-patches/patches/0004-Support-viewing-source-files-over-the-network.-This-.patch @@ -0,0 +1,158 @@ +From b1f97bf51177076f6abb501cf8fe57a77640792e Mon Sep 17 00:00:00 2001 +From: Workers DevProd +Date: Fri, 25 Oct 2024 15:26:38 +0100 +Subject: [PATCH 4/8] Support viewing source files over the network. This + consists of: + +* Replacing the default file viewers (Workspace & Snippets), with a network based one called "Cloudflare" (see sources-meta.ts & js_app.ts) + +* Supporting a `?domain=` query param to customise the network name. Without this, the protocol is used (i.e. `file://`), which isn't super friendly (see NavigatorView.ts) + +* Enable JUST_MY_CODE to hide Wrangler's injected middleware (see MainImpl.ts) + +* Enable AUTHORED_DEPLOYED_GROUPING for a better grouping between source-mapped and compiled code (see MainImpl.ts) + +* Support the mechanism by which Wrangler provides sourcemaps, the wrangler-file:// protocol (see PageResourceLoader.ts & ParsedURL.ts) +--- + front_end/core/common/ParsedURL.ts | 2 +- + front_end/core/sdk/PageResourceLoader.ts | 4 +++- + front_end/entrypoints/js_app/js_app.ts | 16 ++++++-------- + front_end/entrypoints/main/MainImpl.ts | 2 ++ + front_end/panels/sources/NavigatorView.ts | 5 +++-- + front_end/panels/sources/sources-meta.ts | 26 ----------------------- + 6 files changed, 15 insertions(+), 40 deletions(-) + +diff --git a/front_end/core/common/ParsedURL.ts b/front_end/core/common/ParsedURL.ts +index d01ab1856d..e29c0eab46 100644 +--- a/front_end/core/common/ParsedURL.ts ++++ b/front_end/core/common/ParsedURL.ts +@@ -365,7 +365,7 @@ export class ParsedURL { + static completeURL(baseURL: Platform.DevToolsPath.UrlString, href: string): Platform.DevToolsPath.UrlString|null { + // Return special URLs as-is. + if (href.startsWith('data:') || href.startsWith('blob:') || href.startsWith('javascript:') || +- href.startsWith('mailto:')) { ++ href.startsWith('mailto:') || href.startsWith('wrangler:')) { + return href as Platform.DevToolsPath.UrlString; + } + +diff --git a/front_end/core/sdk/PageResourceLoader.ts b/front_end/core/sdk/PageResourceLoader.ts +index 722bf5f80a..cede6fbc20 100644 +--- a/front_end/core/sdk/PageResourceLoader.ts ++++ b/front_end/core/sdk/PageResourceLoader.ts +@@ -381,7 +381,9 @@ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper +Date: Fri, 25 Oct 2024 15:05:56 +0100 +Subject: [PATCH 5/8] Support forcing the devtools theme via a query parameter, + to enable it to fit in with the Cloudflare dashboard when embedded. + +To test, make sure that `?theme=dark`, `?theme=default`, and `?theme=systemPreferred` all correctly force the theme of devtools +--- + front_end/ui/legacy/theme_support/ThemeSupport.ts | 11 ++++++++--- + 1 file changed, 8 insertions(+), 3 deletions(-) + +diff --git a/front_end/ui/legacy/theme_support/ThemeSupport.ts b/front_end/ui/legacy/theme_support/ThemeSupport.ts +index 298ba78952..43ee9341ec 100644 +--- a/front_end/ui/legacy/theme_support/ThemeSupport.ts ++++ b/front_end/ui/legacy/theme_support/ThemeSupport.ts +@@ -152,14 +152,19 @@ export class ThemeSupport extends EventTarget { + } + + #applyThemeToDocument(document: Document): void { ++ // Get theme from url, to allow for theme inheritance from the Cloudflare dashboard ++ const urlParams = new URLSearchParams(document.location.search); ++ const theme = urlParams.get('theme') ?? 'systemPreferred'; ++ + const isForcedColorsMode = window.matchMedia('(forced-colors: active)').matches; + const systemPreferredTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'; + +- const useSystemPreferred = this.setting.get() === 'systemPreferred' || isForcedColorsMode; +- this.themeNameInternal = useSystemPreferred ? systemPreferredTheme : this.setting.get(); ++ const useSystemPreferred = theme === 'systemPreferred' || isForcedColorsMode; ++ this.themeNameInternal = useSystemPreferred ? systemPreferredTheme : theme; + document.documentElement.classList.toggle('theme-with-dark-background', this.themeNameInternal === 'dark'); + +- const useChromeTheme = Common.Settings.moduleSetting('chrome-theme-colors').get(); ++ // Disable this—it adds blue headers which are out of keeping with Cloudflare styling ++ const useChromeTheme = false; + const isIncognito = Root.Runtime.hostConfig.isOffTheRecord === true; + // Baseline is the name of Chrome's default color theme and there are two of these: default and grayscale. + // The collective name for the rest of the color themes is dynamic. +-- +2.39.5 (Apple Git-154) + diff --git a/packages/chrome-devtools-patches/patches/0006-All-about-the-network-tab.patch b/packages/chrome-devtools-patches/patches/0006-All-about-the-network-tab.patch new file mode 100644 index 0000000..3164788 --- /dev/null +++ b/packages/chrome-devtools-patches/patches/0006-All-about-the-network-tab.patch @@ -0,0 +1,116 @@ +From 1729d533ea8866ee71a5e2170253f7cd6f850e0c Mon Sep 17 00:00:00 2001 +From: Workers DevProd +Date: Fri, 25 Oct 2024 16:05:12 +0100 +Subject: [PATCH 6/8] All about the network tab! + +* Display the _content_ of responses (using cfResponse.body, a Cloudflare-specific extension) + +* Enable the network tab for the js_app type + +* Hide unsupported UI (cache disabling & network throttling) +--- + front_end/core/sdk/NetworkManager.ts | 8 ++++++- + front_end/entrypoints/js_app/js_app.ts | 1 + + front_end/panels/network/NetworkPanel.ts | 27 ------------------------ + 3 files changed, 8 insertions(+), 28 deletions(-) + +diff --git a/front_end/core/sdk/NetworkManager.ts b/front_end/core/sdk/NetworkManager.ts +index 1e50c6329f..756679235b 100644 +--- a/front_end/core/sdk/NetworkManager.ts ++++ b/front_end/core/sdk/NetworkManager.ts +@@ -34,6 +34,7 @@ + + import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; + import * as Protocol from '../../generated/protocol.js'; ++import { ContentData } from '../../models/text_utils/ContentData.js'; + import * as TextUtils from '../../models/text_utils/text_utils.js'; + import * as Common from '../common/common.js'; + import type {Serializer} from '../common/Settings.js'; +@@ -846,7 +847,7 @@ export class NetworkDispatcher implements ProtocolProxyApi.NetworkDispatcher { + this.updateNetworkRequest(networkRequest); + } + +- loadingFinished({requestId, timestamp: finishTime, encodedDataLength}: Protocol.Network.LoadingFinishedEvent): void { ++ loadingFinished({requestId, timestamp: finishTime, encodedDataLength, cfResponse}: Protocol.Network.LoadingFinishedEvent & { cfResponse?: Omit }): void { + let networkRequest: NetworkRequest|null|undefined = this.#requestsById.get(requestId); + if (!networkRequest) { + networkRequest = this.maybeAdoptMainResourceRequest(requestId); +@@ -855,6 +856,11 @@ export class NetworkDispatcher implements ProtocolProxyApi.NetworkDispatcher { + return; + } + this.getExtraInfoBuilder(requestId).finished(); ++ if (cfResponse !== undefined) { ++ networkRequest.setContentDataProvider(async () => { ++ return new ContentData(cfResponse.body, cfResponse.base64Encoded, networkRequest.mimeType); ++ }); ++ } + this.finishNetworkRequest(networkRequest, finishTime, encodedDataLength); + this.#manager.dispatchEventToListeners(Events.LoadingFinished, networkRequest); + } +diff --git a/front_end/entrypoints/js_app/js_app.ts b/front_end/entrypoints/js_app/js_app.ts +index 893ac3533f..937e646ab5 100644 +--- a/front_end/entrypoints/js_app/js_app.ts ++++ b/front_end/entrypoints/js_app/js_app.ts +@@ -4,6 +4,7 @@ + + import '../shell/shell.js'; + import '../../panels/js_timeline/js_timeline-meta.js'; ++import '../../panels/network/network-meta.js'; + import '../../panels/mobile_throttling/mobile_throttling-meta.js'; + import '../../panels/network/network-meta.js'; + +diff --git a/front_end/panels/network/NetworkPanel.ts b/front_end/panels/network/NetworkPanel.ts +index f70065800e..ac1316662b 100644 +--- a/front_end/panels/network/NetworkPanel.ts ++++ b/front_end/panels/network/NetworkPanel.ts +@@ -76,14 +76,6 @@ const UIStrings = { + *@description Text to preserve the log after refreshing + */ + preserveLog: 'Preserve log', +- /** +- *@description Text to disable cache while DevTools is open +- */ +- disableCacheWhileDevtoolsIsOpen: 'Disable cache while DevTools is open', +- /** +- *@description Text in Network Config View of the Network panel +- */ +- disableCache: 'Disable cache', + /** + *@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in network panel of the network panel + */ +@@ -181,10 +173,6 @@ const UIStrings = { + *@description Text in Network Panel that is displayed when frames are being fetched. + */ + fetchingFrames: 'Fetching frames…', +- /** +- * @description Text of a button in the Network panel's toolbar that open Network Conditions panel in the drawer. +- */ +- moreNetworkConditions: 'More network conditions…', + } as const; + const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkPanel.ts', UIStrings); + const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); +@@ -456,21 +444,6 @@ export class NetworkPanel extends UI.Panel.Panel implements + this.panelToolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingCheckbox( + this.preserveLogSetting, i18nString(UIStrings.doNotClearLogOnPageReload), i18nString(UIStrings.preserveLog))); + +- this.panelToolbar.appendSeparator(); +- const disableCacheCheckbox = new UI.Toolbar.ToolbarSettingCheckbox( +- Common.Settings.Settings.instance().moduleSetting('cache-disabled'), +- i18nString(UIStrings.disableCacheWhileDevtoolsIsOpen), i18nString(UIStrings.disableCache)); +- this.panelToolbar.appendToolbarItem(disableCacheCheckbox); +- +- this.panelToolbar.appendToolbarItem(this.throttlingSelect); +- +- const networkConditionsButton = new UI.Toolbar.ToolbarButton( +- i18nString(UIStrings.moreNetworkConditions), 'network-settings', undefined, 'network-conditions'); +- networkConditionsButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => { +- void UI.ViewManager.ViewManager.instance().showView('network.config'); +- }, this); +- this.panelToolbar.appendToolbarItem(networkConditionsButton); +- + this.rightToolbar.appendToolbarItem(new UI.Toolbar.ToolbarItem(this.progressBarContainer)); + this.rightToolbar.appendSeparator(); + this.rightToolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingToggle( +-- +2.39.5 (Apple Git-154) + diff --git a/packages/chrome-devtools-patches/patches/0007-Limit-heap-profiling-modes-available.patch b/packages/chrome-devtools-patches/patches/0007-Limit-heap-profiling-modes-available.patch new file mode 100644 index 0000000..dcde323 --- /dev/null +++ b/packages/chrome-devtools-patches/patches/0007-Limit-heap-profiling-modes-available.patch @@ -0,0 +1,31 @@ +From c43fb074f72c73e4c7ad9dd02c061870298a4347 Mon Sep 17 00:00:00 2001 +From: Workers DevProd +Date: Fri, 25 Oct 2024 16:07:24 +0100 +Subject: [PATCH 7/8] Limit heap profiling modes available + +--- + front_end/panels/profiler/HeapProfilerPanel.ts | 8 ++------ + 1 file changed, 2 insertions(+), 6 deletions(-) + +diff --git a/front_end/panels/profiler/HeapProfilerPanel.ts b/front_end/panels/profiler/HeapProfilerPanel.ts +index b25d9e65fc..9a19052de2 100644 +--- a/front_end/panels/profiler/HeapProfilerPanel.ts ++++ b/front_end/panels/profiler/HeapProfilerPanel.ts +@@ -25,12 +25,8 @@ export class HeapProfilerPanel extends ProfilesPanel implements UI.ContextMenu.P + UI.ActionRegistration.ActionDelegate { + constructor() { + const registry = instance; +- const profileTypes = [ +- registry.heapSnapshotProfileType, +- registry.trackingHeapSnapshotProfileType, +- registry.samplingHeapProfileType, +- registry.detachedElementProfileType, +- ]; ++ const profileTypes = ++ [registry.heapSnapshotProfileType]; + super('heap-profiler', profileTypes as ProfileType[], 'profiler.heap-toggle-recording'); + } + +-- +2.39.5 (Apple Git-154) + diff --git a/packages/chrome-devtools-patches/patches/0008-Use-the-worker-name-as-the-title-for-the-Javascript-.patch b/packages/chrome-devtools-patches/patches/0008-Use-the-worker-name-as-the-title-for-the-Javascript-.patch new file mode 100644 index 0000000..d0e393f --- /dev/null +++ b/packages/chrome-devtools-patches/patches/0008-Use-the-worker-name-as-the-title-for-the-Javascript-.patch @@ -0,0 +1,31 @@ +From 641e6a0eb3f7cf138168185f8a51cd93b1d0db74 Mon Sep 17 00:00:00 2001 +From: Workers DevProd +Date: Fri, 25 Oct 2024 16:11:10 +0100 +Subject: [PATCH 8/8] Use the worker name as the title for the Javascript + Isolate name in the Performance tab + +--- + front_end/panels/timeline/IsolateSelector.ts | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/front_end/panels/timeline/IsolateSelector.ts b/front_end/panels/timeline/IsolateSelector.ts +index eea81e20dd..9d19e75d79 100644 +--- a/front_end/panels/timeline/IsolateSelector.ts ++++ b/front_end/panels/timeline/IsolateSelector.ts +@@ -52,8 +52,12 @@ export class IsolateSelector extends UI.Toolbar.ToolbarItem implements SDK.Isola + const name = SDK.TargetManager.TargetManager.instance().rootTarget() !== target ? target.name() : ''; + const parsedURL = new Common.ParsedURL.ParsedURL(target.inspectedURL()); + const domain = parsedURL.isValid ? parsedURL.domain() : ''; ++ ++ const query = new URLSearchParams(location.search); ++ const workerName = query.get('domain'); ++ + const title = +- target.decorateLabel(domain && name ? `${domain}: ${name}` : name || domain || i18nString(UIStrings.empty)); ++ target.decorateLabel(domain && name ? `${domain}: ${name}` : name || domain || workerName || i18nString(UIStrings.empty)); + modelCountByName.set(title, (modelCountByName.get(title) || 0) + 1); + } + itemForIsolate.removeChildren(); +-- +2.39.5 (Apple Git-154) + diff --git a/packages/chrome-devtools-patches/wrangler.jsonc b/packages/chrome-devtools-patches/wrangler.jsonc new file mode 100644 index 0000000..68f024d --- /dev/null +++ b/packages/chrome-devtools-patches/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "cloudflare-devtools", + "compatibility_date": "2025-07-01", + "assets": { + "directory": "devtools-frontend/out/Default/gen/front_end", + }, + "preview_urls": true, + "workers_dev": true, +} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 0000000..f8d5b33 --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,201 @@ +# @cloudflare/cli + +## 0.1.13 + +### Patch Changes + +- Updated dependencies [[`0283a1f`](https://github.com/cloudflare/workers-sdk/commit/0283a1fcdc635244f731010422e513e8b4ab0be3)]: + - @cloudflare/workers-utils@0.26.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies [[`aad35b7`](https://github.com/cloudflare/workers-sdk/commit/aad35b79d07df1bb764a4a5912d6b4328a34474b), [`1ac96a1`](https://github.com/cloudflare/workers-sdk/commit/1ac96a14b7fb022acada114ab8793fe8a4ba79a5), [`1ca8d8f`](https://github.com/cloudflare/workers-sdk/commit/1ca8d8f0bbd012a1d65cabadf7b6987b252775e9)]: + - @cloudflare/workers-utils@0.25.1 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies [[`aa5d580`](https://github.com/cloudflare/workers-sdk/commit/aa5d5801450b7e4417bfdbd477f86de3a4bc6933)]: + - @cloudflare/workers-utils@0.25.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies [[`cfd6205`](https://github.com/cloudflare/workers-sdk/commit/cfd6205fe86f6afd74b5881f09524c93c83b8359), [`cfd6205`](https://github.com/cloudflare/workers-sdk/commit/cfd6205fe86f6afd74b5881f09524c93c83b8359)]: + - @cloudflare/workers-utils@0.24.0 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb)]: + - @cloudflare/workers-utils@0.23.2 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`ecfdd5a`](https://github.com/cloudflare/workers-sdk/commit/ecfdd5a6c60b9c6f99c28f9294da656933c2a5fd)]: + - @cloudflare/workers-utils@0.23.1 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`c4f45e8`](https://github.com/cloudflare/workers-sdk/commit/c4f45e8b8694c60fb1808f7fbb130e4b4893d20c)]: + - @cloudflare/workers-utils@0.23.0 + +## 0.1.6 + +### Patch Changes + +- [#14112](https://github.com/cloudflare/workers-sdk/pull/14112) [`3a746ac`](https://github.com/cloudflare/workers-sdk/commit/3a746ac56a40b805e38f26ef5328e44917b543e6) Thanks [@penalosa](https://github.com/penalosa)! - Pin non-bundled runtime dependencies to exact versions + + Dependencies that are not bundled into a package's published output are installed directly into consumers' dependency trees, so they are now pinned to exact versions instead of semver ranges. This closes a supply-chain gap where an unpinned external dependency could resolve to a compromised upstream release on a fresh install. A new `pnpm check:pinned-deps` lint enforces this for all published packages (and for the shared pnpm catalog) going forward. + +- Updated dependencies [[`e86489a`](https://github.com/cloudflare/workers-sdk/commit/e86489a5743ff9bad7bcb5b444ad3d952d5b0164), [`337e912`](https://github.com/cloudflare/workers-sdk/commit/337e9124cfa461a99ce7ffb800dcc341f7b2f026), [`65b5f9e`](https://github.com/cloudflare/workers-sdk/commit/65b5f9e1855651c2df2c1bdfc8930141e36413d5)]: + - @cloudflare/workers-utils@0.22.1 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`599b27a`](https://github.com/cloudflare/workers-sdk/commit/599b27aafb9bc432524a35eb4e5a414de21bef41), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee), [`e3c862a`](https://github.com/cloudflare/workers-sdk/commit/e3c862a99f9b633ca288306eae8a8c3a900590ee)]: + - @cloudflare/workers-utils@0.22.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies [[`90092c0`](https://github.com/cloudflare/workers-sdk/commit/90092c0bca526e2e08a25fe7969534426eb6fd9f)]: + - @cloudflare/workers-utils@0.21.1 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`248bc08`](https://github.com/cloudflare/workers-sdk/commit/248bc08152cf9f792d98c8c78f8fb1417b1bb3b3)]: + - @cloudflare/workers-utils@0.21.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`f3fed88`](https://github.com/cloudflare/workers-sdk/commit/f3fed8859b612d424388fe45a1d638cf6b1c42c7)]: + - @cloudflare/workers-utils@0.20.0 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`2b8c0cc`](https://github.com/cloudflare/workers-sdk/commit/2b8c0ccb9ede7487bd96cfc51b3262a717bb532c)]: + - @cloudflare/workers-utils@0.19.0 + +## 0.1.0 + +### Minor Changes + +- [#13651](https://github.com/cloudflare/workers-sdk/pull/13651) [`47ac63f`](https://github.com/cloudflare/workers-sdk/commit/47ac63f05ca86d781110490ff21ff88f2828fbbf) Thanks [@penalosa](https://github.com/penalosa)! - Publish `@cloudflare/cli-shared-helpers` and `@cloudflare/workers-utils` to npm + +### Patch Changes + +- Updated dependencies [[`5680287`](https://github.com/cloudflare/workers-sdk/commit/56802879641c123ee11160d77ecaf104915cd826), [`47ac63f`](https://github.com/cloudflare/workers-sdk/commit/47ac63f05ca86d781110490ff21ff88f2828fbbf)]: + - @cloudflare/workers-utils@0.18.0 + +## 1.4.0 + +### Minor Changes + +- [#13144](https://github.com/cloudflare/workers-sdk/pull/13144) [`db60b94`](https://github.com/cloudflare/workers-sdk/commit/db60b94d9620e7608df3e98876d5df4fde952ecf) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Add gitignore helpers for appending Wrangler-related entries + + New `maybeAppendWranglerToGitIgnore` and `maybeAppendWranglerToGitIgnoreLikeFile` functions that automatically append Wrangler-related entries (`.wrangler`, `.dev.vars*`, `.env*`, and their negated example patterns) to `.gitignore` or similar ignore files. Existing entries are detected and skipped to avoid duplicates. + +## 1.3.0 + +### Minor Changes + +- [#13068](https://github.com/cloudflare/workers-sdk/pull/13068) [`e631a94`](https://github.com/cloudflare/workers-sdk/commit/e631a946466ded834763497d094da9e93d3d3721) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Add `runCommand` and `quoteShellArgs`, `installPackages` and `installWrangler` utilities to `@cloudflare/cli/command` + + These utilities are now available from `@cloudflare/cli` as dedicated sub-path exports: `runCommand` and `quoteShellArgs` via `@cloudflare/cli/command`, and `installPackages` and `installWrangler` via `@cloudflare/cli/packages`. This makes them reusable across packages in the SDK without duplication. + +## 1.2.1 + +### Patch Changes + +- [#11940](https://github.com/cloudflare/workers-sdk/pull/11940) [`69ff962`](https://github.com/cloudflare/workers-sdk/commit/69ff9620487a6ae979f369eb1dbac887ce46e246) Thanks [@penalosa](https://github.com/penalosa)! - Mark macOS version compatibility errors as user errors + + When checking macOS version compatibility, the CLI now throws `UserError` instead of generic `Error`. This ensures that version incompatibility issues are properly classified as user-facing errors that shouldn't be reported to Sentry. + +- [#11967](https://github.com/cloudflare/workers-sdk/pull/11967) [`202c59e`](https://github.com/cloudflare/workers-sdk/commit/202c59e4f4f28419fb6ac0aa8c7dc3960a0c8d3e) Thanks [@emily-shen](https://github.com/emily-shen)! - chore: update undici + + The following dependency versions have been updated: + + | Dependency | From | To | + | ---------- | ------ | ------ | + | undici | 7.14.0 | 7.18.2 | + +## 1.2.0 + +### Minor Changes + +- [#11578](https://github.com/cloudflare/workers-sdk/pull/11578) [`4201472`](https://github.com/cloudflare/workers-sdk/commit/4201472291fa1c864dbcca40c173a76e5b571a04) Thanks [@gpanders](https://github.com/gpanders)! - Add showCursor helper function to show or hide the cursor in the terminal + +## 1.1.4 + +### Patch Changes + +- [#11448](https://github.com/cloudflare/workers-sdk/pull/11448) [`2b4813b`](https://github.com/cloudflare/workers-sdk/commit/2b4813b18076817bb739491246313c32b403651f) Thanks [@edmundhung](https://github.com/edmundhung)! - Builds package with esbuild `v0.27.0` + +## 1.1.3 + +### Patch Changes + +- [#10764](https://github.com/cloudflare/workers-sdk/pull/10764) [`79a6b7d`](https://github.com/cloudflare/workers-sdk/commit/79a6b7dd811fea5a413b084fcd281915a418a85a) Thanks [@emily-shen](https://github.com/emily-shen)! - fix: respect the log level set by wrangler when logging using @cloudflare/cli + +## 1.1.2 + +### Patch Changes + +- [#10492](https://github.com/cloudflare/workers-sdk/pull/10492) [`8ae9323`](https://github.com/cloudflare/workers-sdk/commit/8ae9323a5c45f3efe3685e36b1536cc25d39fbfb) Thanks [@gpanders](https://github.com/gpanders)! - Include cursor in text prompts + +## 1.1.1 + +### Patch Changes + +- [#4768](https://github.com/cloudflare/workers-sdk/pull/4768) [`c3e410c2`](https://github.com/cloudflare/workers-sdk/commit/c3e410c2797f5c59b9ea0f63c20feef643366df2) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - ci: bump undici versions to 5.28.2 + +## 1.1.0 + +### Minor Changes + +- [#4779](https://github.com/cloudflare/workers-sdk/pull/4779) [`06bb99e1`](https://github.com/cloudflare/workers-sdk/commit/06bb99e1c91ffe5305343a17353912e734f5bd0c) Thanks [@1000hz](https://github.com/1000hz)! - Added a new `SelectRefreshablePrompt` component + + This component supports infinite scrolling, and the ability to refresh the items on 'R' keypress. See [#4310](https://github.com/cloudflare/workers-sdk/pull/4310) for more details. + +* [#4779](https://github.com/cloudflare/workers-sdk/pull/4779) [`06bb99e1`](https://github.com/cloudflare/workers-sdk/commit/06bb99e1c91ffe5305343a17353912e734f5bd0c) Thanks [@1000hz](https://github.com/1000hz)! - Added `@clack/core`'s `MultiSelectPrompt` component + + See [#4310](https://github.com/cloudflare/workers-sdk/pull/4310) for more details. + +- [#4779](https://github.com/cloudflare/workers-sdk/pull/4779) [`06bb99e1`](https://github.com/cloudflare/workers-sdk/commit/06bb99e1c91ffe5305343a17353912e734f5bd0c) Thanks [@1000hz](https://github.com/1000hz)! - Added `processArguments` helper function + + See [#4310](https://github.com/cloudflare/workers-sdk/pull/4310) for more details. + +### Patch Changes + +- [#4779](https://github.com/cloudflare/workers-sdk/pull/4779) [`06bb99e1`](https://github.com/cloudflare/workers-sdk/commit/06bb99e1c91ffe5305343a17353912e734f5bd0c) Thanks [@1000hz](https://github.com/1000hz)! - Downgraded `chalk` dependency from `^5.2.0` to `^2.4.2` + + This was done for compatibility reasons with the version used in the `wrangler` package. See [#4310](https://github.com/cloudflare/workers-sdk/pull/4310) for more details. + +## 1.0.0 + +### Major Changes + +- [#4189](https://github.com/cloudflare/workers-sdk/pull/4189) [`05798038`](https://github.com/cloudflare/workers-sdk/commit/05798038c85a83afb2c0e8ea9533c31a6fbe3e91) Thanks [@gabivlj](https://github.com/gabivlj)! - Move helper cli files of C3 into @cloudflare/cli and make Wrangler and C3 depend on it + +### Patch Changes + +- [#4271](https://github.com/cloudflare/workers-sdk/pull/4271) [`70016b2b`](https://github.com/cloudflare/workers-sdk/commit/70016b2bb514ea95f1ce0db3582e194c31df4c14) Thanks [@gabivlj](https://github.com/gabivlj)! - change backgrounds of statuses from red to more appropriate ones diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..5398f17 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,5 @@ +# Cloudflare CLI + +> Internal utility package for workers-sdk. Not intended for external use — APIs may change without notice. + +All things related to rendering the CLI for workers-sdk. diff --git a/packages/cli/__tests__/check-macos-version.test.ts b/packages/cli/__tests__/check-macos-version.test.ts new file mode 100644 index 0000000..6cd77a7 --- /dev/null +++ b/packages/cli/__tests__/check-macos-version.test.ts @@ -0,0 +1,161 @@ +import os from "node:os"; +import ci from "ci-info"; +import { beforeEach, describe, it, vi } from "vitest"; +import { checkMacOSVersion } from "../check-macos-version"; + +vi.mock("node:os"); +vi.mock("ci-info", () => ({ + default: { isCI: false }, + isCI: false, +})); + +const mockOs = vi.mocked(os); + +describe("checkMacOSVersion", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(ci).isCI = false; + }); + + it("should not throw on non-macOS platforms", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); + + it("should not throw on macOS 13.5.0", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("22.6.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); + + it("should not throw on macOS 14.0.0", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("23.0.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); + + it("should not throw on macOS 13.6.0", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("22.7.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); + + it("should throw error on macOS 12.7.6", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("21.6.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).toThrow( + "Unsupported macOS version: The Cloudflare Workers runtime cannot run on the current version of macOS (12.6.0)" + ); + }); + + it("should throw error on macOS 13.4.0", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("22.4.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).toThrow( + "Unsupported macOS version: The Cloudflare Workers runtime cannot run on the current version of macOS (13.4.0)" + ); + }); + + it("should handle invalid Darwin version format gracefully", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("invalid-version"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); + + it("should handle very old Darwin versions gracefully", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("19.6.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); + + it("should not throw when running in CI", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + vi.mocked(ci).isCI = true; + mockOs.release.mockReturnValue("21.6.0"); + + expect(() => checkMacOSVersion({ shouldThrow: true })).not.toThrow(); + }); +}); + +describe("checkMacOSVersion with shouldThrow=false", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(ci).isCI = false; + }); + + it("should not warn on non-macOS platforms", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + checkMacOSVersion({ shouldThrow: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it("should not warn on macOS 13.5.0", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("22.6.0"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + checkMacOSVersion({ shouldThrow: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it("should warn on macOS 12.7.6", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("21.6.0"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + checkMacOSVersion({ shouldThrow: false }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining( + "⚠️ Warning: Unsupported macOS version detected (12.6.0)" + ) + ); + }); + + it("should warn on macOS 13.4.0", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("22.4.0"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + checkMacOSVersion({ shouldThrow: false }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining( + "⚠️ Warning: Unsupported macOS version detected (13.4.0)" + ) + ); + }); + + it("should not warn when running in CI", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + vi.mocked(ci).isCI = true; + mockOs.release.mockReturnValue("21.6.0"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + checkMacOSVersion({ shouldThrow: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it("should not warn on invalid Darwin version format", ({ expect }) => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + mockOs.release.mockReturnValue("invalid-version"); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + checkMacOSVersion({ shouldThrow: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/__tests__/cli.test.ts b/packages/cli/__tests__/cli.test.ts new file mode 100644 index 0000000..ea3e3a2 --- /dev/null +++ b/packages/cli/__tests__/cli.test.ts @@ -0,0 +1,10 @@ +import { describe, test } from "vitest"; +import { space } from ".."; + +describe("cli", () => { + test("test spaces", ({ expect }) => { + expect(space(300)).toMatchInlineSnapshot( + '"                                                                                                                                                                                                                                                                                                            "' + ); + }); +}); diff --git a/packages/cli/__tests__/command.test.ts b/packages/cli/__tests__/command.test.ts new file mode 100644 index 0000000..5491069 --- /dev/null +++ b/packages/cli/__tests__/command.test.ts @@ -0,0 +1,72 @@ +import { spawn } from "cross-spawn"; +import { afterEach, beforeEach, describe, test, vi } from "vitest"; +import { quoteShellArgs, runCommand } from "../command"; +import type { ChildProcess } from "node:child_process"; + +// We can change how the mock spawn works by setting these variables +let spawnResultCode = 0; +let spawnStdout: string | undefined = undefined; +let spawnStderr: string | undefined = undefined; + +vi.mock("cross-spawn"); + +describe("Command Helpers", () => { + afterEach(() => { + spawnResultCode = 0; + spawnStdout = undefined; + spawnStderr = undefined; + }); + + beforeEach(() => { + vi.mocked(spawn).mockImplementation(() => { + return { + on: vi.fn().mockImplementation((event, cb) => { + if (event === "close") { + cb(spawnResultCode); + } + }), + stdout: { + on(_event: "data", cb: (data: string) => void) { + if (spawnStdout !== undefined) { + cb(spawnStdout); + } + }, + }, + stderr: { + on(_event: "data", cb: (data: string) => void) { + if (spawnStderr !== undefined) { + cb(spawnStderr); + } + }, + }, + } as unknown as ChildProcess; + }); + }); + + test("runCommand", async ({ expect }) => { + await runCommand(["ls", "-l"]); + expect(spawn).toHaveBeenCalledWith("ls", ["-l"], { + stdio: "inherit", + env: process.env, + signal: expect.any(AbortSignal), + }); + }); + + describe("quoteShellArgs", () => { + test.runIf(process.platform !== "win32")("mac", async ({ expect }) => { + expect(quoteShellArgs([`pages:dev`])).toEqual("pages:dev"); + expect(quoteShellArgs([`24.02 foo-bar`])).toEqual(`'24.02 foo-bar'`); + expect(quoteShellArgs([`foo/10 bar/20-baz/`])).toEqual( + `'foo/10 bar/20-baz/'` + ); + }); + + test.runIf(process.platform === "win32")("windows", async ({ expect }) => { + expect(quoteShellArgs([`pages:dev`])).toEqual("pages:dev"); + expect(quoteShellArgs([`24.02 foo-bar`])).toEqual(`"24.02 foo-bar"`); + expect(quoteShellArgs([`foo/10 bar/20-baz/`])).toEqual( + `"foo/10 bar/20-baz/"` + ); + }); + }); +}); diff --git a/packages/cli/__tests__/gitignore.test.ts b/packages/cli/__tests__/gitignore.test.ts new file mode 100644 index 0000000..90aa28e --- /dev/null +++ b/packages/cli/__tests__/gitignore.test.ts @@ -0,0 +1,445 @@ +import { appendFileSync, existsSync, statSync, writeFileSync } from "node:fs"; +import { readFileSync } from "@cloudflare/workers-utils"; +import { beforeEach, describe, test, vi } from "vitest"; +import { + maybeAppendWranglerToGitIgnore, + maybeAppendWranglerToGitIgnoreLikeFile, +} from "../gitignore"; +import type { PathLike } from "node:fs"; +import type { Mock } from "vitest"; + +vi.mock("node:fs"); +vi.mock("@cloudflare/workers-utils"); +vi.mock("../interactive", () => ({ + spinner: () => ({ + start: vi.fn(), + stop: vi.fn(), + update: vi.fn(), + }), +})); + +describe("maybeAppendWranglerToGitIgnoreLikeFile", () => { + let writeFileSyncMock: Mock; + let appendFileSyncMock: Mock; + + beforeEach(() => { + vi.resetAllMocks(); + + writeFileSyncMock = vi.mocked(writeFileSync); + appendFileSyncMock = vi.mocked(appendFileSync); + }); + + test("should append the wrangler section to a standard gitignore file", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should not touch the file if it already contains all wrangler files", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + !.dev.vars.example + .env* + !.env.example + .vscode + .wrangler + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock).not.toHaveBeenCalled(); + }); + + test("should not touch the file if it contains all wrangler files (and can cope with comments)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .wrangler # This is for wrangler + .dev.vars* # this is for wrangler and getPlatformProxy + !.dev.vars.example # more comments + .env* # even more + !.env.example # and a final one + .vscode + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock).not.toHaveBeenCalled(); + }); + + test("should append to the file the missing wrangler files when some are already present (should add the section heading if including .wrangler and some others)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + # wrangler files + .wrangler + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should append to the file the missing wrangler files when some are already present (should not add the section heading if .wrangler already exists)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .wrangler + .dev.vars* + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should append to the file the missing wrangler files when some are already present (should not add the section heading if only adding .wrangler)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + !.dev.vars.example + .env* + !.env.example + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + .wrangler + ", + ], + ] + `); + }); + + test("when it appends to the file it doesn't include an empty line only if there was one already", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + !.dev.vars.example + .env* + !.env.example + .vscode + + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + .wrangler + ", + ], + ] + `); + }); + + test("should create the file if it didn't exist already", ({ expect }) => { + mockIgnoreFile("my-project/.gitignore", ""); + // pretend that the file doesn't exist + vi.mocked(existsSync).mockImplementation(() => false); + + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + // writeFile wrote the (empty) file + expect(writeFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "", + ], + ] + `); + + // and the correct lines were then added to it + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "# wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should add the wildcard .dev.vars* entry even if a .dev.vars is already included", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars + .vscode + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should not add the .env entries if some form of .env entries are already included", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + .env + .env.* + !.env.example + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + ", + ], + ] + `); + }); + + test("should not add the .wrangler entry if a .wrangler/ is already included", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .wrangler/ # This is for wrangler + .vscode + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + function mockIgnoreFile(path: string, content: string) { + vi.mocked(existsSync).mockImplementation( + (filePath: PathLike) => filePath === path + ); + vi.mocked(readFileSync).mockImplementation((filePath: string) => + filePath === path ? content.replace(/\n\s*/g, "\n") : "" + ); + } +}); + +describe("maybeAppendWranglerToGitIgnore", () => { + let appendFileSyncMock: Mock; + + beforeEach(() => { + vi.resetAllMocks(); + + appendFileSyncMock = vi.mocked(appendFileSync); + + vi.mocked(statSync).mockImplementation(((path: string) => ({ + isDirectory() { + return path.endsWith(".git"); + }, + })) as typeof statSync); + }); + + test("should not create the gitignore file if neither the .git directory not the .gitingore file exist", ({ + expect, + }) => { + // no .gitignore file exists + vi.mocked(existsSync).mockImplementation(() => false); + // neither a .git directory does + vi.mocked(statSync).mockImplementation(() => { + throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + }); + + maybeAppendWranglerToGitIgnore("my-project"); + + expect(vi.mocked(writeFileSync)).not.toHaveBeenCalled(); + expect(appendFileSyncMock).not.toHaveBeenCalled(); + }); + + test("should create a .gitignore file when .git directory exists", ({ + expect, + }) => { + // no .gitignore file exists + vi.mocked(existsSync).mockImplementation(() => false); + vi.mocked(readFileSync).mockImplementation(() => ""); + + maybeAppendWranglerToGitIgnore("my-project"); + + // writeFileSync created the (empty) file + expect(vi.mocked(writeFileSync).mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "", + ], + ] + `); + + // and the correct wrangler lines were appended to it + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "# wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should append wrangler entries when the .gitignore file exists but the .git directory does not", ({ + expect, + }) => { + // .gitignore exists + vi.mocked(existsSync).mockImplementation( + (filePath: PathLike) => filePath === "my-project/.gitignore" + ); + vi.mocked(readFileSync).mockImplementation((filePath: string) => + filePath === "my-project/.gitignore" ? "\nnode_modules\n.vscode\n" : "" + ); + // .git directory does NOT exist + vi.mocked(statSync).mockImplementation(() => { + throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + }); + + maybeAppendWranglerToGitIgnore("my-project"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); +}); diff --git a/packages/cli/__tests__/packages.test.ts b/packages/cli/__tests__/packages.test.ts new file mode 100644 index 0000000..c0b6a55 --- /dev/null +++ b/packages/cli/__tests__/packages.test.ts @@ -0,0 +1,154 @@ +import { writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { parsePackageJSON, readFileSync } from "@cloudflare/workers-utils"; +import { afterEach, describe, test, vi } from "vitest"; +import { runCommand } from "../command"; +import { installPackages, installWrangler } from "../packages"; + +vi.mock("../command"); +vi.mock("@cloudflare/workers-utils", () => ({ + readFileSync: vi.fn(), + parsePackageJSON: vi.fn(), +})); +vi.mock("node:fs/promises", () => ({ + writeFile: vi.fn(), +})); + +describe("Package Helpers", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("installPackages", async () => { + type TestCase = { + pm: "npm" | "pnpm" | "yarn" | "bun"; + initialArgs: string[]; + additionalArgs?: string[]; + }; + + const cases: TestCase[] = [ + { pm: "npm", initialArgs: ["npm", "install"] }, + { pm: "pnpm", initialArgs: ["pnpm", "install"] }, + { pm: "bun", initialArgs: ["bun", "add"] }, + { pm: "yarn", initialArgs: ["yarn", "add"] }, + ]; + + test.for(cases)( + "with $pm", + async ({ pm, initialArgs, additionalArgs }, { expect }) => { + const mockPkgJson = { + dependencies: { + foo: "^1.0.0", + bar: "^2.0.0", + baz: "^1.2.3", + }, + }; + vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockPkgJson)); + vi.mocked(parsePackageJSON).mockReturnValue(mockPkgJson); + + const packages = ["foo", "bar@latest", "baz@1.2.3"]; + await installPackages(pm, packages); + + expect(vi.mocked(runCommand)).toHaveBeenCalledWith( + [...initialArgs, ...packages, ...(additionalArgs ?? [])], + expect.anything() + ); + + if (pm === "npm") { + const writeFileCall = vi.mocked(writeFile).mock.calls[0]; + expect(writeFileCall[0]).toBe(resolve(process.cwd(), "package.json")); + expect(JSON.parse(writeFileCall[1] as string)).toMatchObject({ + dependencies: { + foo: "^1.0.0", + bar: "^2.0.0", + baz: "1.2.3", + }, + }); + } + } + ); + + const devCases: TestCase[] = [ + { pm: "npm", initialArgs: ["npm", "install", "--save-dev"] }, + { pm: "pnpm", initialArgs: ["pnpm", "install", "--save-dev"] }, + { pm: "bun", initialArgs: ["bun", "add", "-d"] }, + { pm: "yarn", initialArgs: ["yarn", "add", "-D"] }, + ]; + + test.for(devCases)( + "with $pm (dev = true)", + async ({ pm, initialArgs, additionalArgs }, { expect }) => { + const mockPkgJson = { + devDependencies: { + foo: "^1.0.0", + bar: "^2.0.0", + baz: "^1.2.3", + }, + }; + vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockPkgJson)); + vi.mocked(parsePackageJSON).mockReturnValue(mockPkgJson); + + const packages = ["foo", "bar@latest", "baz@1.2.3"]; + await installPackages(pm, packages, { dev: true }); + + expect(vi.mocked(runCommand)).toHaveBeenCalledWith( + [...initialArgs, ...packages, ...(additionalArgs ?? [])], + expect.anything() + ); + + if (pm === "npm") { + const writeFileCall = vi.mocked(writeFile).mock.calls[0]; + expect(writeFileCall[0]).toBe(resolve(process.cwd(), "package.json")); + expect(JSON.parse(writeFileCall[1] as string)).toMatchObject({ + devDependencies: { + foo: "^1.0.0", + bar: "^2.0.0", + baz: "1.2.3", + }, + }); + } + } + ); + }); + + test("installWrangler", async ({ expect }) => { + const mockPkgJson = { + devDependencies: { + wrangler: "^4.0.0", + }, + }; + vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockPkgJson)); + vi.mocked(parsePackageJSON).mockReturnValue(mockPkgJson); + + await installWrangler("npm", false); + + expect(vi.mocked(runCommand)).toHaveBeenCalledWith( + ["npm", "install", "--save-dev", "wrangler@latest"], + expect.anything() + ); + }); + + test("installWrangler updates existing workers-types", async ({ expect }) => { + const mockPkgJson = { + devDependencies: { + "@cloudflare/workers-types": "^4.20241011.0", + wrangler: "^3.60.3", + }, + }; + vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockPkgJson)); + vi.mocked(parsePackageJSON).mockReturnValue(mockPkgJson); + + await installWrangler("npm", false); + + expect(vi.mocked(runCommand)).toHaveBeenCalledWith( + [ + "npm", + "install", + "--save-dev", + "wrangler@latest", + "@cloudflare/workers-types@latest", + ], + expect.anything() + ); + }); +}); diff --git a/packages/cli/args.ts b/packages/cli/args.ts new file mode 100644 index 0000000..2e1cf3a --- /dev/null +++ b/packages/cli/args.ts @@ -0,0 +1,21 @@ +import { inputPrompt } from "./interactive"; +import type { Arg, PromptConfig } from "./interactive"; + +export const processArgument = async ( + args: Record, + name: string, + promptConfig: PromptConfig +) => { + const value = args[name]; + const result = await inputPrompt({ + ...promptConfig, + // Accept the default value if the arg is already set + acceptDefault: promptConfig.acceptDefault ?? value !== undefined, + defaultValue: value ?? promptConfig.defaultValue, + }); + + // Update value in args before returning the result + args[name] = result as Arg; + + return result; +}; diff --git a/packages/cli/check-macos-version.ts b/packages/cli/check-macos-version.ts new file mode 100644 index 0000000..b9389bb --- /dev/null +++ b/packages/cli/check-macos-version.ts @@ -0,0 +1,109 @@ +import os from "node:os"; +import { UserError } from "@cloudflare/workers-utils"; +import ci from "ci-info"; + +/** + * Minimum macOS version required for workerd compatibility. + */ +const MINIMUM_MACOS_VERSION = "13.5.0"; + +/** + * Checks the current macOS version for workerd compatibility. + * + * This function is a no-op on non-Darwin platforms and in CI environments. + * + * @param options - Configuration object + * @param options.shouldThrow - If true, throws an error on unsupported versions. If false, logs a warning. + */ +export function checkMacOSVersion(options: { shouldThrow: boolean }): void { + if (process.platform !== "darwin") { + return; + } + + if (ci.isCI) { + return; + } + + const release = os.release(); + const macOSVersion = darwinVersionToMacOSVersion(release); + + if (macOSVersion && isVersionLessThan(macOSVersion, MINIMUM_MACOS_VERSION)) { + if (options.shouldThrow) { + throw new UserError( + `Unsupported macOS version: The Cloudflare Workers runtime cannot run on the current version of macOS (${macOSVersion}). ` + + `The minimum requirement is macOS ${MINIMUM_MACOS_VERSION}+. See https://github.com/cloudflare/workerd?tab=readme-ov-file#running-workerd ` + + `If you cannot upgrade your version of macOS, you could try running in a DevContainer setup with a supported version of Linux (glibc 2.35+ required).`, + { telemetryMessage: false } + ); + } else { + /* eslint-disable-next-line no-console -- this package has no logger to use + (TODO: disable the `no-console` rule on packages that do not use a logger) */ + console.warn( + `⚠️ Warning: Unsupported macOS version detected (${macOSVersion}). ` + + `The Cloudflare Workers runtime may not work correctly on macOS versions below ${MINIMUM_MACOS_VERSION}. ` + + `Consider upgrading to macOS ${MINIMUM_MACOS_VERSION}+ or using a DevContainer setup with a supported version of Linux (glibc 2.35+ required).` + ); + } + } +} + +/** + * Converts Darwin kernel version to macOS version. + * Darwin 21.x.x = macOS 12.x (Monterey) + * Darwin 22.x.x = macOS 13.x (Ventura) + * Darwin 23.x.x = macOS 14.x (Sonoma) + * etc. + */ +function darwinVersionToMacOSVersion(darwinVersion: string): string | null { + const match = darwinVersion.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match) { + return null; + } + + const major = parseInt(match[1], 10); + + if (major >= 20) { + const macOSMajor = major - 9; + const minor = parseInt(match[2], 10); + const patch = parseInt(match[3], 10); + return `${macOSMajor}.${minor}.${patch}`; + } + + return null; +} + +/** + * Simple semver comparison for major.minor.patch versions. + * Validates that both versions follow the M.m.p format before comparison. + */ +function isVersionLessThan(version1: string, version2: string): boolean { + const versionRegex = /^(\d+)\.(\d+)\.(\d+)$/; + + const match1 = version1.match(versionRegex); + const match2 = version2.match(versionRegex); + + if (!match1 || !match2) { + throw new Error( + `Invalid version format. Expected M.m.p format, got: ${version1}, ${version2}` + ); + } + + const [major1, minor1, patch1] = [ + parseInt(match1[1], 10), + parseInt(match1[2], 10), + parseInt(match1[3], 10), + ]; + const [major2, minor2, patch2] = [ + parseInt(match2[1], 10), + parseInt(match2[2], 10), + parseInt(match2[3], 10), + ]; + + if (major1 !== major2) { + return major1 < major2; + } + if (minor1 !== minor2) { + return minor1 < minor2; + } + return patch1 < patch2; +} diff --git a/packages/cli/colors.ts b/packages/cli/colors.ts new file mode 100644 index 0000000..bd172df --- /dev/null +++ b/packages/cli/colors.ts @@ -0,0 +1,34 @@ +import chalk from "chalk"; + +const { white, gray, dim, hidden, bold, cyanBright, bgCyan } = chalk; + +const brandColor = chalk.hex("#BD5B08"); + +const black = chalk.hex("#111"); +const blue = chalk.hex("#0E838F"); +const bgBlue = black.bgHex("#0E838F"); +const red = chalk.hex("#AB2526"); +const bgRed = black.bgHex("#AB2526"); +const green = chalk.hex("#218529"); +const bgGreen = black.bgHex("#218529"); +const yellow = chalk.hex("#7F7322"); +const bgYellow = black.bgHex("#7F7322"); + +export { + blue, + bgBlue, + green, + bgGreen, + red, + bgRed, + yellow, + bgYellow, + cyanBright, + bgCyan, + brandColor, + dim, + white, + gray, + hidden, + bold, +}; diff --git a/packages/cli/command.ts b/packages/cli/command.ts new file mode 100644 index 0000000..e882dac --- /dev/null +++ b/packages/cli/command.ts @@ -0,0 +1,189 @@ +import { spawn } from "cross-spawn"; +import { CancelError } from "./error"; +import { isInteractive, spinner } from "./interactive"; +import { stripAnsi } from "."; + +/** + * Command is a string array, like ['git', 'commit', '-m', '"Initial commit"'] + */ +type Command = string[]; + +export type RunOptions = { + startText?: string; + doneText?: string | ((output: string) => string); + silent?: boolean; + captureOutput?: boolean; + useSpinner?: boolean; + env?: NodeJS.ProcessEnv; + cwd?: string; + /** If defined this function is called to all you to transform the output from the command into a new string. */ + transformOutput?: (output: string) => string; +}; + +type PrintOptions = { + promise: Promise | (() => Promise); + useSpinner?: boolean; + startText: string; + doneText?: string | ((output: T) => string); +}; + +/** + * An awaitable wrapper around `spawn` that optionally displays progress to user and processes/captures the command's output + * + * @param command - The command to run as an array of strings + * @param opts.silent - Should the command's stdout and stderr be dispalyed to the user + * @param opts.captureOutput - Should the output of the command the returned as a string. + * @param opts.env - An object of environment variables to be injected when running the command + * @param opts.cwd - The directory in which the command should be run + * @param opts.useSpinner - Should a spinner be shown when running the command + * @param opts.startText - Spinner start text + * @param opts.endText - Spinner end text + * @param opts.transformOutput - A transformer to be run on command output before returning + * + * @returns Output collected from the stdout of the command, if `captureOutput` was set to true. Otherwise `null`. + */ +export const runCommand = async ( + command: Command, + opts: RunOptions = {} +): Promise => { + return printAsyncStatus({ + useSpinner: opts.useSpinner ?? opts.silent, + startText: opts.startText || quoteShellArgs(command), + doneText: opts.doneText, + promise() { + const [executable, ...args] = command; + const abortController = new AbortController(); + const cmd = spawn(executable, [...args], { + // TODO: ideally inherit stderr, but npm install uses this for warnings + // stdio: [ioMode, ioMode, "inherit"], + stdio: opts.silent ? "pipe" : "inherit", + env: { + ...process.env, + ...opts.env, + }, + cwd: opts.cwd, + signal: abortController.signal, + }); + + let output = ``; + + if (opts.captureOutput ?? opts.silent) { + cmd.stdout?.on("data", (data) => { + output += data; + }); + cmd.stderr?.on("data", (data) => { + output += data; + }); + } + + let cleanup: (() => void) | null = null; + + return new Promise((resolvePromise, reject) => { + const cancel = (signal?: NodeJS.Signals) => { + reject(new CancelError(`Command cancelled`, signal)); + abortController.abort(signal ? `${signal} received` : null); + }; + + process.on("SIGTERM", cancel).on("SIGINT", cancel); + + // To cleanup the signal listeners when the promise settles + cleanup = () => { + process.off("SIGTERM", cancel).off("SIGINT", cancel); + }; + + cmd.on("close", (code) => { + try { + if (code !== 0) { + throw new Error(output, { cause: code }); + } + + // Process any captured output + const transformOutput = + opts.transformOutput ?? ((result: string) => result); + const processedOutput = transformOutput(stripAnsi(output)); + + // Send the captured (and processed) output back to the caller + resolvePromise(processedOutput); + } catch (e) { + // Something went wrong. + // Perhaps the command or the transform failed. + reject(new Error(output, { cause: e })); + } + }); + + cmd.on("error", (error) => { + reject(error); + }); + }).finally(() => { + cleanup?.(); + }); + }, + }); +}; + +const printAsyncStatus = async ({ + promise, + ...opts +}: PrintOptions): Promise => { + let s: ReturnType | undefined; + + if (opts.useSpinner && isInteractive()) { + s = spinner(); + } + + s?.start(opts?.startText); + + if (typeof promise === "function") { + promise = promise(); + } + + try { + const output = await promise; + + const doneText = + typeof opts.doneText === "function" + ? opts.doneText(output) + : opts.doneText; + s?.stop(doneText); + } catch (err) { + s?.stop((err as Error).message); + } finally { + s?.stop(); + } + + return promise; +}; + +/** + * Formats an array of command line arguments to be displayed to the user + * in a platform safe way. Args used in conjunction with `runCommand` are safe + * since we use `cross-spawn` to handle multi-platform support. + * + * However, when we output commands to the user, we have to make sure that they + * are compatible with the platform they are using. + * + * @param args - The arguments to format to the user + */ +export function quoteShellArgs(args: string[]): string { + if (process.platform === "win32") { + // Simple Windows command prompt quoting if there are special characters. + const specialCharsMatcher = /[&<>[\]|{}^=;!'+,`~\s]/; + return args + .map((arg) => + arg.match(specialCharsMatcher) ? `"${arg.replaceAll(`"`, `""`)}"` : arg + ) + .join(" "); + } else { + return args + .map((s) => { + if (/["\s]/.test(s) && !/'/.test(s)) { + return "'" + s.replace(/(['\\])/g, "\\$1") + "'"; + } + if (/["'\s]/.test(s)) { + return '"' + s.replace(/(["\\$`!])/g, "\\$1") + '"'; + } + return s; + }) + .join(" "); + } +} diff --git a/packages/cli/cursor.ts b/packages/cli/cursor.ts new file mode 100644 index 0000000..dd3f29b --- /dev/null +++ b/packages/cli/cursor.ts @@ -0,0 +1,13 @@ +import process from "node:process"; + +export function showCursor(show: boolean, stream = process.stderr) { + if (!stream.isTTY) { + return; + } + + if (show) { + stream.write("\x1b[?25h"); + } else { + stream.write("\x1b[?25l"); + } +} diff --git a/packages/cli/error.ts b/packages/cli/error.ts new file mode 100644 index 0000000..d405afc --- /dev/null +++ b/packages/cli/error.ts @@ -0,0 +1,11 @@ +/** + * Error thrown when an operation is cancelled + */ +export class CancelError extends Error { + constructor( + message?: string, + readonly signal?: NodeJS.Signals + ) { + super(message); + } +} diff --git a/packages/cli/gitignore.ts b/packages/cli/gitignore.ts new file mode 100644 index 0000000..65c67da --- /dev/null +++ b/packages/cli/gitignore.ts @@ -0,0 +1,151 @@ +import { appendFileSync, existsSync, statSync, writeFileSync } from "node:fs"; +import { basename } from "node:path"; +import { readFileSync } from "@cloudflare/workers-utils"; +import { brandColor, dim } from "./colors"; +import { spinner } from "./interactive"; + +/** + * Appends to a file that follows a .gitignore-like structure any missing + * Wrangler-related entries (`.wrangler`, `.dev.vars*`, `.env*`, and their + * negated example patterns). + * + * Creates the file if it does not already exist. + * + * @param filePath Absolute or relative path to the ignore file to update. + */ +export function maybeAppendWranglerToGitIgnoreLikeFile(filePath: string): void { + const filePreExisted = existsSync(filePath); + + if (!filePreExisted) { + writeFileSync(filePath, ""); + } + + const existingGitIgnoreContent = readFileSync(filePath); + const wranglerGitIgnoreFilesToAdd: string[] = []; + + const hasDotWrangler = existingGitIgnoreContent.match( + /^\/?\.wrangler(\/|\s|$)/m + ); + if (!hasDotWrangler) { + wranglerGitIgnoreFilesToAdd.push(".wrangler"); + } + + const hasDotDevDotVars = existingGitIgnoreContent.match( + /^\/?\.dev\.vars\*(\s|$)/m + ); + if (!hasDotDevDotVars) { + wranglerGitIgnoreFilesToAdd.push(".dev.vars*"); + } + + const hasDotDevVarsExample = existingGitIgnoreContent.match( + /^!\/?\.dev\.vars\.example(\s|$)/m + ); + if (!hasDotDevVarsExample) { + wranglerGitIgnoreFilesToAdd.push("!.dev.vars.example"); + } + + /** + * We check for the following type of occurrences: + * + * ``` + * .env + * .env* + * .env. + * .env*. + * ``` + * + * Any of these may alone on a line or be followed by a space and a trailing comment: + * + * ``` + * .env. # some trailing comment + * ``` + */ + const hasDotEnv = existingGitIgnoreContent.match( + /^\/?\.env\*?(\..*?)?(\s|$)/m + ); + if (!hasDotEnv) { + wranglerGitIgnoreFilesToAdd.push(".env*"); + } + + const hasDotEnvExample = existingGitIgnoreContent.match( + /^!\/?\.env\.example(\s|$)/m + ); + if (!hasDotEnvExample) { + wranglerGitIgnoreFilesToAdd.push("!.env.example"); + } + + if (wranglerGitIgnoreFilesToAdd.length === 0) { + return; + } + + const s = spinner(); + s.start(`Adding Wrangler files to the ${basename(filePath)} file`); + + const linesToAppend = [ + ...(filePreExisted + ? ["", ...(!existingGitIgnoreContent.match(/\n\s*$/) ? [""] : [])] + : []), + ]; + + if (!hasDotWrangler && wranglerGitIgnoreFilesToAdd.length > 1) { + linesToAppend.push("# wrangler files"); + } + + wranglerGitIgnoreFilesToAdd.forEach((line) => linesToAppend.push(line)); + + linesToAppend.push(""); + + appendFileSync(filePath, linesToAppend.join("\n")); + + const fileName = basename(filePath); + + s.stop( + `${brandColor(filePreExisted ? "updated" : "created")} ${dim( + `${fileName} file` + )}` + ); +} + +/** + * Appends any missing Wrangler-related entries to the project's `.gitignore` file. + * + * Bails out only when *neither* a `.gitignore` file nor a `.git` directory exists, + * which indicates the project is likely not targeting/using git. If either one is + * present the entries are appended (creating `.gitignore` if needed). + * + * @param projectPath Root directory of the project. + */ +export function maybeAppendWranglerToGitIgnore(projectPath: string): void { + const gitIgnorePath = `${projectPath}/.gitignore`; + const gitIgnorePreExisted = existsSync(gitIgnorePath); + const gitDirExists = directoryExists(`${projectPath}/.git`); + + if (!gitIgnorePreExisted && !gitDirExists) { + // if there is no .gitignore file and neither a .git directory + // then bail as the project is likely not targeting/using git + return; + } + + maybeAppendWranglerToGitIgnoreLikeFile(gitIgnorePath); +} + +/** + * Checks whether a directory exists at the given path. + * + * Returns `false` when the path does not exist (`ENOENT`). Re-throws any + * other filesystem error. + * + * @param path Path to check + * @returns `true` if a directory exists at `path`, `false` otherwise + */ +function directoryExists(path: string): boolean { + try { + const stat = statSync(path); + return stat.isDirectory(); + } catch (error) { + if ((error as { code: string }).code === "ENOENT") { + return false; + } + throw new Error(error as string); + } +} diff --git a/packages/cli/index.ts b/packages/cli/index.ts new file mode 100644 index 0000000..4c8b63d --- /dev/null +++ b/packages/cli/index.ts @@ -0,0 +1,280 @@ +import { exit } from "node:process"; +import { + bgBlue, + bgGreen, + bgRed, + bgYellow, + brandColor, + dim, + gray, + hidden, + white, +} from "./colors"; +import { stderr, stdout } from "./streams"; + +export const shapes = { + diamond: "◇", + dash: "─", + radioInactive: "○", + radioActive: "●", + + backActive: "◀", + backInactive: "◁", + + bar: "│", + leftT: "├", + rigthT: "┤", + + arrows: { + left: "‹", + right: "›", + }, + + corners: { + tl: "╭", + bl: "╰", + tr: "╮", + br: "╯", + }, +}; + +export const status = { + error: bgRed(` ERROR `), + warning: bgYellow(` WARNING `), + info: bgBlue(` INFO `), + success: bgGreen(` SUCCESS `), + cancel: white.bgRed(` X `), +}; + +// Returns a string containing n non-trimmable spaces +// This is useful for places where clack trims lines of output +// but we need leading spaces +export const space = (n = 1) => { + return hidden("\u200A".repeat(n)); +}; + +const LOGGER_LEVELS = { + none: -1, + error: 0, + warn: 1, + info: 2, + log: 3, + debug: 4, +} as const; + +export type LoggerLevel = keyof typeof LOGGER_LEVELS; + +// Global log level that can be set by consuming packages +let currentLogLevel: LoggerLevel = "log"; + +export function setLogLevel(level: LoggerLevel) { + currentLogLevel = level; +} + +export function getLogLevel(): LoggerLevel { + return currentLogLevel; +} + +// Primitive for printing to stdout. Use this instead of +// console.log or printing to stdout directly +export const logRaw = (msg: string) => { + // treat all log calls as 'log' level logs + const currentLevel = getLogLevel(); + + // Only output if current log level allows 'log' level messages + if (LOGGER_LEVELS[currentLevel] >= LOGGER_LEVELS.log) { + stdout.write(`${msg}\n`); + } +}; + +// A simple stylized log for use within a prompt +export const log = (msg: string) => { + const lines = msg + .split("\n") + .map((ln) => `${gray(shapes.bar)}${ln.length > 0 ? " " + white(ln) : ""}`); + + logRaw(lines.join("\n")); +}; + +export const newline = () => { + log(""); +}; + +type FormatOptions = { + linePrefix?: string; + firstLinePrefix?: string; + newlineBefore?: boolean; + newlineAfter?: boolean; + formatLine?: (line: string) => string; + multiline?: boolean; +}; +export const format = ( + msg: string, + { + linePrefix = gray(shapes.bar), + firstLinePrefix = linePrefix, + newlineBefore = false, + newlineAfter = false, + formatLine = (line: string) => white(line), + multiline = true, + }: FormatOptions = {} +) => { + const lines = multiline ? msg.split("\n") : [msg]; + const formattedLines = lines.map( + (line, i) => + (i === 0 ? firstLinePrefix : linePrefix) + space() + formatLine(line) + ); + + if (newlineBefore) { + formattedLines.unshift(linePrefix); + } + if (newlineAfter) { + formattedLines.push(linePrefix); + } + + return formattedLines.join("\n"); +}; + +// Log a simple status update with a style similar to the clack spinner +export const updateStatus = (msg: string, printNewLine = true) => { + logRaw( + format(msg, { + firstLinePrefix: gray(shapes.leftT), + linePrefix: gray(shapes.bar), + newlineAfter: printNewLine, + }) + ); +}; + +export const startSection = ( + heading: string, + subheading?: string, + printNewLine = true +) => { + logRaw( + `${gray(shapes.corners.tl)} ${brandColor(heading)} ${ + subheading ? dim(subheading) : "" + }` + ); + if (printNewLine) { + newline(); + } +}; + +export const endSection = (heading: string, subheading?: string) => { + logRaw( + `${gray(shapes.corners.bl)} ${brandColor(heading)} ${ + subheading ? dim(subheading) : "" + }\n` + ); +}; + +export const cancel = ( + msg: string, + { + // current default is backcompat and makes sense going forward too + shape = shapes.corners.bl, + // current default for backcompat -- TODO: change default to true once all callees have been updated + multiline = false, + } = {} +) => { + logRaw( + format(msg, { + firstLinePrefix: `${gray(shape)} ${status.cancel}`, + linePrefix: gray(shapes.bar), + newlineBefore: true, + formatLine: (line) => dim(line), // for backcompat but it's not ideal for this to be "dim" + multiline, + }) + ); +}; + +export const warn = ( + msg: string, + { + // current default for backcompat -- TODO: change default to shapes.bar once all callees have been updated + shape = shapes.corners.bl, + // current default for backcompat -- TODO: change default to true once all callees have been updated + multiline = false, + newlineBefore = true, + } = {} +) => { + logRaw( + format(msg, { + firstLinePrefix: gray(shape) + space() + status.warning, + linePrefix: gray(shapes.bar), + formatLine: (line) => dim(line), // for backcompat but it's not ideal for this to be "dim" + multiline, + newlineBefore, + }) + ); +}; + +export const success = ( + msg: string, + { + // current default for backcompat -- TODO: change default to shapes.bar once all callees have been updated + shape = shapes.corners.bl, + // current default for backcompat -- TODO: change default to true once all callees have been updated + multiline = false, + } = {} +) => { + logRaw( + format(msg, { + firstLinePrefix: gray(shape) + space() + status.success, + linePrefix: gray(shapes.bar), + newlineBefore: true, + formatLine: (line) => dim(line), // for backcompat but it's not ideal for this to be "dim" + multiline, + }) + ); +}; + +// Strip the ansi color characters out of the line when calculating +// line length, otherwise the padding will be thrown off +// Used from https://github.com/natemoo-re/clack/blob/main/packages/prompts/src/index.ts +export const stripAnsi = (str: string) => { + const pattern = [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", + ].join("|"); + const regex = RegExp(pattern, "g"); + + return str.replace(linkRegex, "$2").replace(regex, ""); +}; + +// Regular Expression that matches a hyperlink +// e.g. `\u001B]8;;http://example.com/\u001B\\This is a link\u001B]8;;\u001B\` +export const linkRegex = + // eslint-disable-next-line no-control-regex -- regex intentionally matches ANSI escape sequences for hyperlink parsing + /\u001B\]8;;(?.+)\u001B\\(?
+ ); +} + +const resources = [ + { + href: "https://reactrouter.com/docs", + text: "React Router Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json b/packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json new file mode 100644 index 0000000..c637606 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/tsconfig.cloudflare.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "include": [ + ".react-router/types/**/*", + "app/**/*", + "app/**/.server/**/*", + "app/**/.client/**/*", + "workers/**/*", + "worker-configuration.d.ts" + ], + "compilerOptions": { + "composite": true, + "strict": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/tsconfig.json b/packages/create-cloudflare/templates/react-router/ts/tsconfig.json new file mode 100644 index 0000000..d7ce9e4 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.cloudflare.json" } + ], + "compilerOptions": { + "checkJs": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json b/packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json new file mode 100644 index 0000000..bf6283b --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": ["vite.config.ts"], + "compilerOptions": { + "composite": true, + "strict": true, + "types": ["node"], + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } +} diff --git a/packages/create-cloudflare/templates/react-router/ts/vite.config.ts b/packages/create-cloudflare/templates/react-router/ts/vite.config.ts new file mode 100644 index 0000000..ccca160 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/vite.config.ts @@ -0,0 +1,15 @@ +import { reactRouter } from "@react-router/dev/vite"; +import { cloudflare } from "@cloudflare/vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + cloudflare({ viteEnvironment: { name: "ssr" } }), + tailwindcss(), + reactRouter(), + ], + resolve: { + tsconfigPaths: true, + }, +}); diff --git a/packages/create-cloudflare/templates/react-router/ts/workers/app.ts b/packages/create-cloudflare/templates/react-router/ts/workers/app.ts new file mode 100644 index 0000000..dd58056 --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/workers/app.ts @@ -0,0 +1,12 @@ +import { createRequestHandler } from "react-router"; + +const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE, +); + +export default { + async fetch(request) { + return requestHandler(request); + }, +} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc b/packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc new file mode 100644 index 0000000..5f6f51c --- /dev/null +++ b/packages/create-cloudflare/templates/react-router/ts/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "", + "compatibility_date": "", + "main": "./workers/app.ts", + "vars": { + "VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare" + } +} diff --git a/packages/create-cloudflare/templates/react/c3.ts b/packages/create-cloudflare/templates/react/c3.ts new file mode 100644 index 0000000..b0ca1d2 --- /dev/null +++ b/packages/create-cloudflare/templates/react/c3.ts @@ -0,0 +1,9 @@ +import pages from "./pages/c3"; +import workers from "./workers/c3"; +import type { MultiPlatformTemplateConfig } from "../../src/templates"; + +const config: MultiPlatformTemplateConfig = { + displayName: "React", + platformVariants: { pages, workers }, +}; +export default config; diff --git a/packages/create-cloudflare/templates/react/pages/c3.ts b/packages/create-cloudflare/templates/react/pages/c3.ts new file mode 100644 index 0000000..adfc872 --- /dev/null +++ b/packages/create-cloudflare/templates/react/pages/c3.ts @@ -0,0 +1,84 @@ +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { inputPrompt } from "@cloudflare/cli-shared-helpers/interactive"; +import { runFrameworkGenerator } from "frameworks/index"; +import { detectPackageManager } from "helpers/packageManagers"; +import type { TemplateConfig } from "../../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + const variant = await getVariant(ctx); + + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--template", + variant, + "--no-immediate", + ]); + + logRaw(""); +}; + +const variantsOptions = [ + { + value: "react-ts", + label: "TypeScript", + }, + { + value: "react-swc-ts", + label: "TypeScript + SWC", + }, + { + value: "react", + label: "JavaScript", + }, + { + value: "react-swc", + label: "JavaScript + SWC", + }, +]; + +async function getVariant(ctx: C3Context) { + if (ctx.args.variant) { + const selected = variantsOptions.find((v) => v.value === ctx.args.variant); + if (!selected) { + throw new Error( + `Unknown variant "${ctx.args.variant}". Valid variants are: ${variantsOptions.map((v) => v.value).join(", ")}` + ); + } + return selected.value; + } + + return await inputPrompt({ + type: "select", + question: "Select a variant:", + label: "variant", + options: variantsOptions, + defaultValue: variantsOptions[0].value, + }); +} + +const config: TemplateConfig = { + configVersion: 1, + id: "react", + // React's documentation now recommends using create-vite. + frameworkCli: "create-vite", + displayName: "React", + platform: "pages", + hidden: true, + path: "templates/react/pages", + copyFiles: { path: "./templates" }, + generate, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run build && wrangler pages deploy`, + preview: `${npm} run build && wrangler pages dev`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/react/pages/templates/wrangler.jsonc b/packages/create-cloudflare/templates/react/pages/templates/wrangler.jsonc new file mode 100644 index 0000000..76834d0 --- /dev/null +++ b/packages/create-cloudflare/templates/react/pages/templates/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "", + "compatibility_date": "", + "pages_build_output_dir": "./dist" + } diff --git a/packages/create-cloudflare/templates/react/workers/c3.ts b/packages/create-cloudflare/templates/react/workers/c3.ts new file mode 100644 index 0000000..26024d1 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/c3.ts @@ -0,0 +1,207 @@ +import assert from "node:assert"; +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { + inputPrompt, + spinner, +} from "@cloudflare/cli-shared-helpers/interactive"; +import { transformFile } from "@cloudflare/codemod"; +import { runFrameworkGenerator } from "frameworks/index"; +import { readJSON, usesTypescript, writeJSON } from "helpers/files"; +import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; +import * as recast from "recast"; +import type { TemplateConfig } from "../../../src/templates"; +import type { types } from "recast"; +import type { C3Context } from "types"; + +const b = recast.types.builders; +const t = recast.types.namedTypes; +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + const variant = await getVariant(ctx); + ctx.args.lang = variant.lang; + + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--template", + variant.value, + "--no-immediate", + ]); + + logRaw(""); +}; + +const configure = async (ctx: C3Context) => { + await installPackages(["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, + }); + + transformViteConfig(ctx); + + if (usesTypescript(ctx)) { + updateTsconfigJson(); + } +}; + +function transformViteConfig(ctx: C3Context) { + const filePath = `vite.config.${usesTypescript(ctx) ? "ts" : "js"}`; + + transformFile(filePath, { + visitProgram(n) { + // Add an import of the @cloudflare/vite-plugin + // ``` + // import {cloudflare} from "@cloudflare/vite-plugin"; + // ``` + const lastImportIndex = n.node.body.findLastIndex( + (statement) => statement.type === "ImportDeclaration" + ); + const lastImport = n.get("body", lastImportIndex); + const importAst = b.importDeclaration( + [b.importSpecifier(b.identifier("cloudflare"))], + b.stringLiteral("@cloudflare/vite-plugin") + ); + lastImport.insertAfter(importAst); + + return this.traverse(n); + }, + visitCallExpression: function (n) { + // Add the imported plugin to the config + // ``` + // defineConfig({ + // plugins: [react(), cloudflare()], + // }); + const callee = n.node.callee as types.namedTypes.Identifier; + if (callee.name !== "defineConfig") { + return this.traverse(n); + } + + const config = n.node.arguments[0]; + assert(t.ObjectExpression.check(config)); + const pluginsProp = config.properties.find((prop) => isPluginsProp(prop)); + assert(pluginsProp && t.ArrayExpression.check(pluginsProp.value)); + pluginsProp.value.elements.push( + b.callExpression(b.identifier("cloudflare"), []) + ); + + return false; + }, + }); +} + +function isPluginsProp( + prop: unknown +): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { + return ( + (t.Property.check(prop) || t.ObjectProperty.check(prop)) && + t.Identifier.check(prop.key) && + prop.key.name === "plugins" + ); +} + +function updateTsconfigJson() { + const s = spinner(); + s.start(`Updating tsconfig.json config`); + // Add a reference to the extra tsconfig.worker.json file. + // ``` + // "references": [ ..., { path: "./tsconfig.worker.json" } ] + // ``` + const tsconfig = readJSON("tsconfig.json") as { references: object[] }; + if (tsconfig && typeof tsconfig === "object") { + tsconfig.references ??= []; + tsconfig.references.push({ path: "./tsconfig.worker.json" }); + } + writeJSON("tsconfig.json", tsconfig); + s.stop(`${brandColor(`updated`)} ${dim(`\`tsconfig.json\``)}`); +} + +async function getVariant(ctx: C3Context) { + const variantsOptions = [ + { + value: "react-ts", + lang: "ts", + label: "TypeScript", + }, + { + value: "react", + lang: "js", + label: "JavaScript", + }, + ]; + + // If variant is provided via CLI args, use it directly + if (ctx.args.variant) { + const deprecatedVariantReplacements: Record = { + "react-swc-ts": "react-ts", + "react-swc": "react", + }; + + const replacement = deprecatedVariantReplacements[ctx.args.variant]; + if (replacement) { + throw new Error( + `The React variant "${ctx.args.variant}" is no longer available. Use "${replacement}" instead.` + ); + } + + const selected = variantsOptions.find( + (variant) => variant.value === ctx.args.variant + ); + if (!selected) { + throw new Error( + `Unknown variant "${ + ctx.args.variant + }". Valid variants are: ${variantsOptions + .map((v) => v.value) + .join(", ")}` + ); + } + return selected; + } + + const value = await inputPrompt({ + type: "select", + question: "Select a variant:", + label: "variant", + options: variantsOptions, + defaultValue: variantsOptions[0].value, + }); + + const selected = variantsOptions.find((variant) => variant.value === value); + assert(selected, "Expected a variant to be selected"); + return selected; +} + +const config: TemplateConfig = { + configVersion: 1, + id: "react", + frameworkCli: "create-vite", + displayName: "React", + platform: "workers", + path: "templates/react/workers", + copyFiles: { + variants: { + ts: { + path: "./ts", + }, + js: { + path: "./js", + }, + }, + }, + generate, + configure, + transformPackageJson: async (_, ctx) => ({ + scripts: { + deploy: `${npm} run build && wrangler deploy`, + preview: `${npm} run build && vite preview`, + ...(usesTypescript(ctx) && { "cf-typegen": `wrangler types` }), + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/react/workers/js/src/App.jsx b/packages/create-cloudflare/templates/react/workers/js/src/App.jsx new file mode 100644 index 0000000..b9b5755 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/js/src/App.jsx @@ -0,0 +1,148 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from './assets/vite.svg' +import cloudflareLogo from './assets/cloudflare.svg' +import heroImg from './assets/hero.png' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + const [name, setName] = useState('unknown') + + return ( + <> +
+
+ + React logo + Vite logo +
+
+

Get started with Cloudflare

+

+ Edit src/App.jsx or worker/index.js and save to test HMR +

+
+
    +
  • + +
  • +
  • + +
  • +
+ + +
+ +
+ +
+
+ +

Documentation

+

Your questions, answered

+ +
+
+ +

Connect with us

+

Join the Vite community

+ +
+
+ +
+
+ + ) +} + +export default App diff --git a/packages/create-cloudflare/templates/react/workers/js/src/assets/cloudflare.svg b/packages/create-cloudflare/templates/react/workers/js/src/assets/cloudflare.svg new file mode 100644 index 0000000..d375872 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/js/src/assets/cloudflare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/create-cloudflare/templates/react/workers/js/worker/index.js b/packages/create-cloudflare/templates/react/workers/js/worker/index.js new file mode 100644 index 0000000..b86210e --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/js/worker/index.js @@ -0,0 +1,13 @@ +export default { + fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + + return new Response(null, { status: 404 }); + }, +} diff --git a/packages/create-cloudflare/templates/react/workers/js/wrangler.jsonc b/packages/create-cloudflare/templates/react/workers/js/wrangler.jsonc new file mode 100644 index 0000000..c8e58db --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/js/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "name": "", + "main": "worker/index.js", + "compatibility_date": "", + "assets": { "not_found_handling": "single-page-application" }, + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates/react/workers/ts/src/App.tsx b/packages/create-cloudflare/templates/react/workers/ts/src/App.tsx new file mode 100644 index 0000000..229efb4 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/ts/src/App.tsx @@ -0,0 +1,148 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from './assets/vite.svg' +import cloudflareLogo from './assets/cloudflare.svg' +import heroImg from './assets/hero.png' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + const [name, setName] = useState('unknown') + + return ( + <> +
+
+ + React logo + Vite logo +
+
+

Get started with Cloudflare

+

+ Edit src/App.tsx or worker/index.ts and save to test HMR +

+
+
    +
  • + +
  • +
  • + +
  • +
+ + +
+ +
+ +
+
+ +

Documentation

+

Your questions, answered

+ +
+
+ +

Connect with us

+

Join the Vite community

+ +
+
+ +
+
+ + ) +} + +export default App diff --git a/packages/create-cloudflare/templates/react/workers/ts/src/assets/cloudflare.svg b/packages/create-cloudflare/templates/react/workers/ts/src/assets/cloudflare.svg new file mode 100644 index 0000000..d375872 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/ts/src/assets/cloudflare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/create-cloudflare/templates/react/workers/ts/tsconfig.worker.json b/packages/create-cloudflare/templates/react/workers/ts/tsconfig.worker.json new file mode 100644 index 0000000..38c0515 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/ts/tsconfig.worker.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", + "types": ["./worker-configuration.d.ts", "vite/client"], + }, + "include": ["worker"] +} diff --git a/packages/create-cloudflare/templates/react/workers/ts/worker/index.ts b/packages/create-cloudflare/templates/react/workers/ts/worker/index.ts new file mode 100644 index 0000000..67cc722 --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/ts/worker/index.ts @@ -0,0 +1,12 @@ +export default { + fetch(request) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + return new Response(null, { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates/react/workers/ts/wrangler.jsonc b/packages/create-cloudflare/templates/react/workers/ts/wrangler.jsonc new file mode 100644 index 0000000..63adfcf --- /dev/null +++ b/packages/create-cloudflare/templates/react/workers/ts/wrangler.jsonc @@ -0,0 +1,9 @@ +{ + "name": "", + "main": "worker/index.ts", + "compatibility_date": "", + "assets": { "not_found_handling": "single-page-application" }, + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates/redwood/c3.ts b/packages/create-cloudflare/templates/redwood/c3.ts new file mode 100644 index 0000000..f6ca13a --- /dev/null +++ b/packages/create-cloudflare/templates/redwood/c3.ts @@ -0,0 +1,33 @@ +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { runFrameworkGenerator } from "frameworks/index"; +import { detectPackageManager } from "helpers/packageManagers"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ctx.project.name]); + + logRaw(""); +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "redwood", + platform: "workers", + frameworkCli: "create-rwsdk", + displayName: "RedwoodSDK", + generate, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run release`, + preview: `${npm} run build && wrangler dev`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "release", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/scheduled/c3.ts b/packages/create-cloudflare/templates/scheduled/c3.ts new file mode 100644 index 0000000..524f4f9 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/c3.ts @@ -0,0 +1,22 @@ +import type { TemplateConfig } from "../../src/templates"; + +const config: TemplateConfig = { + configVersion: 1, + id: "scheduled", + displayName: "Scheduled Worker (Cron Trigger)", + description: + "Create a Worker to be executed on a schedule for periodic (cron) jobs", + platform: "workers", + copyFiles: { + variants: { + js: { + path: "./js", + }, + ts: { + path: "./ts", + }, + }, + }, +}; + +export default config; diff --git a/packages/create-cloudflare/templates/scheduled/js/.editorconfig b/packages/create-cloudflare/templates/scheduled/js/.editorconfig new file mode 100644 index 0000000..a727df3 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/js/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space diff --git a/packages/create-cloudflare/templates/scheduled/js/.gitignore b/packages/create-cloudflare/templates/scheduled/js/.gitignore new file mode 100644 index 0000000..4138168 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/js/.gitignore @@ -0,0 +1,167 @@ +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars* +!.dev.vars.example +.env* +!.env.example +.wrangler/ diff --git a/packages/create-cloudflare/templates/scheduled/js/.prettierrc b/packages/create-cloudflare/templates/scheduled/js/.prettierrc new file mode 100644 index 0000000..5c7b5d3 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/js/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 140, + "singleQuote": true, + "semi": true, + "useTabs": true +} diff --git a/packages/create-cloudflare/templates/scheduled/js/package.json b/packages/create-cloudflare/templates/scheduled/js/package.json new file mode 100644 index 0000000..b813e55 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/js/package.json @@ -0,0 +1,13 @@ +{ + "name": "", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev --test-scheduled", + "start": "wrangler dev --test-scheduled" + }, + "devDependencies": { + "wrangler": "^3.101.0" + } +} diff --git a/packages/create-cloudflare/templates/scheduled/js/src/index.js b/packages/create-cloudflare/templates/scheduled/js/src/index.js new file mode 100644 index 0000000..0c21a75 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/js/src/index.js @@ -0,0 +1,37 @@ +/** + * Welcome to Cloudflare Workers! + * + * This is a template for a Scheduled Worker: a Worker that can run on a + * configurable interval: + * https://developers.cloudflare.com/workers/platform/triggers/cron-triggers/ + * + * - Run `npm run dev` in your terminal to start a development server + * - Run `curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"` to see your worker in action + * - Run `npm run deploy` to publish your worker + * + * Learn more at https://developers.cloudflare.com/workers/ + */ + +export default { + async fetch(req) { + const url = new URL(req.url) + url.pathname = "/__scheduled"; + url.searchParams.append("cron", "* * * * *"); + return new Response(`To test the scheduled handler, ensure you have used the "--test-scheduled" then try running "curl ${url.href}".`); + }, + + // The scheduled handler is invoked at the interval set in our wrangler.jsonc's + // [[triggers]] configuration. + async scheduled(event, env, ctx) { + // A Cron Trigger can make requests to other endpoints on the Internet, + // publish to a Queue, query a D1 Database, and much more. + // + // We'll keep it simple and make an API call to a Cloudflare API: + let resp = await fetch('https://api.cloudflare.com/client/v4/ips'); + let wasSuccessful = resp.ok ? 'success' : 'fail'; + + // You could store this result in KV, write to a D1 Database, or publish to a Queue. + // In this template, we'll just log the result: + console.log(`trigger fired at ${event.cron}: ${wasSuccessful}`); + }, +}; diff --git a/packages/create-cloudflare/templates/scheduled/js/wrangler.jsonc b/packages/create-cloudflare/templates/scheduled/js/wrangler.jsonc new file mode 100644 index 0000000..589cd5d --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/js/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "name": "", + "main": "src/index.js", + "compatibility_date": "", + "observability": { + "enabled": true + }, + "triggers": { + "crons": [ + "* * * * *" + ] + } +} diff --git a/packages/create-cloudflare/templates/scheduled/ts/.editorconfig b/packages/create-cloudflare/templates/scheduled/ts/.editorconfig new file mode 100644 index 0000000..a727df3 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space diff --git a/packages/create-cloudflare/templates/scheduled/ts/.gitignore b/packages/create-cloudflare/templates/scheduled/ts/.gitignore new file mode 100644 index 0000000..4138168 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/.gitignore @@ -0,0 +1,167 @@ +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars* +!.dev.vars.example +.env* +!.env.example +.wrangler/ diff --git a/packages/create-cloudflare/templates/scheduled/ts/.prettierrc b/packages/create-cloudflare/templates/scheduled/ts/.prettierrc new file mode 100644 index 0000000..5c7b5d3 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 140, + "singleQuote": true, + "semi": true, + "useTabs": true +} diff --git a/packages/create-cloudflare/templates/scheduled/ts/package.json b/packages/create-cloudflare/templates/scheduled/ts/package.json new file mode 100644 index 0000000..3e67c24 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/package.json @@ -0,0 +1,15 @@ +{ + "name": "", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev --test-scheduled", + "start": "wrangler dev --test-scheduled", + "cf-typegen": "wrangler types" + }, + "devDependencies": { + "typescript": "^5.5.2", + "wrangler": "^3.101.0" + } +} \ No newline at end of file diff --git a/packages/create-cloudflare/templates/scheduled/ts/src/index.ts b/packages/create-cloudflare/templates/scheduled/ts/src/index.ts new file mode 100644 index 0000000..084ac4e --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/src/index.ts @@ -0,0 +1,40 @@ +/** + * Welcome to Cloudflare Workers! + * + * This is a template for a Scheduled Worker: a Worker that can run on a + * configurable interval: + * https://developers.cloudflare.com/workers/platform/triggers/cron-triggers/ + * + * - Run `npm run dev` in your terminal to start a development server + * - Run `curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"` to see your Worker in action + * - Run `npm run deploy` to publish your Worker + * + * Bind resources to your Worker in `wrangler.jsonc`. After adding bindings, a type definition for the + * `Env` object can be regenerated with `npm run cf-typegen`. + * + * Learn more at https://developers.cloudflare.com/workers/ + */ + +export default { + async fetch(req) { + const url = new URL(req.url); + url.pathname = '/__scheduled'; + url.searchParams.append('cron', '* * * * *'); + return new Response(`To test the scheduled handler, ensure you have used the "--test-scheduled" then try running "curl ${url.href}".`); + }, + + // The scheduled handler is invoked at the interval set in our wrangler.jsonc's + // [[triggers]] configuration. + async scheduled(event, env, ctx): Promise { + // A Cron Trigger can make requests to other endpoints on the Internet, + // publish to a Queue, query a D1 Database, and much more. + // + // We'll keep it simple and make an API call to a Cloudflare API: + let resp = await fetch('https://api.cloudflare.com/client/v4/ips'); + let wasSuccessful = resp.ok ? 'success' : 'fail'; + + // You could store this result in KV, write to a D1 Database, or publish to a Queue. + // In this template, we'll just log the result: + console.log(`trigger fired at ${event.cron}: ${wasSuccessful}`); + }, +} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates/scheduled/ts/tsconfig.json b/packages/create-cloudflare/templates/scheduled/ts/tsconfig.json new file mode 100644 index 0000000..6841f3f --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2024", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2024"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "es2022", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "node", + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true + } +} diff --git a/packages/create-cloudflare/templates/scheduled/ts/wrangler.jsonc b/packages/create-cloudflare/templates/scheduled/ts/wrangler.jsonc new file mode 100644 index 0000000..01cb2f2 --- /dev/null +++ b/packages/create-cloudflare/templates/scheduled/ts/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "name": "", + "main": "src/index.ts", + "compatibility_date": "", + "observability": { + "enabled": true + }, + "triggers": { + "crons": [ + "* * * * *" + ] + } +} diff --git a/packages/create-cloudflare/templates/solid/c3.ts b/packages/create-cloudflare/templates/solid/c3.ts new file mode 100644 index 0000000..3ffd823 --- /dev/null +++ b/packages/create-cloudflare/templates/solid/c3.ts @@ -0,0 +1,77 @@ +import { logRaw, updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue } from "@cloudflare/cli-shared-helpers/colors"; +import { mergeObjectProperties, transformFile } from "@cloudflare/codemod"; +import { runFrameworkGenerator } from "frameworks/index"; +import { usesTypescript } from "helpers/files"; +import { detectPackageManager } from "helpers/packageManagers"; +import * as recast from "recast"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + // Run the create-solid command + // -s flag forces solid-start + await runFrameworkGenerator(ctx, ["-p", ctx.project.name, "-s"]); + + logRaw(""); +}; + +const configure = async (ctx: C3Context) => { + usesTypescript(ctx); + const filePath = `vite.config.${usesTypescript(ctx) ? "ts" : "js"}`; + + updateStatus(`Updating configuration in ${blue(filePath)}`); + + transformFile(filePath, { + visitCallExpression: function (n) { + const callee = n.node.callee as recast.types.namedTypes.Identifier; + if (callee.name !== "nitro") { + return this.traverse(n); + } + + const b = recast.types.builders; + const presetProp = b.objectProperty( + b.identifier("preset"), + b.stringLiteral("cloudflare-module") + ); + + if (n.node.arguments.length === 0) { + n.node.arguments.push(b.objectExpression([presetProp])); + } else { + mergeObjectProperties( + n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, + [presetProp] + ); + } + + return false; + }, + }); +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "solid", + frameworkCli: "create-solid", + displayName: "SolidStart", + platform: "workers", + copyFiles: { + path: "./templates", + }, + path: "templates/solid", + generate, + configure, + transformPackageJson: async () => ({ + scripts: { + preview: `${npm} run build && npx wrangler dev`, + deploy: `${npm} run build && wrangler deploy`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc b/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc new file mode 100644 index 0000000..a45f92a --- /dev/null +++ b/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "name": "", + "compatibility_date": "", + "compatibility_flags": [ + "nodejs_compat" + ], + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates/svelte/c3.ts b/packages/create-cloudflare/templates/svelte/c3.ts new file mode 100644 index 0000000..84b00eb --- /dev/null +++ b/packages/create-cloudflare/templates/svelte/c3.ts @@ -0,0 +1,9 @@ +import pages from "./pages/c3"; +import workers from "./workers/c3"; +import type { MultiPlatformTemplateConfig } from "../../src/templates"; + +const config: MultiPlatformTemplateConfig = { + displayName: "SvelteKit", + platformVariants: { pages, workers }, +}; +export default config; diff --git a/packages/create-cloudflare/templates/svelte/pages/c3.ts b/packages/create-cloudflare/templates/svelte/pages/c3.ts new file mode 100644 index 0000000..d66555f --- /dev/null +++ b/packages/create-cloudflare/templates/svelte/pages/c3.ts @@ -0,0 +1,163 @@ +import { existsSync } from "node:fs"; +import { logRaw, updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue, brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { transformFile } from "@cloudflare/codemod"; +import { runFrameworkGenerator } from "frameworks/index"; +import { usesTypescript } from "helpers/files"; +import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; +import * as recast from "recast"; +import type { TemplateConfig } from "../../../src/templates"; +import type { C3Context, PackageJson } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, ["create", ctx.project.name]); + + logRaw(""); +}; + +const configure = async (ctx: C3Context) => { + // Install the adapter + const pkg = `@sveltejs/adapter-cloudflare`; + await installPackages([pkg], { + dev: true, + startText: "Adding the Cloudflare Pages adapter", + doneText: `${brandColor(`installed`)} ${dim(pkg)}`, + }); + + updateViteConfig(ctx); + updatePlaywrightConfig(usesTypescript(ctx)); + updateTypeDefinitions(ctx); +}; + +const updateViteConfig = (ctx: C3Context) => { + // As of `sv` 0.16, the adapter is configured in the Vite config rather than + // in `svelte.config.js`, so all we need to do is change the adapter import + // statement there. + const configFile = usesTypescript(ctx) ? "vite.config.ts" : "vite.config.js"; + updateStatus(`Changing adapter in ${blue(configFile)}`); + + transformFile(configFile, { + visitImportDeclaration: function (n) { + // importSource is the `x` in `import y from "x"` + const importSource = n.value.source; + if (importSource.value === "@sveltejs/adapter-auto") { + importSource.value = "@sveltejs/adapter-cloudflare"; + } + + // stop traversing this node + return false; + }, + }); +}; + +const updatePlaywrightConfig = (shouldUseTypescript: boolean) => { + const filePath = `playwright.config.${shouldUseTypescript ? "ts" : "js"}`; + if (!existsSync(filePath)) { + return; + } + + updateStatus(`Changing webServer port in ${blue(filePath)}`); + + transformFile(filePath, { + visitObjectExpression: function (n) { + const portProp = n.node.properties.find((prop) => { + if (!("key" in prop) || !("name" in prop.key)) { + return false; + } + + return prop.key.name === "port"; + }); + + if (!portProp || !("value" in portProp) || !("value" in portProp.value)) { + return this.traverse(n); + } + + portProp.value.value = 8788; + return false; + }, + }); +}; + +const updateTypeDefinitions = (ctx: C3Context) => { + if (!usesTypescript(ctx)) { + return; + } + + updateStatus(`Updating global type definitions in ${blue("app.d.ts")}`); + + const b = recast.types.builders; + + transformFile("src/app.d.ts", { + visitTSModuleDeclaration(n) { + if (n.value.id.name === "App" && n.node.body) { + const moduleBlock = n.node + .body as recast.types.namedTypes.TSModuleBlock; + + const platformInterface = b.tsInterfaceDeclaration( + b.identifier("Platform"), + b.tsInterfaceBody([ + b.tsPropertySignature( + b.identifier("env"), + b.tsTypeAnnotation(b.tsTypeReference(b.identifier("Env"))) + ), + b.tsPropertySignature( + b.identifier("cf"), + b.tsTypeAnnotation( + b.tsTypeReference(b.identifier("CfProperties")) + ) + ), + b.tsPropertySignature( + b.identifier("ctx"), + b.tsTypeAnnotation( + b.tsTypeReference(b.identifier("ExecutionContext")) + ) + ), + ]) + ); + + moduleBlock.body.unshift(platformInterface); + } + + this.traverse(n); + }, + }); +}; + +const typesPath = "src/worker-configuration.d.ts"; +const config: TemplateConfig = { + configVersion: 1, + id: "svelte", + frameworkCli: "sv", + displayName: "SvelteKit", + platform: "pages", + hidden: true, + copyFiles: { + path: "./templates", + }, + path: "templates/svelte/pages", + generate, + configure, + transformPackageJson: async (original: PackageJson, ctx: C3Context) => { + let scripts: Record = { + preview: `${npm} run build && wrangler pages dev`, + deploy: `${npm} run build && wrangler pages deploy`, + }; + + if (usesTypescript(ctx)) { + scripts = { + ...scripts, + "cf-typegen": `wrangler types ${typesPath}`, + }; + } + + return { scripts }; + }, + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", + typesPath, +}; +export default config; diff --git a/packages/create-cloudflare/templates/svelte/pages/templates/wrangler.jsonc b/packages/create-cloudflare/templates/svelte/pages/templates/wrangler.jsonc new file mode 100644 index 0000000..916e26c --- /dev/null +++ b/packages/create-cloudflare/templates/svelte/pages/templates/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "", + "compatibility_date": "", + "compatibility_flags": ["nodejs_als"], + "pages_build_output_dir": ".svelte-kit/cloudflare" +} diff --git a/packages/create-cloudflare/templates/svelte/workers/c3.ts b/packages/create-cloudflare/templates/svelte/workers/c3.ts new file mode 100644 index 0000000..7e71e4c --- /dev/null +++ b/packages/create-cloudflare/templates/svelte/workers/c3.ts @@ -0,0 +1,132 @@ +import { logRaw, updateStatus } from "@cloudflare/cli-shared-helpers"; +import { blue, brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { transformFile } from "@cloudflare/codemod"; +import { runFrameworkGenerator } from "frameworks/index"; +import { usesTypescript } from "helpers/files"; +import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; +import * as recast from "recast"; +import type { TemplateConfig } from "../../../src/templates"; +import type { C3Context, PackageJson } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, ["create", ctx.project.name]); + + logRaw(""); +}; + +const configure = async (ctx: C3Context) => { + // Install the adapter + const pkg = `@sveltejs/adapter-cloudflare`; + await installPackages([pkg], { + dev: true, + startText: "Adding the Cloudflare adapter", + doneText: `${brandColor(`installed`)} ${dim(pkg)}`, + }); + + updateViteConfig(ctx); + updateTypeDefinitions(ctx); +}; + +const updateViteConfig = (ctx: C3Context) => { + // As of `sv` 0.16, the adapter is configured in the Vite config rather than + // in `svelte.config.js`, so all we need to do is change the adapter import + // statement there. + const configFile = usesTypescript(ctx) ? "vite.config.ts" : "vite.config.js"; + updateStatus(`Changing adapter in ${blue(configFile)}`); + + transformFile(configFile, { + visitImportDeclaration: function (n) { + // importSource is the `x` in `import y from "x"` + const importSource = n.value.source; + if (importSource.value === "@sveltejs/adapter-auto") { + importSource.value = "@sveltejs/adapter-cloudflare"; + } + + // stop traversing this node + return false; + }, + }); +}; + +const updateTypeDefinitions = (ctx: C3Context) => { + if (!usesTypescript(ctx)) { + return; + } + + updateStatus(`Updating global type definitions in ${blue("app.d.ts")}`); + + const b = recast.types.builders; + + transformFile("src/app.d.ts", { + visitTSModuleDeclaration(n) { + if (n.value.id.name === "App" && n.node.body) { + const moduleBlock = n.node + .body as recast.types.namedTypes.TSModuleBlock; + + const platformInterface = b.tsInterfaceDeclaration( + b.identifier("Platform"), + b.tsInterfaceBody([ + b.tsPropertySignature( + b.identifier("env"), + b.tsTypeAnnotation(b.tsTypeReference(b.identifier("Env"))) + ), + b.tsPropertySignature( + b.identifier("cf"), + b.tsTypeAnnotation( + b.tsTypeReference(b.identifier("CfProperties")) + ) + ), + b.tsPropertySignature( + b.identifier("ctx"), + b.tsTypeAnnotation( + b.tsTypeReference(b.identifier("ExecutionContext")) + ) + ), + ]) + ); + + moduleBlock.body.unshift(platformInterface); + } + + this.traverse(n); + }, + }); +}; + +const typesPath = "./src/worker-configuration.d.ts"; +const config: TemplateConfig = { + configVersion: 1, + id: "svelte", + frameworkCli: "sv", + displayName: "SvelteKit", + platform: "workers", + copyFiles: { + path: "./templates", + }, + path: "templates/svelte/workers", + generate, + configure, + transformPackageJson: async (original: PackageJson, ctx: C3Context) => { + let scripts: Record = { + preview: `${npm} run build && wrangler dev`, + deploy: `${npm} run build && wrangler deploy`, + }; + + if (usesTypescript(ctx)) { + scripts = { + ...scripts, + "cf-typegen": `wrangler types ${typesPath}`, + }; + } + + return { scripts }; + }, + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", + typesPath, +}; +export default config; diff --git a/packages/create-cloudflare/templates/svelte/workers/templates/static/.assetsignore b/packages/create-cloudflare/templates/svelte/workers/templates/static/.assetsignore new file mode 100644 index 0000000..1b006a0 --- /dev/null +++ b/packages/create-cloudflare/templates/svelte/workers/templates/static/.assetsignore @@ -0,0 +1,2 @@ +_worker.js +_routes.json \ No newline at end of file diff --git a/packages/create-cloudflare/templates/svelte/workers/templates/wrangler.jsonc b/packages/create-cloudflare/templates/svelte/workers/templates/wrangler.jsonc new file mode 100644 index 0000000..e7dec61 --- /dev/null +++ b/packages/create-cloudflare/templates/svelte/workers/templates/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "name": "", + "main": ".svelte-kit/cloudflare/_worker.js", + "compatibility_date": "", + "compatibility_flags": ["nodejs_als"], + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare" + }, + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates/tanstack-start/c3.ts b/packages/create-cloudflare/templates/tanstack-start/c3.ts new file mode 100644 index 0000000..f0c20e3 --- /dev/null +++ b/packages/create-cloudflare/templates/tanstack-start/c3.ts @@ -0,0 +1,45 @@ +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { runFrameworkGenerator } from "frameworks/index"; +import { detectPackageManager } from "helpers/packageManagers"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ + // @tanstack/cli uses `create` as a subcommand + "create", + ctx.project.name, + "--deployment", + "cloudflare", + "--framework", + "react", + // c3 will later install the dependencies + "--no-install", + // to prevent asking about git twice, just let c3 do it + "--no-git", + ]); + + logRaw(""); // newline +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "tanstack-start", + platform: "workers", + frameworkCli: "@tanstack/cli", + displayName: "TanStack Start", + generate, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run build && wrangler deploy`, + preview: `${npm} run build && vite preview`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/vike/c3.ts b/packages/create-cloudflare/templates/vike/c3.ts new file mode 100644 index 0000000..44901a8 --- /dev/null +++ b/packages/create-cloudflare/templates/vike/c3.ts @@ -0,0 +1,33 @@ +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { runFrameworkGenerator } from "frameworks/index"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--cloudflare", + // to prevent asking about git twice, just let c3 do it + "--no-git", + ]); + + logRaw(""); // newline +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "vike", + platform: "workers", + frameworkCli: "create-vike", + displayName: "Vike", + generate, + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", + transformPackageJson: async () => ({ + scripts: { + "cf-typegen": `wrangler types`, + }, + }), +}; +export default config; diff --git a/packages/create-cloudflare/templates/vike/experimental-c3.ts b/packages/create-cloudflare/templates/vike/experimental-c3.ts new file mode 100644 index 0000000..d5738bb --- /dev/null +++ b/packages/create-cloudflare/templates/vike/experimental-c3.ts @@ -0,0 +1,30 @@ +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { runFrameworkGenerator } from "frameworks/index"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ + ctx.project.name, + // Note: the vike create CLI supports a `--cloudflare` argument for creating cloudflare-ready + // projects, that works great but here we don't want to use that because we do want to + // exercise the general autoconfig/`wrangler setup` functionality (which amongst other + // things lets up ensure that we can support the migration of existing vike projects) + "--no-git", + ]); + + logRaw(""); // newline +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "vike", + platform: "workers", + frameworkCli: "create-vike", + displayName: "Vike", + generate, + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/vue/c3.ts b/packages/create-cloudflare/templates/vue/c3.ts new file mode 100644 index 0000000..ff3294a --- /dev/null +++ b/packages/create-cloudflare/templates/vue/c3.ts @@ -0,0 +1,9 @@ +import pages from "./pages/c3"; +import workers from "./workers/c3"; +import type { MultiPlatformTemplateConfig } from "../../src/templates"; + +const config: MultiPlatformTemplateConfig = { + displayName: "Vue", + platformVariants: { pages, workers }, +}; +export default config; diff --git a/packages/create-cloudflare/templates/vue/pages/c3.ts b/packages/create-cloudflare/templates/vue/pages/c3.ts new file mode 100644 index 0000000..60dad47 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/pages/c3.ts @@ -0,0 +1,33 @@ +import { runFrameworkGenerator } from "frameworks/index"; +import { detectPackageManager } from "helpers/packageManagers"; +import type { TemplateConfig } from "../../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ctx.project.name]); +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "vue", + frameworkCli: "create-vue", + displayName: "Vue", + platform: "pages", + hidden: true, + path: "templates/vue/pages", + copyFiles: { path: "./templates" }, + generate, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run build && wrangler pages deploy`, + preview: `${npm} run build && wrangler pages dev`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/vue/pages/templates/wrangler.jsonc b/packages/create-cloudflare/templates/vue/pages/templates/wrangler.jsonc new file mode 100644 index 0000000..76834d0 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/pages/templates/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "", + "compatibility_date": "", + "pages_build_output_dir": "./dist" + } diff --git a/packages/create-cloudflare/templates/vue/workers/c3.ts b/packages/create-cloudflare/templates/vue/workers/c3.ts new file mode 100644 index 0000000..e9c5283 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/c3.ts @@ -0,0 +1,101 @@ +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { + inputPrompt, + spinner, +} from "@cloudflare/cli-shared-helpers/interactive"; +import { runFrameworkGenerator } from "frameworks/index"; +import { readJSON, usesTypescript, writeJSON } from "helpers/files"; +import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; +import type { TemplateConfig } from "../../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + const lang = + ctx.args.lang ?? + (await inputPrompt({ + type: "select", + question: "Would you like to use TypeScript?", + label: "Language", + options: [ + { label: "TypeScript", value: "ts" }, + { label: "JavaScript", value: "js" }, + ], + })); + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--router", + ...(lang === "ts" ? ["--ts"] : []), + ]); + logRaw(""); +}; + +const configure = async (ctx: C3Context) => { + await installPackages(["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, + }); + + if (usesTypescript(ctx)) { + updateTsconfigJson(); + } +}; + +function updateTsconfigJson() { + const s = spinner(); + s.start(`Updating tsconfig.json config`); + // Add a reference to the extra tsconfig.worker.json file. + // ``` + // "references": [ ..., { path: "./tsconfig.worker.json" } ] + // ``` + const tsconfig = readJSON("tsconfig.json") as { references: object[] }; + if (tsconfig && typeof tsconfig === "object") { + tsconfig.references ??= []; + tsconfig.references.push({ path: "./tsconfig.worker.json" }); + } + writeJSON("tsconfig.json", tsconfig); + s.stop(`${brandColor(`updated`)} ${dim(`\`tsconfig.json\``)}`); +} + +const config: TemplateConfig = { + configVersion: 1, + id: "vue", + frameworkCli: "create-vue", + platform: "workers", + displayName: "Vue", + path: "templates/vue/workers", + copyFiles: { + async selectVariant(ctx) { + // Note: this `selectVariant` function should not be needed + // this is just a quick workaround until + // https://github.com/cloudflare/workers-sdk/issues/7495 + // is resolved + return usesTypescript(ctx) ? "ts" : "js"; + }, + variants: { + ts: { + path: "./ts", + }, + js: { + path: "./js", + }, + }, + }, + configure, + generate, + transformPackageJson: async (_, ctx) => ({ + scripts: { + deploy: `${npm} run build && wrangler deploy`, + preview: `${npm} run build && wrangler dev`, + ...(usesTypescript(ctx) && { "cf-typegen": `wrangler types` }), + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/vue/workers/js/server/index.js b/packages/create-cloudflare/templates/vue/workers/js/server/index.js new file mode 100644 index 0000000..0d41342 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/js/server/index.js @@ -0,0 +1,13 @@ +export default { + fetch(request) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + + return new Response(null, { status: 404 }); + }, +}; diff --git a/packages/create-cloudflare/templates/vue/workers/js/src/App.vue b/packages/create-cloudflare/templates/vue/workers/js/src/App.vue new file mode 100644 index 0000000..899b9aa --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/js/src/App.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/packages/create-cloudflare/templates/vue/workers/js/src/components/HelloWorld.vue b/packages/create-cloudflare/templates/vue/workers/js/src/components/HelloWorld.vue new file mode 100644 index 0000000..06f02e4 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/js/src/components/HelloWorld.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/create-cloudflare/templates/vue/workers/js/vite.config.js b/packages/create-cloudflare/templates/vue/workers/js/vite.config.js new file mode 100644 index 0000000..7ec8387 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/js/vite.config.js @@ -0,0 +1,21 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +import { cloudflare } from "@cloudflare/vite-plugin" + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + cloudflare() + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +}) diff --git a/packages/create-cloudflare/templates/vue/workers/js/wrangler.jsonc b/packages/create-cloudflare/templates/vue/workers/js/wrangler.jsonc new file mode 100644 index 0000000..c4c1c3a --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/js/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "name": "", + "compatibility_date": "", + "main": "server/index.js", + "assets": { + "not_found_handling": "single-page-application", + }, + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates/vue/workers/ts/server/index.ts b/packages/create-cloudflare/templates/vue/workers/ts/server/index.ts new file mode 100644 index 0000000..18b51f6 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/ts/server/index.ts @@ -0,0 +1,12 @@ +export default { + fetch(request) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + return new Response(null, { status: 404 }); + }, +} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates/vue/workers/ts/src/App.vue b/packages/create-cloudflare/templates/vue/workers/ts/src/App.vue new file mode 100644 index 0000000..3f9e89b --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/ts/src/App.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/packages/create-cloudflare/templates/vue/workers/ts/src/components/HelloWorld.vue b/packages/create-cloudflare/templates/vue/workers/ts/src/components/HelloWorld.vue new file mode 100644 index 0000000..5f42326 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/ts/src/components/HelloWorld.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/create-cloudflare/templates/vue/workers/ts/tsconfig.worker.json b/packages/create-cloudflare/templates/vue/workers/ts/tsconfig.worker.json new file mode 100644 index 0000000..b48c424 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/ts/tsconfig.worker.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", + "types": [ "./worker-configuration.d.ts","vite/client"], + }, + "include": ["server"], +} diff --git a/packages/create-cloudflare/templates/vue/workers/ts/vite.config.ts b/packages/create-cloudflare/templates/vue/workers/ts/vite.config.ts new file mode 100644 index 0000000..7ec8387 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/ts/vite.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +import { cloudflare } from "@cloudflare/vite-plugin" + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + cloudflare() + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +}) diff --git a/packages/create-cloudflare/templates/vue/workers/ts/wrangler.jsonc b/packages/create-cloudflare/templates/vue/workers/ts/wrangler.jsonc new file mode 100644 index 0000000..a6841d2 --- /dev/null +++ b/packages/create-cloudflare/templates/vue/workers/ts/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "name": "", + "compatibility_date": "", + "main": "server/index.ts", + "assets": { + "not_found_handling": "single-page-application", + }, + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates/waku/c3.ts b/packages/create-cloudflare/templates/waku/c3.ts new file mode 100644 index 0000000..74a221c --- /dev/null +++ b/packages/create-cloudflare/templates/waku/c3.ts @@ -0,0 +1,78 @@ +import { resolve } from "node:path"; +import { logRaw } from "@cloudflare/cli-shared-helpers"; +import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; +import { spinner } from "@cloudflare/cli-shared-helpers/interactive"; +import { runFrameworkGenerator } from "frameworks/index"; +import { removeFile } from "helpers/files"; +import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + // We use the default `create-waku` template and overlay our Cloudflare-specific + // files via `copyFiles`. This avoids depending on an external example repository + // for the scaffolded project content (which could be renamed, moved, or deleted). + await runFrameworkGenerator(ctx, [ + "--project-name", + ctx.project.name, + // c3 installs dependencies itself once the template files have been copied over + "--skip-install", + ]); + + logRaw(""); // newline +}; + +const configure = async (ctx: C3Context) => { + // `npmInstall` has already run with the default Waku template's `package.json`, + // which doesn't include the Cloudflare build dependencies that our overlaid + // `waku.config.ts` and Workers setup need. Install them now (this also adds them + // to `package.json`). `wrangler` is installed separately by `installWrangler()` + // in the main configure flow before this runs. + await installPackages( + ["@cloudflare/vite-plugin", "miniflare", "@types/node"], + { + dev: true, + startText: "Installing Cloudflare build dependencies", + doneText: `${brandColor("installed")} ${dim( + "@cloudflare/vite-plugin, miniflare, @types/node" + )}`, + } + ); + + // The default Waku template ships a Hono trailing-slash dev middleware. On + // Cloudflare this is handled by `html_handling: "drop-trailing-slash"` in + // `wrangler.jsonc` (and `public/_headers`), so remove the redundant middleware. + const s = spinner(); + s.start("Removing non-Cloudflare artifacts from template"); + removeFile(resolve(ctx.project.path, "src/middleware/no-trailing-slash.ts")); + s.stop(`${brandColor("removed")} ${dim("trailing-slash dev middleware")}`); +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "waku", + frameworkCli: "create-waku", + platform: "workers", + displayName: "Waku", + copyFiles: { + path: "./templates", + }, + path: "templates/waku", + generate, + configure, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run build && wrangler deploy`, + preview: `NODE_ENV=production ${npm} run build && wrangler dev`, + start: `wrangler dev`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/waku/experimental-c3.ts b/packages/create-cloudflare/templates/waku/experimental-c3.ts new file mode 100644 index 0000000..67a5b37 --- /dev/null +++ b/packages/create-cloudflare/templates/waku/experimental-c3.ts @@ -0,0 +1,30 @@ +import { runFrameworkGenerator } from "frameworks/index"; +import { detectPackageManager } from "helpers/packageManagers"; +import type { TemplateConfig } from "../../src/templates"; +import type { C3Context } from "types"; + +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ + "--project-name", + ctx.project.name, + // Note: we could point the waku create CLI to a cloudflare-ready template, that works great + // but here we don't want to use that because we do want to exercise the general + // autoconfig/`wrangler setup` functionality (which amongst other things lets up ensure + // that we can support the migration of existing waku projects) + ]); +}; + +const config: TemplateConfig = { + configVersion: 1, + id: "waku", + frameworkCli: "create-waku", + platform: "workers", + displayName: "Waku", + generate, + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/waku/templates/public/404.html b/packages/create-cloudflare/templates/waku/templates/public/404.html new file mode 100644 index 0000000..03332b4 --- /dev/null +++ b/packages/create-cloudflare/templates/waku/templates/public/404.html @@ -0,0 +1 @@ +

Not Found

diff --git a/packages/create-cloudflare/templates/waku/templates/public/_headers b/packages/create-cloudflare/templates/waku/templates/public/_headers new file mode 100644 index 0000000..bba7111 --- /dev/null +++ b/packages/create-cloudflare/templates/waku/templates/public/_headers @@ -0,0 +1,4 @@ +/RSC/* + X-Robots-Tag: noindex +/assets/* + Cache-Control: public, max-age=31536000, immutable diff --git a/packages/create-cloudflare/templates/waku/templates/src/waku.server.tsx b/packages/create-cloudflare/templates/waku/templates/src/waku.server.tsx new file mode 100644 index 0000000..6e3737b --- /dev/null +++ b/packages/create-cloudflare/templates/waku/templates/src/waku.server.tsx @@ -0,0 +1,18 @@ +import { fsRouter } from 'waku'; +import adapter from 'waku/adapters/cloudflare'; + +export default adapter(fsRouter(import.meta.glob('./pages/**/*.{tsx,ts}')), { + handlers: { + // Define additional Cloudflare Workers handlers here + // https://developers.cloudflare.com/workers/runtime-apis/handlers/ + // async queue( + // batch: MessageBatch, + // _env: Env, + // _ctx: ExecutionContext, + // ): Promise { + // for (const message of batch.messages) { + // console.log('Received', message); + // } + // }, + } satisfies ExportedHandler, +}); diff --git a/packages/create-cloudflare/templates/waku/templates/waku.config.ts b/packages/create-cloudflare/templates/waku/templates/waku.config.ts new file mode 100644 index 0000000..59b669c --- /dev/null +++ b/packages/create-cloudflare/templates/waku/templates/waku.config.ts @@ -0,0 +1,41 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import babel from '@rolldown/plugin-babel'; +import tailwindcss from '@tailwindcss/vite'; +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; +import { defineConfig } from 'waku/config'; + +export default defineConfig({ + vite: { + environments: { + rsc: { + optimizeDeps: { + include: ['hono/tiny'], + }, + build: { + rolldownOptions: { + platform: 'neutral', + }, + }, + }, + ssr: { + optimizeDeps: { + include: ['waku > rsc-html-stream/server'], + }, + build: { + rolldownOptions: { + platform: 'neutral', + }, + }, + }, + }, + plugins: [ + tailwindcss(), + react(), + babel({ presets: [reactCompilerPreset()] }), + cloudflare({ + viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] }, + inspectorPort: false, + }), + ], + }, +}); diff --git a/packages/create-cloudflare/templates/waku/templates/wrangler.jsonc b/packages/create-cloudflare/templates/waku/templates/wrangler.jsonc new file mode 100644 index 0000000..b0647aa --- /dev/null +++ b/packages/create-cloudflare/templates/waku/templates/wrangler.jsonc @@ -0,0 +1,26 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "", + "main": "./src/waku.server", + // nodejs_als is required for Waku server-side request context + // It can be removed if only building static pages + "compatibility_flags": ["nodejs_als"], + // https://developers.cloudflare.com/workers/platform/compatibility-dates + "compatibility_date": "", + "assets": { + // https://developers.cloudflare.com/workers/static-assets/binding/ + "binding": "ASSETS", + "directory": "./dist/public", + "html_handling": "drop-trailing-slash" + }, + "vars": { + "MAX_ITEMS": 10 + }, + "rules": [ + { + "type": "ESModule", + "globs": ["**/*.js", "**/*.mjs"] + } + ], + "no_bundle": true +} diff --git a/packages/create-cloudflare/tsconfig.json b/packages/create-cloudflare/tsconfig.json new file mode 100644 index 0000000..e369248 --- /dev/null +++ b/packages/create-cloudflare/tsconfig.json @@ -0,0 +1,32 @@ +{ + "root": true, + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "include": ["**/*.ts", "**/*.mts"], + "exclude": [ + "node_modules", + "dist", + "scripts/snippets/*", + "e2e/tests/frameworks/fixtures/*", + // exclude all template files other than those are directly imported into the program, + // e.g. the c3.ts files. + "templates*" + ], + "compilerOptions": { + "target": "ESNext", + "module": "preserve", + "moduleResolution": "bundler", + "allowJs": true, + "strict": true, + "esModuleInterop": true, + "outDir": "dist", + "types": ["node"], + "resolveJsonModule": true, + "paths": { + "helpers/*": ["./src/helpers/*"], + "templates/*": ["./templates/*"], + "templates-experimental/*": ["./templates-experimental/*"], + "frameworks/*": ["./src/frameworks/*"], + "types": ["./src/types.ts"] + } + } +} diff --git a/packages/create-cloudflare/turbo.json b/packages/create-cloudflare/turbo.json new file mode 100644 index 0000000..918fd28 --- /dev/null +++ b/packages/create-cloudflare/turbo.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "env": [ + "npm_config_user_agent", + "CREATE_CLOUDFLARE_RELAUNCHED", + "CREATE_CLOUDFLARE_TELEMETRY_DISABLED", + "CREATE_CLOUDFLARE_TELEMETRY_DEBUG", + "SPARROW_SOURCE_KEY" + ], + "inputs": ["$TURBO_DEFAULT$", "!e2e/**", "!vitest-e2e.config.ts"], + "outputs": ["dist/**"] + }, + "test:ci": { + "dependsOn": ["build"] + }, + "test:e2e": { + "passThroughEnv": ["GITHUB_TOKEN"], + "env": [ + "$TURBO_EXTENDS$", + "E2E_EXPERIMENTAL", + "E2E_TEST_FILTER", + "E2E_TEST_PM", + "E2E_TEST_PM_VERSION", + "E2E_WORKER_TEMPLATE_TO_TEST", + "E2E_FRAMEWORK_TEMPLATE_TO_TEST", + "E2E_PROJECT_PATH", + "E2E_TEST_RETRIES", + "E2E_RUN_DEPLOY_TESTS" + ], + "dependsOn": ["build"], + "inputs": ["e2e/**", "vitest-e2e.config.ts", "!e2e/README.md"], + "outputs": [".e2e-logs", ".e2e-logs-experimental"] + } + } +} diff --git a/packages/create-cloudflare/vitest-e2e.config.mts b/packages/create-cloudflare/vitest-e2e.config.mts new file mode 100644 index 0000000..212664e --- /dev/null +++ b/packages/create-cloudflare/vitest-e2e.config.mts @@ -0,0 +1,18 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; +import { isExperimental, testPackageManager } from "./e2e/helpers/constants"; +import { getLogFolder } from "./e2e/helpers/log-stream"; + +export default defineConfig({ + plugins: [tsconfigPaths({ projects: ["tsconfig.json"] })], + test: { + include: ["e2e/tests/**/*.test.ts"], + testTimeout: 1000 * 60 * 10, // 10 min for lengthy installs + setupFiles: ["e2e/helpers/to-exist.ts"], + globalSetup: ["e2e/helpers/global-setup.ts"], + reporters: ["json", "verbose", "hanging-process"], + outputFile: { + json: `${getLogFolder(isExperimental, testPackageManager)}/results.json`, + }, + }, +}); diff --git a/packages/create-cloudflare/vitest.config.mts b/packages/create-cloudflare/vitest.config.mts new file mode 100644 index 0000000..1313762 --- /dev/null +++ b/packages/create-cloudflare/vitest.config.mts @@ -0,0 +1,15 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + plugins: [tsconfigPaths({ projects: ["tsconfig.json"] })], + test: { + include: ["src/**/__tests__/**.test.ts"], + mockReset: true, + setupFiles: ["vitest.setup.ts"], + }, + }) +); diff --git a/packages/create-cloudflare/vitest.setup.ts b/packages/create-cloudflare/vitest.setup.ts new file mode 100644 index 0000000..a0a5076 --- /dev/null +++ b/packages/create-cloudflare/vitest.setup.ts @@ -0,0 +1,12 @@ +import { PassThrough } from "node:stream"; +import { vi } from "vitest"; + +vi.mock("log-update"); + +vi.mock("@cloudflare/cli-shared-helpers/streams", () => { + return { + __esModule: true, + stdout: new PassThrough(), + stderr: new PassThrough(), + }; +}); diff --git a/packages/deploy-helpers/CHANGELOG.md b/packages/deploy-helpers/CHANGELOG.md new file mode 100644 index 0000000..d3d0c5a --- /dev/null +++ b/packages/deploy-helpers/CHANGELOG.md @@ -0,0 +1,190 @@ +# @cloudflare/deploy-helpers + +## 0.4.0 + +### Minor Changes + +- [#14591](https://github.com/cloudflare/workers-sdk/pull/14591) [`0283a1f`](https://github.com/cloudflare/workers-sdk/commit/0283a1fcdc635244f731010422e513e8b4ab0be3) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - Export `collectPackageDependencies` for npm dependency metadata collection + + The package dependency discovery logic (collecting installed npm package names and versions from a project's `package.json`) is now owned by `deploy-helpers` and called internally during deploy and version uploads, rather than being pre-computed in wrangler and passed through as a prop. + +### Patch Changes + +- Updated dependencies [[`0283a1f`](https://github.com/cloudflare/workers-sdk/commit/0283a1fcdc635244f731010422e513e8b4ab0be3), [`1b965c5`](https://github.com/cloudflare/workers-sdk/commit/1b965c51babff16ae7657335d93badebd50c310f)]: + - @cloudflare/workers-utils@0.26.0 + - miniflare@4.20260708.1 + - @cloudflare/cli-shared-helpers@0.1.13 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies [[`e3f0cd6`](https://github.com/cloudflare/workers-sdk/commit/e3f0cd69e08c0eed9d75f61221d1076b6c287eef), [`8511ddf`](https://github.com/cloudflare/workers-sdk/commit/8511ddf769a603f49576b8cf632ea330c353001f), [`2fedb1f`](https://github.com/cloudflare/workers-sdk/commit/2fedb1fc811efb3f7544c569e57383cabd4f14f8)]: + - miniflare@4.20260708.0 + - @cloudflare/workers-utils@0.25.1 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [[`0852346`](https://github.com/cloudflare/workers-sdk/commit/08523467752daa79f0f8950a01f35797aa6f3052)]: + - miniflare@4.20260706.0 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`e7e5780`](https://github.com/cloudflare/workers-sdk/commit/e7e5780ea2db48fe43233ecedf01979db6c5ce9d), [`aad35b7`](https://github.com/cloudflare/workers-sdk/commit/aad35b79d07df1bb764a4a5912d6b4328a34474b), [`d88555e`](https://github.com/cloudflare/workers-sdk/commit/d88555edb671668ed7f73e587af9effe6e782f53), [`1ac96a1`](https://github.com/cloudflare/workers-sdk/commit/1ac96a14b7fb022acada114ab8793fe8a4ba79a5), [`f416dd9`](https://github.com/cloudflare/workers-sdk/commit/f416dd983e9c6e4d292317e077dfbe839d2f30c8), [`1ca8d8f`](https://github.com/cloudflare/workers-sdk/commit/1ca8d8f0bbd012a1d65cabadf7b6987b252775e9), [`16fbf81`](https://github.com/cloudflare/workers-sdk/commit/16fbf81d923760d295c7f05b0bd660b7be510e5d)]: + - miniflare@4.20260702.0 + - @cloudflare/workers-utils@0.25.1 + - @cloudflare/cli-shared-helpers@0.1.12 + +## 0.3.0 + +### Minor Changes + +- [#14474](https://github.com/cloudflare/workers-sdk/pull/14474) [`aa5d580`](https://github.com/cloudflare/workers-sdk/commit/aa5d5801450b7e4417bfdbd477f86de3a4bc6933) Thanks [@WillTaylorDev](https://github.com/WillTaylorDev)! - Add cache options for WorkerEntrypoint exports + + You can now set cache options on `WorkerEntrypoint` exports and configure cross-version cache behavior globally: + + ```jsonc + // wrangler.json + { + "cache": { "enabled": true, "cross_version_cache": true }, + "exports": { + "default": { + "type": "worker", + "cache": { "enabled": false } + }, + "Admin": { + "type": "worker", + "cache": { "enabled": true } + } + } + } + ``` + + Wrangler sends the `exports` config to the deploy and version upload APIs alongside the global `cache.enabled` and `cache.cross_version_cache` settings. The platform resolves those global settings plus cache overrides on exports and validates which entrypoint names are cacheable. + +### Patch Changes + +- [#14305](https://github.com/cloudflare/workers-sdk/pull/14305) [`98793d8`](https://github.com/cloudflare/workers-sdk/commit/98793d8e00567462518d983d974e0e89b6a474c3) Thanks [@jbwcloudflare](https://github.com/jbwcloudflare)! - Improve asset upload performance with single-file uploads + + Asset uploads now use a more efficient per-file upload path when the platform enables it. This is rolled out server-side and requires no configuration changes. Existing upload behavior is unchanged when the new path is not enabled. + +- Updated dependencies [[`aa5d580`](https://github.com/cloudflare/workers-sdk/commit/aa5d5801450b7e4417bfdbd477f86de3a4bc6933), [`6b0ce98`](https://github.com/cloudflare/workers-sdk/commit/6b0ce986b01ec4559e2ac16feb410a186c42f9e1)]: + - @cloudflare/workers-utils@0.25.0 + - miniflare@4.20260701.0 + - @cloudflare/cli-shared-helpers@0.1.11 + +## 0.2.5 + +### Patch Changes + +- [#14490](https://github.com/cloudflare/workers-sdk/pull/14490) [`75d8cb0`](https://github.com/cloudflare/workers-sdk/commit/75d8cb0e32e0f4d66b699e88016d01f1666d8d8a) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - remove skipLastDeployedFromApiCheck + + This was a temporary option to bypass an API issue, which has been fixed API side. + +- Updated dependencies [[`75d8cb0`](https://github.com/cloudflare/workers-sdk/commit/75d8cb0e32e0f4d66b699e88016d01f1666d8d8a), [`f10d4ad`](https://github.com/cloudflare/workers-sdk/commit/f10d4ad99a3e67e04c16425fe25b6c61ec0c54db), [`75d8cb0`](https://github.com/cloudflare/workers-sdk/commit/75d8cb0e32e0f4d66b699e88016d01f1666d8d8a), [`75d8cb0`](https://github.com/cloudflare/workers-sdk/commit/75d8cb0e32e0f4d66b699e88016d01f1666d8d8a)]: + - miniflare@4.20260630.0 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`3b743c1`](https://github.com/cloudflare/workers-sdk/commit/3b743c1b86ad80c40fd9d2d678cd5a8cb66e86fa)]: + - miniflare@4.20260625.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`a085dec`](https://github.com/cloudflare/workers-sdk/commit/a085deca12d7126c21e500b3dd4298edfd13f8cd), [`9a0de8f`](https://github.com/cloudflare/workers-sdk/commit/9a0de8f71f50bb7d1884288e376259082084a315), [`fab565f`](https://github.com/cloudflare/workers-sdk/commit/fab565fdb1a912c73232d72ccdf1963fd96f9ad5)]: + - miniflare@4.20260623.0 + +## 0.2.2 + +### Patch Changes + +- [#14354](https://github.com/cloudflare/workers-sdk/pull/14354) [`7649895`](https://github.com/cloudflare/workers-sdk/commit/764989568ecbfadd111fc399c83d71dd9ce6cf1b) Thanks [@emily-shen](https://github.com/emily-shen)! - Move resource provisioning into deploy helpers + + Worker deploy and versions upload now share the deploy helpers implementation for provisioning bindings, reducing Wrangler-specific callback wiring while preserving existing behavior. + +- Updated dependencies [[`b38823f`](https://github.com/cloudflare/workers-sdk/commit/b38823fb35a8bdcd00004e74404ab18d7b070dbf), [`cfd6205`](https://github.com/cloudflare/workers-sdk/commit/cfd6205fe86f6afd74b5881f09524c93c83b8359), [`cfd6205`](https://github.com/cloudflare/workers-sdk/commit/cfd6205fe86f6afd74b5881f09524c93c83b8359)]: + - miniflare@4.20260617.1 + - @cloudflare/workers-utils@0.24.0 + - @cloudflare/cli-shared-helpers@0.1.10 + +## 0.2.1 + +### Patch Changes + +- [#14347](https://github.com/cloudflare/workers-sdk/pull/14347) [`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update undici from 7.24.8 to 7.28.0 + +- [#14340](https://github.com/cloudflare/workers-sdk/pull/14340) [`f6e49dd`](https://github.com/cloudflare/workers-sdk/commit/f6e49dd59190328007331477450651e8bca2def8) Thanks [@emily-shen](https://github.com/emily-shen)! - add `skipLastDeployedFromApiCheck` to override deploy source check + +- [#14269](https://github.com/cloudflare/workers-sdk/pull/14269) [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb) Thanks [@mattjohnsonpint](https://github.com/mattjohnsonpint)! - Support `dev.plugin` on typed services bindings + + Wrangler only honored `dev.plugin` on `unsafe.bindings` entries, so users authoring a service binding via `services[]` could not wire it to a local Miniflare plugin during `wrangler dev` — they had to fall back to `unsafe.bindings` and accept a "directly supported by wrangler" warning. Typed services bindings now accept the same `dev: { plugin, options? }` shape, route the binding through Miniflare's external-plugin pathway in `wrangler dev`, and strip the field at deploy time. Validation rejects malformed `dev` shapes. + +- Updated dependencies [[`673b09e`](https://github.com/cloudflare/workers-sdk/commit/673b09e0fa26368125fb527596a8eb5d31c27302), [`e930bd4`](https://github.com/cloudflare/workers-sdk/commit/e930bd4ca9880eb0b68ce6d1933c1d9ce290317d), [`5c3bb11`](https://github.com/cloudflare/workers-sdk/commit/5c3bb118a99da70c5c1efb07df37f685e7044ba6), [`296ad65`](https://github.com/cloudflare/workers-sdk/commit/296ad659305ee150d61451991f04a135fe99d264), [`5dfb788`](https://github.com/cloudflare/workers-sdk/commit/5dfb788595a2104b4b0922cfce3d69a2f1d881eb)]: + - @cloudflare/workers-utils@0.23.2 + - miniflare@4.20260617.0 + - @cloudflare/cli-shared-helpers@0.1.9 + +## 0.2.0 + +### Minor Changes + +- [#14321](https://github.com/cloudflare/workers-sdk/pull/14321) [`88662aa`](https://github.com/cloudflare/workers-sdk/commit/88662aa727a6b9d576578dc9cdc3b2911efcd89b) Thanks [@emily-shen](https://github.com/emily-shen)! - re-export OutputWorkerSchema from internal config package + +- [#14265](https://github.com/cloudflare/workers-sdk/pull/14265) [`e3fce14`](https://github.com/cloudflare/workers-sdk/commit/e3fce14228f6e064d5d7408508107774fa38c296) Thanks [@emily-shen](https://github.com/emily-shen)! - Expose Cloudflare config conversion utilities from deploy helpers + + `@cloudflare/deploy-helpers` now re-exports `ConfigSchema` and `convertToWranglerConfig` from the internal `@cloudflare/config` package, so consumers can parse and convert Cloudflare config files without depending on the unpublished package directly. + +### Patch Changes + +- [#14265](https://github.com/cloudflare/workers-sdk/pull/14265) [`e3fce14`](https://github.com/cloudflare/workers-sdk/commit/e3fce14228f6e064d5d7408508107774fa38c296) Thanks [@emily-shen](https://github.com/emily-shen)! - Bundle private internal dependencies in deploy helpers + + `@cloudflare/deploy-helpers` no longer declares private workspace packages as runtime dependencies, so installing the package from npm does not require unpublished internal packages. + +- [#14304](https://github.com/cloudflare/workers-sdk/pull/14304) [`ee82c76`](https://github.com/cloudflare/workers-sdk/commit/ee82c76b07844f7ae9068b01d29a2a0adf34eed0) Thanks [@emily-shen](https://github.com/emily-shen)! - Skip resource provisioning for asset-only deployments + + Previously, asset-only deployments would provision resources even when there was no user Worker script. On a subsequent deploy, we would re-attempt provisioning as the previous asset-only upload would/could not be bound to the previously provisioned resource. Provisioning would then error as the resource had already been created, blocking the deploy. + +- Updated dependencies [[`0e055d3`](https://github.com/cloudflare/workers-sdk/commit/0e055d39c51dda77717515adb1a33610d385a724), [`27db82c`](https://github.com/cloudflare/workers-sdk/commit/27db82c808743f690f023f84be5cde9e223c22d1), [`2a6a26b`](https://github.com/cloudflare/workers-sdk/commit/2a6a26b02f27ac18b1773a5460e1e7e37721a5cb), [`9a424ed`](https://github.com/cloudflare/workers-sdk/commit/9a424ed747009c716db77463c72f8d974e048914), [`ecfdd5a`](https://github.com/cloudflare/workers-sdk/commit/ecfdd5a6c60b9c6f99c28f9294da656933c2a5fd), [`41f391f`](https://github.com/cloudflare/workers-sdk/commit/41f391fdda4112ee333782aad02d16dacaa95f8f)]: + - miniflare@4.20260616.0 + - @cloudflare/workers-utils@0.23.1 + - @cloudflare/cli-shared-helpers@0.1.8 + +## 0.1.3 + +### Patch Changes + +- [#14259](https://github.com/cloudflare/workers-sdk/pull/14259) [`2ae6099`](https://github.com/cloudflare/workers-sdk/commit/2ae6099db77c076fb7e6e782d2f0ebd7ba86dbbb) Thanks [@emily-shen](https://github.com/emily-shen)! - Move worker build step earlier in deploy/upload step, before upload specific config validation + +- Updated dependencies [[`f3990b2`](https://github.com/cloudflare/workers-sdk/commit/f3990b2358ef49cd6e1ab16de27e25dcd949896f), [`4597f08`](https://github.com/cloudflare/workers-sdk/commit/4597f085d25c7d066ecf056de313e194f41094d1), [`2047a32`](https://github.com/cloudflare/workers-sdk/commit/2047a32cf78886b71b794a3dfac946a146ab3ffe)]: + - miniflare@4.20260611.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`c6c61b5`](https://github.com/cloudflare/workers-sdk/commit/c6c61b59431443b2bcda25f3af7624dd2ce19b9b), [`b502d54`](https://github.com/cloudflare/workers-sdk/commit/b502d5445b9e9e030020a3d65c0334507393aa64), [`c4f45e8`](https://github.com/cloudflare/workers-sdk/commit/c4f45e8b8694c60fb1808f7fbb130e4b4893d20c)]: + - @cloudflare/workers-utils@0.23.0 + +## 0.1.1 + +### Patch Changes + +- [#14063](https://github.com/cloudflare/workers-sdk/pull/14063) [`65b5f9e`](https://github.com/cloudflare/workers-sdk/commit/65b5f9e1855651c2df2c1bdfc8930141e36413d5) Thanks [@emily-shen](https://github.com/emily-shen)! - Move fetch helpers into `@cloudflare/workers-utils` + + Shared Cloudflare API fetch helper types and plumbing now live in `@cloudflare/workers-utils` so Wrangler and other clients can use the same implementation. + +## 0.1.0 + +### Minor Changes + +- [#14014](https://github.com/cloudflare/workers-sdk/pull/14014) [`d042705`](https://github.com/cloudflare/workers-sdk/commit/d042705c7a8715184e6e16d399c17adb958d0e80) Thanks [@emily-shen](https://github.com/emily-shen)! - Add `@cloudflare/deploy-helpers` package. + + This introduces a shared internal package for deploy-related helper types and code. diff --git a/packages/deploy-helpers/package.json b/packages/deploy-helpers/package.json new file mode 100644 index 0000000..6027898 --- /dev/null +++ b/packages/deploy-helpers/package.json @@ -0,0 +1,84 @@ +{ + "name": "@cloudflare/deploy-helpers", + "version": "0.4.0", + "description": "Internal deploy helpers for workers-sdk. Not intended for external use — APIs may change without notice.", + "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/deploy-helpers#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "MIT OR Apache-2.0", + "author": "workers-devprod@cloudflare.com", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/workers-sdk.git", + "directory": "packages/deploy-helpers" + }, + "files": [ + "dist" + ], + "sideEffects": false, + "exports": { + ".": { + "import": "./dist/index.mjs", + "types": "./dist/index.d.mts" + }, + "./context": { + "import": "./dist/context.mjs", + "types": "./dist/context.d.mts" + } + }, + "scripts": { + "build": "tsup", + "check:type": "tsc -p ./tsconfig.json", + "deploy": "echo 'no deploy'", + "dev": "concurrently -c black,blue --kill-others-on-fail false \"pnpm tsup --watch src\" \"pnpm run check:type --watch --preserveWatchOutput\"", + "test": "vitest", + "test:ci": "vitest run", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "dependencies": { + "@cloudflare/cli-shared-helpers": "workspace:*", + "@cloudflare/workers-utils": "workspace:*", + "blake3-wasm": "2.1.5", + "chalk": "catalog:default", + "command-exists": "catalog:default", + "dotenv": "catalog:default", + "miniflare": "workspace:*", + "p-queue": "9.0.0", + "pretty-bytes": "6.1.1", + "undici": "catalog:default" + }, + "devDependencies": { + "@cloudflare/config": "workspace:*", + "@cloudflare/containers-shared": "workspace:*", + "@cloudflare/workers-shared": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cspotcode/source-map-support": "0.8.1", + "@types/command-exists": "^1.2.0", + "@types/json-diff": "^1.0.3", + "@types/node": "catalog:default", + "concurrently": "^8.2.2", + "devtools-protocol": "^0.0.1182435", + "esbuild": "catalog:default", + "json-diff": "^1.0.6", + "ts-dedent": "^2.2.0", + "tsup": "8.3.0", + "typescript": "catalog:default", + "vitest": "catalog:default", + "zod": "^4.4.3" + }, + "peerDependencies": { + "zod": "^4.4.3" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "prerelease": true + } +} diff --git a/packages/deploy-helpers/scripts/deps.ts b/packages/deploy-helpers/scripts/deps.ts new file mode 100644 index 0000000..125466e --- /dev/null +++ b/packages/deploy-helpers/scripts/deps.ts @@ -0,0 +1,28 @@ +/** + * Dependencies that _are not_ bundled along with @cloudflare/deploy-helpers. + * + * These must be explicitly documented with a reason why they cannot be bundled. + * This list is validated by `tools/deployments/validate-package-dependencies.ts`. + */ +export const EXTERNAL_DEPENDENCIES = [ + // Workspace packages kept external so consumers share a single copy of + // types and runtime code (e.g. ParseError instanceof checks). + "@cloudflare/cli-shared-helpers", + "@cloudflare/workers-utils", + "miniflare", + + // These are externalized to avoid duplication in wrangler's bundle, + // which already bundles these packages itself. + "blake3-wasm", + "chalk", + "command-exists", + "dotenv", + "p-queue", + "pretty-bytes", + "undici", + + // Externalized so wrangler bundles a single shared zod copy instead of + // inlining one via this package. Declared as a peerDependency (the + // consumer provides zod). + "zod", +]; diff --git a/packages/deploy-helpers/src/config.ts b/packages/deploy-helpers/src/config.ts new file mode 100644 index 0000000..9374efa --- /dev/null +++ b/packages/deploy-helpers/src/config.ts @@ -0,0 +1,18 @@ +import { + InputWorkerSchema, + OutputWorkerSchema, + convertToWranglerConfig, +} from "@cloudflare/config"; +import type { + ParsedInputWorkerConfig, + ParsedOutputWorkerConfig, + ModuleType, +} from "@cloudflare/config"; +export { + InputWorkerSchema, + OutputWorkerSchema, + convertToWranglerConfig, + ParsedInputWorkerConfig, + ParsedOutputWorkerConfig, + ModuleType, +}; diff --git a/packages/deploy-helpers/src/deploy/deploy.ts b/packages/deploy-helpers/src/deploy/deploy.ts new file mode 100644 index 0000000..c4a4851 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/deploy.ts @@ -0,0 +1,765 @@ +import assert from "node:assert"; +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { URLSearchParams } from "node:url"; +import { cancel } from "@cloudflare/cli-shared-helpers"; +import { verifyDockerInstalled } from "@cloudflare/containers-shared"; +import { + APIError, + formatTime, + getDockerPath, + hasDurableObjectExports, + parseNonHyphenedUuid, + retryOnAPIFailure, + UserError, +} from "@cloudflare/workers-utils"; +import { Response } from "undici"; +import { fetchResult, logger } from "../shared/context"; +import { triggersDeploy } from "../triggers/deploy"; +import { + buildAssetManifest, + resolveAssetOptions, + syncAssets, +} from "./helpers/assets"; +import { getBindings } from "./helpers/binding-utils"; +import { printBundleSize } from "./helpers/bundle-reporter"; +import { confirmLatestDeploymentOverwrite } from "./helpers/confirm-latest-deployment-overwrite"; +import { createWorkerUploadForm } from "./helpers/create-worker-upload-form"; +import { deployWfpUserWorker } from "./helpers/deploy-wfp"; +import { + applyServiceAndEnvironmentTags, + tagsAreEqual, + warnOnErrorUpdatingServiceAndEnvironmentTags, +} from "./helpers/environments"; +import { EXPORTS_RECONCILIATION_ERROR_CODE } from "./helpers/error-codes"; +import { resolveExportsUploadPayload } from "./helpers/exports"; +import { + isExportsReconciliationErrorDetails, + renderExportsReconciliationError, + renderExportsReconciliationSuccess, +} from "./helpers/exports-reconciliation"; +import { helpIfErrorIsSizeOrScriptStartup } from "./helpers/friendly-validator-errors"; +import { collectPackageDependencies } from "./helpers/package-dependencies"; +import { parseBulkInputToObject } from "./helpers/parse-bulk-input"; +import { parseConfigPlacement } from "./helpers/placement"; +import { printBindings } from "./helpers/print-bindings"; +import { provisionBindings } from "./helpers/provision-bindings"; +import { + addRequiredSecretsInheritBindings, + handleMissingSecretsError, +} from "./helpers/secrets-validation"; +import { + getSourceMappedString, + maybeRetrieveFileSourceMap, +} from "./helpers/sourcemap"; +import { + preUploadApiChecks, + validateWorkerProps, +} from "./helpers/validate-worker-props"; +import { + createDeployment, + patchNonVersionedScriptSettings, +} from "./helpers/versions-api"; +import { addWorkersSitesBindings } from "./helpers/workers-sites-bindings"; +import type { DeployProps, WorkerBuildResult } from "../shared/types"; +import type { AssetUploadStats } from "./helpers/assets"; +import type { RetrieveSourceMapFunction } from "./helpers/sourcemap"; +import type { + ApiVersion, + Percentage, + VersionId, +} from "./helpers/versions-types"; +import type { + ContainerNormalizedConfig, + ImageURIConfig, +} from "@cloudflare/containers-shared"; +import type { + CfModule, + CfWorkerInit, + ComplianceConfig, + Config, + ExportsReconciliationResult, + LegacyAssetPaths, +} from "@cloudflare/workers-utils"; +import type { FormData } from "undici"; + +/** + * Wrangler-specific functions injected into `deploy()`. These remain in + * wrangler because they depend on wrangler-only systems (account selection, + * metrics, the dev-mode worker registry, container orchestration, etc.). + */ +export type DeployCallbacks = { + syncWorkersSite: + | (( + complianceConfig: ComplianceConfig, + accountId: string | undefined, + scriptName: string, + siteAssets: LegacyAssetPaths | undefined, + preview: boolean, + dryRun: boolean | undefined, + oldAssetTTL: number | undefined + ) => Promise<{ + manifest: { [filePath: string]: string } | undefined; + namespace: string | undefined; + }>) + | undefined; + getNormalizedContainerOptions: + | (( + config: Config, + args: { + containersRollout?: "gradual" | "immediate" | "none"; + dryRun?: boolean; + } + ) => Promise) + | undefined; + buildContainer: + | (( + containerConfig: Exclude, + imageTag: string, + dryRun: boolean, + pathToDocker: string, + verifyDockerIsRunning: boolean + ) => Promise) + | undefined; + deployContainers: + | (( + config: Config, + normalisedContainerConfig: ContainerNormalizedConfig[], + args: { versionId: string; accountId: string; scriptName: string } + ) => Promise) + | undefined; + analyseBundle: + | ((workerBundle: string | FormData) => Promise>) + | undefined; +}; + +export default async function deploy( + props: DeployProps, + config: Config, + buildResult: WorkerBuildResult, + callbacks: DeployCallbacks +): Promise<{ + sourceMapSize?: number; + versionId: string | null; + workerTag: string | null; + assetUploadStats?: AssetUploadStats; + targets?: string[]; +}> { + const { entry, compatibilityDate, compatibilityFlags, keepVars, accountId } = + props; + + const assetsOptions = resolveAssetOptions(props, config); + + // Any validation that does not require API calls should go in validateWorkerProps() + const { name } = validateWorkerProps({ ...props, assetsOptions }, config); + + // any validation that DOES require API calls should go in preUploadApiChecks() + const { workerTag, tags, workerExists, aborted } = await preUploadApiChecks( + props, + config + ); + + if (aborted) { + return { versionId: null, workerTag }; + } + + let versionId: string | null = null; + const scriptName = name; + + const start = Date.now(); + const workerName = scriptName; + const workerUrl = props.dispatchNamespace + ? `/accounts/${accountId}/workers/dispatch/namespaces/${props.dispatchNamespace}/scripts/${scriptName}` + : `/accounts/${accountId}/workers/scripts/${scriptName}`; + + const { format } = entry; + const { projectRoot } = entry; + + if (!props.dispatchNamespace && accountId && scriptName) { + const yes = await confirmLatestDeploymentOverwrite( + config, + accountId, + scriptName + ); + if (!yes) { + cancel("Aborting deploy..."); + return { versionId, workerTag }; + } + } + + const isDryRun = props.dryRun; + + const normalisedContainerConfig = callbacks.getNormalizedContainerOptions + ? await callbacks.getNormalizedContainerOptions(config, props) + : []; + const { + modules, + dependencies, + resolvedEntryPointPath, + bundleType, + content, + sourceMaps, + } = buildResult; + // Durable Object lifecycle is expressed through either legacy `migrations` + // or the declarative `exports` map. Only one is sent on each upload. + const { migrations, exports } = await resolveExportsUploadPayload({ + scriptName, + isDryRun, + accountId, + config, + dispatchNamespace: props.dispatchNamespace, + }); + + // Upload assets if assets is being used + const assetsUploadResult = + assetsOptions && !isDryRun + ? await syncAssets( + config, + accountId, + assetsOptions.directory, + scriptName, + props.dispatchNamespace + ) + : undefined; + const assetsJwt = assetsUploadResult?.jwt; + const assetUploadStats = assetsUploadResult?.assetUploadStats; + + // validate asset directory + if (assetsOptions && isDryRun) { + await buildAssetManifest(assetsOptions.directory); + } + + const workersSitesAssets = callbacks.syncWorkersSite + ? await callbacks.syncWorkersSite( + config, + accountId, + scriptName, + props.legacyAssetPaths, + false, + isDryRun, + props.oldAssetTtl + ) + : { manifest: undefined, namespace: undefined }; + + const bindings = getBindings(config); + + // Vars from the CLI (--var) are hidden so their values aren't logged to the terminal + for (const [bindingName, value] of Object.entries(props.cliVars)) { + bindings[bindingName] = { + type: "plain_text", + value, + hidden: true, + }; + } + + if (props.secretsFile) { + const secretsResult = await parseBulkInputToObject(props.secretsFile); + if (secretsResult) { + for (const [secretName, secretValue] of Object.entries( + secretsResult.content + )) { + bindings[secretName] = { + type: "secret_text", + value: secretValue, + }; + } + } + } + + addRequiredSecretsInheritBindings(config, bindings, { + type: "deploy", + workerExists, + }); + + if (workersSitesAssets.manifest) { + modules.push({ + name: "__STATIC_CONTENT_MANIFEST", + filePath: undefined, + content: JSON.stringify(workersSitesAssets.manifest), + type: "text", + }); + } + + const placement = parseConfigPlacement(config); + + const entryPointName = path.basename(resolvedEntryPointPath); + const main: CfModule = { + name: entryPointName, + filePath: resolvedEntryPointPath, + content: content, + type: bundleType, + }; + const worker: CfWorkerInit = { + name: scriptName, + main, + migrations, + exports, + modules, + containers: config.containers, + sourceMaps, + compatibility_date: compatibilityDate, + compatibility_flags: compatibilityFlags, + keepVars, + keepSecrets: keepVars || !!props.secretsFile, + logpush: props.logpush, + placement, + tail_consumers: config.tail_consumers, + streaming_tail_consumers: config.streaming_tail_consumers, + limits: config.limits, + annotations: + props.tag || props.message + ? { + "workers/message": props.message, + "workers/tag": props.tag, + } + : undefined, + assets: + assetsOptions && assetsJwt + ? { + jwt: assetsJwt, + routerConfig: assetsOptions.routerConfig, + assetConfig: assetsOptions.assetConfig, + _redirects: assetsOptions._redirects, + _headers: assetsOptions._headers, + run_worker_first: assetsOptions.run_worker_first, + } + : undefined, + observability: config.observability, + cache: config.cache, + package_dependencies: + config.dependencies_instrumentation?.enabled !== false && projectRoot + ? await collectPackageDependencies(projectRoot) + : undefined, + }; + + const sourceMapSize = worker.sourceMaps?.reduce( + (acc, m) => acc + m.content.length, + 0 + ); + + await printBundleSize( + { name: path.basename(resolvedEntryPointPath), content: content }, + modules + ); + + // We can use the new versions/deployments APIs if we: + // * are uploading a worker that already exists + // * aren't a dispatch namespace deploy + // * aren't a service Worker + // * we don't have DO migrations or Durable Object `exports`. + // Worker exports do not apply lifecycle changes, so they can use this path. + // * we aren't an fpw + // * not a container worker + const canUseNewVersionsDeploymentsApi = + workerExists && + props.dispatchNamespace === undefined && + format === "modules" && + migrations === undefined && + !hasDurableObjectExports(config.exports) && + !config.first_party_worker && + config.containers === undefined; + + let workerBundle: FormData; + const dockerPath = getDockerPath(); + + // lets fail earlier in the case where docker isn't installed + // and we have containers so that we don't get into a + // disjointed state where the worker updates but the container + // fails. + if (normalisedContainerConfig.length && props.containersRollout !== "none") { + // if you have a registry url specified, you don't need docker + const containersWithDockerfile = normalisedContainerConfig.filter( + (container) => "dockerfile" in container + ); + if (containersWithDockerfile.length > 0) { + await verifyDockerInstalled({ + dockerPath, + operation: `deploying${isDryRun ? " (even in dry-run mode)" : ""}`, + imageNoun: + containersWithDockerfile.length !== 1 + ? "the configured images" + : "the configured image", + hint: "If you cannot run Docker locally, you can still deploy your Worker by passing --containers-rollout=none. This will not deploy or update your Container.", + }); + } + } + + if (isDryRun) { + if (normalisedContainerConfig.length) { + for (const container of normalisedContainerConfig) { + if ( + "dockerfile" in container && + props.containersRollout !== "none" && + callbacks.buildContainer + ) { + await callbacks.buildContainer( + container, + workerTag ?? "worker-tag", + isDryRun, + dockerPath, + false + ); + } + } + } + + workerBundle = createWorkerUploadForm( + worker, + addWorkersSitesBindings( + bindings ?? {}, + workersSitesAssets.namespace, + workersSitesAssets.manifest, + format + ), + { + dryRun: true, + unsafe: config.unsafe, + } + ); + + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + config.containers, + { warnIfNoBindings: true, unsafeMetadata: config.unsafe?.metadata } + ); + } else { + assert(accountId, "Missing accountId"); + + if (assetsOptions?.routerConfig.has_user_worker === false) { + logger.debug("skipping provisioning on assets-only project"); + } else if (props.resourcesProvision) { + await provisionBindings( + bindings ?? {}, + accountId, + scriptName, + props.experimentalAutoCreate, + config, + { + skipConfigWriteback: props.skipProvisioningConfigWriteback, + } + ); + } + + workerBundle = createWorkerUploadForm( + worker, + addWorkersSitesBindings( + bindings ?? {}, + workersSitesAssets.namespace, + workersSitesAssets.manifest, + format + ), + { + unsafe: config.unsafe, + } + ); + + let bindingsPrinted = false; + + // Upload the script so it has time to propagate. + try { + let result: { + id: string | null; + etag: string | null; + pipeline_hash: string | null; + mutable_pipeline_id: string | null; + deployment_id: string | null; + startup_time_ms?: number; + }; + + // If we're using the new APIs, first upload the version + if (canUseNewVersionsDeploymentsApi) { + // Upload new version + const versionResult = await retryOnAPIFailure( + async () => + fetchResult( + config, + `/accounts/${accountId}/workers/scripts/${scriptName}/versions`, + { + method: "POST", + body: workerBundle, + headers: props.sendMetrics + ? { metricsEnabled: "true" } + : undefined, + }, + new URLSearchParams({ bindings_inherit: "strict" }) + ), + logger + ); + + // Deploy new version to 100% + const versionMap = new Map(); + versionMap.set(versionResult.id, 100); + await createDeployment( + config, + accountId, + scriptName, + versionMap, + props.message, + undefined + ); + + // Update service and environment tags when using environments + const nextTags = applyServiceAndEnvironmentTags(config, tags); + + try { + // Update tail consumers, logpush, and observability settings + await patchNonVersionedScriptSettings(config, accountId, scriptName, { + tail_consumers: worker.tail_consumers, + logpush: worker.logpush, + // If the user hasn't specified observability assume that they want it disabled if they have it on. + // This is a no-op in the event that they don't have observability enabled, but will remove observability + // if it has been removed from their Wrangler configuration file + observability: worker.observability ?? { enabled: false }, + tags: nextTags, + }); + } catch { + warnOnErrorUpdatingServiceAndEnvironmentTags(); + } + + result = { + id: null, // fpw - ignore + etag: versionResult.resources.script.etag, + pipeline_hash: null, // fpw - ignore + mutable_pipeline_id: null, // fpw - ignore + deployment_id: versionResult.id, // version id not deployment id but easier to adapt here + startup_time_ms: versionResult.startup_time_ms, + }; + } else { + const uploadResult = await retryOnAPIFailure( + async () => + fetchResult<{ + id: string | null; + etag: string | null; + pipeline_hash: string | null; + mutable_pipeline_id: string | null; + deployment_id: string | null; + startup_time_ms: number; + exports_reconciliation?: ExportsReconciliationResult; + }>( + config, + workerUrl, + { + method: "PUT", + body: workerBundle, + headers: props.sendMetrics + ? { metricsEnabled: "true" } + : undefined, + }, + new URLSearchParams({ + // pass excludeScript so the whole body of the + // script doesn't get included in the response + excludeScript: "true", + bindings_inherit: "strict", + }) + ), + logger + ); + result = uploadResult; + if (uploadResult.exports_reconciliation) { + renderExportsReconciliationSuccess( + uploadResult.exports_reconciliation + ); + } + + // Update service and environment tags when using environments + const nextTags = applyServiceAndEnvironmentTags(config, tags); + if (!tagsAreEqual(tags, nextTags)) { + try { + await patchNonVersionedScriptSettings( + config, + accountId, + scriptName, + { + tags: nextTags, + } + ); + } catch { + warnOnErrorUpdatingServiceAndEnvironmentTags(); + } + } + } + + if (result.startup_time_ms) { + logger.log("Worker Startup Time:", result.startup_time_ms, "ms"); + } + bindingsPrinted = true; + + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + config.containers, + { unsafeMetadata: config.unsafe?.metadata } + ); + + versionId = parseNonHyphenedUuid(result.deployment_id); + + if (config.first_party_worker) { + // Print some useful information returned after publishing + // Not all fields will be populated for every worker + // These fields are likely to be scraped by tools, so do not rename + if (result.id) { + logger.log("Worker ID: ", result.id); + } + if (result.etag) { + logger.log("Worker ETag: ", result.etag); + } + if (result.pipeline_hash) { + logger.log("Worker PipelineHash: ", result.pipeline_hash); + } + if (result.mutable_pipeline_id) { + logger.log( + "Worker Mutable PipelineID (Development ONLY!):", + result.mutable_pipeline_id + ); + } + } + } catch (err) { + if (!bindingsPrinted) { + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + config.containers, + { unsafeMetadata: config.unsafe?.metadata } + ); + } + + // Reconciliation errors include structured per-class details. + if ( + err instanceof APIError && + err.code === EXPORTS_RECONCILIATION_ERROR_CODE && + isExportsReconciliationErrorDetails(err.meta?.details) + ) { + err.preventReport(); + throw new UserError( + renderExportsReconciliationError(err.meta.details), + { telemetryMessage: "deploy do exports reconciliation failed" } + ); + } + + const message = await helpIfErrorIsSizeOrScriptStartup( + err, + dependencies, + workerBundle, + projectRoot, + callbacks.analyseBundle + ); + if (message !== null) { + logger.error(message); + } + + handleMissingSecretsError(err, config, { + type: "deploy", + workerExists, + }); + + // Apply source mapping to validation startup errors if possible + if ( + err instanceof APIError && + "code" in err && + err.code === 10021 /* validation error */ && + err.notes.length > 0 + ) { + err.preventReport(); + + if ( + err.notes[0].text === + "binding DB of type d1 must have a valid `id` specified [code: 10021]" + ) { + throw new UserError( + "You must use a real database in the database_id configuration. You can find your databases using 'wrangler d1 list', or read how to develop locally with D1 here: https://developers.cloudflare.com/d1/configuration/local-development", + { telemetryMessage: "deploy d1 database binding invalid id" } + ); + } + + const maybeNameToFilePath = (moduleName: string) => { + // If this is a service worker, always return the entrypoint path. + // Service workers can't have additional JavaScript modules. + if (bundleType === "commonjs") { + return resolvedEntryPointPath; + } + // Similarly, if the name matches the entrypoint, return its path + if (moduleName === entryPointName) { + return resolvedEntryPointPath; + } + // Otherwise, return the file path of the matching module (if any) + for (const module of modules) { + if (moduleName === module.name) { + return module.filePath; + } + } + }; + const retrieveSourceMap: RetrieveSourceMapFunction = (moduleName) => + maybeRetrieveFileSourceMap(maybeNameToFilePath(moduleName)); + + err.notes[0].text = getSourceMappedString( + err.notes[0].text, + retrieveSourceMap + ); + } + + throw err; + } + } + if (props.outfile) { + // we're using a custom output file, + // so let's first ensure it's parent directory exists + mkdirSync(path.dirname(props.outfile), { recursive: true }); + + const serializedFormData = await new Response(workerBundle).arrayBuffer(); + + writeFileSync(props.outfile, Buffer.from(serializedFormData)); + } + + if (isDryRun) { + logger.log(`--dry-run: exiting now.`); + return { versionId, workerTag }; + } + + const uploadMs = Date.now() - start; + + logger.log("Uploaded", workerName, formatTime(uploadMs)); + + if ( + normalisedContainerConfig.length && + props.containersRollout !== "none" && + callbacks.deployContainers + ) { + assert(versionId && accountId); + await callbacks.deployContainers(config, normalisedContainerConfig, { + versionId, + accountId, + scriptName, + }); + } + + // Early exit for WfP since it doesn't need the below code + if (props.dispatchNamespace !== undefined) { + deployWfpUserWorker(props.dispatchNamespace, versionId); + return { versionId, workerTag, assetUploadStats }; + } + assert(accountId); + // deploy triggers + const targets = await triggersDeploy({ + config, + accountId, + scriptName, + env: props.env, + crons: props.triggers, + firstDeploy: !workerExists, + routes: props.routes, + }); + + logger.log("Current Version ID:", versionId); + + return { + sourceMapSize, + versionId, + workerTag, + assetUploadStats, + targets: targets ?? [], + }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/assets.ts b/packages/deploy-helpers/src/deploy/helpers/assets.ts new file mode 100644 index 0000000..1a37224 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/assets.ts @@ -0,0 +1,576 @@ +import assert from "node:assert"; +import { createReadStream } from "node:fs"; +import { readdir, readFile, stat } from "node:fs/promises"; +import * as path from "node:path"; +import { parseStaticRouting } from "@cloudflare/workers-shared/utils/configuration/parseStaticRouting"; +import { + CF_ASSETS_IGNORE_FILENAME, + HEADERS_FILENAME, + MAX_ASSET_SIZE, + REDIRECTS_FILENAME, +} from "@cloudflare/workers-shared/utils/constants"; +import { + createAssetsIgnoreFunction, + getContentType, + maybeGetFile, + normalizeFilePath, +} from "@cloudflare/workers-shared/utils/helpers"; +import { + APIError, + FatalError, + formatTime, + LOGGER_LEVELS, + UserError, +} from "@cloudflare/workers-utils"; +import chalk from "chalk"; +import PQueue from "p-queue"; +import prettyBytes from "pretty-bytes"; +import { FormData } from "undici"; +import { fetchResult, logger } from "../../shared/context"; +import { hashFile } from "./hash"; +import { decodeJwtPayload, isJwtExpired } from "./jwt"; +import type { SharedDeployVersionsProps } from "../../shared/types"; +import type { AssetConfig, RouterConfig } from "@cloudflare/workers-shared"; +import type { + AssetsOptions, + ComplianceConfig, + Config, +} from "@cloudflare/workers-utils"; + +export type AssetManifest = { [path: string]: { hash: string; size: number } }; + +type InitializeAssetsResponse = { + // string of file hashes per bucket + buckets: string[][]; + jwt: string; +}; + +type UploadResponse = { + jwt?: string; +}; + +export type AssetUploadStats = { + assetUploadDurationMs: number; + assetUploadIsBulk: boolean; + assetUploadFileCount: number; + assetUploadTotalBytes: number; +}; + +export type AssetsUploadResult = { + jwt: string; + assetUploadStats: AssetUploadStats; +}; + +// constants same as Pages for now +const BULK_UPLOAD_CONCURRENCY = 3; +const EDGE_KV_UPLOAD_CONCURRENCY = 50; +const MAX_UPLOAD_ATTEMPTS = 5; +const MAX_UPLOAD_GATEWAY_ERRORS = 5; + +const MAX_DIFF_LINES = 100; + +export const syncAssets = async ( + complianceConfig: ComplianceConfig, + accountId: string | undefined, + assetDirectory: string, + scriptName: string, + dispatchNamespace?: string +): Promise => { + assert(accountId, "Missing accountId"); + + // 1. generate asset manifest + logger.info("🌀 Building list of assets..."); + const manifest = await buildAssetManifest(assetDirectory); + + const url = dispatchNamespace + ? `/accounts/${accountId}/workers/dispatch/namespaces/${dispatchNamespace}/scripts/${scriptName}/assets-upload-session` + : `/accounts/${accountId}/workers/scripts/${scriptName}/assets-upload-session`; + + // 2. fetch buckets w/ hashes + logger.info("🌀 Starting asset upload..."); + const initializeAssetsResponse = + await fetchResult(complianceConfig, url, { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify({ manifest: manifest }), + }); + + // In the past we've seen the endpoint return that incorrectly doesn't contain + // a null response (see: https://github.com/cloudflare/workers-sdk/issues/9465). + // So just to be extra sure here we check the object and provide a clear error message to the user + // if it is falsy. + if (!initializeAssetsResponse) { + throw new FatalError( + "An unexpected response has been received from the Cloudflare API for assets upload. Please try again.", + { code: 1, telemetryMessage: "assets upload unexpected api response" } + ); + } + + const filesToUpload = initializeAssetsResponse.buckets.flat(); + const useSingleAssetUpload = isSingleAssetUploadMode( + initializeAssetsResponse.jwt + ); + + // if nothing to upload, return + if (filesToUpload.length === 0) { + if (!initializeAssetsResponse.jwt) { + throw new FatalError( + "Could not find assets information to attach to deployment. Please try again.", + { code: 1, telemetryMessage: "assets upload missing completion token" } + ); + } + logger.info( + `No updated asset files to upload. Proceeding with deployment...` + ); + return { + jwt: initializeAssetsResponse.jwt, + assetUploadStats: { + assetUploadDurationMs: 0, + assetUploadIsBulk: !useSingleAssetUpload, + assetUploadFileCount: 0, + assetUploadTotalBytes: 0, + }, + }; + } + + // 3. fill buckets and upload assets + const numberFilesToUpload = filesToUpload.length; + logger.info( + `🌀 Found ${numberFilesToUpload} new or modified static asset${ + numberFilesToUpload > 1 ? "s" : "" + } to upload. Proceeding with upload...` + ); + + // Create the buckets outside of doUpload so we can retry without losing track of potential duplicate files + // But don't add the actual content until uploading so we don't run out of memory + const manifestLookup = Object.entries(manifest); + let assetLogCount = 0; + const assetBuckets = initializeAssetsResponse.buckets.map((bucket) => { + return bucket.map((fileHash) => { + const manifestEntry = manifestLookup.find( + (file) => file[1].hash === fileHash + ); + if (manifestEntry === undefined) { + throw new FatalError( + `A file was requested that does not appear to exist.`, + { + code: 1, + telemetryMessage: + "A file was requested that does not appear to exist. (asset manifest upload)", + } + ); + } + // just logging file uploads at the moment... + // unsure how to log deletion vs unchanged file ignored/if we want to log this + assetLogCount = logAssetUpload(`+ ${manifestEntry[0]}`, assetLogCount); + return manifestEntry; + }); + }); + + const uploadBuckets = useSingleAssetUpload + ? assetBuckets.flat().map((entry) => [entry]) + : assetBuckets; + + const concurrency = useSingleAssetUpload + ? getEdgeKvUploadConcurrency(initializeAssetsResponse.jwt) + : BULK_UPLOAD_CONCURRENCY; + if (useSingleAssetUpload) { + logger.debug(`Edge KV asset upload concurrency: ${concurrency}`); + } + const queue = new PQueue({ concurrency }); + const queuePromises: Array> = []; + const start = Date.now(); + let completionJwt = ""; + let uploadedAssetsCount = 0; + let uploadedBytes = 0; + + for (const [bucketIndex, bucket] of uploadBuckets.entries()) { + let attempts = 0; + let gatewayErrors = 0; + const doUpload = async (): Promise => { + const uploadedFiles: string[] = []; + for (const manifestEntry of bucket) { + uploadedFiles.push(manifestEntry[0]); + } + + try { + let res: UploadResponse; + if (useSingleAssetUpload) { + const manifestEntry = bucket[0]; + const absFilePath = path.join(assetDirectory, manifestEntry[0]); + const contentType = getContentType(absFilePath); + res = await fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/assets/upload/${manifestEntry[1].hash}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${initializeAssetsResponse.jwt}`, + "Content-Type": contentType ?? "application/null", + }, + body: createReadStream(absFilePath), + duplex: "half", + } + ); + } else { + // Populate the payload only when actually uploading (this is limited to 3 concurrent uploads at 50 MiB per bucket meaning we'd only load in a max of ~150 MiB) + // This is so we don't run out of memory trying to upload the files. + const payload = new FormData(); + for (const manifestEntry of bucket) { + const absFilePath = path.join(assetDirectory, manifestEntry[0]); + payload.append( + manifestEntry[1].hash, + new File( + [(await readFile(absFilePath)).toString("base64")], + manifestEntry[1].hash, + { + // Most formdata body encoders (incl. undici's) will override with "application/octet-stream" if you use a falsy value here + // Additionally, it appears that undici doesn't support non-standard main types (e.g. "null") + // So, to make it easier for any other clients, we'll just parse "application/null" on the API + // to mean actually null (signal to not send a Content-Type header with the response) + type: getContentType(absFilePath) ?? "application/null", + } + ), + manifestEntry[1].hash + ); + } + res = await fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/assets/upload?base64=true`, + { + method: "POST", + headers: { + Authorization: `Bearer ${initializeAssetsResponse.jwt}`, + }, + body: payload, + } + ); + } + uploadedAssetsCount += bucket.length; + uploadedBytes += bucket.reduce( + (total, manifestEntry) => total + manifestEntry[1].size, + 0 + ); + logAssetsUploadStatus( + numberFilesToUpload, + uploadedAssetsCount, + uploadedFiles + ); + return res; + } catch (e) { + if (attempts < MAX_UPLOAD_ATTEMPTS) { + logger.info( + chalk.dim( + `Asset upload failed. Retrying... ${attempts + 1} of ${MAX_UPLOAD_ATTEMPTS} attempts.\n` + ) + ); + logger.debug(e); + // Exponential backoff, 1 second first time, then 2 second, then 4 second etc. + await new Promise((resolvePromise) => + setTimeout(resolvePromise, Math.pow(2, attempts) * 1000) + ); + if (e instanceof APIError && e.isGatewayError()) { + // Gateway problem, wait for some additional time and set concurrency to 1 + queue.concurrency = 1; + await new Promise((resolvePromise) => + setTimeout(resolvePromise, Math.pow(2, gatewayErrors) * 5000) + ); + gatewayErrors++; + // only count as a failed attempt after a few initial gateway errors + if (gatewayErrors >= MAX_UPLOAD_GATEWAY_ERRORS) { + attempts++; + } + } else { + attempts++; + } + return doUpload(); + } else if (isJwtExpired(initializeAssetsResponse.jwt)) { + throw new FatalError( + `Upload took too long.\n` + + `Asset upload took too long on bucket ${bucketIndex + 1}/${ + uploadBuckets.length + }. Please try again.\n` + + `Assets already uploaded have been saved, so the next attempt will automatically resume from this point.`, + { telemetryMessage: "Asset upload took too long" } + ); + } else { + throw e; + } + } + }; + // add to queue and run it if we haven't reached concurrency limit + queuePromises.push( + queue.add(() => + doUpload().then((res) => { + completionJwt = res.jwt || completionJwt; + }) + ) + ); + } + queue.on("error", (error) => { + logger.error(error.message); + throw error; + }); + // using Promise.all() here instead of queue.onIdle() to ensure + // we actually throw errors that occur within queued promises. + await Promise.all(queuePromises); + + // if queue finishes without receiving JWT from asset upload service (AUS) + // AUS only returns this in the final bucket upload response + if (!completionJwt) { + throw new FatalError("Failed to complete asset upload. Please try again.", { + code: 1, + telemetryMessage: "assets upload completion failed", + }); + } + + const uploadMs = Date.now() - start; + const skipped = Object.keys(manifest).length - numberFilesToUpload; + const skippedMessage = skipped > 0 ? `(${skipped} already uploaded) ` : ""; + + logger.log( + `✨ Success! Uploaded ${numberFilesToUpload} file${ + numberFilesToUpload > 1 ? "s" : "" + } ${skippedMessage}${formatTime(uploadMs)}\n` + ); + + return { + jwt: completionJwt, + assetUploadStats: { + assetUploadDurationMs: uploadMs, + assetUploadIsBulk: !useSingleAssetUpload, + assetUploadFileCount: uploadedAssetsCount, + assetUploadTotalBytes: uploadedBytes, + }, + }; +}; + +function isSingleAssetUploadMode(jwt: string): boolean { + try { + return decodeJwtPayload(jwt).wrangler_single_asset_uploads === true; + } catch { + return false; + } +} + +export function getEdgeKvUploadConcurrency(jwt: string): number { + try { + const value = Number(decodeJwtPayload(jwt).edge_kv_upload_concurrency); + return value > 0 ? Math.floor(value) : EDGE_KV_UPLOAD_CONCURRENCY; + } catch { + return EDGE_KV_UPLOAD_CONCURRENCY; + } +} + +export const buildAssetManifest = async (dir: string) => { + const files = await readdir(dir, { recursive: true }); + logReadFilesFromDirectory(dir, files); + + const manifest: AssetManifest = {}; + + const { assetsIgnoreFunction, assetsIgnoreFilePresent } = + await createAssetsIgnoreFunction(dir); + + await Promise.all( + files.map(async (relativeFilepath) => { + if (assetsIgnoreFunction(relativeFilepath)) { + logger.debug("Ignoring asset:", relativeFilepath); + // This file should not be included in the manifest. + return; + } + + const filepath = path.join(dir, relativeFilepath); + const filestat = await stat(filepath); + + if (filestat.isSymbolicLink() || filestat.isDirectory()) { + return; + } else { + errorOnLegacyPagesWorkerJSAsset( + relativeFilepath, + assetsIgnoreFilePresent + ); + + if (filestat.size > MAX_ASSET_SIZE) { + throw new UserError( + `Asset too large.\n` + + `Cloudflare Workers supports assets with sizes of up to ${prettyBytes( + MAX_ASSET_SIZE, + { + binary: true, + } + )}. We found a file ${filepath} with a size of ${prettyBytes( + filestat.size, + { + binary: true, + } + )}.\n` + + `Ensure all assets in your assets directory "${dir}" conform with the Workers maximum size requirement.`, + { telemetryMessage: "Asset too large" } + ); + } + manifest[normalizeFilePath(relativeFilepath)] = { + hash: hashFile(filepath), + size: filestat.size, + }; + } + }) + ); + return manifest; +}; + +function logAssetUpload(line: string, diffCount: number) { + const level = logger.loggerLevel ?? "log"; + if (LOGGER_LEVELS[level] >= LOGGER_LEVELS.debug) { + // If we're logging as debug level, we want *all* diff lines to be logged + // at debug level, not just the first MAX_DIFF_LINES + logger.debug(line); + } else if (diffCount < MAX_DIFF_LINES) { + // Otherwise, log the first MAX_DIFF_LINES diffs at info level... + logger.info(line); + } else if (diffCount === MAX_DIFF_LINES) { + // ...and warn when we start to truncate it + const msg = + " (truncating changed assets log, set `WRANGLER_LOG=debug` environment variable to see full diff)"; + logger.info(chalk.dim(msg)); + } + return ++diffCount; +} + +/** + * Logs a summary of the assets upload status ("Uploaded of assets"), + * and the list of uploaded files if in debug log level. + */ +function logAssetsUploadStatus( + numberFilesToUpload: number, + uploadedAssetsCount: number, + uploadedAssetFiles: string[] +) { + logger.info( + `Uploaded ${uploadedAssetsCount} of ${numberFilesToUpload} asset${ + numberFilesToUpload === 1 ? "" : "s" + }` + ); + uploadedAssetFiles.forEach((file) => logger.debug(`✨ ${file}`)); +} + +/** + * Logs a summary of files read from a given directory ("Read + * files from directory "), and the list of read files if in + * debug log level. + */ +function logReadFilesFromDirectory(directory: string, assetFiles: string[]) { + logger.info( + `✨ Read ${assetFiles.length} file${ + assetFiles.length === 1 ? "" : "s" + } from the assets directory ${directory}` + ); + assetFiles.forEach((file) => logger.debug(`/${file}`)); +} + +const WORKER_JS_FILENAME = "_worker.js"; + +/** + * Throws an error if the project has no `.assetsIgnore` file and is uploading + * _worker.js code as an asset, which could expose server-side code publicly. + */ +function errorOnLegacyPagesWorkerJSAsset( + file: string, + hasAssetsIgnoreFile: boolean +) { + if (!hasAssetsIgnoreFile) { + const workerJsType: "file" | "directory" | null = + file === WORKER_JS_FILENAME + ? "file" + : file.startsWith(WORKER_JS_FILENAME) + ? "directory" + : null; + if (workerJsType !== null) { + throw new UserError( + ` +Uploading a Pages ${WORKER_JS_FILENAME} ${workerJsType} as an asset. +This could expose your private server-side code to the public Internet. Is this intended? +If you do not want to upload this ${workerJsType}, either remove it or add an "${CF_ASSETS_IGNORE_FILENAME}" file, to the root of your asset directory, containing "${WORKER_JS_FILENAME}" to avoid uploading. +If you do want to upload this ${workerJsType}, you can add an empty "${CF_ASSETS_IGNORE_FILENAME}" file, to the root of your asset directory, to hide this error. + `.trim(), + { telemetryMessage: "assets validation legacy pages worker asset" } + ); + } + } +} + +export function resolveAssetOptions( + { assetsDir, main }: Pick, + config: Config +): AssetsOptions | undefined { + if (!assetsDir) { + return undefined; + } + + const { directory, binding, directoryExists } = assetsDir; + + const routerConfig: RouterConfig = { + has_user_worker: main !== undefined, + }; + + if (typeof config.assets?.run_worker_first === "boolean") { + routerConfig.invoke_user_worker_ahead_of_assets = + config.assets.run_worker_first; + } else if (Array.isArray(config.assets?.run_worker_first)) { + routerConfig.static_routing = parseStaticRouting( + config.assets.run_worker_first + ); + } + + // User Worker always ahead of assets, but no assets binding provided + if ( + routerConfig.invoke_user_worker_ahead_of_assets && + !config?.assets?.binding + ) { + logger.warn( + "run_worker_first=true set without an assets binding\n" + + "Setting run_worker_first to true will always invoke your Worker script.\n" + + "To fetch your assets from your Worker, please set the [assets.binding] key in your configuration file.\n\n" + + "Read more: https://developers.cloudflare.com/workers/static-assets/binding/#binding" + ); + } + + // Using run_worker_first but didn't provide a Worker script + if ( + !routerConfig.has_user_worker && + (routerConfig.invoke_user_worker_ahead_of_assets === true || + routerConfig.static_routing) + ) { + throw new UserError( + "Cannot set run_worker_first without a Worker script.\n" + + "Please remove run_worker_first from your configuration file, or provide a Worker script in your configuration file (`main`).", + { telemetryMessage: "assets router missing worker script" } + ); + } + + const _redirects = directoryExists + ? maybeGetFile(path.join(directory, REDIRECTS_FILENAME)) + : undefined; + const _headers = directoryExists + ? maybeGetFile(path.join(directory, HEADERS_FILENAME)) + : undefined; + + // defaults are set in asset worker + const assetConfig: AssetConfig = { + html_handling: config.assets?.html_handling, + not_found_handling: config.assets?.not_found_handling, + // The _redirects and _headers files are parsed in Miniflare in dev and parsing is not required for deploy + compatibility_date: config.compatibility_date, + compatibility_flags: config.compatibility_flags, + }; + + return { + directory, + binding, + routerConfig, + assetConfig, + _redirects, + _headers, + // raw static routing rules for upload. routerConfig.static_routing contains the rules processed for dev. + run_worker_first: config.assets?.run_worker_first, + }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/binding-depends-on-export.ts b/packages/deploy-helpers/src/deploy/helpers/binding-depends-on-export.ts new file mode 100644 index 0000000..6df4843 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/binding-depends-on-export.ts @@ -0,0 +1,20 @@ +/** + * Build the user-facing error message for EWC code 100406 + * (an actor binding references a Durable Object class declared in `exports` + * but not yet provisioned, on `wrangler versions upload`). + * + * EWC's own message is already fully actionable — it names the offending + * binding and class and spells out both remediations (deploy to provision, or + * drop the binding and use `ctx.exports.`). We surface it verbatim + * rather than re-deriving a less specific message client-side. The fallback is + * only used in the unlikely event the server sends an empty message. + */ +export function renderBindingDependsOnExportError( + serverMessage: string +): string { + const trimmed = serverMessage.trim(); + if (trimmed.length > 0) { + return trimmed; + } + return "A Durable Object binding references a class declared in `exports` but not yet provisioned. Declarative `exports` are reconciled when the version is deployed. Deploy this version to provision the class, or remove the binding and access the Durable Object via `ctx.exports.` until then."; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/binding-utils.ts b/packages/deploy-helpers/src/deploy/helpers/binding-utils.ts new file mode 100644 index 0000000..bb22968 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/binding-utils.ts @@ -0,0 +1,400 @@ +import type { + Binding, + Config, + ConfigBindingFieldName, +} from "@cloudflare/workers-utils"; + +function assertNever(_value: never) {} + +interface ConvertBindingsOptions { + /** + * Use preview IDs (preview_id, preview_bucket_name, preview_database_id) instead of production IDs when resolving a binding ID. + * This means that the rest of Wrangler does not need to be aware of preview IDs, and can just use regular IDs. + */ + usePreviewIds?: boolean; + /** + * Exclude bindings that Pages doesn't support + */ + pages?: boolean; +} + +/** + * Convert Config to the Record format for consistent internal use. + */ +export function convertConfigToBindings( + config: Partial>, + options?: ConvertBindingsOptions +): Record { + const { usePreviewIds = false, pages = false } = options ?? {}; + const output: Record = {}; + + type Entries = { [K in keyof T]: [K, T[K]] }[keyof T][]; + type ConfigIterable = Entries>>; + const configIterable = Object.entries(config) as ConfigIterable; + + for (const [type, info] of configIterable) { + if (info === undefined) { + continue; + } + + switch (type) { + case "vars": { + for (const [key, value] of Object.entries(info)) { + if (typeof value === "string") { + output[key] = { type: "plain_text", value }; + } else { + output[key] = { type: "json", value }; + } + } + break; + } + case "kv_namespaces": { + for (const { binding, ...x } of info) { + output[binding] = { + type: "kv_namespace", + ...x, + id: usePreviewIds ? (x.preview_id ?? x.id) : x.id, + }; + } + break; + } + case "send_email": { + if (pages) { + break; + } + for (const { name, ...x } of info) { + output[name] = { type: "send_email", ...x }; + } + break; + } + case "wasm_modules": { + if (pages) { + break; + } + for (const [key, value] of Object.entries(info)) { + if (typeof value === "string") { + output[key] = { type: "wasm_module", source: { path: value } }; + } else { + output[key] = { type: "wasm_module", source: { contents: value } }; + } + } + break; + } + case "text_blobs": { + if (pages) { + break; + } + for (const [key, value] of Object.entries(info)) { + output[key] = { type: "text_blob", source: { path: value } }; + } + break; + } + case "data_blobs": { + if (pages) { + break; + } + for (const [key, value] of Object.entries(info)) { + if (typeof value === "string") { + output[key] = { type: "data_blob", source: { path: value } }; + } else { + output[key] = { type: "data_blob", source: { contents: value } }; + } + } + break; + } + case "browser": { + const { binding, ...x } = info; + output[binding] = { type: "browser", ...x }; + break; + } + case "durable_objects": { + for (const { name, ...x } of info.bindings ?? []) { + output[name] = { type: "durable_object_namespace", ...x }; + } + break; + } + case "workflows": { + for (const { binding, ...x } of info) { + output[binding] = { type: "workflow", ...x }; + } + break; + } + case "queues": { + for (const { binding, ...x } of info.producers ?? []) { + output[binding] = { + type: "queue", + queue_name: x.queue, + ...x, + }; + } + break; + } + case "r2_buckets": { + for (const { binding, ...x } of info) { + output[binding] = { + type: "r2_bucket", + ...x, + bucket_name: usePreviewIds + ? (x.preview_bucket_name ?? x.bucket_name) + : x.bucket_name, + }; + } + break; + } + case "d1_databases": { + for (const { binding, ...x } of info) { + output[binding] = { + type: "d1", + ...x, + database_id: usePreviewIds + ? (x.preview_database_id ?? x.database_id) + : x.database_id, + }; + } + break; + } + case "services": { + for (const { binding, ...x } of info) { + output[binding] = { type: "service", ...x }; + } + break; + } + case "analytics_engine_datasets": { + for (const { binding, ...x } of info) { + output[binding] = { type: "analytics_engine", ...x }; + } + break; + } + case "dispatch_namespaces": { + if (pages) { + break; + } + for (const { binding, ...x } of info) { + output[binding] = { type: "dispatch_namespace", ...x }; + } + break; + } + case "mtls_certificates": { + for (const { binding, ...x } of info) { + output[binding] = { type: "mtls_certificate", ...x }; + } + break; + } + case "logfwdr": { + if (pages) { + break; + } + for (const { name, ...x } of info.bindings ?? []) { + output[name] = { type: "logfwdr", ...x }; + } + break; + } + case "ai": { + const { binding, ...x } = info; + output[binding] = { type: "ai", ...x }; + break; + } + case "images": { + const { binding, ...x } = info; + output[binding] = { type: "images", ...x }; + break; + } + case "stream": { + const { binding, ...x } = info; + output[binding] = { type: "stream", ...x }; + break; + } + case "version_metadata": { + const { binding, ...x } = info; + output[binding] = { type: "version_metadata", ...x }; + break; + } + case "hyperdrive": { + for (const { binding, ...x } of info) { + output[binding] = { type: "hyperdrive", ...x }; + } + break; + } + case "vectorize": { + for (const { binding, ...x } of info) { + output[binding] = { type: "vectorize", ...x }; + } + break; + } + case "ai_search_namespaces": { + for (const { binding, ...x } of info) { + output[binding] = { type: "ai_search_namespace", ...x }; + } + break; + } + case "ai_search": { + for (const { binding, ...x } of info) { + output[binding] = { type: "ai_search", ...x }; + } + break; + } + case "websearch": { + const { binding, ...x } = info; + output[binding] = { type: "websearch", ...x }; + break; + } + case "agent_memory": { + for (const { binding, ...x } of info) { + output[binding] = { type: "agent_memory", ...x }; + } + break; + } + case "unsafe": { + if (pages) { + break; + } + for (const { type: unsafeType, name, ...data } of info.bindings ?? []) { + output[name] = { type: `unsafe_${unsafeType}`, ...data }; + } + break; + } + case "assets": { + if (pages) { + break; + } + if (info.binding) { + output[info.binding] = { type: "assets" }; + } + break; + } + case "pipelines": { + if (pages) { + break; + } + for (const { binding, ...x } of info) { + output[binding] = { type: "pipeline", ...x }; + } + break; + } + case "secrets_store_secrets": { + for (const { binding, ...x } of info) { + output[binding] = { type: "secrets_store_secret", ...x }; + } + break; + } + case "artifacts": { + for (const { binding, ...x } of info) { + output[binding] = { type: "artifacts", ...x }; + } + break; + } + case "unsafe_hello_world": { + if (pages) { + break; + } + for (const { binding, ...x } of info) { + output[binding] = { type: "unsafe_hello_world", ...x }; + } + break; + } + case "flagship": { + for (const { binding, ...x } of info) { + output[binding] = { type: "flagship", ...x }; + } + break; + } + case "ratelimits": { + for (const { name, ...x } of info) { + output[name] = { type: "ratelimit", ...x }; + } + break; + } + case "worker_loaders": { + for (const { binding, ...x } of info) { + output[binding] = { type: "worker_loader", ...x }; + } + break; + } + case "vpc_services": { + for (const { binding, ...x } of info) { + output[binding] = { type: "vpc_service", ...x }; + } + break; + } + case "vpc_networks": { + for (const { binding, ...x } of info) { + output[binding] = { type: "vpc_network", ...x }; + } + break; + } + case "media": { + const { binding, ...x } = info; + output[binding] = { type: "media", ...x }; + break; + } + default: + assertNever(type); + } + } + + return output; +} + +export function isUnsafeBindingType(type: string): type is `unsafe_${string}` { + return type.startsWith("unsafe_"); +} + +/** + * What configuration key does this binding use for referring to it's binding name? + */ +const nameBindings = [ + "durable_object_namespace", + "logfwdr", + "ratelimit", + "unsafe_ratelimit", + "send_email", +] as const; + +function getBindingKey(type: Binding["type"]) { + if ((nameBindings as readonly string[]).includes(type)) { + return "name"; + } + return "binding"; +} + +type FlatBinding = Extract & + (Type extends (typeof nameBindings)[number] + ? { + name: string; + } + : { + binding: string; + }); + +export function extractBindingsOfType( + type: Type, + bindings: Record | undefined +): FlatBinding[] { + return Object.entries(bindings ?? {}) + .filter( + (binding): binding is [string, Extract] => + binding[1].type === type + ) + .map((binding) => ({ + ...binding[1], + [getBindingKey(type)]: binding[0], + })) as FlatBinding[]; +} + +/** + * Get bindings from a Config object in the standard Record format. + */ +export function getBindings( + config: Config | undefined, + options?: { + pages?: boolean; + } +): Record { + if (!config) { + return {}; + } + return convertConfigToBindings(config, { + usePreviewIds: false, + pages: options?.pages, + }); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/bundle-reporter.ts b/packages/deploy-helpers/src/deploy/helpers/bundle-reporter.ts new file mode 100644 index 0000000..ab891b3 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/bundle-reporter.ts @@ -0,0 +1,44 @@ +import { Blob } from "node:buffer"; +import { gzipSync } from "node:zlib"; +import chalk from "chalk"; +import { logger } from "../../shared/context"; +import type { CfModule } from "@cloudflare/workers-utils"; + +const ONE_KIB_BYTES = 1024; +// Current max is 3 MiB for free accounts, 10 MiB for paid accounts. +// See https://developers.cloudflare.com/workers/platform/limits/#worker-size +const MAX_GZIP_SIZE_BYTES = 3 * ONE_KIB_BYTES * ONE_KIB_BYTES; + +async function getSize(modules: Pick[]) { + const gzipSize = gzipSync( + await new Blob(modules.map((file) => file.content)).arrayBuffer() + ).byteLength; + const aggregateSize = new Blob(modules.map((file) => file.content)).size; + + return { size: aggregateSize, gzipSize }; +} + +export async function printBundleSize( + main: { + name: string; + content: string; + }, + modules: CfModule[] +) { + const { size, gzipSize } = await getSize([...modules, main]); + + const bundleReport = `${(size / ONE_KIB_BYTES).toFixed(2)} KiB / gzip: ${( + gzipSize / ONE_KIB_BYTES + ).toFixed(2)} KiB`; + + const percentage = (gzipSize / MAX_GZIP_SIZE_BYTES) * 100; + + const colorizedReport = + percentage > 90 + ? chalk.red(bundleReport) + : percentage > 70 + ? chalk.yellow(bundleReport) + : chalk.green(bundleReport); + + logger.log(`Total Upload: ${colorizedReport}`); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/capnp.ts b/packages/deploy-helpers/src/deploy/helpers/capnp.ts new file mode 100644 index 0000000..4899edd --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/capnp.ts @@ -0,0 +1,39 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { UserError } from "@cloudflare/workers-utils"; +import { sync as commandExistsSync } from "command-exists"; +import type { CfCapnp } from "@cloudflare/workers-utils"; + +export function handleUnsafeCapnp(capnp: CfCapnp): Buffer { + if (capnp.compiled_schema) { + return readFileSync(resolve(capnp.compiled_schema)); + } + + const { base_path, source_schemas } = capnp; + const capnpSchemas = (source_schemas ?? []).map((x) => + resolve(base_path as string, x) + ); + if (!commandExistsSync("capnp")) { + throw new UserError( + "The capnp compiler is required to upload capnp schemas, but is not present.", + { telemetryMessage: "capnp compiler missing" } + ); + } + const srcPrefix = resolve(base_path ?? "."); + const capnpProcess = spawnSync( + "capnp", + ["compile", "-o-", `--src-prefix=${srcPrefix}`, ...capnpSchemas], + // This number was chosen arbitrarily. If you get ENOBUFS because your compiled schema is still + // too large, then we may need to bump this again or figure out another approach. + // https://github.com/cloudflare/workers-sdk/pull/10217 + { maxBuffer: 3 * 1024 * 1024 } + ); + if (capnpProcess.error) { + throw capnpProcess.error; + } + if (capnpProcess.stderr.length) { + throw new Error(capnpProcess.stderr.toString()); + } + return capnpProcess.stdout; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/check-remote-secrets-override.ts b/packages/deploy-helpers/src/deploy/helpers/check-remote-secrets-override.ts new file mode 100644 index 0000000..23c53b9 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/check-remote-secrets-override.ts @@ -0,0 +1,173 @@ +import { fetchResult } from "../../shared/context"; +import { isWorkerNotFoundError } from "./worker-not-found-error"; +import type { Config } from "@cloudflare/workers-utils"; + +export async function fetchSecrets( + config: Config, + scriptName: string, + accountId: string +): Promise<{ name: string; type: string }[]> { + const url = `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`; + + return fetchResult<{ name: string; type: string }[]>(config, url); +} + +export async function checkRemoteSecretsOverride( + config: Config, + scriptName: string, + accountId: string +): Promise< + | { + override: false; + } + | { + override: true; + deployErrorMessage: string; + } +> { + const envVarNames = Object.keys(config.vars ?? {}); + const bindingNames = extractBindingNames(config); + + if (envVarNames.length + bindingNames.length > 0) { + const secretNames = new Set(); + + try { + const secrets = await fetchSecrets(config, scriptName, accountId); + + for (const secret of secrets) { + secretNames.add(secret.name); + } + } catch (e) { + if (isWorkerNotFoundError(e)) { + return { override: false }; + } + throw e; + } + + const envVarNamesOverridingSecrets = envVarNames.filter((name) => + secretNames.has(name) + ); + + const bindingNamesOverridingSecrets = bindingNames.filter((name) => + secretNames.has(name) + ); + + if ( + envVarNamesOverridingSecrets.length + + bindingNamesOverridingSecrets.length === + 0 + ) { + return { override: false }; + } + + if ( + envVarNamesOverridingSecrets.length && + !bindingNamesOverridingSecrets.length + ) { + return { + override: true, + deployErrorMessage: constructSingleTypeDeployErrorMessage( + envVarNamesOverridingSecrets, + "variable" + ), + }; + } + + if ( + bindingNamesOverridingSecrets.length && + !envVarNamesOverridingSecrets.length + ) { + return { + override: true, + deployErrorMessage: constructSingleTypeDeployErrorMessage( + bindingNamesOverridingSecrets, + "binding" + ), + }; + } + + const affectedSecrets = [ + ...envVarNamesOverridingSecrets, + ...bindingNamesOverridingSecrets, + ]; + + return { + override: true, + deployErrorMessage: `Configuration values (${listNames(affectedSecrets)}) conflict with existing remote secrets. This deployment will replace these remote secrets with the configuration values.`, + }; + } + + return { override: false }; +} + +function extractBindingNames(config: Config): string[] { + return Object.entries(config).flatMap((entry) => { + const key = entry[0] as keyof Config; + const untypedValue = entry[1]; + + switch (key) { + case "durable_objects": { + const value: Config[typeof key] = untypedValue; + return value.bindings.map((doBinding) => doBinding.name); + } + case "workflows": + case "d1_databases": + case "kv_namespaces": + case "r2_buckets": + case "vectorize": + case "ai_search_namespaces": + case "ai_search": + case "agent_memory": + case "services": + case "mtls_certificates": + case "dispatch_namespaces": + case "vpc_services": + case "vpc_networks": { + const value: Config[typeof key] = untypedValue; + return (value ?? []).map((workflowBinding) => workflowBinding.binding); + } + case "browser": + case "ai": + case "websearch": { + const value: Config[typeof key] = untypedValue; + return value ? [value.binding] : []; + } + case "queues": { + const value: Config[typeof key] = untypedValue; + return (value.producers ?? []).map( + (queueProducer) => queueProducer.binding + ); + } + default: + return []; + } + }); +} + +function listNames(names: string[]): string { + if (names.length <= 1) { + return `\`${names[0]}\``; + } + + if (names.length == 2) { + return `\`${names[0]}\` and \`${names[1]}\``; + } + + return `${names + .slice(0, -1) + .map((name) => `\`${name}\`, `) + .join("")}and \`${names.at(-1)}\``; +} + +function constructSingleTypeDeployErrorMessage( + names: string[], + type: "variable" | "binding" +) { + const multiple = names.length > 1; + + const conflictMessage = `${type === "variable" ? "Environment variable" : "Binding"}${multiple ? "s" : ""} ${listNames(names)} conflict${multiple ? "" : "s"} with ${multiple ? "" : "an "}existing remote secret${multiple ? "s" : ""}.`; + + const deploymentMessage = `This deployment will replace ${multiple ? "these" : "the"} remote secret${multiple ? "s" : ""} with your ${type === "variable" ? "environment variable" : "binding"}${multiple ? "s" : ""}.`; + + return `${conflictMessage} ${deploymentMessage}`; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/check-workflow-conflicts.ts b/packages/deploy-helpers/src/deploy/helpers/check-workflow-conflicts.ts new file mode 100644 index 0000000..c0724e5 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/check-workflow-conflicts.ts @@ -0,0 +1,91 @@ +import { APIError } from "@cloudflare/workers-utils"; +import { fetchResult } from "../../shared/context"; +import type { Config } from "@cloudflare/workers-utils"; + +export type Workflow = { + name: string; + id: string; + created_on: string; + modified_on: string; + script_name: string; + class_name: string; +}; + +export interface WorkflowConflict { + name: string; + currentOwner: string; +} + +export const WORKFLOW_NOT_FOUND_CODE = 10200; + +/** + * Fetches a workflow by name from the Cloudflare API. + * + * @param config - The compliance/config object for API requests + * @param accountId - The account ID + * @param workflowName - The name of the workflow to fetch + * @returns The workflow if it exists, or `null` if not found (API error code 10200) + * @throws {APIError} Re-throws any API error that is not a "workflow not found" error (e.g., network errors, auth errors, rate limits) + */ +async function getWorkflow( + config: Config, + accountId: string, + workflowName: string +): Promise { + try { + return await fetchResult( + config, + `/accounts/${accountId}/workflows/${workflowName}` + ); + } catch (e) { + if (e instanceof APIError && e.code === WORKFLOW_NOT_FOUND_CODE) { + return null; + } + throw e; + } +} + +export async function checkWorkflowConflicts( + config: Config, + accountId: string, + scriptName: string +): Promise< + | { hasConflicts: false } + | { hasConflicts: true; conflicts: WorkflowConflict[]; message: string } +> { + const workflowsToDeploy = config.workflows?.filter( + (w) => w.script_name === undefined || w.script_name === scriptName + ); + + if (!workflowsToDeploy?.length) { + return { hasConflicts: false }; + } + + const workflowChecks = await Promise.all( + workflowsToDeploy.map(async (workflow) => { + const existing = await getWorkflow(config, accountId, workflow.name); + if (existing && existing.script_name !== scriptName) { + return { name: workflow.name, currentOwner: existing.script_name }; + } + return null; + }) + ); + + const conflicts = workflowChecks.filter( + (c): c is WorkflowConflict => c !== null + ); + + if (conflicts.length === 0) { + return { hasConflicts: false }; + } + + const conflictList = conflicts + .map((c) => ` - "${c.name}" (currently belongs to "${c.currentOwner}")`) + .join("\n"); + + const message = + `The following workflow(s) already exist and belong to different workers:\n${conflictList}\n\n` + + `Deploying will reassign these workflows to "${scriptName}". Workflow names must be unique per account. If this reassignment is unintended, rename the workflow(s) in the Wrangler config.`; + + return { hasConflicts: true, conflicts, message }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/config-diffs.ts b/packages/deploy-helpers/src/deploy/helpers/config-diffs.ts new file mode 100644 index 0000000..6b6c68b --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/config-diffs.ts @@ -0,0 +1,688 @@ +import assert from "node:assert"; +import { getSubdomainValuesAPIMock } from "../../triggers/deploy"; +import { + diffJsonObjects, + isModifiedDiffValue, + isNonDestructive, +} from "./diff-json"; +import type { JsonLike } from "./diff-json"; +import type { + Config, + ConfigBindingFieldName, + RawConfig, +} from "@cloudflare/workers-utils"; + +// Exhaustive map of all binding keys in CfWorkerInit["bindings"]. +// When a new binding type is added, TypeScript will error here until it is handled. +const reorderableBindings = { + // Top-level binding arrays + kv_namespaces: true, + r2_buckets: true, + d1_databases: true, + services: true, + send_email: true, + vectorize: true, + ai_search_namespaces: true, + ai_search: true, + agent_memory: true, + hyperdrive: true, + workflows: true, + dispatch_namespaces: true, + mtls_certificates: true, + pipelines: true, + secrets_store_secrets: true, + artifacts: true, + ratelimits: true, + analytics_engine_datasets: true, + unsafe_hello_world: true, + flagship: true, + worker_loaders: true, + vpc_services: true, + vpc_networks: true, + + // Wrapper objects containing binding arrays + durable_objects: true, + queues: true, + logfwdr: true, + + // Non-array bindings (nothing to reorder) + vars: false, + wasm_modules: false, + text_blobs: false, + data_blobs: false, + browser: false, + ai: false, + images: false, + stream: false, + media: false, + websearch: false, + version_metadata: false, + unsafe: false, + assets: false, +} satisfies Record; + +/** Extracts the keys of T whose values are `true` */ +type ReorderableKeys> = { + [K in keyof T]: T[K] extends true ? K : never; +}[keyof T]; + +/** + * Object representing the difference of two configuration objects. + */ +type ConfigDiff = { + /** The actual (raw) computed diff of the two objects */ + diff: Record | null; + /** + * Flag indicating whether the difference includes some destructive changes. + * + * In other words, if the second config is not applying any change or only adding options, such diff is considered non destructive, on the other hand if the config is removing or modifying values it is considered destructive instead. + */ + nonDestructive: boolean; +}; + +/** + * Computes the difference between a remote representation of a Worker's config and a local configuration. + * + * @param remoteConfig The remote representation of a Worker's config + * @param localResolvedConfig The local (resolved) config + * @returns Object containing the diffing information + */ +export function getRemoteConfigDiff( + remoteConfig: RawConfig, + localResolvedConfig: Config +): ConfigDiff { + const normalizedLocalConfig = + normalizeLocalResolvedConfigAsRemote(localResolvedConfig); + const normalizedRemoteConfig = normalizeRemoteConfigAsResolvedLocal( + remoteConfig, + normalizedLocalConfig + ); + + const diff = diffJsonObjects( + normalizedRemoteConfig as unknown as Record, + normalizedLocalConfig as unknown as Record + ); + + return { + diff, + nonDestructive: isNonDestructive(diff), + }; +} + +/** + * Normalized a local (resolved) config object so that it can be compared against + * the remote config object. This mainly means resolving and setting defaults to + * the local configuration to match the values in the remote one. + * + * @param localResolvedConfig The local (resolved) config object to normalize + * @returns The normalized config + */ +function normalizeLocalResolvedConfigAsRemote( + localResolvedConfig: Config +): Config { + const subdomainValues = getSubdomainValuesAPIMock( + localResolvedConfig.workers_dev, + localResolvedConfig.preview_urls, + localResolvedConfig.routes ?? [] + ); + const normalizedConfig: Config = { + ...structuredClone(localResolvedConfig), + workers_dev: subdomainValues.workers_dev, + preview_urls: subdomainValues.preview_urls, + observability: normalizeObservability(localResolvedConfig.observability), + }; + + removeRemoteConfigFieldFromBindings(normalizedConfig); + + // Currently remotely we only get the assets' binding name, so we need remove + // everything else, if present, from the local one + if (normalizedConfig.assets) { + normalizedConfig.assets = { + binding: normalizedConfig.assets.binding, + }; + } + + return normalizedConfig; +} + +/** + * Given a configuration object removes all the `remote` config settings from all the bindings + * in the configuration (this is used as part of the config normalization since the `remote` + * key is not present in the remote configuration object) + * + * @param normalizedConfig The target configuration object (which gets updated side-effectfully) + */ +function removeRemoteConfigFieldFromBindings(normalizedConfig: Config): void { + for (const bindingField of [ + "kv_namespaces", + "r2_buckets", + "d1_databases", + ] as const) { + if (normalizedConfig[bindingField]?.length) { + normalizedConfig[bindingField] = normalizedConfig[bindingField].map( + ({ remote: _, ...binding }) => binding + ); + } + } + + if (normalizedConfig.services?.length) { + normalizedConfig.services = normalizedConfig.services.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.vpc_services?.length) { + normalizedConfig.vpc_services = normalizedConfig.vpc_services.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.vpc_networks?.length) { + normalizedConfig.vpc_networks = normalizedConfig.vpc_networks.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.workflows?.length) { + normalizedConfig.workflows = normalizedConfig.workflows.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.dispatch_namespaces?.length) { + normalizedConfig.dispatch_namespaces = + normalizedConfig.dispatch_namespaces.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.mtls_certificates?.length) { + normalizedConfig.mtls_certificates = normalizedConfig.mtls_certificates.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.pipelines?.length) { + normalizedConfig.pipelines = normalizedConfig.pipelines.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.vectorize?.length) { + normalizedConfig.vectorize = normalizedConfig.vectorize.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.queues?.producers?.length) { + normalizedConfig.queues.producers = normalizedConfig.queues.producers.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.send_email) { + normalizedConfig.send_email = normalizedConfig.send_email.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.ai_search_namespaces?.length) { + normalizedConfig.ai_search_namespaces = + normalizedConfig.ai_search_namespaces.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.ai_search?.length) { + normalizedConfig.ai_search = normalizedConfig.ai_search.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.agent_memory?.length) { + normalizedConfig.agent_memory = normalizedConfig.agent_memory.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.flagship?.length) { + normalizedConfig.flagship = normalizedConfig.flagship.map( + ({ remote: _, ...binding }) => binding + ); + } + + if (normalizedConfig.artifacts?.length) { + normalizedConfig.artifacts = normalizedConfig.artifacts.map( + ({ remote: _, ...binding }) => binding + ); + } + + const singleBindingFields = [ + "browser", + "ai", + "images", + "stream", + "media", + "websearch", + ] as const; + for (const singleBindingField of singleBindingFields) { + if ( + normalizedConfig[singleBindingField] && + "remote" in normalizedConfig[singleBindingField] + ) { + delete normalizedConfig[singleBindingField].remote; + } + } +} + +/** + * Normalizes an observability config object (either the remote or resolved local one) to a fully filled form, this + * helps us resolve any inconsistencies between the local and remote default values. + * + * @param obs The observability config object to normalize + * @returns The normalized observability object + */ +function normalizeObservability( + obs: RawConfig["observability"] +): Config["observability"] { + const normalized = structuredClone(obs); + + const enabled = obs?.enabled === true ? true : false; + + const fullObservabilityDefaults = { + enabled, + head_sampling_rate: 1, + logs: { + enabled, + head_sampling_rate: 1, + invocation_logs: true, + persist: true, + }, + traces: { enabled: false, persist: true, head_sampling_rate: 1 }, + } as const; + + if (!normalized) { + return fullObservabilityDefaults; + } + + const fillUndefinedFields = ( + target: Record, + defaults: Record + ) => { + Object.entries(defaults).forEach(([key, value]) => { + if (target[key] === undefined) { + target[key] = value; + return; + } + + if ( + typeof value === "object" && + value !== null && + typeof target[key] === "object" && + target[key] !== null + ) { + fillUndefinedFields( + target[key] as Record, + value as Record + ); + } + }); + }; + + fillUndefinedFields( + normalized as Record, + fullObservabilityDefaults + ); + + return normalized; +} + +/** + * Normalizes a remote config object (or more precisely our representation of it) into an object that can be + * compared to the local target config. + * + * The normalization is comprized of: + * - making sure that the various config fields are in the same order + * - adding to the remote config object all the non-remote config keys + * - removing from the remote config all the default values that in the local config are either not present or undefined + * + * @param remoteConfig The remote config object to normalize + * @param localConfig The target/local (resolved) config object + * @returns The remote config object normalized and ready to be compared with the local one + */ +function normalizeRemoteConfigAsResolvedLocal( + remoteConfig: RawConfig, + localConfig: Config +): Config { + let normalizedRemote = {} as Config; + + // We start by adding all the local configs to the normalized remote config object + // in this way we can make sure that local-only configurations are not shown as + // differences between local and remote configs + Object.entries(localConfig).forEach(([key, value]) => { + if ( + // We want to skip observability since it has a remote default behavior + // different from that of wrangler + key !== "observability" && + // We want to skip assets since it is a special case, the issue being that + // remotely assets configs only include at most the binding name and we + // already address that in the local config normalization already + key !== "assets" + ) { + (normalizedRemote as unknown as Record)[key] = value; + } + }); + + // We then override the configs present in the remote config object + Object.entries(remoteConfig).forEach(([key, value]) => { + if (key !== "main" && value !== undefined) { + (normalizedRemote as unknown as Record)[key] = value; + } + }); + + normalizedRemote.observability = normalizeObservability( + normalizedRemote.observability + ); + + // We reorder the remote config so that its ordering follows that + // of the local one (this ensures that the diff users see lists + // the configuration options in the same order as their config file) + normalizedRemote = orderObjectFields( + normalizedRemote as unknown as Record, + localConfig as unknown as Record + ) as unknown as Config; + + // Reorder binding arrays to match local's order so the diff is intuitive. + // Binding array order doesn't matter semantically, but positional diffing + // would show spurious changes if the same elements appear in different order. + for (const [bindingKey, shouldReorder] of Object.entries( + reorderableBindings + )) { + if (!shouldReorder) { + continue; + } + + const key = bindingKey as ReorderableKeys; + + // Handle wrapper objects that contain binding arrays as nested properties + if (key === "queues") { + // Only producers are bindings (accessible from Worker code). + // Consumers configure message delivery to the Worker and are + // managed through the Queues API, not the Worker bindings API, + // so they don't appear in the remote config. + if (normalizedRemote.queues?.producers && localConfig.queues?.producers) { + normalizedRemote.queues.producers = reorderBindings( + normalizedRemote.queues.producers, + localConfig.queues.producers + ); + } + continue; + } + + if (key === "durable_objects") { + if ( + normalizedRemote.durable_objects?.bindings && + localConfig.durable_objects?.bindings + ) { + normalizedRemote.durable_objects.bindings = reorderBindings( + normalizedRemote.durable_objects.bindings, + localConfig.durable_objects.bindings + ); + } + continue; + } + + if (key === "logfwdr") { + if (normalizedRemote.logfwdr?.bindings && localConfig.logfwdr?.bindings) { + normalizedRemote.logfwdr.bindings = reorderBindings( + normalizedRemote.logfwdr.bindings, + localConfig.logfwdr.bindings + ); + } + continue; + } + + // Top-level binding arrays + reorderConfigBindings(normalizedRemote, localConfig, key); + } + + return normalizedRemote; +} + +/** + * Generates a stable key for a binding object by JSON-serializing it with sorted keys, + * so that objects with the same properties in different order produce the same key. + */ +function getBindingKey(obj: unknown): string { + return JSON.stringify(obj, (_, v) => + v && typeof v === "object" && !Array.isArray(v) + ? Object.fromEntries( + Object.keys(v) + .sort() + .map((k) => [k, v[k]]) + ) + : v + ); +} + +/** + * Reorders a remote binding array to match the local array's order. + * Elements present in both arrays are placed first (in local order), + * followed by elements only in the remote array. + */ +function reorderBindings(remote: T[], local: T[]): T[] { + const remoteByKey = new Map(remote.map((el) => [getBindingKey(el), el])); + const used = new Set(); + const result: T[] = []; + for (const binding of local) { + const key = getBindingKey(binding); + const remoteEl = remoteByKey.get(key); + if (remoteEl !== undefined) { + result.push(remoteEl); + used.add(key); + } + } + for (const binding of remote) { + if (!used.has(getBindingKey(binding))) { + result.push(binding); + } + } + return result; +} + +/** + * Reorders a top-level binding array on the remote config to match the local config's order. + * Uses a generic key parameter so TypeScript can correlate the types of both accesses. + */ +function reorderConfigBindings< + K extends ReorderableKeys, +>(normalizedRemote: Config, localConfig: Config, key: K): void { + const remoteArr = normalizedRemote[key]; + const localArr = localConfig[key]; + if (Array.isArray(remoteArr) && Array.isArray(localArr)) { + normalizedRemote[key] = reorderBindings(remoteArr, localArr) as Config[K]; + } +} + +/** + * This function reorders the fields of a given object so that they follow a given target object. + * All the fields of the given object not present in the target object will be ordered last. + */ +function orderObjectFields>( + source: T, + target: Record +): T { + const targetKeysIndexesMap = Object.fromEntries( + Object.keys(target).map((key, i) => [key, i]) + ); + + const orderedSource = Object.fromEntries( + Object.entries(source).sort(([keyA], [keyB]) => { + if (keyA in target && !(keyB in target)) { + return -1; + } + + if (!(keyA in target) && keyB in target) { + return 1; + } + + if (!(keyA in target) && !(keyB in target)) { + return 0; + } + + return targetKeysIndexesMap[keyA] - targetKeysIndexesMap[keyB]; + }) + ) as T; + + for (const [key, value] of Object.entries(orderedSource)) { + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + typeof target[key] === "object" && + target[key] !== null && + !Array.isArray(target[key]) + ) { + (orderedSource as Record)[key] = orderObjectFields( + value as Record, + target[key] as Record + ); + } + } + + return orderedSource; +} + +/** + * Given a config diff generates a patch object that can be passed to `experimental_patchConfig` to revert the + * changes in the config object that are described by the config diff. + * + * If the config is for a specific target environment, only the environment config object will be targeted for the patch. + * + * @param configDiff The target config diff + * @param targetEnvironment the target environment if any + * @returns The patch object to pass to `experimental_patchConfig` to revert the changes + */ +export function getConfigPatch( + configDiff: { + diff: Record | null; + nonDestructive: boolean; + }["diff"], + targetEnvironment?: string | undefined +): RawConfig { + const patchObj: RawConfig = {}; + + populateConfigPatch( + configDiff, + patchObj as Record, + targetEnvironment + ); + + return patchObj; +} + +function populateConfigPatch( + diff: JsonLike, + patchObj: Record | JsonLike[], + targetEnvironment?: string +): void { + if (!diff || typeof diff !== "object") { + return; + } + + if (Array.isArray(diff)) { + assert(Array.isArray(patchObj)); + return populateConfigPatchArray(diff, patchObj); + } + + assert(!Array.isArray(patchObj)); + return populateConfigPatchObject(diff, patchObj, targetEnvironment); +} + +function populateConfigPatchArray(diff: JsonLike[], patchArray: JsonLike[]) { + const elementsToAppend: JsonLike[] = []; + + Object.values(diff).forEach((element) => { + if (!Array.isArray(element)) { + return; + } + + if (element.length === 1 && element[0] === " ") { + patchArray.push({}); + return; + } + + if (element.length === 2) { + if (element[0] === "-") { + elementsToAppend.push(element[1]); + return; + } + + if (element[0] === "~" && element[1]) { + const patchEl = {}; + populateConfigPatch(element[1], patchEl); + patchArray.push(patchEl); + return; + } + } + }); + elementsToAppend.forEach((el) => patchArray.push(el)); +} + +function populateConfigPatchObject( + diff: { [id: string]: JsonLike }, + patchObj: Record, + targetEnvironment?: string +) { + const getEnvObj = (targetEnv: string) => { + patchObj.env ??= {}; + const patchObjEnv = patchObj.env as Record>; + patchObjEnv[targetEnv] ??= {}; + return patchObjEnv[targetEnv]; + }; + Object.keys(diff) + .filter((key) => diff[key] && typeof diff[key] === "object") + .forEach((key) => { + if (isModifiedDiffValue(diff[key])) { + if (targetEnvironment) { + getEnvObj(targetEnvironment)[key] = diff[key].__old; + } else { + patchObj[key] = diff[key].__old; + } + return; + } + + if (targetEnvironment) { + getEnvObj(targetEnvironment)[key] ??= Array.isArray(diff[key]) + ? [] + : {}; + } else { + patchObj[key] ??= Array.isArray(diff[key]) ? [] : {}; + } + + Object.entries(diff[key] as Record).forEach( + ([entryKey, entryValue]) => { + if (entryKey.endsWith("__deleted")) { + let patchObjectToUpdate = patchObj[key] as Record; + if (targetEnvironment) { + const envObj = getEnvObj(targetEnvironment); + envObj[key] ??= {}; + patchObjectToUpdate = envObj[key] as Record; + } + patchObjectToUpdate[entryKey.replace("__deleted", "")] = entryValue; + return; + } + } + ); + + if (diff[key] && typeof diff[key] === "object") { + populateConfigPatch( + diff[key], + (targetEnvironment + ? getEnvObj(targetEnvironment)[key] + : patchObj[key]) as Record | JsonLike[] + ); + return; + } + }); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/confirm-latest-deployment-overwrite.ts b/packages/deploy-helpers/src/deploy/helpers/confirm-latest-deployment-overwrite.ts new file mode 100644 index 0000000..d243330 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/confirm-latest-deployment-overwrite.ts @@ -0,0 +1,102 @@ +import * as cli from "@cloudflare/cli-shared-helpers"; +import { brandColor, gray, white } from "@cloudflare/cli-shared-helpers/colors"; +import { inputPrompt, leftT } from "@cloudflare/cli-shared-helpers/interactive"; +import { isNonInteractiveOrCI } from "../../shared/context"; +import { fetchDeploymentVersions, fetchLatestDeployment } from "./versions-api"; +import { isWorkerNotFoundError } from "./worker-not-found-error"; +import type { + ApiDeployment, + ApiVersion, + Percentage, + VersionCache, + VersionId, +} from "./versions-types"; +import type { Config } from "@cloudflare/workers-utils"; + +const BLANK_INPUT = "-"; + +export async function confirmLatestDeploymentOverwrite( + config: Config, + accountId: string, + scriptName: string +) { + try { + const latest = await fetchLatestDeployment(config, accountId, scriptName); + if (latest && latest.versions.length >= 2) { + const versionCache: VersionCache = new Map(); + + cli.warn( + `Your last deployment has multiple versions. To progress that deployment use "wrangler versions deploy" instead.`, + { shape: cli.shapes.corners.tl, newlineBefore: false } + ); + cli.newline(); + await printDeployment( + config, + accountId, + scriptName, + latest, + "last", + versionCache + ); + + return inputPrompt({ + type: "confirm", + question: `"wrangler deploy" will upload a new version and deploy it globally immediately.\nAre you sure you want to continue?`, + label: "", + defaultValue: isNonInteractiveOrCI(), + acceptDefault: isNonInteractiveOrCI(), + }); + } + } catch (e) { + if (!isWorkerNotFoundError(e)) { + throw e; + } + } + return true; +} + +async function printDeployment( + config: Config, + accountId: string, + workerName: string, + deployment: ApiDeployment | undefined, + adjective: "current" | "last", + versionCache: VersionCache +) { + const [versions, traffic] = await fetchDeploymentVersions( + config, + accountId, + workerName, + deployment, + versionCache + ); + cli.logRaw( + `${leftT} Your ${adjective} deployment has ${versions.length} version(s):` + ); + printVersions(versions, traffic); +} + +export function printVersions( + versions: ApiVersion[], + traffic: Map +) { + cli.newline(); + cli.log(formatVersions(versions, traffic)); + cli.newline(); +} + +function formatVersions( + versions: ApiVersion[], + traffic: Map +) { + return versions + .map((version) => { + const trafficString = brandColor(`(${traffic.get(version.id)}%)`); + const versionIdString = white(version.id); + return gray(`${trafficString} ${versionIdString} + Created: ${version.metadata.created_on} + Tag: ${version.annotations?.["workers/tag"] ?? BLANK_INPUT} + Message: ${version.annotations?.["workers/message"] ?? BLANK_INPUT}`); + }) + .join("\n\n"); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts b/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts new file mode 100644 index 0000000..911680c --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/create-worker-upload-form.ts @@ -0,0 +1,889 @@ +import assert from "node:assert"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { INHERIT_SYMBOL, UserError } from "@cloudflare/workers-utils"; +import { FormData } from "undici"; +import { extractBindingsOfType, isUnsafeBindingType } from "./binding-utils"; +import { handleUnsafeCapnp } from "./capnp"; +import type { + AssetConfigMetadata, + Binding, + CfCapnp, + CfModuleType, + CfSendEmailBindings, + CfWorkerInit, + WorkerMetadata, + WorkerMetadataBinding, +} from "@cloudflare/workers-utils"; + +export const moduleTypeMimeType: { + [type in CfModuleType]: string | undefined; +} = { + esm: "application/javascript+module", + commonjs: "application/javascript", + "compiled-wasm": "application/wasm", + buffer: "application/octet-stream", + text: "text/plain", + python: "text/x-python", + "python-requirement": "text/x-python-requirement", +}; + +function toMimeType(type: CfModuleType): string { + const mimeType = moduleTypeMimeType[type]; + if (mimeType === undefined) { + throw new TypeError("Unsupported module: " + type); + } + + return mimeType; +} + +export function fromMimeType(mimeType: string): CfModuleType { + const moduleType = Object.keys(moduleTypeMimeType).find( + (type) => moduleTypeMimeType[type as CfModuleType] === mimeType + ) as CfModuleType | undefined; + if (moduleType === undefined) { + throw new TypeError("Unsupported mime type: " + mimeType); + } + + return moduleType; +} + +/** + * Creates a `FormData` upload from Worker data and bindings + */ +export function createWorkerUploadForm( + worker: Omit, + bindings: Record | undefined, + options?: { + dryRun?: true; + unsafe?: { metadata?: Record; capnp?: CfCapnp }; + } +): FormData { + const formData = new FormData(); + const { + main, + sourceMaps, + migrations, + exports: configuredExports, + compatibility_date, + compatibility_flags, + keepVars, + keepSecrets, + keepBindings, + logpush, + placement, + tail_consumers, + streaming_tail_consumers, + limits, + annotations, + keep_assets, + assets, + observability, + cache, + package_dependencies, + } = worker; + + const assetConfig: AssetConfigMetadata = { + html_handling: assets?.assetConfig?.html_handling, + not_found_handling: assets?.assetConfig?.not_found_handling, + run_worker_first: assets?.run_worker_first, + _redirects: assets?._redirects, + _headers: assets?._headers, + }; + + // short circuit if static assets upload only + if (assets && !assets.routerConfig.has_user_worker) { + formData.set( + "metadata", + JSON.stringify({ + assets: { + jwt: assets.jwt, + config: assetConfig, + }, + ...(annotations && { annotations }), + ...(compatibility_date && { compatibility_date }), + ...(compatibility_flags && { compatibility_flags }), + }) + ); + return formData; + } + let { modules } = worker; + + const metadataBindings: WorkerMetadataBinding[] = []; + + const plain_text = extractBindingsOfType("plain_text", bindings); + const json_bindings = extractBindingsOfType("json", bindings); + const secret_text = extractBindingsOfType("secret_text", bindings); + const kv_namespaces = extractBindingsOfType("kv_namespace", bindings); + const send_email = extractBindingsOfType("send_email", bindings); + const durable_objects = extractBindingsOfType( + "durable_object_namespace", + bindings + ); + const workflows = extractBindingsOfType("workflow", bindings); + const queues = extractBindingsOfType("queue", bindings); + const r2_buckets = extractBindingsOfType("r2_bucket", bindings); + const d1_databases = extractBindingsOfType("d1", bindings); + const vectorize = extractBindingsOfType("vectorize", bindings); + const ai_search_namespaces = extractBindingsOfType( + "ai_search_namespace", + bindings + ); + const ai_search = extractBindingsOfType("ai_search", bindings); + const websearch = extractBindingsOfType("websearch", bindings)[0]; + const agent_memory = extractBindingsOfType("agent_memory", bindings); + const hyperdrive = extractBindingsOfType("hyperdrive", bindings); + const secrets_store_secrets = extractBindingsOfType( + "secrets_store_secret", + bindings + ); + const artifacts = extractBindingsOfType("artifacts", bindings); + const unsafe_hello_world = extractBindingsOfType( + "unsafe_hello_world", + bindings + ); + const flagship = extractBindingsOfType("flagship", bindings); + const ratelimits = extractBindingsOfType("ratelimit", bindings); + const vpc_services = extractBindingsOfType("vpc_service", bindings); + const vpc_networks = extractBindingsOfType("vpc_network", bindings); + const services = extractBindingsOfType("service", bindings); + const analytics_engine_datasets = extractBindingsOfType( + "analytics_engine", + bindings + ); + const dispatch_namespaces = extractBindingsOfType( + "dispatch_namespace", + bindings + ); + const mtls_certificates = extractBindingsOfType("mtls_certificate", bindings); + const pipelines = extractBindingsOfType("pipeline", bindings); + const worker_loaders = extractBindingsOfType("worker_loader", bindings); + const logfwdr = extractBindingsOfType("logfwdr", bindings); + const wasm_modules = extractBindingsOfType("wasm_module", bindings); + const browser = extractBindingsOfType("browser", bindings)[0]; + const ai = extractBindingsOfType("ai", bindings)[0]; + const images = extractBindingsOfType("images", bindings)[0]; + const stream = extractBindingsOfType("stream", bindings)[0]; + const media = extractBindingsOfType("media", bindings)[0]; + const version_metadata = extractBindingsOfType( + "version_metadata", + bindings + )[0]; + const assetsBinding = extractBindingsOfType("assets", bindings)[0]; + const text_blobs = extractBindingsOfType("text_blob", bindings); + const data_blobs = extractBindingsOfType("data_blob", bindings); + const inherit_bindings = extractBindingsOfType("inherit", bindings); + + inherit_bindings.forEach(({ binding }) => { + metadataBindings.push({ name: binding, type: "inherit" }); + }); + + plain_text.forEach(({ binding, value }) => { + metadataBindings.push({ name: binding, type: "plain_text", text: value }); + }); + json_bindings.forEach(({ binding, value }) => { + metadataBindings.push({ name: binding, type: "json", json: value }); + }); + secret_text.forEach(({ binding, value }) => { + metadataBindings.push({ name: binding, type: "secret_text", text: value }); + }); + + kv_namespaces.forEach(({ id, binding, raw }) => { + // If we're doing a dry run there's no way to know whether or not a KV namespace + // is inheritable or requires provisioning (since that would require hitting the API). + // As such, _assume_ any undefined IDs are inheritable when doing a dry run. + // When this Worker is actually deployed, some may be provisioned at the point of deploy + if (options?.dryRun) { + id ??= INHERIT_SYMBOL; + } + + if (id === undefined) { + throw new UserError(`${binding} bindings must have an "id" field`, { + telemetryMessage: "kv namespace binding missing id", + }); + } + + if (id === INHERIT_SYMBOL) { + metadataBindings.push({ + name: binding, + type: "inherit", + }); + } else { + metadataBindings.push({ + name: binding, + type: "kv_namespace", + namespace_id: id, + raw, + }); + } + }); + + send_email.forEach((emailBinding: CfSendEmailBindings) => { + const destination_address = + "destination_address" in emailBinding + ? emailBinding.destination_address + : undefined; + const allowed_destination_addresses = + "allowed_destination_addresses" in emailBinding + ? emailBinding.allowed_destination_addresses + : undefined; + const allowed_sender_addresses = + "allowed_sender_addresses" in emailBinding + ? emailBinding.allowed_sender_addresses + : undefined; + metadataBindings.push({ + name: emailBinding.name, + type: "send_email", + destination_address, + allowed_destination_addresses, + allowed_sender_addresses, + }); + }); + + durable_objects.forEach(({ name, class_name, script_name, environment }) => { + metadataBindings.push({ + name, + type: "durable_object_namespace", + class_name: class_name, + ...(script_name && { script_name }), + ...(environment && { environment }), + }); + }); + + workflows.forEach(({ binding, name, class_name, script_name, raw }) => { + metadataBindings.push({ + type: "workflow", + name: binding, + workflow_name: name, + class_name, + script_name, + raw, + }); + }); + + queues.forEach(({ binding, queue_name, delivery_delay, raw }) => { + metadataBindings.push({ + type: "queue", + name: binding, + queue_name, + delivery_delay, + raw, + }); + }); + + r2_buckets.forEach(({ binding, bucket_name, jurisdiction, raw }) => { + if (options?.dryRun) { + bucket_name ??= INHERIT_SYMBOL; + } + if (bucket_name === undefined) { + throw new UserError( + `${binding} bindings must have a "bucket_name" field`, + { telemetryMessage: "r2 bucket binding missing bucket_name" } + ); + } + + if (bucket_name === INHERIT_SYMBOL) { + metadataBindings.push({ + name: binding, + type: "inherit", + }); + } else { + metadataBindings.push({ + name: binding, + type: "r2_bucket", + bucket_name, + jurisdiction, + raw, + }); + } + }); + + d1_databases.forEach( + ({ binding, database_id, database_internal_env, raw }) => { + if (options?.dryRun) { + database_id ??= INHERIT_SYMBOL; + } + if (database_id === undefined) { + throw new UserError( + `${binding} bindings must have a "database_id" field`, + { telemetryMessage: "d1 database binding missing database_id" } + ); + } + + if (database_id === INHERIT_SYMBOL) { + metadataBindings.push({ + name: binding, + type: "inherit", + }); + } else { + metadataBindings.push({ + name: binding, + type: "d1", + id: database_id, + internalEnv: database_internal_env, + raw, + }); + } + } + ); + + vectorize.forEach(({ binding, index_name, raw }) => { + metadataBindings.push({ + name: binding, + type: "vectorize", + index_name: index_name, + raw, + }); + }); + + ai_search_namespaces.forEach(({ binding, namespace }) => { + if (options?.dryRun) { + namespace ??= INHERIT_SYMBOL; + } + if (namespace === undefined) { + throw new UserError(`${binding} bindings must have a "namespace" field`, { + telemetryMessage: "ai search namespace binding missing namespace", + }); + } + + if (namespace === INHERIT_SYMBOL) { + metadataBindings.push({ + name: binding, + type: "inherit", + }); + } else { + metadataBindings.push({ + name: binding, + type: "ai_search_namespace", + namespace, + }); + } + }); + + ai_search.forEach(({ binding, instance_name }) => { + metadataBindings.push({ + name: binding, + type: "ai_search", + instance_name, + }); + }); + + if (websearch !== undefined) { + metadataBindings.push({ + name: websearch.binding, + type: "websearch", + }); + } + + agent_memory.forEach(({ binding, namespace }) => { + if (options?.dryRun) { + namespace ??= INHERIT_SYMBOL; + } + if (namespace === undefined) { + throw new UserError(`${binding} bindings must have a "namespace" field`, { + telemetryMessage: false, + }); + } + + if (namespace === INHERIT_SYMBOL) { + metadataBindings.push({ + name: binding, + type: "inherit", + }); + } else { + metadataBindings.push({ + name: binding, + type: "agent_memory", + namespace, + }); + } + }); + + hyperdrive.forEach(({ binding, id }) => { + metadataBindings.push({ + name: binding, + type: "hyperdrive", + id: id, + }); + }); + + secrets_store_secrets.forEach(({ binding, store_id, secret_name }) => { + metadataBindings.push({ + name: binding, + type: "secrets_store_secret", + store_id, + secret_name, + }); + }); + + artifacts.forEach(({ binding, namespace }) => { + metadataBindings.push({ + name: binding, + type: "artifacts", + namespace, + }); + }); + + unsafe_hello_world.forEach(({ binding, enable_timer }) => { + metadataBindings.push({ + name: binding, + type: "unsafe_hello_world", + enable_timer, + }); + }); + + flagship.forEach(({ binding, app_id }) => { + metadataBindings.push({ + name: binding, + type: "flagship", + app_id, + }); + }); + + ratelimits.forEach(({ name, namespace_id, simple }) => { + metadataBindings.push({ + name, + type: "ratelimit", + namespace_id, + simple, + }); + }); + + vpc_services.forEach(({ binding, service_id }) => { + metadataBindings.push({ + name: binding, + type: "vpc_service", + service_id, + }); + }); + + vpc_networks.forEach(({ binding, tunnel_id, network_id }) => { + metadataBindings.push({ + name: binding, + type: "vpc_network", + ...(tunnel_id !== undefined ? { tunnel_id } : { network_id }), + }); + }); + + services.forEach( + ({ + binding, + service, + environment, + entrypoint, + props, + cross_account_grant, + }) => { + metadataBindings.push({ + name: binding, + type: "service", + service, + cross_account_grant, + ...(environment && { environment }), + ...(entrypoint && { entrypoint }), + ...(props && { props }), + }); + } + ); + + analytics_engine_datasets.forEach(({ binding, dataset }) => { + metadataBindings.push({ + name: binding, + type: "analytics_engine", + dataset, + }); + }); + + dispatch_namespaces.forEach(({ binding, namespace, outbound }) => { + metadataBindings.push({ + name: binding, + type: "dispatch_namespace", + namespace, + ...(outbound && { + outbound: { + worker: { + service: outbound.service, + environment: outbound.environment, + }, + params: outbound.parameters?.map((p) => ({ name: p })), + }, + }), + }); + }); + + mtls_certificates.forEach(({ binding, certificate_id }) => { + metadataBindings.push({ + name: binding, + type: "mtls_certificate", + certificate_id, + }); + }); + + pipelines.forEach(({ binding, stream: pipelineStream, pipeline }) => { + if (pipelineStream) { + metadataBindings.push({ + name: binding, + type: "pipelines", + stream: pipelineStream, + }); + } else if (pipeline) { + metadataBindings.push({ + name: binding, + type: "pipelines", + pipeline, + }); + } else { + throw new Error("Pipeline binding must specify a stream or pipeline"); + } + }); + + worker_loaders.forEach(({ binding }) => { + metadataBindings.push({ + name: binding, + type: "worker_loader", + }); + }); + + logfwdr.forEach(({ name, destination }) => { + metadataBindings.push({ + name: name, + type: "logfwdr", + destination, + }); + }); + + wasm_modules.forEach(({ binding: name, source }) => { + metadataBindings.push({ + name, + type: "wasm_module", + part: name, + }); + + formData.set( + name, + new File( + [ + "contents" in source + ? source.contents + : readFileSync(source.path as string), + ], + "path" in source ? (source.path ?? name) : name, + { type: "application/wasm" } + ) + ); + }); + + if (browser !== undefined) { + metadataBindings.push({ + name: browser.binding, + type: "browser", + raw: browser.raw, + }); + } + + if (ai !== undefined) { + metadataBindings.push({ + name: ai.binding, + staging: ai.staging, + type: "ai", + raw: ai.raw, + }); + } + + if (images !== undefined) { + metadataBindings.push({ + name: images.binding, + type: "images", + raw: images.raw, + }); + } + + if (stream !== undefined) { + metadataBindings.push({ + name: stream.binding, + type: "stream", + }); + } + + if (media !== undefined) { + metadataBindings.push({ + name: media.binding, + type: "media", + }); + } + + if (version_metadata !== undefined) { + metadataBindings.push({ + name: version_metadata.binding, + type: "version_metadata", + }); + } + + if (assetsBinding !== undefined) { + metadataBindings.push({ + name: assetsBinding.binding, + type: "assets", + }); + } + + text_blobs.forEach(({ binding: name, source }) => { + metadataBindings.push({ + name, + type: "text_blob", + part: name, + }); + + if (name !== "__STATIC_CONTENT_MANIFEST") { + if ("contents" in source) { + formData.set( + name, + new File([source.contents], source.path ?? name, { + type: "text/plain", + }) + ); + } else { + formData.set( + name, + new File([readFileSync(source.path)], source.path, { + type: "text/plain", + }) + ); + } + } + }); + + data_blobs.forEach(({ binding: name, source }) => { + metadataBindings.push({ + name, + type: "data_blob", + part: name, + }); + + formData.set( + name, + new File( + [ + "contents" in source + ? source.contents + : readFileSync(source.path as string), + ], + "path" in source ? (source.path ?? name) : name, + { type: "application/octet-stream" } + ) + ); + }); + + // Handle generic unsafe_* bindings (excluding unsafe_hello_world which is handled above) + for (const [bindingName, config] of Object.entries(bindings ?? {})) { + if ( + isUnsafeBindingType(config.type) && + config.type !== "unsafe_hello_world" + ) { + const { type, ...data } = config; + metadataBindings.push({ + name: bindingName, + type: type.slice("unsafe_".length), + ...data, + } as WorkerMetadataBinding); + } + } + + const manifestModuleName = "__STATIC_CONTENT_MANIFEST"; + const hasManifest = modules?.some(({ name }) => name === manifestModuleName); + if (hasManifest && main.type === "esm") { + assert(modules !== undefined); + // Each modules-format worker has a virtual file system for module + // resolution. For example, uploading modules with names `1.mjs`, + // `a/2.mjs` and `a/b/3.mjs`, creates virtual directories `a` and `a/b`. + // `1.mjs` is in the virtual root directory. + // + // The above code adds the `__STATIC_CONTENT_MANIFEST` module to the root + // directory. This means `import manifest from "__STATIC_CONTENT_MANIFEST"` + // will only work if the importing module is also in the root. If the + // importing module was `a/b/3.mjs` for example, the import would need to + // be `import manifest from "../../__STATIC_CONTENT_MANIFEST"`. + // + // When Wrangler bundles all user code, this isn't a problem, as code is + // only ever uploaded to the root. However, once `--no-bundle` or + // `find_additional_modules` is enabled, the user controls the directory + // structure. + // + // To fix this, if we've got a modules-format worker, we add stub modules + // in each subdirectory that re-export the manifest module from the root. + // This allows the manifest to be imported as `__STATIC_CONTENT_MANIFEST` + // in every directory, whilst avoiding duplication of the manifest. + + // Collect unique subdirectories + const subDirs = new Set( + modules.map((module) => path.posix.dirname(module.name)) + ); + for (const subDir of subDirs) { + // Ignore `.` as it's not a subdirectory, and we don't want to + // register the manifest module in the root twice. + if (subDir === ".") { + continue; + } + const relativePath = path.posix.relative(subDir, manifestModuleName); + const filePath = path.posix.join(subDir, manifestModuleName); + modules.push({ + name: filePath, + filePath, + content: `export { default } from ${JSON.stringify(relativePath)};`, + type: "esm", + }); + } + } + + if (main.type === "commonjs") { + // This is a service-worker format worker. + for (const module of Object.values([...(modules || [])])) { + if (module.name === "__STATIC_CONTENT_MANIFEST") { + // Add the manifest to the form data. + formData.set( + module.name, + new File([module.content], module.name, { + type: "text/plain", + }) + ); + // And then remove it from the modules collection + modules = modules?.filter((m) => m !== module); + } else if ( + module.type === "compiled-wasm" || + module.type === "text" || + module.type === "buffer" + ) { + // Convert all wasm/text/data modules into `wasm_module`/`text_blob`/`data_blob` bindings. + // The "name" of the module is a file path. We use it + // to instead be a "part" of the body, and a reference + // that we can use inside our source. This identifier has to be a valid + // JS identifier, so we replace all non alphanumeric characters + // with an underscore. + const name = module.name.replace(/[^a-zA-Z0-9_$]/g, "_"); + metadataBindings.push({ + name, + type: + module.type === "compiled-wasm" + ? "wasm_module" + : module.type === "text" + ? "text_blob" + : "data_blob", + part: name, + }); + + // Add the module to the form data. + formData.set( + name, + new File([module.content], module.name, { + type: + module.type === "compiled-wasm" + ? "application/wasm" + : module.type === "text" + ? "text/plain" + : "application/octet-stream", + }) + ); + // And then remove it from the modules collection + modules = modules?.filter((m) => m !== module); + } + } + } + + let capnpSchemaOutputFile: string | undefined; + if (options?.unsafe?.capnp) { + const capnpOutput = handleUnsafeCapnp(options.unsafe.capnp); + capnpSchemaOutputFile = `./capnp-${Date.now()}.compiled`; + formData.set( + capnpSchemaOutputFile, + new File([capnpOutput], capnpSchemaOutputFile, { + type: "application/octet-stream", + }) + ); + } + + let keep_bindings: WorkerMetadata["keep_bindings"] = undefined; + if (keepVars) { + keep_bindings ??= []; + keep_bindings.push("plain_text", "json"); + } + if (keepSecrets) { + keep_bindings ??= []; + keep_bindings.push("secret_text", "secret_key"); + } + if (keepBindings) { + keep_bindings ??= []; + keep_bindings.push(...keepBindings); + } + + const metadata: WorkerMetadata = { + ...(main.type !== "commonjs" + ? { main_module: main.name } + : { body_part: main.name }), + bindings: metadataBindings, + containers: + worker.containers === undefined + ? undefined + : worker.containers.map((c) => ({ class_name: c.class_name })), + + ...(compatibility_date && { compatibility_date }), + ...(compatibility_flags && { + compatibility_flags, + }), + ...(migrations && { migrations }), + ...(configuredExports && + Object.keys(configuredExports).length > 0 && { + exports: configuredExports, + }), + capnp_schema: capnpSchemaOutputFile, + ...(keep_bindings && { keep_bindings }), + ...(logpush !== undefined && { logpush }), + ...(placement && { placement }), + ...(tail_consumers && { tail_consumers }), + ...(streaming_tail_consumers && { streaming_tail_consumers }), + ...(limits && { limits }), + ...(annotations && { annotations }), + ...(keep_assets !== undefined && { keep_assets }), + ...(assets && { + assets: { + jwt: assets.jwt, + config: assetConfig, + }, + }), + ...(observability && { observability }), + ...(cache && { cache_options: cache }), + ...(package_dependencies?.length && { package_dependencies }), + }; + + if (options?.unsafe?.metadata !== undefined) { + for (const key of Object.keys(options.unsafe.metadata)) { + metadata[key] = options.unsafe.metadata[key]; + } + } + + formData.set("metadata", JSON.stringify(metadata)); + + if (main.type === "commonjs" && modules && modules.length > 0) { + throw new TypeError( + "More than one module can only be specified when type = 'esm'" + ); + } + + for (const module of [main].concat(modules || [])) { + formData.set( + module.name, + new File([module.content], module.name, { + type: toMimeType(module.type ?? main.type ?? "esm"), + }) + ); + } + + for (const sourceMap of sourceMaps || []) { + formData.set( + sourceMap.name, + new File([sourceMap.content], sourceMap.name, { + type: "application/source-map", + }) + ); + } + + return formData; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/deploy-confirm.ts b/packages/deploy-helpers/src/deploy/helpers/deploy-confirm.ts new file mode 100644 index 0000000..d69b48b --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/deploy-confirm.ts @@ -0,0 +1,23 @@ +import { confirm, isNonInteractiveOrCI, logger } from "../../shared/context"; + +export function getDeployConfirmFunction(options: { + strictMode?: boolean; +}): (text: string) => Promise { + const { strictMode = false } = options; + const nonInteractive = isNonInteractiveOrCI(); + + if (nonInteractive && strictMode) { + return async () => { + logger.error( + "Aborting the upload operation because of conflicts. To override and upload anyway, remove the `--strict` flag" + ); + process.exitCode = 1; + return false; + }; + } else if (nonInteractive) { + // if its not in strict mode, continue without asking + return async () => true; + } + + return confirm; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/deploy-wfp.ts b/packages/deploy-helpers/src/deploy/helpers/deploy-wfp.ts new file mode 100644 index 0000000..5dd550b --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/deploy-wfp.ts @@ -0,0 +1,10 @@ +import { logger } from "../../shared/context"; + +export function deployWfpUserWorker( + dispatchNamespace: string, + versionId: string | null +) { + // Will go under the "Uploaded" text + logger.log(" Dispatch Namespace:", dispatchNamespace); + logger.log("Current Version ID:", versionId); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/diff-json.ts b/packages/deploy-helpers/src/deploy/helpers/diff-json.ts new file mode 100644 index 0000000..c434d96 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/diff-json.ts @@ -0,0 +1,113 @@ +import jsonDiff from "json-diff"; + +export type JsonLike = + | string + | number + | boolean + | null + | JsonLike[] + | undefined // undefined is not a JSON type but it needs to be included here since it is present in the diff objects + | { [id: string]: JsonLike }; + +/** + * Given two objects A and B that are Json serializable this function computes the difference between them + * + * The difference object includes: + * - fields in object B but not in object A included as `` + * - fields in object A but not in object B included as `` + * - fields present in both objects but modified as `: { __old: , __new: }` + * + * Additionally the difference object contains a `toString` method that can be used to generate a string representation + * of the difference between the two objects (to be presented to users) + * + * @param jsonObjA The first target object + * @param jsonObjB The second target object + * @returns An object representing the diff between the two objects, or null if the objects are equal + */ +export function diffJsonObjects( + jsonObjA: Record, + jsonObjB: Record +): Record | null { + const result = jsonDiff.diff(jsonObjA, jsonObjB); + + if (result) { + result.toString = () => jsonDiff.diffString(jsonObjA, jsonObjB); + return result; + } else { + return null; + } +} + +/** + * Given a diff object (generated by `diffJsonObjects`) this function computes whether the + * difference is non-destructive, i.e. if the second object only contained additions to the + * first one and no removal nor modifications. + * + * @param diff The difference object to use (generated by `diffJsonObjects`) + * @returns `true` if the difference is non-destructive, `false` if it is + */ +export function isNonDestructive(diff: JsonLike): boolean { + if (diff === null || typeof diff !== "object") { + return true; + } + + if ( + Object.keys(diff).some( + (key) => key === "__old" || key.endsWith("__deleted") + ) + ) { + return false; + } + + if (Array.isArray(diff)) { + for (const element of diff) { + if (Array.isArray(element) && element.length === 2) { + if (element[0] === "-") { + return false; + } else if (element[0] === "~") { + return false; + } else if (element[0] !== "+") { + continue; + } + + for (const innerElement of element) { + if (!isNonDestructive(innerElement)) { + return false; + } + } + } else if (!isNonDestructive(element)) { + return false; + } + } + } else { + for (const field in diff) { + if (!isNonDestructive(diff[field])) { + return false; + } + } + } + + return true; +} + +/** + * A modified value in json-diff is represented as an object with two properties: + * `__old` and `__new`. Where the former contains the old version of the value and + * the latter the new one. + * This utility, given an arbitrary value, discerns whether the value represents the + * diff of a modified value. + * + * @param value The target value to check + * @returns True if the value represents a value modified, false otherwise + */ +export function isModifiedDiffValue( + value: unknown +): value is { __old: T; __new: T } { + return !!( + value && + typeof value === "object" && + Object.keys(value).length === 2 && + "__old" in value && + "__new" in value + ); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/download-worker-config.ts b/packages/deploy-helpers/src/deploy/helpers/download-worker-config.ts new file mode 100644 index 0000000..d987373 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/download-worker-config.ts @@ -0,0 +1,140 @@ +import { + COMPLIANCE_REGION_CONFIG_UNKNOWN, + constructWranglerConfig, +} from "@cloudflare/workers-utils"; +import { fetchResult } from "../../shared/context"; +import type { + RawConfig, + ServiceMetadataRes, + WorkerMetadata, +} from "@cloudflare/workers-utils"; + +type CustomDomainsRes = { + id: string; + zone_id: string; + zone_name: string; + hostname: string; + service: string; + environment: string; + cert_id: string; + enabled: boolean; + previews_enabled: boolean; +}[]; + +type WorkerSubdomainRes = { + enabled: boolean; + previews_enabled: boolean; +}; +type CronTriggersRes = { + schedules: { + cron: string; + created_on: Date; + modified_on: Date; + }[]; +}; + +type RoutesRes = { + id: string; + pattern: string; + zone_name: string; + script: string; +}[]; + +/** + * Downloads all information required to construct a Wrangler config file for a Worker from the API + */ +export async function fetchWorkerConfig( + accountId: string, + workerName: string, + environment: string +) { + const [ + bindings, + routes, + customDomains, + subdomainStatus, + serviceEnvMetadata, + cronTriggers, + ] = await Promise.all([ + fetchResult( + COMPLIANCE_REGION_CONFIG_UNKNOWN, + `/accounts/${accountId}/workers/services/${workerName}/environments/${environment}/bindings` + ), + fetchResult( + COMPLIANCE_REGION_CONFIG_UNKNOWN, + `/accounts/${accountId}/workers/services/${workerName}/environments/${environment}/routes?show_zonename=true` + ), + fetchResult( + COMPLIANCE_REGION_CONFIG_UNKNOWN, + `/accounts/${accountId}/workers/domains/records?page=0&per_page=5&service=${workerName}&environment=${environment}` + ), + fetchResult( + COMPLIANCE_REGION_CONFIG_UNKNOWN, + `/accounts/${accountId}/workers/services/${workerName}/environments/${environment}/subdomain` + ), + fetchResult( + COMPLIANCE_REGION_CONFIG_UNKNOWN, + `/accounts/${accountId}/workers/services/${workerName}/environments/${environment}` + ), + fetchResult( + COMPLIANCE_REGION_CONFIG_UNKNOWN, + `/accounts/${accountId}/workers/scripts/${workerName}/schedules` + ), + ]).catch((e) => { + throw new Error( + `Error Occurred: Unable to fetch bindings, routes, or services metadata from the dashboard. Please try again later.`, + { cause: e } + ); + }); + return { + bindings, + routes, + customDomains, + subdomainStatus, + serviceEnvMetadata, + cronTriggers, + }; +} + +/** + * Downloads all the remote information we can gather for a worker and from them generates a raw configuration object that + * approximates what a wrangler config object for the worker was/would have been. + */ +export async function downloadWorkerConfig( + workerName: string, + environment: string, + entrypoint: string, + accountId: string +): Promise { + const { + bindings, + routes, + customDomains, + subdomainStatus, + serviceEnvMetadata, + cronTriggers, + } = await fetchWorkerConfig(accountId, workerName, environment); + + return constructWranglerConfig({ + name: workerName, + entrypoint, + compatibility_date: serviceEnvMetadata.script.compatibility_date, + compatibility_flags: serviceEnvMetadata.script.compatibility_flags, + tags: serviceEnvMetadata.script.tags, + migration_tag: serviceEnvMetadata.script.migration_tag, + tail_consumers: serviceEnvMetadata.script.tail_consumers, + observability: serviceEnvMetadata.script.observability, + limits: serviceEnvMetadata.script.limits, + bindings, + routes, + domains: customDomains, + subdomain: subdomainStatus, + schedules: cronTriggers.schedules.map((s) => ({ + cron: s.cron, + })), + placement: serviceEnvMetadata.script.placement_mode + ? { mode: serviceEnvMetadata.script.placement_mode } + : undefined, + logpush: undefined, + }); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/durable.ts b/packages/deploy-helpers/src/deploy/helpers/durable.ts new file mode 100644 index 0000000..03a1c03 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/durable.ts @@ -0,0 +1,122 @@ +import assert from "node:assert"; +import { + configFileName, + getDurableObjectExports, +} from "@cloudflare/workers-utils"; +import { fetchResult, logger } from "../../shared/context"; +import { isWorkerNotFoundError } from "./worker-not-found-error"; +import type { CfWorkerInit, Config } from "@cloudflare/workers-utils"; + +/** + * For a given Worker + migrations config, figure out which migrations + * to upload based on the current migration tag of the deployed Worker. + */ +export async function getMigrationsToUpload( + scriptName: string, + props: { + accountId: string | undefined; + config: Config; + dispatchNamespace: string | undefined; + } +): Promise { + const { config, accountId } = props; + + assert(accountId, "Missing accountId"); + let migrations; + if (config.migrations.length > 0) { + type ScriptData = { id: string; migration_tag?: string }; + let script: ScriptData | undefined; + if (props.dispatchNamespace) { + try { + const scriptData = await fetchResult<{ script: ScriptData }>( + config, + `/accounts/${accountId}/workers/dispatch/namespaces/${props.dispatchNamespace}/scripts/${scriptName}` + ); + script = scriptData.script; + } catch (err) { + suppressNotFoundError(err); + } + } else { + const scripts = await fetchResult( + config, + `/accounts/${accountId}/workers/scripts` + ); + script = scripts.find(({ id }) => id === scriptName); + } + + if (script?.migration_tag) { + const scriptMigrationTag = script.migration_tag; + const foundIndex = config.migrations.findIndex( + (migration) => migration.tag === scriptMigrationTag + ); + if (foundIndex === -1) { + logger.warn( + `The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in your ${configFileName(config.configPath)} file. You may have already deleted it. Applying all available migrations to the script...` + ); + migrations = { + old_tag: script.migration_tag, + new_tag: config.migrations[config.migrations.length - 1].tag, + steps: config.migrations.map(({ tag: _tag, ...rest }) => rest), + }; + } else { + if (foundIndex !== config.migrations.length - 1) { + migrations = { + old_tag: script.migration_tag, + new_tag: config.migrations[config.migrations.length - 1].tag, + steps: config.migrations + .slice(foundIndex + 1) + .map(({ tag: _tag, ...rest }) => rest), + }; + } + } + } else { + migrations = { + new_tag: config.migrations[config.migrations.length - 1].tag, + steps: config.migrations.map(({ tag: _tag, ...rest }) => rest), + }; + } + } + return migrations; +} + +const suppressNotFoundError = (err: unknown) => { + if (!isWorkerNotFoundError(err) && (err as { code: number }).code !== 10092) { + throw err; + } +}; + +/** + * Resolve which Durable Object lifecycle payload to send with the upload. + */ +export async function resolveDoLifecyclePayload(props: { + scriptName: string; + isDryRun: boolean | undefined; + accountId: string | undefined; + config: Config; + dispatchNamespace: string | undefined; +}): Promise<{ + migrations: CfWorkerInit["migrations"]; + exports: CfWorkerInit["exports"]; +}> { + const durableObjectExports = getDurableObjectExports(props.config.exports); + const hasDurableObjectExports = Object.keys(durableObjectExports).length > 0; + if (hasDurableObjectExports) { + return { + migrations: undefined, + exports: durableObjectExports, + }; + } + + const migrations = !props.isDryRun + ? await getMigrationsToUpload(props.scriptName, { + accountId: props.accountId, + config: props.config, + dispatchNamespace: props.dispatchNamespace, + }) + : undefined; + + return { + migrations, + exports: undefined, + }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/environments.ts b/packages/deploy-helpers/src/deploy/helpers/environments.ts new file mode 100644 index 0000000..e171d49 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/environments.ts @@ -0,0 +1,55 @@ +import { + ENVIRONMENT_TAG_PREFIX, + SERVICE_TAG_PREFIX, +} from "@cloudflare/workers-utils"; +import { logger } from "../../shared/context"; +import type { Config } from "@cloudflare/workers-utils"; + +export function hasDefinedEnvironments(config: Config) { + return Boolean(config.definedEnvironments?.length); +} + +export function applyServiceAndEnvironmentTags(config: Config, tags: string[]) { + const env = config.targetEnvironment; + const serviceName = config.topLevelName; + const shouldApplyTags = hasDefinedEnvironments(config); + + if (shouldApplyTags && !serviceName) { + logger.warn( + "No top-level `name` has been defined in Wrangler configuration. Add a top-level `name` to group this Worker together with its sibling environments in the Cloudflare dashboard." + ); + } + + const serviceTag = + shouldApplyTags && serviceName + ? `${SERVICE_TAG_PREFIX}${serviceName}` + : null; + const environmentTag = + serviceTag && env ? `${ENVIRONMENT_TAG_PREFIX}${env}` : null; + + tags = tags.filter( + (tag) => + !tag.startsWith(SERVICE_TAG_PREFIX) && + !tag.startsWith(ENVIRONMENT_TAG_PREFIX) + ); + + if (serviceTag) { + tags.push(serviceTag); + } + + if (environmentTag) { + tags.push(environmentTag); + } + + return tags; +} + +export function warnOnErrorUpdatingServiceAndEnvironmentTags() { + logger.warn( + "Could not apply service and environment tags. This Worker will not appear grouped together with its sibling environments in the Cloudflare dashboard." + ); +} + +export function tagsAreEqual(a: string[], b: string[]) { + return a.length === b.length && a.every((el, i) => b[i] === el); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/error-codes.ts b/packages/deploy-helpers/src/deploy/helpers/error-codes.ts new file mode 100644 index 0000000..92bf6a2 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/error-codes.ts @@ -0,0 +1,41 @@ +/** + * Cloudflare API error codes used by deploy-helpers. + */ + +/** The inherit binding references a binding that does not exist on the previous version. */ +export const INVALID_INHERIT_BINDING_CODE = 10057 as const; + +/** + * Blocking-error code for declarative DO exports reconciliation failures. + * Used to distinguish the reconciliation error envelope from other 4xx upload + * errors so we can render the structured per-class details. + */ +export const EXPORTS_RECONCILIATION_ERROR_CODE = 100402; + +/** + * Blocking-error code returned by EWC when a multi-version (percentage-split) + * deployment contains versions with divergent declarative DO `exports`. All + * versions in a percentage-split deploy must agree on the end-state, otherwise + * traffic on one branch could route to code referencing unprovisioned (or + * just-deleted) DO namespaces. Single-version (100%) deploys are unaffected. + * Resolution: deploy the version that changes `exports` at 100% first, then + * run the percentage-split deploy. + */ +export const INCONSISTENT_EXPORTS_ACROSS_VERSIONS_CODE = 100405; + +/** + * Blocking-error code returned by EWC when a `wrangler versions upload` payload + * contains an actor binding (`durable_objects.bindings`) that references a + * Durable Object class declared in `exports` but not yet provisioned. + * Declarative `exports` reconcile when the version is *deployed*, so the + * namespace must exist before a binding can reference it. This is the + * exports-aware sibling of `ErrActorBindingDependsOnMigration` (10123) that + * already exists for the legacy `migrations` flow. + * Resolution: stage the new class via `ctx.exports.` (no binding) + * on `versions upload` and add the binding at deploy time, or use + * `wrangler deploy` to provision and bind in one step. + */ +export const ACTOR_BINDING_DEPENDS_ON_EXPORT_CODE = 100406; + +/** Workflows API code for `workflow.cron_requires_paid_plan`. */ +export const WORKFLOW_CRON_REQUIRES_PAID_PLAN_CODE = 10208; diff --git a/packages/deploy-helpers/src/deploy/helpers/exports-reconciliation.ts b/packages/deploy-helpers/src/deploy/helpers/exports-reconciliation.ts new file mode 100644 index 0000000..72b257e --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/exports-reconciliation.ts @@ -0,0 +1,139 @@ +import { bold, dim, red, yellow } from "@cloudflare/cli-shared-helpers/colors"; +import { logger } from "../../shared/context"; +import type { + ExportsReconciliationErrorDetail, + ExportsReconciliationResult, +} from "@cloudflare/workers-utils"; + +/** + * Render the success-side `exports_reconciliation` envelope to the logger. + * Emits nothing when the result has no entries to report (so a re-deploy + * with no DO changes doesn't add noise to the deploy output). + * + * Warnings are rendered prominently; info and removable-entry hints are lower + * visibility. + */ +export function renderExportsReconciliationSuccess( + result: ExportsReconciliationResult +): void { + const hasAnyContent = + result.created.length > 0 || + result.updated.length > 0 || + result.deleted.length > 0 || + result.renamed.length > 0 || + result.transferred.length > 0 || + result.transfer_pending.length > 0 || + result.warnings.length > 0 || + result.info.length > 0 || + result.removable_entries.length > 0; + + if (!hasAnyContent) { + return; + } + + logger.log(""); + logger.log(bold("Durable Object exports reconciliation:")); + + if (result.created.length > 0) { + logger.log(` Created: ${result.created.join(", ")}`); + } + if (result.updated.length > 0) { + logger.log(` Updated: ${result.updated.join(", ")}`); + } + if (result.deleted.length > 0) { + logger.log(` Deleted: ${result.deleted.join(", ")}`); + } + if (result.renamed.length > 0) { + const renames = result.renamed.map((r) => `${r.from} → ${r.to}`).join(", "); + logger.log(` Renamed: ${renames}`); + } + + for (const t of result.transferred) { + logger.log(` Transferred (${t.phase}): ${t.class} → ${t.to}`); + } + for (const t of result.transfer_pending) { + logger.log(` Transfer pending: ${t.class} ← ${t.from}`); + } + + if (result.warnings.length > 0) { + logger.log(""); + logger.warn(bold(" Warnings:")); + for (const w of result.warnings) { + logger.warn(yellow(` [${w.scenario}] ${w.class}: ${w.message}`)); + } + } + + if (result.info.length > 0) { + logger.log(""); + logger.log(dim(" Info:")); + for (const info of result.info) { + let line = dim(` [${info.scenario}] ${info.class}: ${info.message}`); + if (info.referencing_scripts && info.referencing_scripts.length > 0) { + line += dim(` (referenced by: ${info.referencing_scripts.join(", ")})`); + } + logger.log(line); + } + } + + if (result.removable_entries.length > 0) { + logger.log(""); + logger.log( + dim( + ` Safe to remove from \`exports\`: ${result.removable_entries.join(", ")}` + ) + ); + } + + logger.log(""); +} + +/** + * Format the structured reconciliation error details from the upload error + * envelope's `meta.details` field. Returns a multi-line string suitable for + * inclusion in a `UserError`'s message or notes. Each per-class entry is + * rendered with a red ✘ prefix, the scenario tag, the message, and any + * optional suggestion / referencing-scripts metadata. + */ +export function renderExportsReconciliationError( + details: ExportsReconciliationErrorDetail[] +): string { + const lines: string[] = ["Durable Object exports reconciliation failed:"]; + for (const detail of details) { + lines.push( + red(` ✘ [${detail.scenario}] class '${detail.class}': ${detail.message}`) + ); + if (detail.suggestion) { + lines.push(` Suggestion: ${detail.suggestion}`); + } + if (detail.referencing_scripts && detail.referencing_scripts.length > 0) { + lines.push( + ` Referencing scripts: ${detail.referencing_scripts.join(", ")}` + ); + } + } + return lines.join("\n"); +} + +/** + * Type guard for `meta.details` extracted from an `APIError`'s `meta` field. + * Validate the array shape so the renderer can rely on the typed fields. + */ +export function isExportsReconciliationErrorDetails( + value: unknown +): value is ExportsReconciliationErrorDetail[] { + if (!Array.isArray(value)) { + return false; + } + for (const entry of value) { + if ( + typeof entry !== "object" || + entry === null || + typeof (entry as Record).class !== "string" || + typeof (entry as Record).scenario !== "string" || + typeof (entry as Record).message !== "string" + ) { + return false; + } + } + return true; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/exports.ts b/packages/deploy-helpers/src/deploy/helpers/exports.ts new file mode 100644 index 0000000..4037d6d --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/exports.ts @@ -0,0 +1,29 @@ +import { partitionExports } from "@cloudflare/workers-utils"; +import { resolveDoLifecyclePayload } from "./durable"; +import type { CfWorkerInit } from "@cloudflare/workers-utils"; + +type ResolveExportsUploadPayloadProps = Parameters< + typeof resolveDoLifecyclePayload +>[0]; + +export async function resolveExportsUploadPayload( + props: ResolveExportsUploadPayloadProps +): Promise<{ + migrations: CfWorkerInit["migrations"]; + exports: CfWorkerInit["exports"]; +}> { + const partitionedExports = partitionExports(props.config.exports); + const { migrations, exports: durableObjectExports } = + await resolveDoLifecyclePayload(props); + // Durable Object exports replace migrations. Worker exports can upload with + // either path, but not both. + const exports = { + ...partitionedExports.worker, + ...(durableObjectExports ?? {}), + }; + + return { + migrations, + exports: Object.keys(exports).length > 0 ? exports : undefined, + }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/friendly-validator-errors.ts b/packages/deploy-helpers/src/deploy/helpers/friendly-validator-errors.ts new file mode 100644 index 0000000..0e5efe7 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/friendly-validator-errors.ts @@ -0,0 +1,171 @@ +import { writeFile } from "node:fs/promises"; +import path from "node:path"; +import { getWranglerTmpDir, ParseError } from "@cloudflare/workers-utils"; +import dedent from "ts-dedent"; +import { logger } from "../../shared/context"; +import type { Metafile } from "esbuild"; +import type { FormData } from "undici"; + +export async function helpIfErrorIsSizeOrScriptStartup( + err: unknown, + dependencies: { [path: string]: { bytesInOutput: number } }, + workerBundle: FormData | string, + projectRoot: string | undefined, + analyseBundle?: (bundle: FormData | string) => Promise +): Promise { + if (errIsScriptSize(err)) { + return diagnoseScriptSizeError(err, dependencies); + } + if (errIsStartupErr(err)) { + return await diagnoseStartupError( + err, + workerBundle, + projectRoot, + analyseBundle + ); + } + return null; +} + +/** + * Returns a formatted error message that describes the script size error. + * It includes the largest dependencies if available. + */ +export function diagnoseScriptSizeError( + err: ParseError, + dependencies: { [path: string]: { bytesInOutput: number } } +): string { + let message = dedent` + Your Worker failed validation because it exceeded size limits. + + ${err.text} + ${err.notes.map((note) => ` - ${note.text}`).join("\n")} + `; + + const dependenciesMessage = getOffendingDependenciesMessage(dependencies); + if (dependenciesMessage) { + message += dependenciesMessage; + } + + return message; +} + +/** + * Returns a formatted error message that describes the startup error. + * If profiling is successful, it will include a link to the generated CPU profile. + */ +export async function diagnoseStartupError( + err: ParseError, + workerBundle: FormData | string, + projectRoot: string | undefined, + analyseBundle?: (bundle: FormData | string) => Promise +): Promise { + let errorMessage = dedent` + Your Worker failed validation because it exceeded startup limits. + + ${err.text} + ${err.notes.map((note) => ` - ${note.text}`).join("\n")} + + To ensure fast responses, there are constraints on Worker startup, such as how much CPU it can use, or how long it can take. Your Worker has hit one of these startup limits. Try reducing the amount of work done during startup (outside the event handler), either by removing code or relocating it inside the event handler. + + Refer to https://developers.cloudflare.com/workers/platform/limits/#worker-startup-time for more details`; + + try { + if (!analyseBundle) { + return errorMessage; + } + const cpuProfile = await analyseBundle(workerBundle); + const tmpDir = await getWranglerTmpDir( + projectRoot, + "startup-profile", + false + ); + const profile = path.relative( + projectRoot ?? process.cwd(), + path.join(tmpDir.path, `worker.cpuprofile`) + ); + await writeFile(profile, JSON.stringify(cpuProfile)); + + errorMessage += dedent` + + A CPU Profile of your Worker's startup phase has been written to ${profile} - load it into the Chrome DevTools profiler (or directly in VSCode) to view a flamegraph.`; + } catch (profilingError) { + logger.debug( + `An error occurred while trying to locally profile the Worker: ${profilingError}` + ); + } + + return errorMessage; +} + +/** + * Gets a message that describes the largest dependencies in the script or `null` if there are none. + */ +function getOffendingDependenciesMessage( + dependencies: Metafile["outputs"][string]["inputs"] +): string | null { + const dependenciesSorted = Object.entries(dependencies); + if (dependenciesSorted.length === 0) { + return null; + } + + dependenciesSorted.sort( + ([, aData], [, bData]) => bData.bytesInOutput - aData.bytesInOutput + ); + + const topLargest = dependenciesSorted.slice(0, 5); + const ONE_KIB_BYTES = 1024; + return [ + "", + `Here are the ${topLargest.length} largest dependencies included in your script:`, + "", + ...topLargest.map( + ([dep, data]) => + `- ${dep} - ${(data.bytesInOutput / ONE_KIB_BYTES).toFixed(2)} KiB` + ), + "", + "If these are unnecessary, consider removing them", + "", + ].join("\n"); +} + +/** + * Returns true if the error is a script size error. + */ +function errIsScriptSize(err: unknown): err is ParseError & { code: 10027 } { + if (!(err instanceof ParseError)) { + return false; + } + + // 10027 = workers.api.error.script_too_large + if ("code" in err && err.code === 10027) { + return true; + } + + return false; +} + +/** + * Returns true if the error is a startup error. + */ +function errIsStartupErr(err: unknown): err is ParseError & { code: 10021 } { + if (!(err instanceof ParseError)) { + return false; + } + + // 10021 = validation error + // no explicit error code for more granular errors than "invalid script" + // but the error will contain a string error message directly from the + // validator. + // the error always SHOULD look like "Script startup exceeded CPU limit." + // (or the less likely "Script startup exceeded memory limits.") + if ( + "code" in err && + err.code === 10021 && + /startup/i.test(err.notes[0]?.text) + ) { + return true; + } + + return false; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/hash.ts b/packages/deploy-helpers/src/deploy/helpers/hash.ts new file mode 100644 index 0000000..dca3ffd --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/hash.ts @@ -0,0 +1,13 @@ +import { readFileSync } from "node:fs"; +import { extname } from "node:path"; +import { hash as blake3hash } from "blake3-wasm"; + +export const hashFile = (filepath: string) => { + const contents = readFileSync(filepath); + const base64Contents = contents.toString("base64"); + const extension = extname(filepath).substring(1); + + return blake3hash(base64Contents + extension) + .toString("hex") + .slice(0, 32); +}; diff --git a/packages/deploy-helpers/src/deploy/helpers/inconsistent-exports.ts b/packages/deploy-helpers/src/deploy/helpers/inconsistent-exports.ts new file mode 100644 index 0000000..20cfe16 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/inconsistent-exports.ts @@ -0,0 +1,25 @@ +/** + * Build the user-facing error message for EWC code 100405 + * (inconsistent declarative DO `exports` across versions in a + * percentage-split deployment). + * + * The server's own message is already actionable; we augment it with a + * concrete next-step that points back at `wrangler versions deploy` and a + * link to the gradual-deployments docs. + */ +export function renderInconsistentExportsAcrossVersionsError( + serverMessage: string +): string { + return [ + serverMessage, + "", + "All versions in a percentage-split deployment must declare identical Durable Object `exports`. Cloudflare requires this so traffic on one branch can't route to code referencing unprovisioned or just-deleted DO namespaces.", + "", + "What to do:", + " 1. Deploy the version that changes `exports` at 100% first:", + " wrangler versions deploy @100%", + " 2. Once that deploy is stable, run your percentage-split deploy.", + "", + "Learn more: https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#gradual-deployments-for-durable-objects", + ].join("\n"); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/jwt.ts b/packages/deploy-helpers/src/deploy/helpers/jwt.ts new file mode 100644 index 0000000..d59c9da --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/jwt.ts @@ -0,0 +1,26 @@ +export const decodeJwtPayload = (token: string) => { + return JSON.parse(Buffer.from(token.split(".")[1], "base64").toString()); +}; + +export const isJwtExpired = (token: string): boolean | undefined => { + // During testing we don't use valid JWTs, so don't try and parse them. + if ( + "vitest" in globalThis && + (token === "<>" || + token === "<>" || + token === "<>") + ) { + return false; + } + try { + const decodedJwt = decodeJwtPayload(token); + + const dateNow = new Date().getTime() / 1000; + + return decodedJwt.exp <= dateNow; + } catch (e) { + if (e instanceof Error) { + throw new Error(`Invalid token: ${e.message}`); + } + } +}; diff --git a/packages/deploy-helpers/src/deploy/helpers/match-tag.ts b/packages/deploy-helpers/src/deploy/helpers/match-tag.ts new file mode 100644 index 0000000..22b6c05 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/match-tag.ts @@ -0,0 +1,90 @@ +import { + APIError, + configFileName, + FatalError, + formatConfigSnippet, + getCIMatchTag, + getEnvironmentVariableFactory, +} from "@cloudflare/workers-utils"; +import { fetchResult, logger } from "../../shared/context"; +import { isWorkerNotFoundError } from "./worker-not-found-error"; +import type { + ComplianceConfig, + ServiceMetadataRes, +} from "@cloudflare/workers-utils"; + +const getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({ + variableName: "CLOUDFLARE_ACCOUNT_ID", + deprecatedName: "CF_ACCOUNT_ID", +}); + +export async function verifyWorkerMatchesCITag( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + configPath: string | undefined +) { + const matchTag = getCIMatchTag(); + + logger.debug( + `Starting verifyWorkerMatchesCITag() with tag: ${matchTag}, name: ${workerName}` + ); + + if (!matchTag) { + logger.debug( + "No WRANGLER_CI_MATCH_TAG variable provided, aborting verifyWorkerMatchesCITag()" + ); + return; + } + + const envAccountID = getCloudflareAccountIdFromEnv(); + + if (accountId !== envAccountID) { + throw new FatalError( + `The \`account_id\` in your ${configFileName(configPath)} file must match the \`account_id\` for this account. Please update your ${configFileName(configPath)} file with \`${formatConfigSnippet({ account_id: envAccountID }, configPath, false)}\``, + { telemetryMessage: "ci match tag account mismatch" } + ); + } + + let tag; + + try { + const worker = await fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/services/${workerName}` + ); + tag = worker.default_environment.script.tag; + logger.debug(`API returned with tag: ${tag} for worker: ${workerName}`); + } catch (e) { + logger.debug(e); + if (isWorkerNotFoundError(e)) { + throw new FatalError( + `The name in your ${configFileName(configPath)} file (${workerName}) must match the name of your Worker. Please update the name field in your ${configFileName(configPath)} file.`, + { telemetryMessage: "ci match tag worker not found" } + ); + } else if (e instanceof APIError) { + throw new FatalError( + "An error occurred while trying to validate that the Worker name matches what is expected by the build system.\n" + + e.message + + "\n" + + e.notes.map((note) => note.text).join("\n"), + { telemetryMessage: "ci match tag validation api error" } + ); + } else { + throw new FatalError( + "Wrangler cannot validate that your Worker name matches what is expected by the build system. Please retry the build. " + + "If the problem persists, please contact support.", + { telemetryMessage: "ci match tag validation failed" } + ); + } + } + if (tag !== matchTag) { + logger.debug( + `Failed to match Worker tag. The API returned "${tag}", but the CI system expected "${matchTag}"` + ); + throw new FatalError( + `The name in your ${configFileName(configPath)} file (${workerName}) must match the name of your Worker. Please update the name field in your ${configFileName(configPath)} file.`, + { telemetryMessage: "ci match tag tag mismatch" } + ); + } +} diff --git a/packages/deploy-helpers/src/deploy/helpers/node-compat.ts b/packages/deploy-helpers/src/deploy/helpers/node-compat.ts new file mode 100644 index 0000000..805fce6 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/node-compat.ts @@ -0,0 +1,54 @@ +import { UserError } from "@cloudflare/workers-utils"; +import { getNodeCompat } from "miniflare"; +import { logger } from "../../shared/context"; +import type { NodeJSCompatMode } from "miniflare"; + +/** + * Computes and validates the Node.js compatibility mode we are running. + * + * NOTES: + * - The v2 mode is configured via `nodejs_compat_v2` compat flag or via `nodejs_compat` plus a compatibility date of Sept 23rd. 2024 or later. + * - See `EnvironmentInheritable` for `noBundle`. + * + * @param compatibilityDateStr The compatibility date + * @param compatibilityFlags The compatibility flags + * @param noBundle Whether to skip internal build steps and directly deploy script + * + */ export function validateNodeCompatMode( + compatibilityDateStr: string = "2000-01-01", // Default to some arbitrary old date + compatibilityFlags: string[], + { + noBundle = undefined, + }: { + noBundle?: boolean; + } +): NodeJSCompatMode { + const { + mode, + hasNodejsCompatFlag, + hasNodejsCompatV2Flag, + hasExperimentalNodejsCompatV2Flag, + } = getNodeCompat(compatibilityDateStr, compatibilityFlags); + + if (hasExperimentalNodejsCompatV2Flag) { + throw new UserError( + "The `experimental:` prefix on `nodejs_compat_v2` is no longer valid. Please remove it and try again.", + { telemetryMessage: "experimental nodejs compat v2 prefix unsupported" } + ); + } + + if (hasNodejsCompatFlag && hasNodejsCompatV2Flag) { + throw new UserError( + "The `nodejs_compat` and `nodejs_compat_v2` compatibility flags cannot be used in together. Please select just one.", + { telemetryMessage: "conflicting nodejs compat flags" } + ); + } + + if (noBundle && hasNodejsCompatV2Flag) { + logger.warn( + "`nodejs_compat_v2` compatibility flag and `--no-bundle` can't be used together. If you want to polyfill Node.js built-ins and disable Wrangler's bundling, please polyfill as part of your own bundling process." + ); + } + + return mode; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts new file mode 100644 index 0000000..c15cc73 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/package-dependencies.ts @@ -0,0 +1,106 @@ +import { access, readFile } from "node:fs/promises"; +import path from "node:path"; +import { + getInstalledPackageVersion, + parsePackageJSON, +} from "@cloudflare/workers-utils"; +import { logger } from "../../shared/context"; + +/** + * A single npm package dependency entry, matching the upload API schema + * see: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update. + */ +export type PackageDependency = { + /** The npm package name, e.g. "lodash" or "@cloudflare/workers-types". */ + name: string; + /** The version constraint as written in package.json, e.g. "^4.17.21". */ + packageJsonVersion: string; + /** The exact version resolved and installed by the package manager, e.g. "4.17.22". */ + installedVersion: string; +}; + +/** + * Maximum number of dependency entries to include in a single upload. + * This also gets truncated to this limit server-side, but we cap client-side + * to avoid sending unnecessarily large payloads. + */ +const MAX_PACKAGE_DEPENDENCIES = 200; + +/** + * Collects npm package dependency metadata from the project's package.json. + * + * Reads both `dependencies` and `devDependencies`, resolves each package's + * installed version from node_modules, and filters out: + * - Workspace packages (version prefixed with `workspace:`) + * - Pnpm catalog packages (version prefixed with `catalog:`) + * - Local packages (version prefixed with `file:` or `link:`) + * - Packages whose installed version cannot be resolved + * + * The result is capped at {@link MAX_PACKAGE_DEPENDENCIES} entries. + * + * @param projectPath - Path to the project directory (where package.json is located) + * @returns An array of package dependency entries, or `undefined` if package.json + * cannot be read or no valid dependencies are found + */ +export async function collectPackageDependencies( + projectPath: string +): Promise { + const packageJsonPath = path.join(projectPath, "package.json"); + + try { + await access(packageJsonPath); + } catch { + return undefined; + } + + try { + const content = await readFile(packageJsonPath, "utf-8"); + const packageJson = parsePackageJSON(content, packageJsonPath); + + const allDependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + } as Record; + + const result: PackageDependency[] = []; + + for (const [dependencyName, packageJsonVersion] of Object.entries( + allDependencies + )) { + if (result.length >= MAX_PACKAGE_DEPENDENCIES) { + break; + } + + if ( + packageJsonVersion.startsWith("workspace:") || + packageJsonVersion.startsWith("catalog:") || + packageJsonVersion.startsWith("file:") || + packageJsonVersion.startsWith("link:") + ) { + continue; + } + + const installedVersion = getInstalledPackageVersion( + dependencyName, + projectPath + ); + + if (!installedVersion) { + continue; + } + + result.push({ + name: dependencyName, + packageJsonVersion, + installedVersion, + }); + } + + return result.length > 0 ? result : undefined; + } catch (error) { + logger.debug( + `Failed to collect package dependencies: ${error instanceof Error ? error.message : error}` + ); + return undefined; + } +} diff --git a/packages/deploy-helpers/src/deploy/helpers/parse-bulk-input.ts b/packages/deploy-helpers/src/deploy/helpers/parse-bulk-input.ts new file mode 100644 index 0000000..6c91c8e --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/parse-bulk-input.ts @@ -0,0 +1,125 @@ +import path from "node:path"; +import readline from "node:readline"; +import { + FatalError, + parseJSON, + readFileSync, + UserError, +} from "@cloudflare/workers-utils"; +import { parse as dotenvParse } from "dotenv"; + +export function validateFileSecrets( + content: unknown, + jsonFilePath: string +): content is Record { + if (content === null || typeof content !== "object") { + throw new FatalError( + `The contents of "${jsonFilePath}" is not valid. It should be a JSON object of string values.`, + { telemetryMessage: "secret bulk file invalid contents" } + ); + } + const entries = Object.entries(content); + for (const [key, value] of entries) { + if (value != null && typeof value !== "string") { + throw new FatalError( + `The value for "${key}" in "${jsonFilePath}" is not null or a "string" instead it is of type "${typeof value}"`, + { telemetryMessage: "secret bulk file invalid value type" } + ); + } + } + return true; +} + +/** Error thrown when no input is provided to parseBulkInputToObject */ +export class NoInputError extends Error { + constructor() { + super("No input provided"); + this.name = "NoInputError"; + } +} + +/** Result from parsing bulk secret input without nullable values, including metadata for analytics */ +export type BulkInputResult = { + content: Record; + secretSource: "file" | "stdin"; + secretFormat: "json" | "dotenv"; +}; + +/** Result from parsing bulk secret input with nullable values, including metadata for analytics */ +export type BulkInputNullableResult = { + content: Record; + secretSource: "file" | "stdin"; + secretFormat: "json" | "dotenv"; +}; + +/** Override for callers that need non-nullable */ +export async function parseBulkInputToObject( + input?: string, + includeNull?: false +): Promise; + +/** Override for callers that need nullable */ +export async function parseBulkInputToObject( + input?: string, + includeNull?: true +): Promise; + +export async function parseBulkInputToObject( + input?: string, + includeNull: boolean = false +): Promise { + let content: Record; + let secretSource: "file" | "stdin"; + let secretFormat: "json" | "dotenv"; + + if (input) { + secretSource = "file"; + const jsonFilePath = path.resolve(input); + const fileContent = readFileSync(jsonFilePath); + try { + content = parseJSON(fileContent) as Record; + secretFormat = "json"; + } catch { + content = dotenvParse(fileContent); + secretFormat = "dotenv"; + // dotenvParse does not error unless fileContent is undefined, no keys === error + if (Object.keys(content).length === 0) { + throw new UserError(`The contents of "${input}" is not valid.`, { + telemetryMessage: "secret bulk invalid input", + }); + } + } + } else { + secretSource = "stdin"; + try { + const rl = readline.createInterface({ input: process.stdin }); + const pipedInputLines: string[] = []; + for await (const line of rl) { + pipedInputLines.push(line); + } + const pipedInput = pipedInputLines.join("\n"); + try { + content = parseJSON(pipedInput) as Record; + secretFormat = "json"; + } catch (e) { + content = dotenvParse(pipedInput); + secretFormat = "dotenv"; + // dotenvParse does not error unless fileContent is undefined, no keys === error + if (Object.keys(content).length === 0) { + throw e; + } + } + } catch { + return; + } + } + validateFileSecrets(content, input ?? "piped input"); + if (!includeNull) { + content = Object.fromEntries( + Object.entries(content).filter( + (entry): entry is [string, string] => entry[1] != null + ) + ); + } + return { content, secretSource, secretFormat }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/placement.ts b/packages/deploy-helpers/src/deploy/helpers/placement.ts new file mode 100644 index 0000000..17d3dac --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/placement.ts @@ -0,0 +1,31 @@ +import type { CfPlacement, Config } from "@cloudflare/workers-utils"; + +/** + * Parse placement out of a Config + */ +export function parseConfigPlacement(config: Config): CfPlacement | undefined { + if (config.placement) { + const configPlacement = config.placement; + const hint = "hint" in configPlacement ? configPlacement.hint : undefined; + + if (!hint && configPlacement.mode === "off") { + return undefined; + } else if (hint || configPlacement.mode === "smart") { + return { mode: "smart", hint: hint }; + } else { + // mode is undefined or "targeted", which both map to the targeted variant + // TypeScript needs explicit checks to narrow the union type + if ("region" in configPlacement && configPlacement.region) { + return { mode: "targeted", region: configPlacement.region }; + } else if ("host" in configPlacement && configPlacement.host) { + return { mode: "targeted", host: configPlacement.host }; + } else if ("hostname" in configPlacement && configPlacement.hostname) { + return { mode: "targeted", hostname: configPlacement.hostname }; + } else { + return undefined; + } + } + } else { + return undefined; + } +} diff --git a/packages/deploy-helpers/src/deploy/helpers/preview-alias.ts b/packages/deploy-helpers/src/deploy/helpers/preview-alias.ts new file mode 100644 index 0000000..88bb186 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/preview-alias.ts @@ -0,0 +1,107 @@ +import { execSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { getWorkersCIBranchName } from "@cloudflare/workers-utils"; +import { logger } from "../../shared/context"; + +const MAX_DNS_LABEL_LENGTH = 63; +const HASH_LENGTH = 4; +const ALIAS_VALIDATION_REGEX = /^[a-z](?:[a-z0-9-]*[a-z0-9])?$/i; + +/** + * Sanitizes a branch name to create a valid DNS label alias. + * Converts to lowercase, replaces invalid chars with dashes, removes consecutive dashes. + */ +export function sanitizeBranchName(branchName: string): string { + return branchName + .replace(/[^a-zA-Z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase(); +} + +/** + * Gets the current branch name from CI environment or git. + */ +export function getBranchName(): string | undefined { + // Try CI environment variable first + const ciBranchName = getWorkersCIBranchName(); + if (ciBranchName) { + return ciBranchName; + } + + // Fall back to git commands + try { + execSync(`git rev-parse --is-inside-work-tree`, { stdio: "ignore" }); + return execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim(); + } catch { + return undefined; + } +} + +/** + * Creates a truncated alias with hash suffix when the branch name is too long. + * Hash from original branch name to preserve uniqueness. + */ +export function createTruncatedAlias( + branchName: string, + sanitizedAlias: string, + availableSpace: number +): string | undefined { + const spaceForHash = HASH_LENGTH + 1; // +1 for hyphen separator + const maxPrefixLength = availableSpace - spaceForHash; + + if (maxPrefixLength < 1) { + // Not enough space even with truncation + return undefined; + } + + const hash = createHash("sha256") + .update(branchName) + .digest("hex") + .slice(0, HASH_LENGTH); + + const truncatedPrefix = sanitizedAlias.slice(0, maxPrefixLength); + return `${truncatedPrefix}-${hash}`; +} + +/** + * Generates a preview alias based on the current git branch. + * Alias must be <= 63 characters, alphanumeric + dashes only, and start with a letter. + * Returns undefined if not in a git directory or requirements cannot be met. + */ +export function generatePreviewAlias(scriptName: string): string | undefined { + const warnAndExit = () => { + logger.warn( + `Preview alias generation requested, but could not be autogenerated.` + ); + return undefined; + }; + + const branchName = getBranchName(); + if (!branchName) { + return warnAndExit(); + } + + const sanitizedAlias = sanitizeBranchName(branchName); + + // Validate the sanitized alias meets DNS label requirements + if (!ALIAS_VALIDATION_REGEX.test(sanitizedAlias)) { + return warnAndExit(); + } + + const availableSpace = MAX_DNS_LABEL_LENGTH - scriptName.length - 1; + + // If the sanitized alias fits within the remaining space, return it, + // otherwise try truncation with hash suffixed + if (sanitizedAlias.length <= availableSpace) { + return sanitizedAlias; + } + + const truncatedAlias = createTruncatedAlias( + branchName, + sanitizedAlias, + availableSpace + ); + + return truncatedAlias || warnAndExit(); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/print-bindings.ts b/packages/deploy-helpers/src/deploy/helpers/print-bindings.ts new file mode 100644 index 0000000..69a85d7 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/print-bindings.ts @@ -0,0 +1,1076 @@ +import { stripVTControlCharacters } from "node:util"; +import { brandColor, dim, white } from "@cloudflare/cli-shared-helpers/colors"; +import { + assertNever, + getBindingLocalSupport, + getBindingTypeFriendlyName, + UserError, +} from "@cloudflare/workers-utils"; +import chalk from "chalk"; +import { logger } from "../../shared/context"; +import { extractBindingsOfType, isUnsafeBindingType } from "./binding-utils"; +import type { Binding, StartDevWorkerInput } from "@cloudflare/workers-utils"; +import type { + CfSendEmailBindings, + CfTailConsumer, + ContainerApp, +} from "@cloudflare/workers-utils"; +import type { WorkerRegistry } from "miniflare"; + +/** + * Tracks whether we have already explained the connected status + */ +let isConnectedStatusExplained = false; + +type PrintContext = { + log?: (message: string) => void; + registry?: WorkerRegistry | null; + local?: boolean; + isMultiWorker?: boolean; + remoteBindingsDisabled?: boolean; + name?: string; + provisioning?: boolean; + warnIfNoBindings?: boolean; + unsafeMetadata?: Record; +}; + +/** + * Print all the bindings a worker would have access to. + * Accepts StartDevWorkerInput["bindings"] format + */ +export function printBindings( + bindings: StartDevWorkerInput["bindings"], + tailConsumers: CfTailConsumer[] = [], + streamingTailConsumers: CfTailConsumer[] = [], + containers: ContainerApp[] = [], + context: PrintContext = {} +) { + let hasConnectionStatus = false; + + const log = context.log ?? logger.log; + const isMultiWorker = context.isMultiWorker ?? false; + const getMode = createGetMode({ + isProvisioning: context.provisioning, + isLocalDev: context.local, + }); + const truncate = (item: string | Record, maxLength = 40) => { + const s = typeof item === "string" ? item : JSON.stringify(item); + if (s.length < maxLength) { + return s; + } + + return `${s.substring(0, maxLength - 3)}...`; + }; + + const output: { + name: string; + type: string; + value: string | undefined | symbol; + mode: string | undefined; + }[] = []; + + // Extract bindings by type + const data_blobs = extractBindingsOfType("data_blob", bindings); + const durable_objects = extractBindingsOfType( + "durable_object_namespace", + bindings + ); + const workflows = extractBindingsOfType("workflow", bindings); + const kv_namespaces = extractBindingsOfType("kv_namespace", bindings); + const send_email = extractBindingsOfType("send_email", bindings); + const queues = extractBindingsOfType("queue", bindings); + const d1_databases = extractBindingsOfType("d1", bindings); + const vectorize = extractBindingsOfType("vectorize", bindings); + const ai_search_namespaces = extractBindingsOfType( + "ai_search_namespace", + bindings + ); + const ai_search = extractBindingsOfType("ai_search", bindings); + const websearch = extractBindingsOfType("websearch", bindings); + const agent_memory = extractBindingsOfType("agent_memory", bindings); + const hyperdrive = extractBindingsOfType("hyperdrive", bindings); + const r2_buckets = extractBindingsOfType("r2_bucket", bindings); + const logfwdr = extractBindingsOfType("logfwdr", bindings); + const secrets_store_secrets = extractBindingsOfType( + "secrets_store_secret", + bindings + ); + const artifacts = extractBindingsOfType("artifacts", bindings); + const services = extractBindingsOfType("service", bindings); + const vpc_services = extractBindingsOfType("vpc_service", bindings); + const vpc_networks = extractBindingsOfType("vpc_network", bindings); + const analytics_engine_datasets = extractBindingsOfType( + "analytics_engine", + bindings + ); + const text_blobs = extractBindingsOfType("text_blob", bindings); + const browser = extractBindingsOfType("browser", bindings); + const images = extractBindingsOfType("images", bindings); + const stream = extractBindingsOfType("stream", bindings); + const ai = extractBindingsOfType("ai", bindings); + const version_metadata = extractBindingsOfType("version_metadata", bindings); + // Extract all vars (plain_text, json, secret_text) together to preserve insertion order + const vars = Object.entries(bindings ?? {}) + .filter( + ([_, binding]) => + binding.type === "plain_text" || + binding.type === "json" || + binding.type === "secret_text" + ) + .map(([name, binding]) => ({ + binding: name, + ...(binding as + | Extract + | Extract + | Extract), + })); + const wasm_modules = extractBindingsOfType("wasm_module", bindings); + const dispatch_namespaces = extractBindingsOfType( + "dispatch_namespace", + bindings + ); + const mtls_certificates = extractBindingsOfType("mtls_certificate", bindings); + const pipelines = extractBindingsOfType("pipeline", bindings); + const ratelimits = extractBindingsOfType("ratelimit", bindings); + const assets = extractBindingsOfType("assets", bindings); + const unsafe_hello_world = extractBindingsOfType( + "unsafe_hello_world", + bindings + ); + const flagship = extractBindingsOfType("flagship", bindings); + const media = extractBindingsOfType("media", bindings); + const worker_loaders = extractBindingsOfType("worker_loader", bindings); + + // Extract generic unsafe bindings (type starts with "unsafe_" but isn't "unsafe_hello_world") + const unsafe_bindings = Object.entries(bindings ?? {}) + .filter( + ([_, binding]) => + isUnsafeBindingType(binding.type) && + binding.type !== "unsafe_hello_world" + ) + .map(([name, binding]) => ({ name, ...binding })); + + if (data_blobs.length > 0) { + output.push( + ...data_blobs.map(({ binding, source }) => ({ + name: binding, + type: getBindingTypeFriendlyName("data_blob"), + value: "contents" in source ? "" : truncate(source.path), + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + + if (durable_objects.length > 0) { + output.push( + ...durable_objects.map(({ name, class_name, script_name }) => { + let value = class_name; + let mode = undefined; + if (script_name) { + if (context.local && context.registry !== null) { + const registryDefinition = context.registry?.[script_name]; + + hasConnectionStatus = true; + if (registryDefinition && registryDefinition.debugPortAddress) { + value += `, defined in ${script_name}`; + mode = getMode({ isSimulatedLocally: true, connected: true }); + } else { + value += `, defined in ${script_name}`; + mode = getMode({ isSimulatedLocally: true, connected: false }); + } + } else { + value += `, defined in ${script_name}`; + mode = getMode({ isSimulatedLocally: true }); + } + } else { + mode = getMode({ isSimulatedLocally: true }); + } + + return { + name, + type: getBindingTypeFriendlyName("durable_object_namespace"), + value: value, + mode, + }; + }) + ); + } + + if (workflows.length > 0) { + output.push( + ...workflows.map(({ class_name, script_name, binding, remote }) => { + let value = class_name; + if (script_name) { + value += ` (defined in ${script_name})`; + } + + return { + name: binding, + type: getBindingTypeFriendlyName("workflow"), + value: value, + mode: getMode({ + isSimulatedLocally: + script_name && !context.remoteBindingsDisabled ? !remote : true, + }), + }; + }) + ); + } + + if (kv_namespaces.length > 0) { + output.push( + ...kv_namespaces.map(({ binding, id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("kv_namespace"), + value: id, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + }; + }) + ); + } + + if (send_email.length > 0) { + output.push( + ...send_email.map((emailBinding: CfSendEmailBindings) => { + const destination_address = + "destination_address" in emailBinding + ? emailBinding.destination_address + : undefined; + const allowed_destination_addresses = + "allowed_destination_addresses" in emailBinding + ? emailBinding.allowed_destination_addresses + : undefined; + const allowed_sender_addresses = + "allowed_sender_addresses" in emailBinding + ? emailBinding.allowed_sender_addresses + : undefined; + let value = + destination_address || + allowed_destination_addresses?.join(", ") || + "unrestricted"; + + if (allowed_sender_addresses) { + value += ` - senders: ${allowed_sender_addresses.join(", ")}`; + } + return { + name: emailBinding.name, + type: getBindingTypeFriendlyName("send_email"), + value, + mode: getMode({ + isSimulatedLocally: + context.remoteBindingsDisabled || !emailBinding.remote, + }), + }; + }) + ); + } + + if (queues.length > 0) { + output.push( + ...queues.map(({ binding, queue_name, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("queue"), + value: queue_name, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + }; + }) + ); + } + + if (d1_databases.length > 0) { + output.push( + ...d1_databases.map( + ({ + binding, + database_name, + database_id, + preview_database_id, + remote, + }) => { + const value = + typeof database_id == "symbol" + ? database_id + : (preview_database_id ?? database_name ?? database_id); + + return { + name: binding, + type: getBindingTypeFriendlyName("d1"), + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + value, + }; + } + ) + ); + } + + if (vectorize.length > 0) { + output.push( + ...vectorize.map(({ binding, index_name, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("vectorize"), + value: index_name, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + + if (ai_search_namespaces.length > 0) { + output.push( + ...ai_search_namespaces.map(({ binding, namespace }) => ({ + name: binding, + type: getBindingTypeFriendlyName("ai_search_namespace"), + // Preserve `namespace` as-is so `typeof === "symbol"` handling + // downstream can render INHERIT_SYMBOL as `"inherited"`. Using + // `String(namespace)` would stringify it to + // `"Symbol(inherit_binding)"` and defeat that check. + value: namespace ?? undefined, + mode: getMode({ isSimulatedLocally: false }), + })) + ); + } + + if (ai_search.length > 0) { + output.push( + ...ai_search.map(({ binding, instance_name }) => ({ + name: binding, + type: getBindingTypeFriendlyName("ai_search"), + value: instance_name ? String(instance_name) : undefined, + mode: getMode({ isSimulatedLocally: false }), + })) + ); + } + + if (websearch.length > 0) { + output.push( + ...websearch.map(({ binding }) => ({ + name: binding, + type: getBindingTypeFriendlyName("websearch"), + value: undefined, + mode: getMode({ isSimulatedLocally: false }), + })) + ); + } + + if (agent_memory.length > 0) { + output.push( + ...agent_memory.map(({ binding, namespace }) => ({ + name: binding, + type: getBindingTypeFriendlyName("agent_memory"), + value: namespace ?? undefined, + mode: getMode({ isSimulatedLocally: false }), + })) + ); + } + + if (hyperdrive.length > 0) { + output.push( + ...hyperdrive.map(({ binding, id }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("hyperdrive"), + value: id, + mode: getMode({ isSimulatedLocally: true }), + }; + }) + ); + } + + if (vpc_services.length > 0) { + output.push( + ...vpc_services.map(({ binding, service_id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("vpc_service"), + value: service_id, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + + if (vpc_networks.length > 0) { + output.push( + ...vpc_networks.map(({ binding, tunnel_id, network_id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("vpc_network"), + value: tunnel_id ?? network_id, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + + if (r2_buckets.length > 0) { + output.push( + ...r2_buckets.map(({ binding, bucket_name, jurisdiction, remote }) => { + const value = + typeof bucket_name === "symbol" + ? bucket_name + : bucket_name + ? `${bucket_name}${jurisdiction ? ` (${jurisdiction})` : ""}` + : undefined; + + return { + name: binding, + type: getBindingTypeFriendlyName("r2_bucket"), + value: value, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + }; + }) + ); + } + + if (logfwdr.length > 0) { + output.push( + ...logfwdr.map(({ name, destination }) => { + return { + name, + type: getBindingTypeFriendlyName("logfwdr"), + value: destination, + mode: getMode(), + }; + }) + ); + } + + if (secrets_store_secrets.length > 0) { + output.push( + ...secrets_store_secrets.map(({ binding, store_id, secret_name }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("secrets_store_secret"), + value: `${store_id}/${secret_name}`, + mode: getMode({ isSimulatedLocally: true }), + }; + }) + ); + } + + if (artifacts.length > 0) { + output.push( + ...artifacts.map(({ binding, namespace }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("artifacts"), + value: namespace, + mode: getMode({ isSimulatedLocally: false }), + }; + }) + ); + } + + if (unsafe_hello_world.length > 0) { + output.push( + ...unsafe_hello_world.map(({ binding, enable_timer }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("unsafe_hello_world"), + value: enable_timer ? `Timer enabled` : `Timer disabled`, + mode: getMode({ isSimulatedLocally: true }), + }; + }) + ); + } + + if (flagship.length > 0) { + output.push( + ...flagship.map(({ binding, app_id }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("flagship"), + value: app_id, + mode: getMode({ + isSimulatedLocally: !context.remoteBindingsDisabled + ? false + : undefined, + }), + }; + }) + ); + } + + if (services.length > 0) { + output.push( + ...services.map(({ binding, service, entrypoint, remote, dev }) => { + let value = service; + let mode = undefined; + + if (entrypoint) { + value += `#${entrypoint}`; + } + + if (dev !== undefined && context.local) { + mode = getMode({ isSimulatedLocally: true }); + } else if (remote) { + mode = getMode({ isSimulatedLocally: false }); + } else if (context.local && context.registry !== null) { + const isSelfBinding = service === context.name; + + if (isSelfBinding) { + hasConnectionStatus = true; + mode = getMode({ isSimulatedLocally: true, connected: true }); + } else { + const registryDefinition = context.registry?.[service]; + hasConnectionStatus = true; + + if (registryDefinition && registryDefinition.debugPortAddress) { + mode = getMode({ isSimulatedLocally: true, connected: true }); + } else { + mode = getMode({ isSimulatedLocally: true, connected: false }); + } + } + } + + return { + name: binding, + type: getBindingTypeFriendlyName("service"), + value, + mode, + }; + }) + ); + } + + if (analytics_engine_datasets.length > 0) { + output.push( + ...analytics_engine_datasets.map(({ binding, dataset }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("analytics_engine"), + value: dataset ?? binding, + mode: getMode({ isSimulatedLocally: true }), + }; + }) + ); + } + + if (text_blobs.length > 0) { + output.push( + ...text_blobs.map(({ binding, source }) => ({ + name: binding, + type: getBindingTypeFriendlyName("text_blob"), + value: + "contents" in source + ? truncate(source.contents) + : "path" in source + ? truncate(source.path) + : undefined, + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + + if (browser.length > 0) { + output.push( + ...browser.map(({ binding, remote }) => ({ + name: binding, + type: getBindingTypeFriendlyName("browser"), + value: undefined, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + })) + ); + } + + if (images.length > 0) { + output.push( + ...images.map(({ binding, remote }) => ({ + name: binding, + type: getBindingTypeFriendlyName("images"), + value: undefined, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + })) + ); + } + + if (stream.length > 0) { + output.push( + ...stream.map(({ binding, remote }) => ({ + name: binding, + type: getBindingTypeFriendlyName("stream"), + value: undefined, + mode: getMode({ + isSimulatedLocally: + (remote === true || remote === undefined) && + !context.remoteBindingsDisabled + ? false + : undefined, + }), + })) + ); + } + + if (media.length > 0) { + output.push( + ...media.map(({ binding, remote }) => ({ + name: binding, + type: getBindingTypeFriendlyName("media"), + value: undefined, + mode: getMode({ + isSimulatedLocally: + (remote === true || remote === undefined) && + !context.remoteBindingsDisabled + ? false + : undefined, + }), + })) + ); + } + + if (ai.length > 0) { + output.push( + ...ai.map(({ binding, staging, remote }) => ({ + name: binding, + type: getBindingTypeFriendlyName("ai"), + value: staging ? `staging` : undefined, + mode: getMode({ + isSimulatedLocally: + (remote === true || remote === undefined) && + !context.remoteBindingsDisabled + ? false + : undefined, + }), + })) + ); + } + + if (pipelines.length > 0) { + output.push( + ...pipelines.map( + ({ binding, stream: pipelineStream, pipeline, remote }) => ({ + name: binding, + type: getBindingTypeFriendlyName("pipeline"), + value: pipelineStream || pipeline, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + }) + ) + ); + } + + if (ratelimits.length > 0) { + output.push( + ...ratelimits.map(({ name, simple }) => ({ + name, + type: getBindingTypeFriendlyName("ratelimit"), + value: `${simple.limit} requests/${simple.period}s`, + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + + if (assets.length > 0) { + output.push( + ...assets.map(({ binding }) => ({ + name: binding, + type: getBindingTypeFriendlyName("assets"), + value: undefined, + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + + if (version_metadata.length > 0) { + output.push( + ...version_metadata.map(({ binding }) => ({ + name: binding, + type: getBindingTypeFriendlyName("version_metadata"), + value: undefined, + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + if (unsafe_bindings.length > 0) { + output.push( + ...unsafe_bindings.map((binding) => { + const dev = "dev" in binding ? binding.dev : undefined; + // Strip the "unsafe_" prefix to get the original binding type for display + const originalType = binding.type.slice("unsafe_".length); + return { + name: binding.name, + type: dev + ? dev.plugin.name + : getBindingTypeFriendlyName(binding.type), + value: originalType, + mode: getMode({ + isSimulatedLocally: !!dev, + }), + }; + }) + ); + } + + if (vars.length > 0) { + output.push( + ...vars.map((variable) => { + const { binding, type: varType, value: varValue } = variable; + let parsedValue; + /* eslint-disable-next-line @typescript-eslint/no-deprecated -- + * hidden is a deprecated property, but still needs to be supported for backwards compatibility + * so we need to handle appropriately here + * + * see packages/workers-utils/src/types.ts for details on the hidden property + */ + if (varType === "plain_text" && variable.hidden !== true) { + parsedValue = `"${truncate(varValue)}"`; + } else if (varType === "json") { + parsedValue = truncate(JSON.stringify(varValue)); + } else { + parsedValue = `"(hidden)"`; + } + return { + name: binding, + type: getBindingTypeFriendlyName(varType), + value: parsedValue, + mode: getMode({ isSimulatedLocally: true }), + }; + }) + ); + } + + if (wasm_modules.length > 0) { + output.push( + ...wasm_modules.map(({ binding, source }) => ({ + name: binding, + type: getBindingTypeFriendlyName("wasm_module"), + value: "contents" in source ? "" : truncate(source.path), + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + + if (dispatch_namespaces.length > 0) { + output.push( + ...dispatch_namespaces.map(({ binding, namespace, outbound, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("dispatch_namespace"), + value: outbound + ? `${namespace} (outbound -> ${outbound.service})` + : namespace, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + + if (mtls_certificates.length > 0) { + output.push( + ...mtls_certificates.map(({ binding, certificate_id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("mtls_certificate"), + value: certificate_id, + mode: getMode({ + isSimulatedLocally: + remote && !context.remoteBindingsDisabled ? false : undefined, + }), + }; + }) + ); + } + + if (worker_loaders.length > 0) { + output.push( + ...worker_loaders.map(({ binding }) => ({ + name: binding, + type: getBindingTypeFriendlyName("worker_loader"), + value: undefined, + mode: getMode({ isSimulatedLocally: true }), + })) + ); + } + + if (output.length === 0) { + if (context.warnIfNoBindings) { + if (context.name && isMultiWorker) { + log(`No bindings found for ${chalk.blue(context.name)}`); + } else { + log("No bindings found."); + } + } + } else { + let title: string; + if (context.provisioning) { + title = `${chalk.red("Experimental:")} The following bindings need to be provisioned:`; + } else if (context.name && isMultiWorker) { + title = `${chalk.blue(context.name)} has access to the following bindings:`; + } else { + title = "Your Worker has access to the following bindings:"; + } + + const headings = { + binding: "Binding", + resource: "Resource", + mode: "Mode", + } as const; + + const maxValueLength = Math.max( + ...output.map((b) => + typeof b.value === "symbol" + ? "inherited".length + : (b.value?.length ?? 0) + ) + ); + const maxNameLength = Math.max(...output.map((b) => b.name.length)); + const maxTypeLength = Math.max( + ...output.map((b) => b.type.length), + headings.resource.length + ); + const maxModeLength = Math.max( + ...output.map((b) => + b.mode ? stripVTControlCharacters(b.mode).length : headings.mode.length + ) + ); + + const hasMode = output.some((b) => b.mode); + const bindingPrefix = `env.`; + const bindingLength = + bindingPrefix.length + + maxNameLength + + " (".length + + maxValueLength + + ")".length; + + const columnGapSpaces = 6; + const columnGapSpacesWrapped = 4; + + const shouldWrap = + bindingLength + + columnGapSpaces + + maxTypeLength + + columnGapSpaces + + maxModeLength >= + process.stdout.columns; + + log(title); + const columnGap = shouldWrap + ? " ".repeat(columnGapSpacesWrapped) + : " ".repeat(columnGapSpaces); + + log( + `${padEndAnsi(dim(headings.binding), shouldWrap ? bindingPrefix.length + maxNameLength : bindingLength)}${columnGap}${padEndAnsi(dim(headings.resource), maxTypeLength)}${columnGap}${hasMode ? dim(headings.mode) : ""}` + ); + + for (const binding of output) { + const bindingValue = dim( + typeof binding.value === "symbol" + ? chalk.italic("inherited") + : (binding.value ?? "") + ); + const bindingString = padEndAnsi( + `${white(`env.${binding.name}`)}${binding.value && !shouldWrap ? ` (${bindingValue})` : ""}`, + shouldWrap ? bindingPrefix.length + maxNameLength : bindingLength + ); + + const suffix = shouldWrap + ? binding.value + ? `\n ${bindingValue}` + : "" + : ""; + + log( + `${bindingString}${columnGap}${brandColor(binding.type.padEnd(maxTypeLength))}${columnGap}${hasMode ? binding.mode : ""}${suffix}` + ); + } + log(""); + } + let title: string; + if (context.name && isMultiWorker) { + title = `${chalk.blue(context.name)} is sending Tail events to the following Workers:`; + } else { + title = "Your Worker is sending Tail events to the following Workers:"; + } + + const allTailConsumers = [ + ...(tailConsumers ?? []).map((c) => ({ + service: c.service, + streaming: false, + })), + ...(streamingTailConsumers ?? []).map((c) => ({ + service: c.service, + streaming: true, + })), + ]; + if (allTailConsumers.length > 0) { + log( + `${title}\n${allTailConsumers + .map(({ service, streaming }) => { + const displayName = `${service}${streaming ? ` (streaming)` : ""}`; + if (context.local && context.registry !== null) { + const registryDefinition = context.registry?.[service]; + hasConnectionStatus = true; + + if (registryDefinition) { + return `- ${displayName} ${chalk.green("[connected]")}`; + } else { + return `- ${displayName} ${chalk.red("[not connected]")}`; + } + } else { + return `- ${displayName}`; + } + }) + .join("\n")}` + ); + } + + if (containers.length > 0 && !context.provisioning) { + let containersTitle = "The following containers are available:"; + if (context.name && isMultiWorker) { + containersTitle = `The following containers are available from ${chalk.blue(context.name)}:`; + } + + log( + `${containersTitle}\n${containers + .map((c) => `- ${c.name} (${c.image})`) + .join("\n")}` + ); + log(""); + } + + if (context.unsafeMetadata) { + log("The following unsafe metadata will be attached to your Worker:"); + log(JSON.stringify(context.unsafeMetadata, null, 2)); + } + + if (hasConnectionStatus && !isConnectedStatusExplained) { + log( + dim( + `\nService bindings, Durable Object bindings, and Tail consumers connect to other Wrangler or Vite dev processes running locally, with their connection status indicated by ${chalk.green("[connected]")} or ${chalk.red("[not connected]")}. For more details, refer to https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/#local-development\n` + ) + ); + isConnectedStatusExplained = true; + } +} + +// Exactly the same as String.padEnd, but doesn't miscount ANSI control characters +function padEndAnsi(str: string, length: number) { + return ( + str + " ".repeat(Math.max(0, length - stripVTControlCharacters(str).length)) + ); +} + +/** + * Creates a function for adding a suffix to the value of a binding in the console. + * + * The suffix is only for local dev so it can be used to determine whether a binding is + * simulated locally or connected to a remote resource. + */ +function createGetMode({ + isProvisioning = false, + isLocalDev = false, +}: { + isProvisioning?: boolean; + isLocalDev?: boolean; +}) { + return function bindingMode({ + isSimulatedLocally, + connected, + }: { + // Is this binding running locally? + isSimulatedLocally?: boolean; + // If this is an external service/tail/etc... binding, is it connected? + // true = connected via the dev registry + // false = trying to connect via the dev registry, but the target is not found + // undefined = dev registry is disabled or the binding is in remote mode (which always implies connection) + connected?: boolean; + } = {}): string | undefined { + if (isProvisioning || !isLocalDev) { + return undefined; + } + if (isSimulatedLocally === undefined) { + return dim("not supported"); + } + + return `${isSimulatedLocally ? chalk.blue("local") : chalk.yellow("remote")}${connected === undefined ? "" : connected ? chalk.green(" [connected]") : chalk.red(" [not connected]")}`; + }; +} + +/** + * Validates the user's `remote` setting for a given binding against the + * binding type's local-development capabilities (sourced from + * {@link getBindingLocalSupport}). Throws `UserError` for invalid combinations + * and emits warnings for valid-but-noteworthy ones. + */ +export function warnOrError( + type: Binding["type"], + remote: boolean | undefined +) { + const support = getBindingLocalSupport(type); + switch (support) { + case "local-and-remote": + return; + case "local-only": + if (remote === true) { + throw new UserError( + `${getBindingTypeFriendlyName(type)} bindings do not support accessing remote resources.`, + { + telemetryMessage: "utils bindings unsupported remote resources", + } + ); + } + return; + case "remote": + if (remote === false) { + throw new UserError( + `${getBindingTypeFriendlyName(type)} bindings do not support local development. You can set \`remote: true\` for the binding definition in your configuration file to access a remote version of the resource.`, + { + telemetryMessage: "utils bindings unsupported local development", + } + ); + } + if (remote === undefined) { + logger.warn( + `${getBindingTypeFriendlyName(type)} bindings do not support local development, and so parts of your Worker may not work correctly. You can set \`remote: true\` for the binding definition in your configuration file to access a remote version of the resource.` + ); + } + return; + case "DO-NOT-USE-this-resource-will-never-have-a-local-simulator": + if (remote === false) { + throw new UserError( + `${getBindingTypeFriendlyName(type)} bindings do not support local development. You can set \`remote: true\` for the binding definition in your configuration file to access a remote version of the resource.`, + { + telemetryMessage: + "utils bindings unsupported local development always remote", + } + ); + } + if (remote === undefined) { + logger.warn( + `${getBindingTypeFriendlyName(type)} bindings always access remote resources, and so may incur usage charges even in local dev. To suppress this warning, set \`remote: true\` for the binding definition in your configuration file.` + ); + } + return; + default: + assertNever(support); + } +} diff --git a/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts b/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts new file mode 100644 index 0000000..b9cea83 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/provision-bindings.ts @@ -0,0 +1,1289 @@ +import assert from "node:assert"; +import { + APIError, + experimental_patchConfig, + experimental_readRawConfig, + INHERIT_SYMBOL, + PatchConfigError, + UserError, +} from "@cloudflare/workers-utils"; +import { + fetchResult, + isNonInteractiveOrCI, + logger, + prompt, + select, +} from "../../shared/context"; +import { printBindings } from "./print-bindings"; +import type { + Binding, + CfAgentMemory, + CfAISearchNamespace, + CfD1Database, + CfKvNamespace, + CfR2Bucket, + ComplianceConfig, + Config, + RawConfig, + StartDevWorkerInput, + WorkerMetadataBinding, +} from "@cloudflare/workers-utils"; + +export type Settings = { + bindings: Array; +}; + +type DatabaseCreationResult = { + uuid: string; + name: string; +}; + +type ConcreteDatabase = { + uuid: string; + name: string; +}; + +type DatabaseInfo = { + uuid: string; + name: string; +}; + +type R2BucketInfo = { + name: string; + creation_date: string; + location?: string; + storage_class?: string; +}; + +type AISearchNamespace = { + name: string; +}; + +type AgentMemoryNamespace = { + id: string; + name: string; + account_id: string; + created_at: string; + updated_at: string; +}; + +type KVNamespaceInfo = { + id: string; + title: string; + supports_url_encoding?: boolean; +}; + +abstract class ProvisionResourceHandler< + T extends WorkerMetadataBinding["type"], + B extends ProvisionableBinding, +> { + constructor( + public type: T, + public bindingName: string, + public binding: B, + public idField: keyof B, + public complianceConfig: ComplianceConfig, + public accountId: string + ) {} + + // Does this resource already exist in the currently deployed version of the Worker? + // If it does, that means we can inherit from it. + abstract canInherit( + settings: Settings | undefined + ): boolean | Promise; + + inherit(): void { + // @ts-expect-error idField is a key of this.binding + this.binding[this.idField] = INHERIT_SYMBOL; + } + connect(id: string): void { + // @ts-expect-error idField is a key of this.binding + this.binding[this.idField] = id; + } + + abstract create(name: string): Promise; + + abstract get name(): string | undefined; + + async provision(name: string): Promise { + const id = await this.create(name); + this.connect(id); + } + + // This binding is fully specified and can't/shouldn't be provisioned. + // This is usually when it has an id (e.g. D1 `database_id`). + isFullySpecified(): boolean { + return false; + } + + // Does this binding need to be provisioned? + // Some bindings are not fully specified, but don't need provisioning + // (e.g. R2 binding, with a bucket_name that already exists). + async isConnectedToExistingResource(): Promise { + return false; + } + + // Should this resource be provisioned? + async shouldProvision(settings: Settings | undefined) { + // If the resource is fully specified, don't provision. + if (!this.isFullySpecified()) { + // If we can inherit, do that and don't provision. + if (await this.canInherit(settings)) { + this.inherit(); + } else { + // If the resource is connected to a remote resource that _exists_ + // (see comments on the individual functions for why this is different to isFullySpecified()). + const connected = await this.isConnectedToExistingResource(); + if (connected) { + if (typeof connected === "string") { + // Basically a special case for D1: the resource is specified by name in config + // and exists, but needs to be specified by ID for the first deploy to work. + this.connect(connected); + } + return false; + } + return true; + } + } + return false; + } +} + +class R2Handler extends ProvisionResourceHandler< + "r2_bucket", + Extract +> { + get name(): string | undefined { + return this.binding.bucket_name as string; + } + + async create(name: string) { + await createR2Bucket( + this.complianceConfig, + this.accountId, + name, + undefined, + this.binding.jurisdiction + ); + return name; + } + constructor( + bindingName: string, + binding: Extract, + complianceConfig: ComplianceConfig, + accountId: string + ) { + super( + "r2_bucket", + bindingName, + binding, + "bucket_name", + complianceConfig, + accountId + ); + } + + /** + * Inheriting an R2 binding replaces the id property (bucket_name for R2) with the inheritance symbol. + * This works when deploying (and is appropriate for all other binding types), but it means that the + * bucket_name for an R2 bucket is not displayed when deploying. As such, only use the inheritance symbol + * if the R2 binding has no `bucket_name`. + */ + override inherit(): void { + this.binding.bucket_name ??= INHERIT_SYMBOL; + } + + /** + * R2 bindings can be inherited if the binding name and jurisdiction match. + * Additionally, if the user has specified a bucket_name in config, make sure that matches. + */ + canInherit(settings: Settings | undefined): boolean { + return !!settings?.bindings.find( + (existing) => + existing.type === this.type && + existing.name === this.bindingName && + existing.jurisdiction === this.binding.jurisdiction && + (this.binding.bucket_name + ? this.binding.bucket_name === existing.bucket_name + : true) + ); + } + async isConnectedToExistingResource(): Promise { + assert(typeof this.binding.bucket_name !== "symbol"); + + // If the user hasn't specified a bucket_name in config, we always provision. + if (!this.binding.bucket_name) { + return false; + } + try { + await getR2Bucket( + this.complianceConfig, + this.accountId, + this.binding.bucket_name, + this.binding.jurisdiction + ); + // This bucket_name exists! We don't need to provision it. + return true; + } catch (e) { + if (!(e instanceof APIError && e.code === 10006)) { + // This is an error that is not "bucket not found", so we do want to throw. + throw e; + } + + // This bucket_name doesn't exist, so let's provision. + return false; + } + } +} + +class AISearchNamespaceHandler extends ProvisionResourceHandler< + "ai_search_namespace", + Extract +> { + get name(): string | undefined { + return this.binding.namespace as string; + } + + async create(name: string) { + await createAISearchNamespace(this.complianceConfig, this.accountId, name); + return name; + } + + constructor( + bindingName: string, + binding: Extract, + complianceConfig: ComplianceConfig, + accountId: string + ) { + super( + "ai_search_namespace", + bindingName, + binding, + "namespace", + complianceConfig, + accountId + ); + } + + canInherit(settings: Settings | undefined): boolean { + return !!settings?.bindings.find( + (existing) => + existing.type === this.type && + existing.name === this.bindingName && + (this.binding.namespace + ? this.binding.namespace === existing.namespace + : true) + ); + } + + async isConnectedToExistingResource(): Promise { + assert(typeof this.binding.namespace !== "symbol"); + + if (!this.binding.namespace) { + return false; + } + + const namespace = await getAISearchNamespace( + this.complianceConfig, + this.accountId, + this.binding.namespace + ); + + return namespace !== null; + } +} + +class AgentMemoryNamespaceHandler extends ProvisionResourceHandler< + "agent_memory", + Extract +> { + get name(): string | undefined { + return this.binding.namespace as string; + } + + async create(name: string) { + await createAgentMemoryNamespace( + this.complianceConfig, + this.accountId, + name + ); + return name; + } + + constructor( + bindingName: string, + binding: Extract, + complianceConfig: ComplianceConfig, + accountId: string + ) { + super( + "agent_memory", + bindingName, + binding, + "namespace", + complianceConfig, + accountId + ); + } + + canInherit(settings: Settings | undefined): boolean { + return !!settings?.bindings.find( + (existing) => + existing.type === this.type && + existing.name === this.bindingName && + (this.binding.namespace + ? this.binding.namespace === existing.namespace + : true) + ); + } + + async isConnectedToExistingResource(): Promise { + assert(typeof this.binding.namespace !== "symbol"); + + if (!this.binding.namespace) { + return false; + } + + const namespace = await getAgentMemoryNamespace( + this.complianceConfig, + this.accountId, + this.binding.namespace + ); + + return namespace !== null; + } +} + +class KVHandler extends ProvisionResourceHandler< + "kv_namespace", + Extract +> { + get name(): string | undefined { + return undefined; + } + async create(name: string) { + return await createKVNamespace(this.complianceConfig, this.accountId, name); + } + constructor( + bindingName: string, + binding: Extract, + complianceConfig: ComplianceConfig, + accountId: string + ) { + super( + "kv_namespace", + bindingName, + binding, + "id", + complianceConfig, + accountId + ); + } + canInherit(settings: Settings | undefined): boolean { + return !!settings?.bindings.find( + (existing) => + existing.type === this.type && existing.name === this.bindingName + ); + } + isFullySpecified(): boolean { + return !!this.binding.id; + } +} + +class D1Handler extends ProvisionResourceHandler< + "d1", + Extract +> { + get name(): string | undefined { + return this.binding.database_name as string; + } + async create(name: string) { + const db = await createD1Database( + this.complianceConfig, + this.accountId, + name + ); + return db.uuid; + } + constructor( + bindingName: string, + binding: Extract, + complianceConfig: ComplianceConfig, + accountId: string + ) { + super( + "d1", + bindingName, + binding, + "database_id", + complianceConfig, + accountId + ); + } + async canInherit(settings: Settings | undefined): Promise { + const maybeInherited = settings?.bindings.find( + (existing) => + existing.type === this.type && existing.name === this.bindingName + ) as Extract | undefined; + // A D1 binding with the same binding name exists is already present on the worker... + if (maybeInherited) { + // ...and the user hasn't specified a name in their config, so we don't need to check if the database_name matches. + if (!this.binding.database_name) { + return true; + } + + // ...and the user HAS specified a name in their config, so we need to check if the database_name they provided + // matches the database_name of the existing binding (which isn't present in settings, so we'll need to make an API call to check). + const dbFromId = await getDatabaseInfoFromIdOrName( + this.complianceConfig, + this.accountId, + maybeInherited.id + ); + if (this.binding.database_name === dbFromId.name) { + return true; + } + } + return false; + } + async isConnectedToExistingResource(): Promise { + assert(typeof this.binding.database_name !== "symbol"); + + // If the user hasn't specified a database_name in config, we always provision. + if (!this.binding.database_name) { + return false; + } + try { + const db = await getDatabaseInfoFromIdOrName( + this.complianceConfig, + this.accountId, + this.binding.database_name + ); + + // This database_name exists! We don't need to provision it. + return db.uuid; + } catch (e) { + if (!(e instanceof APIError && e.code === 7404)) { + // This is an error that is not "database not found", so we do want to throw. + throw e; + } + + // This database_name doesn't exist, so let's provision. + return false; + } + } + isFullySpecified(): boolean { + return !!this.binding.database_id; + } +} + +type ProvisionableBinding = + | Extract + | Extract + | Extract + | Extract + | Extract; + +const HANDLERS = { + kv_namespace: { + Handler: KVHandler, + sort: 0, + name: "KV Namespace", + keyDescription: "title or id", + configField: "kv_namespaces" as const, + load: async (complianceConfig: ComplianceConfig, accountId: string) => { + const preExistingKV = await listKVNamespaces( + complianceConfig, + accountId, + true + ); + return preExistingKV.map((ns) => ({ title: ns.title, value: ns.id })); + }, + toConfig: ( + bindingName: string, + binding: Extract + ): CfKvNamespace => { + const { type: _, ...rest } = binding; + return { + ...rest, + binding: bindingName, + }; + }, + }, + d1: { + Handler: D1Handler, + sort: 1, + name: "D1 Database", + keyDescription: "name or id", + configField: "d1_databases" as const, + load: async (complianceConfig: ComplianceConfig, accountId: string) => { + const preExisting = await listDatabases( + complianceConfig, + accountId, + true, + 1000 + ); + return preExisting.map((db) => ({ title: db.name, value: db.uuid })); + }, + toConfig: ( + bindingName: string, + binding: Extract + ): CfD1Database => { + const { type: _, ...rest } = binding; + return { + ...rest, + binding: bindingName, + }; + }, + }, + r2_bucket: { + Handler: R2Handler, + sort: 2, + name: "R2 Bucket", + keyDescription: "name", + configField: "r2_buckets" as const, + load: async (complianceConfig: ComplianceConfig, accountId: string) => { + const preExisting = await listR2Buckets(complianceConfig, accountId); + return preExisting.map((bucket) => ({ + title: bucket.name, + value: bucket.name, + })); + }, + toConfig: ( + bindingName: string, + binding: Extract + ): CfR2Bucket => { + const { type: _, ...rest } = binding; + return { + ...rest, + binding: bindingName, + }; + }, + }, + ai_search_namespace: { + Handler: AISearchNamespaceHandler, + sort: 3, + name: "AI Search Namespace", + keyDescription: "namespace name", + configField: "ai_search_namespaces" as const, + load: async (_complianceConfig: ComplianceConfig, _accountId: string) => { + // AI Search namespaces don't have a general list API in this context. + // The provisioning system will create them if they don't exist. + return []; + }, + toConfig: ( + bindingName: string, + binding: Extract + ): CfAISearchNamespace => { + const { type: _, ...rest } = binding; + return { + ...rest, + binding: bindingName, + }; + }, + }, + agent_memory: { + Handler: AgentMemoryNamespaceHandler, + sort: 4, + name: "Agent Memory", + keyDescription: "namespace name", + configField: "agent_memory" as const, + load: async (_complianceConfig: ComplianceConfig, _accountId: string) => { + // `load` only populates the interactive picker in `runProvisioningFlow` + // (the "Would you like to connect an existing X or create a new one?" + // prompt). For agent_memory, `namespace` is required in config — so + // `handler.name` is always set at provision time and `runProvisioningFlow` + // goes straight to the "Resource name found in config" branch without + // ever consulting this list. Whether or not the namespace already exists + // is decided by `isConnectedToExistingResource()` (which hits the GET + // /namespaces/:name endpoint), not by this list. Returning [] is + // therefore safe and avoids an unnecessary list call at deploy time. + return []; + }, + toConfig: ( + bindingName: string, + binding: Extract + ): CfAgentMemory => { + const { type: _, ...rest } = binding; + return { + ...rest, + binding: bindingName, + }; + }, + }, +}; + +type PendingResource = { + binding: string; + resourceType: + | "kv_namespace" + | "d1" + | "r2_bucket" + | "ai_search_namespace" + | "agent_memory"; + handler: + | KVHandler + | D1Handler + | R2Handler + | AISearchNamespaceHandler + | AgentMemoryNamespaceHandler; +}; + +function isProvisionableBinding( + binding: Binding +): binding is ProvisionableBinding { + return binding.type in HANDLERS; +} + +function createHandler( + bindingName: string, + binding: ProvisionableBinding, + complianceConfig: ComplianceConfig, + accountId: string +): + | KVHandler + | D1Handler + | R2Handler + | AISearchNamespaceHandler + | AgentMemoryNamespaceHandler { + switch (binding.type) { + case "kv_namespace": + return new KVHandler(bindingName, binding, complianceConfig, accountId); + case "d1": + return new D1Handler(bindingName, binding, complianceConfig, accountId); + case "r2_bucket": + return new R2Handler(bindingName, binding, complianceConfig, accountId); + case "ai_search_namespace": + return new AISearchNamespaceHandler( + bindingName, + binding, + complianceConfig, + accountId + ); + case "agent_memory": + return new AgentMemoryNamespaceHandler( + bindingName, + binding, + complianceConfig, + accountId + ); + } +} + +function toConfigBinding( + bindingName: string, + binding: ProvisionableBinding +): + | CfKvNamespace + | CfR2Bucket + | CfD1Database + | CfAISearchNamespace + | CfAgentMemory { + switch (binding.type) { + case "kv_namespace": + return HANDLERS.kv_namespace.toConfig(bindingName, binding); + case "d1": + return HANDLERS.d1.toConfig(bindingName, binding); + case "r2_bucket": + return HANDLERS.r2_bucket.toConfig(bindingName, binding); + case "ai_search_namespace": + return HANDLERS.ai_search_namespace.toConfig(bindingName, binding); + case "agent_memory": + return HANDLERS.agent_memory.toConfig(bindingName, binding); + } +} + +async function collectPendingResources( + complianceConfig: ComplianceConfig, + accountId: string, + scriptName: string, + bindings: StartDevWorkerInput["bindings"] +): Promise { + let settings: Settings | undefined; + + try { + settings = await getSettings(complianceConfig, accountId, scriptName); + } catch { + logger.debug("No settings found"); + } + + const pendingResources: PendingResource[] = []; + + for (const [bindingName, binding] of Object.entries(bindings ?? {})) { + if (!isProvisionableBinding(binding)) { + continue; + } + + const handler = createHandler( + bindingName, + binding, + complianceConfig, + accountId + ); + + if (await handler.shouldProvision(settings)) { + pendingResources.push({ + binding: bindingName, + resourceType: binding.type, + handler, + }); + } + } + + return pendingResources.sort( + (a, b) => HANDLERS[a.resourceType].sort - HANDLERS[b.resourceType].sort + ); +} + +export async function provisionBindings( + bindings: StartDevWorkerInput["bindings"], + accountId: string, + scriptName: string, + autoCreate: boolean, + config: Config, + options: { + skipConfigWriteback?: boolean; + } +): Promise { + const configPath = config.userConfigPath ?? config.configPath; + const pendingResources = await collectPendingResources( + config, + accountId, + scriptName, + bindings + ); + + if (pendingResources.length > 0) { + assert( + configPath, + "Provisioning resources is not possible without a config file" + ); + + logger.log(); + + printBindings( + Object.fromEntries( + pendingResources.map((resource) => [ + resource.binding, + { type: resource.resourceType }, + ]) + ) as Record, + config.tail_consumers, + config.streaming_tail_consumers, + config.containers, + { provisioning: true } + ); + logger.log(); + + const existingResources: Record = {}; + + for (const resource of pendingResources) { + existingResources[resource.resourceType] ??= await HANDLERS[ + resource.resourceType + ].load(config, accountId); + + await runProvisioningFlow( + resource, + existingResources[resource.resourceType], + HANDLERS[resource.resourceType].name, + scriptName, + autoCreate + ); + } + + const patch: RawConfig = {}; + + const existingBindingNames = new Set(); + + const isUsingRedirectedConfig = + config.userConfigPath && config.userConfigPath !== config.configPath; + + // If we're using a redirected config, then the redirected config potentially has injected + // bindings that weren't originally in the user config. These can be provisioned, but we + // should not write the IDs back to the user config file (because the bindings weren't there in the first place). + if (isUsingRedirectedConfig) { + const { rawConfig: unredirectedConfig } = + await experimental_readRawConfig( + { config: config.userConfigPath }, + { useRedirectIfAvailable: false } + ); + for (const resourceType of Object.keys( + HANDLERS + ) as (keyof typeof HANDLERS)[]) { + const configField = HANDLERS[resourceType].configField; + for (const binding of unredirectedConfig[configField] ?? []) { + existingBindingNames.add(binding.binding); + } + } + } + + for (const [bindingName, binding] of Object.entries(bindings ?? {})) { + if (!isProvisionableBinding(binding)) { + continue; + } + + // See above for why we skip writing back some bindings to the config file. + if (isUsingRedirectedConfig && !existingBindingNames.has(bindingName)) { + continue; + } + + const resourceType = HANDLERS[binding.type].configField; + + patch[resourceType] ??= []; + + const bindingToWrite = toConfigBinding(bindingName, binding); + + (patch[resourceType] as unknown as Array>).push( + Object.fromEntries( + Object.entries(bindingToWrite).filter( + // Make sure all the values are JSON serialisable. + // Otherwise we end up with "undefined" in the config. + ([_, value]) => typeof value === "string" + ) + ) + ); + } + + // If the user is performing an interactive deploy, write the provisioned IDs back to the config file. + // This is not necessary, as future deploys can use inherited resources, but it can help with + // portability of the config file, and adds robustness to bindings being renamed. + if (options.skipConfigWriteback) { + logger.log( + "Your Worker was deployed with provisioned resources. You may add the resource IDs to your config file if you wish, but future deploys will continue to work even without IDs." + ); + } else if (!isNonInteractiveOrCI()) { + try { + await experimental_patchConfig(configPath, patch, false); + logger.log( + "Your Worker was deployed with provisioned resources. We've written the IDs of these resources to your config file, which you can choose to save or discard. Either way future deploys will continue to work." + ); + } catch (e) { + // no-op — if the user is using TOML config we can't update it. + if (!(e instanceof PatchConfigError)) { + throw e; + } + } + } + + logger.log(`🎉 All resources provisioned, continuing with deployment...\n`); + } +} + +export function getSettings( + complianceConfig: ComplianceConfig, + accountId: string, + scriptName: string +) { + return fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/scripts/${scriptName}/settings` + ); +} + +function printDivider() { + logger.log(); +} + +type NormalisedResourceInfo = { + /** The name of the resource */ + title: string; + /** The id of the resource */ + value: string; +}; + +async function runProvisioningFlow( + item: PendingResource, + preExisting: NormalisedResourceInfo[], + friendlyBindingName: string, + scriptName: string, + autoCreate: boolean +) { + const NEW_OPTION_VALUE = "__WRANGLER_INTERNAL_NEW"; + const SEARCH_OPTION_VALUE = "__WRANGLER_INTERNAL_SEARCH"; + const MAX_OPTIONS = 4; + // NB preExisting does not actually contain all resources on the account - we max out at ~30 d1 databases, ~100 kv, and ~20 r2. + const options = preExisting.slice(0, MAX_OPTIONS - 1); + if (options.length < preExisting.length) { + options.push({ + title: "Other (too many to list)", + value: SEARCH_OPTION_VALUE, + }); + } + + const defaultName = autoProvisionedResourceName(scriptName, item.binding); + logger.log("Provisioning", item.binding, `(${friendlyBindingName})...`); + + if (item.handler.name) { + logger.log("Resource name found in config:", item.handler.name); + logger.log( + `🌀 Creating new ${friendlyBindingName} "${item.handler.name}"...` + ); + await item.handler.provision(item.handler.name); + } else if (autoCreate) { + logger.log(`🌀 Creating new ${friendlyBindingName} "${defaultName}"...`); + await item.handler.provision(defaultName); + } else { + let action: string = NEW_OPTION_VALUE; + + if (options.length > 0) { + action = await select( + `Would you like to connect an existing ${friendlyBindingName} or create a new one?`, + { + choices: options.concat([ + { title: "Create new", value: NEW_OPTION_VALUE }, + ]), + defaultOption: options.length, + fallbackOption: options.length, + } + ); + } + + if (action === NEW_OPTION_VALUE) { + const name = await prompt( + `Enter a name for your new ${friendlyBindingName}`, + { + defaultValue: defaultName, + } + ); + logger.log(`🌀 Creating new ${friendlyBindingName} "${name}"...`); + await item.handler.provision(name); + } else if (action === SEARCH_OPTION_VALUE) { + // Search through pre-existing resources that weren't listed. + let foundResource: NormalisedResourceInfo | undefined; + while (foundResource === undefined) { + const input = await prompt( + `Enter the ${HANDLERS[item.resourceType].keyDescription} for an existing ${friendlyBindingName}` + ); + foundResource = preExisting.find( + (resource) => resource.title === input || resource.value === input + ); + if (foundResource) { + item.handler.connect(foundResource.value); + } else { + logger.log( + `No ${friendlyBindingName} with that ${HANDLERS[item.resourceType].keyDescription} "${input}" found. Please try again.` + ); + } + } + } else { + item.handler.connect(action); + } + } + + logger.log(`✨ ${item.binding} provisioned 🎉`); + printDivider(); +} + +/** + * The resource name that auto-provisioning will create for a given binding. + */ +function autoProvisionedResourceName( + scriptName: string, + bindingName: string +): string { + return `${scriptName}-${bindingName.toLowerCase().replaceAll("_", "-")}`; +} + +/** + * Create a new KV namespace under the given `accountId` with the given `title`. + * + * @returns the generated id of the created namespace. + */ +async function createKVNamespace( + complianceConfig: ComplianceConfig, + accountId: string, + title: string +): Promise { + const response = await fetchResult<{ id: string }>( + complianceConfig, + `/accounts/${accountId}/storage/kv/namespaces`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title, + }), + } + ); + + return response.id; +} + +/** + * Fetch a list of all KV namespaces under the given `accountId`. + */ +async function listKVNamespaces( + complianceConfig: ComplianceConfig, + accountId: string, + limitCalls = false +): Promise { + const pageSize = 100; + let page = 1; + const results: KVNamespaceInfo[] = []; + while (results.length % pageSize === 0) { + const json = await fetchResult( + complianceConfig, + `/accounts/${accountId}/storage/kv/namespaces`, + {}, + new URLSearchParams({ + per_page: pageSize.toString(), + order: "title", + direction: "asc", + page: page.toString(), + }) + ); + page++; + results.push(...json); + if (limitCalls) { + break; + } + if (json.length < pageSize) { + break; + } + } + return results; +} + +async function createD1Database( + complianceConfig: ComplianceConfig, + accountId: string, + name: string +) { + try { + return await fetchResult( + complianceConfig, + `/accounts/${accountId}/d1/database`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name }), + } + ); + } catch (e) { + const errorCode = (e as { code: number }).code; + + if (errorCode === 7502) { + throw new UserError("A database with that name already exists", { + telemetryMessage: "d1 create database already exists", + }); + } + + if (errorCode === 7406) { + throw new UserError( + "You have reached the maximum number of D1 databases for your account. Please consider deleting unused databases, or visit the D1 documentation to learn more: https://developers.cloudflare.com/d1/", + { telemetryMessage: "d1 create database limit reached" } + ); + } + + throw e; + } +} + +async function listDatabases( + complianceConfig: ComplianceConfig, + accountId: string, + limitCalls = false, + pageSize = 10 +): Promise> { + let page = 1; + const results = []; + while (results.length % pageSize === 0) { + const json: Array = await fetchResult( + complianceConfig, + `/accounts/${accountId}/d1/database`, + {}, + new URLSearchParams({ + per_page: pageSize.toString(), + page: page.toString(), + }) + ); + page++; + results.push(...json); + if (limitCalls && page > 3) { + break; + } + if (json.length < pageSize) { + break; + } + } + return results; +} + +async function getDatabaseInfoFromIdOrName( + complianceConfig: ComplianceConfig, + accountId: string, + databaseIdOrName: string +): Promise { + return await fetchResult( + complianceConfig, + `/accounts/${accountId}/d1/database/${databaseIdOrName}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); +} + +/** + * Fetch a list of all R2 buckets under the given `accountId`. + */ +async function listR2Buckets( + complianceConfig: ComplianceConfig, + accountId: string, + jurisdiction?: string +): Promise { + const headers: Record = {}; + if (jurisdiction !== undefined) { + headers["cf-r2-jurisdiction"] = jurisdiction; + } + const results = await fetchResult<{ + buckets: R2BucketInfo[]; + }>(complianceConfig, `/accounts/${accountId}/r2/buckets`, { headers }); + return results.buckets; +} + +async function getR2Bucket( + complianceConfig: ComplianceConfig, + accountId: string, + bucketName: string, + jurisdiction?: string +): Promise { + const headers: Record = {}; + if (jurisdiction !== undefined) { + headers["cf-r2-jurisdiction"] = jurisdiction; + } + return await fetchResult( + complianceConfig, + `/accounts/${accountId}/r2/buckets/${bucketName}`, + { + method: "GET", + headers, + } + ); +} + +/** + * Create an R2 bucket with the given `bucketName` within the account given by `accountId`. + * + * A 400 is returned if the account already owns a bucket with this name. + * A bucket must be explicitly deleted to be replaced. + */ +async function createR2Bucket( + complianceConfig: ComplianceConfig, + accountId: string, + bucketName: string, + location?: string, + jurisdiction?: string, + storageClass?: string +): Promise { + const headers: Record = {}; + if (jurisdiction !== undefined) { + headers["cf-r2-jurisdiction"] = jurisdiction; + } + return await fetchResult( + complianceConfig, + `/accounts/${accountId}/r2/buckets`, + { + method: "POST", + body: JSON.stringify({ + name: bucketName, + ...(storageClass !== undefined && { storageClass }), + ...(location !== undefined && { locationHint: location }), + }), + headers, + } + ); +} + +/** + * Get an AI Search namespace for the given account. + * Returns `null` if the namespace does not exist (404); other errors propagate. + */ +async function getAISearchNamespace( + complianceConfig: ComplianceConfig, + accountId: string, + namespaceName: string +): Promise { + try { + return await fetchResult( + complianceConfig, + `/accounts/${accountId}/ai-search/namespaces/${namespaceName}`, + { method: "GET" } + ); + } catch (e) { + if (e instanceof APIError && e.status === 404) { + return null; + } + throw e; + } +} + +/** + * Create an AI Search namespace for the given account. + * Used by the provisioning system when a namespace doesn't exist at deploy time. + */ +async function createAISearchNamespace( + complianceConfig: ComplianceConfig, + accountId: string, + namespaceName: string +): Promise { + await fetchResult( + complianceConfig, + `/accounts/${accountId}/ai-search/namespaces`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: namespaceName }), + } + ); +} + +/** + * Get an Agent Memory namespace for the given account. + * Returns `null` if the namespace does not exist (404); other errors propagate. + * + * Used by the provisioning system at deploy time to decide whether a + * configured namespace needs to be created. The caller (not this function) + * is responsible for provisioning when `null` is returned. + */ +async function getAgentMemoryNamespace( + complianceConfig: ComplianceConfig, + accountId: string, + namespaceName: string +): Promise { + try { + return await fetchResult( + complianceConfig, + `/accounts/${accountId}/agent-memory/namespaces/${namespaceName}` + ); + } catch (e) { + if (e instanceof APIError && e.status === 404) { + return null; + } + throw e; + } +} + +/** + * Create an Agent Memory namespace for the given account. + * Used by the provisioning system when a namespace doesn't exist at deploy time. + */ +async function createAgentMemoryNamespace( + complianceConfig: ComplianceConfig, + accountId: string, + namespaceName: string +): Promise { + await fetchResult( + complianceConfig, + `/accounts/${accountId}/agent-memory/namespaces`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: namespaceName }), + } + ); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/secrets-validation.ts b/packages/deploy-helpers/src/deploy/helpers/secrets-validation.ts new file mode 100644 index 0000000..42d020b --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/secrets-validation.ts @@ -0,0 +1,85 @@ +import { APIError, UserError } from "@cloudflare/workers-utils"; +import { INVALID_INHERIT_BINDING_CODE } from "./error-codes"; +import type { Binding, Config } from "@cloudflare/workers-utils"; + +type SecretsValidationOptions = + | { type: "deploy"; workerExists: boolean } + | { type: "upload" }; + +/** + * When `secrets.required` is defined in config, validate the secrets exist on the Worker. + * For deploy, if the Worker doesn't exist yet, fail immediately. + * For upload, always add inherit bindings — the API handles the case where + * the Worker doesn't exist (versions upload cannot create new Workers). + * Secrets already provided (e.g. via --secrets-file) are excluded since + * they are part of the upload and don't need to be inherited. + */ +export function addRequiredSecretsInheritBindings( + config: Config, + bindings: Record, + options: SecretsValidationOptions +): void { + if (!config.secrets?.required?.length) { + return; + } + + const inheritedSecrets = config.secrets.required.filter( + (secretName) => !(secretName in bindings) + ); + + if (inheritedSecrets.length === 0) { + return; + } + + if (options.type === "deploy" && !options.workerExists) { + throw new UserError( + `The following required secrets have not been set: ${inheritedSecrets.join(", ")}\n` + + `This Worker does not exist yet, so secrets cannot be set in advance with \`wrangler secret put\`.\n` + + `To deploy a new Worker with secrets, supply them via a secrets file:\n` + + ` wrangler deploy --secrets-file \n` + + `where the file contains lines in the format \`SECRET_NAME=value\` (or JSON).\n` + + `See https://developers.cloudflare.com/workers/configuration/secrets/#secrets-on-deployed-workers for more information.`, + { telemetryMessage: "required secrets missing before first deploy" } + ); + } + + for (const secretName of inheritedSecrets) { + bindings[secretName] = { type: "inherit" }; + } +} + +/** + * Reformats API errors for strict inherit binding validation failures into + * user-friendly messages listing the missing required secrets. + * The API returns all missing inherit bindings at once, each as a separate + * error in response.errors, which maps to individual err.notes entries. + */ +export function handleMissingSecretsError( + err: unknown, + config: Config, + options: SecretsValidationOptions +): void { + if (!(err instanceof APIError) || err.code !== INVALID_INHERIT_BINDING_CODE) { + return; + } + + const missingSecretNames = err.notes + .map((note) => note.text.match(/^inherit binding '(.+?)' is invalid/)) + .filter((match): match is RegExpMatchArray => match !== null) + .map((match) => match[1]) + .filter((secretName) => config.secrets?.required?.includes(secretName)); + + if (missingSecretNames.length > 0) { + err.preventReport(); + const secretPutCommand = `wrangler ${options.type === "deploy" ? "" : "versions "}secret put`; + const secretsFileCommand = `wrangler ${options.type === "deploy" ? "deploy" : "versions upload"} --secrets-file `; + const action = options.type === "deploy" ? "deploying" : "uploading"; + throw new UserError( + `The following required secrets have not been set: ${missingSecretNames.join(", ")}\n` + + `Use \`${secretPutCommand} \` to set secrets before ${action},\n` + + `or supply them when ${action} with \`${secretsFileCommand}\`.\n` + + `See https://developers.cloudflare.com/workers/configuration/secrets/#secrets-on-deployed-workers for more information.`, + { telemetryMessage: "required secrets missing during upload or deploy" } + ); + } +} diff --git a/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts b/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts new file mode 100644 index 0000000..1ca2579 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/sourcemap.ts @@ -0,0 +1,348 @@ +import assert from "node:assert"; +import url from "node:url"; +import { maybeGetFile } from "@cloudflare/workers-shared"; +import { getFreshSourceMapSupport } from "miniflare"; +import { logger } from "../../shared/context"; +import type { Options } from "@cspotcode/source-map-support"; +import type Protocol from "devtools-protocol"; + +export type RetrieveSourceMapFunction = NonNullable< + Options["retrieveSourceMap"] +>; +export function maybeRetrieveFileSourceMap( + filePath?: string +): ReturnType { + if (filePath === undefined) { + return null; + } + const contents = maybeGetFile(filePath); + if (contents === undefined) { + return null; + } + + // Find the last source mapping URL if any + const mapRegexp = /# sourceMappingURL=(.+)/g; + const matches = [...contents.matchAll(mapRegexp)]; + // If we couldn't find a source mapping URL, there's nothing we can do + if (matches.length === 0) { + return null; + } + const mapMatch = matches[matches.length - 1]; + + // Get the source map + const fileUrl = url.pathToFileURL(filePath); + const mapUrl = new URL(mapMatch[1], fileUrl); + if ( + mapUrl.protocol === "data:" && + mapUrl.pathname.startsWith("application/json;base64,") + ) { + // sourceMappingURL=data:application/json;base64,ew... + const base64 = mapUrl.href.substring(mapUrl.href.indexOf(",") + 1); + const map = Buffer.from(base64, "base64").toString(); + return { map, url: fileUrl.href }; + } else { + const map = maybeGetFile(mapUrl); + if (map === undefined) { + return null; + } + return { map, url: mapUrl.href }; + } +} + +// `sourceMappingPrepareStackTrace` is initialised on the first call to +// `getSourceMappingPrepareStackTrace()`. Subsequent calls to +// `getSourceMappingPrepareStackTrace()` will not update it. We'd like to be +// able to customise source map retrieval on each call though. Therefore, we +// make `retrieveSourceMapOverride` a module level variable, so +// `sourceMappingPrepareStackTrace` always has access to the latest override. +let sourceMappingPrepareStackTrace: typeof Error.prepareStackTrace; +let retrieveSourceMapOverride: RetrieveSourceMapFunction | undefined; + +function getSourceMappingPrepareStackTrace( + retrieveSourceMap?: RetrieveSourceMapFunction +): NonNullable { + // Source mapping is synchronous, so setting a module level variable is fine + retrieveSourceMapOverride = retrieveSourceMap; + // If we already have a source mapper, return it + if (sourceMappingPrepareStackTrace !== undefined) { + return sourceMappingPrepareStackTrace; + } + + const support: typeof import("@cspotcode/source-map-support") = + getFreshSourceMapSupport(); + const originalPrepareStackTrace = Error.prepareStackTrace; + support.install({ + environment: "node", + // Don't add Node `uncaughtException` handler + handleUncaughtExceptions: false, + // Don't hook Node `require` function + hookRequire: false, + redirectConflictingLibrary: false, + // Make sure we're using fresh copies of files each time we source map + emptyCacheBetweenOperations: true, + // Allow retriever to be overridden at prepare stack trace time + retrieveSourceMap(path) { + return retrieveSourceMapOverride?.(path) ?? null; + }, + }); + sourceMappingPrepareStackTrace = Error.prepareStackTrace; + assert(sourceMappingPrepareStackTrace !== undefined); + Error.prepareStackTrace = originalPrepareStackTrace; + + return sourceMappingPrepareStackTrace; +} + +export function getSourceMappedStack( + details: Protocol.Runtime.ExceptionDetails +): string { + const description = details.exception?.description ?? ""; + const callFrames = details.stackTrace?.callFrames; + // If this exception didn't come with `callFrames`, we can't do any source + // mapping without parsing the stack, so just return the description as is + if (callFrames === undefined) { + return description; + } + + const nameMessage = details.exception?.description?.split("\n")[0] ?? ""; + const colonIndex = nameMessage.indexOf(":"); + const error = new Error(nameMessage.substring(colonIndex + 2)); + error.name = nameMessage.substring(0, colonIndex); + const callSites = callFrames.map(callFrameToCallSite); + return getSourceMappingPrepareStackTrace()(error, callSites); +} + +function callFrameToCallSite(frame: Protocol.Runtime.CallFrame): CallSite { + return new CallSite({ + typeName: null, + functionName: frame.functionName, + methodName: null, + fileName: frame.url, + lineNumber: frame.lineNumber + 1, + columnNumber: frame.columnNumber + 1, + native: false, + }); +} + +/** + * Calls `prepareStack` and returns `null` if it throws (e.g. when a truncated + * stderr chunk produces an invalid column number). Returning `null` lets + * `getSourceMappedString` fall back to the original string without executing + * the replacement loop against a partially-computed result. + */ +function tryPrepareStack( + prepareStack: ReturnType, + error: Error, + callSites: NodeJS.CallSite[] +): string | null { + try { + return prepareStack(error, callSites); + } catch (err) { + logger?.debug( + `Source map application failed, falling back to original stack trace: ${err}` + ); + return null; + } +} + +const placeholderError = new Error(); +export function getSourceMappedString( + value: string, + retrieveSourceMap?: RetrieveSourceMapFunction +): string { + // We could use `.replace()` here with a function replacer, but + // `getSourceMappingPrepareStackTrace()` clears its source map caches between + // operations. It's likely call sites in this `value` will share source maps, + // so instead we find all call sites, source map them together, then replace. + // Note this still works if there are multiple instances of the same call site + // (e.g. stack overflow error), as the final `.replace()`s will only replace + // the first instance. If they replace the value with itself, all instances + // of the call site would've been replaced with the same thing. + const callSiteLines = Array.from(value.matchAll(CALL_SITE_REGEXP)); + const callSites = callSiteLines.map(lineMatchToCallSite); + const prepareStack = getSourceMappingPrepareStackTrace(retrieveSourceMap); + const sourceMappedStackTrace = tryPrepareStack( + prepareStack, + placeholderError, + callSites + ); + if (sourceMappedStackTrace === null) { + return value; + } + const sourceMappedCallSiteLines = sourceMappedStackTrace.split("\n").slice(1); + + for (let i = 0; i < callSiteLines.length; i++) { + // If a call site doesn't have a file name, it's likely invalid, so don't + // apply source mapping (see cloudflare/workers-sdk#4668) + if (callSites[i].getFileName() === null) { + continue; + } + + const callSiteLine = callSiteLines[i][0]; + const callSiteAtIndex = callSiteLine.indexOf("at"); + assert(callSiteAtIndex !== -1); // Matched against `CALL_SITE_REGEXP` + const callSiteLineLeftPad = callSiteLine.substring(0, callSiteAtIndex); + value = value.replace( + callSiteLine, + callSiteLineLeftPad + sourceMappedCallSiteLines[i].trimStart() + ); + } + return value; +} + +// Adapted from `node-stack-trace`: +/*! + * Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +const CALL_SITE_REGEXP = + // Validation errors from `wrangler deploy` have a 2 space indent, whereas + // regular stack traces have a 4 space indent. + // eslint-disable-next-line no-control-regex -- Intentionally matches ANSI escape sequences in stack traces + /^(?:\s+(?:\x1B\[\d+m)?'?)? {2,4}at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/gm; +function lineMatchToCallSite(lineMatch: RegExpMatchArray): CallSite { + let object: string | null = null; + let method: string | null = null; + let functionName: string | null = null; + let typeName: string | null = null; + let methodName: string | null = null; + const isNative = lineMatch[5] === "native"; + + if (lineMatch[1]) { + functionName = lineMatch[1]; + let methodStart = functionName.lastIndexOf("."); + if (functionName[methodStart - 1] == ".") { + methodStart--; + } + if (methodStart > 0) { + object = functionName.substring(0, methodStart); + method = functionName.substring(methodStart + 1); + const objectEnd = object.indexOf(".Module"); + if (objectEnd > 0) { + functionName = functionName.substring(objectEnd + 1); + object = object.substring(0, objectEnd); + } + } + } + + if (method) { + typeName = object; + methodName = method; + } + + if (method === "") { + methodName = null; + functionName = null; + } + + return new CallSite({ + typeName, + functionName, + methodName, + fileName: lineMatch[2], + lineNumber: parseInt(lineMatch[3]) || null, + columnNumber: parseInt(lineMatch[4]) || null, + native: isNative, + }); +} + +interface CallSiteOptions { + typeName: string | null; + functionName: string | null; + methodName: string | null; + fileName: string; + lineNumber: number | null; + columnNumber: number | null; + native: boolean; +} + +// https://v8.dev/docs/stack-trace-api#customizing-stack-traces +// This class supports the subset of options implemented by `node-stack-trace`: +// https://github.com/felixge/node-stack-trace/blob/4c41a4526e74470179b3b6dd5d75191ca8c56c17/index.js +class CallSite implements NodeJS.CallSite { + constructor(private readonly opts: CallSiteOptions) {} + getScriptHash(): string { + throw new Error("Method not implemented."); + } + getEnclosingColumnNumber(): number { + throw new Error("Method not implemented."); + } + getEnclosingLineNumber(): number { + throw new Error("Method not implemented."); + } + getPosition(): number { + throw new Error("Method not implemented."); + } + getThis(): unknown { + return null; + } + getTypeName(): string | null { + return this.opts.typeName; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- V8 CallSite interface requires Function return type + getFunction(): Function | undefined { + return undefined; + } + getFunctionName(): string | null { + return this.opts.functionName; + } + getMethodName(): string | null { + return this.opts.methodName; + } + getFileName(): string | null { + return this.opts.fileName ?? null; + } + getScriptNameOrSourceURL(): string { + return this.opts.fileName; + } + getLineNumber(): number | null { + return this.opts.lineNumber; + } + getColumnNumber(): number | null { + return this.opts.columnNumber; + } + getEvalOrigin(): string | undefined { + return undefined; + } + isToplevel(): boolean { + return false; + } + isEval(): boolean { + return false; + } + isNative(): boolean { + return this.opts.native; + } + isConstructor(): boolean { + return false; + } + isAsync(): boolean { + return false; + } + isPromiseAll(): boolean { + return false; + } + isPromiseAny(): boolean { + return false; + } + getPromiseIndex(): number | null { + return null; + } +} diff --git a/packages/deploy-helpers/src/deploy/helpers/validate-routes.ts b/packages/deploy-helpers/src/deploy/helpers/validate-routes.ts new file mode 100644 index 0000000..56a7c5e --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/validate-routes.ts @@ -0,0 +1,71 @@ +import path from "node:path"; +import { UserError } from "@cloudflare/workers-utils"; +import { logger } from "../../shared/context"; +import type { AssetsOptions, Route } from "@cloudflare/workers-utils"; + +export const validateRoutes = ( + routes: Route[], + assets: AssetsOptions | undefined +) => { + const invalidRoutes: Record = {}; + const mountedAssetRoutes: string[] = []; + + for (const route of routes) { + if (typeof route !== "string" && route.custom_domain) { + if (route.pattern.includes("*")) { + invalidRoutes[route.pattern] ??= []; + invalidRoutes[route.pattern].push( + `Wildcard operators (*) are not allowed in Custom Domains` + ); + } + if (route.pattern.includes("/")) { + invalidRoutes[route.pattern] ??= []; + invalidRoutes[route.pattern].push( + `Paths are not allowed in Custom Domains` + ); + } + } else if ( + // If we have Assets but we're not always hitting the Worker then validate + assets?.directory !== undefined && + assets.routerConfig.invoke_user_worker_ahead_of_assets !== true + ) { + const pattern = typeof route === "string" ? route : route.pattern; + const components = pattern.split("/"); + + // If this isn't `domain.com/*` then we're mounting to a path + if (!(components.length === 2 && components[1] === "*")) { + mountedAssetRoutes.push(pattern); + } + } + } + if (Object.keys(invalidRoutes).length > 0) { + throw new UserError( + `Invalid Routes:\n` + + Object.entries(invalidRoutes) + .map(([route, errors]) => `${route}:\n` + errors.join("\n")) + .join(`\n\n`), + { telemetryMessage: "deploy invalid routes" } + ); + } + + if (mountedAssetRoutes.length > 0 && assets?.directory !== undefined) { + const relativeAssetsDir = path.relative(process.cwd(), assets.directory); + + const warnFn = logger.once?.warn ?? logger.warn; + warnFn( + `Warning: The following routes will attempt to serve Assets on a configured path:\n${mountedAssetRoutes + .map((route) => { + const routeNoScheme = route.replace(/https?:\/\//g, ""); + const assetPath = path.join( + relativeAssetsDir, + routeNoScheme.substring(routeNoScheme.indexOf("/")) + ); + return ` • ${route} (Will match assets: ${assetPath})`; + }) + .join("\n")}` + + (assets?.routerConfig.has_user_worker + ? "\n\nRequests not matching an asset will be forwarded to the Worker's code." + : "") + ); + } +}; diff --git a/packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts b/packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts new file mode 100644 index 0000000..4a02659 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/validate-worker-props.ts @@ -0,0 +1,309 @@ +import assert from "node:assert"; +import { + configFileName, + experimental_patchConfig, + formatConfigSnippet, + getTodaysCompatDate, + UserError, +} from "@cloudflare/workers-utils"; +import { + confirm, + fetchResult, + isNonInteractiveOrCI, + logger, +} from "../../shared/context"; +import { ensureQueuesExistByConfig } from "../../triggers/queue-consumers"; +import { checkRemoteSecretsOverride } from "./check-remote-secrets-override"; +import { checkWorkflowConflicts } from "./check-workflow-conflicts"; +import { getConfigPatch, getRemoteConfigDiff } from "./config-diffs"; +import { getDeployConfirmFunction } from "./deploy-confirm"; +import { downloadWorkerConfig } from "./download-worker-config"; +import { verifyWorkerMatchesCITag } from "./match-tag"; +import { validateRoutes } from "./validate-routes"; +import { isWorkerNotFoundError } from "./worker-not-found-error"; +import type { DeployProps, VersionsUploadProps } from "../../shared/types"; +import type { + AssetsOptions, + Config, + RawConfig, +} from "@cloudflare/workers-utils"; + +/** + * + * Any validation of props (merged args and config) that does not require API calls + * should go here, and NOT inline in deploy() or versionsUpload() + * + * The order should be: + * 1. generic validation checks + * 2. deploy or versions upload specific checks + */ +type ValidateWorkerPropsInput = + | (DeployProps & { assetsOptions?: AssetsOptions }) + | VersionsUploadProps; + +export function validateWorkerProps( + props: T, + config: Config +): T & { name: string } { + const { name, compatibilityDate } = props; + const { format } = props.entry; + if (!name) { + throw new UserError( + `You need to provide the name of your worker. Either pass it as a cli arg with --name or in your config file as ${formatConfigSnippet({ name: "" }, config.userConfigPath)}`, + { + telemetryMessage: + props.command === "deploy" + ? "deploy command missing worker name" + : "versions upload missing worker name", + } + ); + } + + if (!compatibilityDate) { + const compatibilityDateStr = getTodaysCompatDate(); + throw new UserError( + `A compatibility_date is required when uploading a Worker. Add the following to your ${configFileName(config.configPath)} file: + \`\`\` + ${formatConfigSnippet({ compatibility_date: compatibilityDateStr }, config.configPath, false)} + \`\`\` + Or you could pass it in your terminal as \`--compatibility-date ${compatibilityDateStr}\` +See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information.`, + { + telemetryMessage: + props.command === "deploy" + ? "missing compatibility date when deploying" + : "versions upload missing compatibility date", + } + ); + } + + if (config.wasm_modules && format === "modules") { + throw new UserError( + "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code", + { telemetryMessage: "wasm_modules with es module worker" } + ); + } + + if (config.text_blobs && format === "modules") { + throw new UserError( + `You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[rules]\` in your ${configFileName(config.configPath)} file`, + { telemetryMessage: "text_blobs with es module worker" } + ); + } + + if (config.data_blobs && format === "modules") { + throw new UserError( + `You cannot configure [data_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[rules]\` in your ${configFileName(config.configPath)} file`, + { telemetryMessage: "data_blobs with es module worker" } + ); + } + + if (props.command === "deploy") { + validateRoutes(props.routes, props.assetsOptions); + assert( + !config.site || config.site.bucket, + "A [site] definition requires a `bucket` field with a path to the site's assets directory." + ); + if ( + !props.isWorkersSite && + Boolean(props.legacyAssetPaths) && + format === "service-worker" + ) { + throw new UserError( + "You cannot use the service-worker format with an `assets` directory yet. For information on how to migrate to the module-worker format, see: https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/", + { telemetryMessage: "deploy service worker assets unsupported" } + ); + } + } else { + if (config.containers && config.containers.length > 0) { + logger.warn( + `Your Worker has Containers configured. Container configuration changes (such as image, max_instances, etc.) will not be gradually rolled out with versions. These changes will only take effect after running \`deploy\`.` + ); + } + } + + return { ...props, name }; +} + +export type PreUploadApiChecksResult = { + workerTag: string | null; + tags: string[]; + workerExists: boolean; + aborted: boolean; +}; + +/** + * + * Any validation that requires API calls should go here. + * This is skipped on dry runs (for now) + */ +export async function preUploadApiChecks( + props: DeployProps | VersionsUploadProps, + config: Config +): Promise { + const { accountId, name } = props; + + if (props.dryRun || !accountId || !name) { + return { + workerTag: null, + tags: [], + workerExists: true, + aborted: false, + }; + } + + await verifyWorkerMatchesCITag(config, accountId, name, config.configPath); + + const deployConfirm = getDeployConfirmFunction({ + strictMode: props.strict, + }); + + // TODO: warn if git/hg has uncommitted changes + let workerTag: string | null = null; + let tags: string[] = []; // arbitrary metadata tags, not to be confused with script tag or annotations + let workerExists = true; + + // Skip the service metadata fetch for dispatch namespace deploys (Workers for Platforms). + // Dispatch namespace scripts don't have standard service metadata. + const skipMetadataFetch = + props.command === "deploy" && !!props.dispatchNamespace; + + if (!skipMetadataFetch) { + try { + const serviceMetaData = await fetchResult<{ + default_environment: { + environment: string; + script: { + tag: string; + tags: string[] | null; + last_deployed_from: "dash" | "api"; + }; + }; + }>(config, `/accounts/${accountId}/workers/services/${name}`); + const { + default_environment: { script }, + } = serviceMetaData; + workerTag = script.tag; + tags = script.tags ?? tags; + + // This non-interactive deploy could not prove the local project owns + // the target Worker name (no config file names it, and the name was + // either generated automatically or carried across by the + // Pages-to-Workers delegation). A same-named remote Worker is a + // probable collision rather than an intended redeploy, so abort instead + // of silently overwriting it. Reuses the service-metadata fetch above; + // the catch below only swallows not-found errors, so this UserError + // propagates. + if (props.command === "deploy" && props.failIfWorkerNameTaken) { + throw new UserError( + `A Worker named "${name}" already exists in your account. This deploy could not confirm that it should update that Worker, so it stopped instead of overwriting it. To update the existing Worker, add a Wrangler configuration file naming "${name}"; to deploy separately, use a different name.`, + { telemetryMessage: "deploy unverified worker name already exists" } + ); + } + + if (script.last_deployed_from === "dash") { + const remoteWorkerConfig = await downloadWorkerConfig( + name, + serviceMetaData.default_environment.environment, + props.entry.file, + accountId + ); + + const configForDiff = + props.command === "deploy" + ? { + ...config, + // We also want to include all the routes used for deployment + routes: props.routes, + } + : config; + + const configDiff = getRemoteConfigDiff( + remoteWorkerConfig, + configForDiff + ); + + // If there are only additive changes (or no changes at all) there should be no problem, + // just using the local config (and override the remote one) should be totally fine + if (!configDiff.nonDestructive) { + logger.warn( + "The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard:" + + `\n${configDiff.diff}\n\n` + + "Uploading the Worker will override the remote configuration with your local one." + ); + if (!(await deployConfirm("Would you like to continue?"))) { + if ( + // only patch if json/jsonc + config.userConfigPath && + /\.jsonc?$/.test(config.userConfigPath) && + // skip the patch if we're in non-interactive mode and strict + !(props.strict && isNonInteractiveOrCI()) && + (await confirm( + "Would you like to update the local config file with the remote values?", + { + defaultValue: true, + fallbackValue: true, + } + )) + ) { + const patchObj: RawConfig = getConfigPatch( + configDiff.diff, + props.env + ); + + experimental_patchConfig(config.userConfigPath, patchObj, false); + } + + return { workerTag, tags, workerExists, aborted: true }; + } + } + } else if (script.last_deployed_from === "api") { + logger.warn( + `You are about to upload a Worker that was last updated via the script API.\nEdits that have been made via the script API will be overridden by your local code and config.` + ); + if (!(await deployConfirm("Would you like to continue?"))) { + return { workerTag, tags, workerExists, aborted: true }; + } + } + } catch (e) { + if (isWorkerNotFoundError(e)) { + if (props.command === "versions upload") { + throw new UserError( + "You cannot upload a new version of a Worker that does not yet exist. Please run the `deploy` command first.", + { telemetryMessage: "versions upload worker not found" } + ); + } + workerExists = false; + } else { + throw e; + } + } + } + + const remoteSecretsCheck = await checkRemoteSecretsOverride( + config, + name, + accountId + ); + + if (remoteSecretsCheck?.override) { + logger.warn(remoteSecretsCheck.deployErrorMessage); + if (!(await deployConfirm("Would you like to continue?"))) { + return { workerTag, tags, workerExists, aborted: true }; + } + } + + if (config.workflows?.length) { + const workflowCheck = await checkWorkflowConflicts(config, accountId, name); + + if (workflowCheck.hasConflicts) { + logger.warn(workflowCheck.message); + if (!(await deployConfirm("Do you want to continue?"))) { + return { workerTag, tags, workerExists, aborted: true }; + } + } + } + + await ensureQueuesExistByConfig(config, accountId); + return { workerTag, tags, workerExists, aborted: false }; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/versions-api.ts b/packages/deploy-helpers/src/deploy/helpers/versions-api.ts new file mode 100644 index 0000000..4c7ad7c --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/versions-api.ts @@ -0,0 +1,187 @@ +import { fetchResult } from "../../shared/context"; +import type { + ApiDeployment, + ApiVersion, + Percentage, + VersionCache, + VersionId, +} from "./versions-types"; +import type { + ComplianceConfig, + Observability, + StreamingTailConsumer, + TailConsumer, +} from "@cloudflare/workers-utils"; + +export async function fetchVersion( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + versionId: VersionId, + versionCache: VersionCache | undefined +): Promise { + const cachedVersion = versionCache?.get(versionId); + if (cachedVersion) { + return cachedVersion; + } + + const version = await fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/scripts/${workerName}/versions/${versionId}` + ); + + versionCache?.set(version.id, version); + + return version; +} + +export async function fetchVersions( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + versionCache: VersionCache | undefined, + versionIds: VersionId[] +) { + return Promise.all( + versionIds.map((versionId) => + fetchVersion( + complianceConfig, + accountId, + workerName, + versionId, + versionCache + ) + ) + ); +} + +export async function fetchLatestDeployments( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string +): Promise { + const { deployments } = await fetchResult<{ + deployments: ApiDeployment[]; + }>( + complianceConfig, + `/accounts/${accountId}/workers/scripts/${workerName}/deployments` + ); + + return deployments; +} +export async function fetchLatestDeployment( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string +): Promise { + const deployments = await fetchLatestDeployments( + complianceConfig, + accountId, + workerName + ); + + return deployments.at(0); +} + +export async function fetchDeploymentVersions( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + deployment: ApiDeployment | undefined, + versionCache: VersionCache +): Promise<[ApiVersion[], Map]> { + if (!deployment) { + return [[], new Map()]; + } + + const versionTraffic = new Map( + deployment.versions.map((v) => [v.version_id, v.percentage]) + ); + + const versions = await fetchVersions( + complianceConfig, + accountId, + workerName, + versionCache, + [...versionTraffic.keys()] + ); + + return [versions, versionTraffic]; +} + +export async function fetchDeployableVersions( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + versionCache: VersionCache +): Promise { + const { items: versions } = await fetchResult<{ + items: ApiVersion[]; + }>( + complianceConfig, + `/accounts/${accountId}/workers/scripts/${workerName}/versions?deployable=true` + ); + + for (const version of versions) { + versionCache.set(version.id, version); + } + + return versions; +} + +export async function createDeployment( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + versionTraffic: Map, + message: string | undefined, + force: boolean | undefined +) { + return await fetchResult<{ id: string }>( + complianceConfig, + `/accounts/${accountId}/workers/scripts/${workerName}/deployments${force ? "?force=true" : ""}`, + + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + strategy: "percentage", + versions: Array.from(versionTraffic).map( + ([version_id, percentage]) => ({ version_id, percentage }) + ), + annotations: { + "workers/message": message, + }, + }), + } + ); +} + +export type NonVersionedScriptSettings = { + logpush: boolean; + tags: string[] | null; + tail_consumers: TailConsumer[]; + streaming_tail_consumers: StreamingTailConsumer[]; + observability: Observability; +}; + +export async function patchNonVersionedScriptSettings( + complianceConfig: ComplianceConfig, + accountId: string, + workerName: string, + settings: Partial +) { + const res = await fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/scripts/${workerName}/script-settings`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(settings), + } + ); + + // TODO: handle specific errors + + return res; +} diff --git a/packages/deploy-helpers/src/deploy/helpers/versions-types.ts b/packages/deploy-helpers/src/deploy/helpers/versions-types.ts new file mode 100644 index 0000000..2ad2dce --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/versions-types.ts @@ -0,0 +1,55 @@ +import type { + CfUserLimits, + WorkerMetadataBinding, +} from "@cloudflare/workers-utils"; + +export type Percentage = number; +type UUID = string; +export type VersionId = UUID; + +export type ApiDeployment = { + id: string; + source: "api" | string; + strategy: "percentage" | string; + author_email: string; + annotations?: Record; + created_on: string; + versions: Array<{ + version_id: VersionId; + percentage: Percentage; + }>; +}; +export type ApiVersion = { + id: VersionId; + number: number; + metadata: { + created_on: string; + modified_on: string; + source: "api" | string; + author_id: string; + author_email: string; + }; + annotations?: { + "workers/triggered_by"?: "upload" | string; + "workers/message"?: string; + "workers/tag"?: string; + }; + resources: { + bindings: WorkerMetadataBinding[]; + script: { + etag: string; + handlers: string[] | null; + placement_mode?: "smart"; + last_deployed_from: string; + }; + script_runtime: { + compatibility_date?: string; + compatibility_flags?: string[]; + usage_model: "bundled" | "unbound" | "standard"; + limits: CfUserLimits; + }; + }; + startup_time_ms?: number; +}; + +export type VersionCache = Map; diff --git a/packages/deploy-helpers/src/deploy/helpers/worker-not-found-error.ts b/packages/deploy-helpers/src/deploy/helpers/worker-not-found-error.ts new file mode 100644 index 0000000..dacb1e5 --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/worker-not-found-error.ts @@ -0,0 +1,31 @@ +/** + This is the error code from the Cloudflare API signaling that a worker could not be found on the target account + */ +export const WORKER_NOT_FOUND_ERR_CODE = 10007 as const; + +/** + This is the error code from the Cloudflare API signaling that a worker environment (legacy) could not be found on the target account + */ +export const WORKER_LEGACY_ENVIRONMENT_NOT_FOUND_ERR_CODE = 10090 as const; + +/** + This is the error message from the Cloudflare API signaling that a worker could not be found on the target account + */ +export const workerNotFoundErrorMessage = + "This Worker does not exist on your account."; + +/** + * Given an error from the Cloudflare API discerns whether it is caused by a worker that could not be found on the target account + * + * @param error The error object + * @returns true if the object represents an error from the Cloudflare API caused by a not found worker, false otherwise + */ +export function isWorkerNotFoundError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + (error.code === WORKER_NOT_FOUND_ERR_CODE || + error.code === WORKER_LEGACY_ENVIRONMENT_NOT_FOUND_ERR_CODE) + ); +} diff --git a/packages/deploy-helpers/src/deploy/helpers/workers-sites-bindings.ts b/packages/deploy-helpers/src/deploy/helpers/workers-sites-bindings.ts new file mode 100644 index 0000000..b27c65c --- /dev/null +++ b/packages/deploy-helpers/src/deploy/helpers/workers-sites-bindings.ts @@ -0,0 +1,32 @@ +import type { Binding, CfScriptFormat } from "@cloudflare/workers-utils"; + +/** + * Inject bindings into the Worker to support Workers Sites. These are injected at the last minute so that + * they don't display in the output of `printBindings()` + */ +export function addWorkersSitesBindings( + bindings: Record, + namespace: string | undefined, + manifest: + | { + [filePath: string]: string; + } + | undefined, + format: CfScriptFormat +) { + const withSites = { ...bindings }; + if (namespace) { + withSites["__STATIC_CONTENT"] = { + type: "kv_namespace", + id: namespace, + }; + } + + if (manifest && format === "service-worker") { + withSites["__STATIC_CONTENT_MANIFEST"] = { + type: "text_blob", + source: { contents: "__STATIC_CONTENT_MANIFEST" }, + }; + } + return withSites; +} diff --git a/packages/deploy-helpers/src/deploy/versions-upload.ts b/packages/deploy-helpers/src/deploy/versions-upload.ts new file mode 100644 index 0000000..70c78be --- /dev/null +++ b/packages/deploy-helpers/src/deploy/versions-upload.ts @@ -0,0 +1,434 @@ +import assert from "node:assert"; +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { blue, gray } from "@cloudflare/cli-shared-helpers/colors"; +import { + APIError, + formatTime, + ParseError, + retryOnAPIFailure, + UserError, +} from "@cloudflare/workers-utils"; +import { Response } from "undici"; +import { fetchResult, logger } from "../shared/context"; +import { getWorkersDevSubdomain } from "../triggers/subdomain"; +import { resolveAssetOptions, syncAssets } from "./helpers/assets"; +import { renderBindingDependsOnExportError } from "./helpers/binding-depends-on-export"; +import { getBindings } from "./helpers/binding-utils"; +import { printBundleSize } from "./helpers/bundle-reporter"; +import { createWorkerUploadForm } from "./helpers/create-worker-upload-form"; +import { + applyServiceAndEnvironmentTags, + tagsAreEqual, + warnOnErrorUpdatingServiceAndEnvironmentTags, +} from "./helpers/environments"; +import { ACTOR_BINDING_DEPENDS_ON_EXPORT_CODE } from "./helpers/error-codes"; +import { resolveExportsUploadPayload } from "./helpers/exports"; +import { helpIfErrorIsSizeOrScriptStartup } from "./helpers/friendly-validator-errors"; +import { collectPackageDependencies } from "./helpers/package-dependencies"; +import { parseBulkInputToObject } from "./helpers/parse-bulk-input"; +import { parseConfigPlacement } from "./helpers/placement"; +import { printBindings } from "./helpers/print-bindings"; +import { provisionBindings } from "./helpers/provision-bindings"; +import { + addRequiredSecretsInheritBindings, + handleMissingSecretsError, +} from "./helpers/secrets-validation"; +import { + getSourceMappedString, + maybeRetrieveFileSourceMap, +} from "./helpers/sourcemap"; +import { + preUploadApiChecks, + validateWorkerProps, +} from "./helpers/validate-worker-props"; +import { patchNonVersionedScriptSettings } from "./helpers/versions-api"; +import type { VersionsUploadProps, WorkerBuildResult } from "../shared/types"; +import type { DeployCallbacks } from "./deploy"; +import type { AssetUploadStats } from "./helpers/assets"; +import type { RetrieveSourceMapFunction } from "./helpers/sourcemap"; +import type { CfWorkerInit, Config } from "@cloudflare/workers-utils"; +import type { FormData } from "undici"; + +export type VersionsUploadCallbacks = Pick; + +export default async function versionsUpload( + props: VersionsUploadProps, + config: Config, + buildResult: WorkerBuildResult, + callbacks: VersionsUploadCallbacks +): Promise<{ + versionId: string | null; + workerTag: string | null; + assetUploadStats?: AssetUploadStats; + versionPreviewUrl?: string | undefined; + versionPreviewAliasUrl?: string | undefined; +}> { + const { entry, compatibilityDate, compatibilityFlags, keepVars, accountId } = + props; + + const assetsOptions = resolveAssetOptions(props, config); + + // Any validation that does not require API calls should go in validateWorkerProps() + const { name } = validateWorkerProps(props, config); + + // any validation that DOES require API calls should go in preUploadApiChecks() + const { workerTag, tags, aborted } = await preUploadApiChecks(props, config); + if (aborted) { + return { versionId: null, workerTag }; + } + + let versionId: string | null = null; + const scriptName = name; + + const start = Date.now(); + const workerName = scriptName; + const workerUrl = `/accounts/${accountId}/workers/scripts/${scriptName}`; + + const projectRoot = entry.projectRoot; + + let hasPreview = false; + + const { + modules, + dependencies, + resolvedEntryPointPath, + bundleType, + content, + sourceMaps, + } = buildResult; + const bindings = getBindings(config); + + // Vars from the CLI (--var) are hidden so their values aren't logged to the terminal + for (const [bindingName, value] of Object.entries(props.cliVars)) { + bindings[bindingName] = { + type: "plain_text", + value, + hidden: true, + }; + } + + // Resolve which Durable Object lifecycle payload to forward — either + // the legacy `migrations` steps or the declarative `exports` map. The + // server's versions POST controller persists `exports` on the new + // script_version row with `SkipDeploy:true`, so reconciliation is + // deferred to the subsequent deploy (either `wrangler deploy` or + // `wrangler versions deploy `). + const { migrations, exports } = await resolveExportsUploadPayload({ + scriptName, + isDryRun: props.dryRun, + accountId, + config, + dispatchNamespace: undefined, + }); + + // Upload assets if assets is being used + const assetsUploadResult = + assetsOptions && !props.dryRun + ? await syncAssets(config, accountId, assetsOptions.directory, scriptName) + : undefined; + const assetsJwt = assetsUploadResult?.jwt; + + if (props.secretsFile) { + const secretsResult = await parseBulkInputToObject(props.secretsFile); + if (secretsResult) { + for (const [secretName, secretValue] of Object.entries( + secretsResult.content + )) { + bindings[secretName] = { + type: "secret_text", + value: secretValue, + }; + } + } + } + + addRequiredSecretsInheritBindings(config, bindings, { type: "upload" }); + + const placement = parseConfigPlacement(config); + + const entryPointName = path.basename(resolvedEntryPointPath); + const main = { + name: entryPointName, + filePath: resolvedEntryPointPath, + content: content, + type: bundleType, + }; + const worker: CfWorkerInit = { + name: scriptName, + main, + migrations, + exports, + modules, + containers: config.containers, + sourceMaps, + compatibility_date: compatibilityDate, + compatibility_flags: compatibilityFlags, + keepVars, + // we never delete secret bindings when uploading, even if we are setting secrets from a file + // so inherit all unchanged secrets from the previous Worker Version + keepSecrets: true, + placement, + tail_consumers: config.tail_consumers, + limits: config.limits, + annotations: { + "workers/message": props.message, + "workers/tag": props.tag, + "workers/alias": props.previewAlias, + }, + assets: + assetsOptions && assetsJwt + ? { + jwt: assetsJwt, + routerConfig: assetsOptions.routerConfig, + assetConfig: assetsOptions.assetConfig, + _redirects: assetsOptions._redirects, + _headers: assetsOptions._headers, + run_worker_first: assetsOptions.run_worker_first, + } + : undefined, + logpush: undefined, // logpush and observability are non-versioned settings + observability: undefined, + cache: config.cache, // cache is a versioned setting + package_dependencies: + config.dependencies_instrumentation?.enabled !== false && projectRoot + ? await collectPackageDependencies(projectRoot) + : undefined, + }; + + await printBundleSize( + { name: path.basename(resolvedEntryPointPath), content: content }, + modules + ); + + let workerBundle: FormData; + + if (props.dryRun) { + workerBundle = createWorkerUploadForm(worker, bindings, { + dryRun: true, + unsafe: config.unsafe, + }); + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + undefined, + { unsafeMetadata: config.unsafe?.metadata } + ); + } else { + assert(accountId, "Missing accountId"); + if (assetsOptions?.routerConfig.has_user_worker === false) { + logger.debug("skipping provisioning on assets-only project"); + } else if (props.resourcesProvision) { + await provisionBindings( + bindings, + accountId, + scriptName, + props.experimentalAutoCreate, + config, + { + skipConfigWriteback: props.skipProvisioningConfigWriteback, + } + ); + } + workerBundle = createWorkerUploadForm(worker, bindings, { + unsafe: config.unsafe, + }); + + let bindingsPrinted = false; + + // Upload the version. + try { + const result = await retryOnAPIFailure( + async () => + fetchResult<{ + id: string; + startup_time_ms: number; + metadata: { + has_preview: boolean; + }; + }>( + config, + `${workerUrl}/versions`, + { + method: "POST", + body: workerBundle, + headers: props.sendMetrics + ? { metricsEnabled: "true" } + : undefined, + }, + new URLSearchParams({ bindings_inherit: "strict" }) + ), + logger + ); + + logger.log("Worker Startup Time:", result.startup_time_ms, "ms"); + bindingsPrinted = true; + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + undefined, + { unsafeMetadata: config.unsafe?.metadata } + ); + versionId = result.id; + hasPreview = result.metadata.has_preview; + } catch (err) { + if (!bindingsPrinted) { + printBindings( + bindings, + config.tail_consumers, + config.streaming_tail_consumers, + undefined, + { unsafeMetadata: config.unsafe?.metadata } + ); + } + + // A binding references a DO class declared in `exports` but not yet + // provisioned. Declarative `exports` reconcile at deploy time, so + // the namespace can't exist when the version is merely uploaded. + // EWC's message already spells out the remediation, so surface it + // verbatim (stripping the trailing ` [code: N]` the cfetch layer + // appended) rather than the generic upload failure. + if ( + err instanceof APIError && + err.code === ACTOR_BINDING_DEPENDS_ON_EXPORT_CODE + ) { + err.preventReport(); + const serverMessage = + err.notes[0]?.text + .replace(` [code: ${ACTOR_BINDING_DEPENDS_ON_EXPORT_CODE}]`, "") + .trim() ?? ""; + throw new UserError(renderBindingDependsOnExportError(serverMessage), { + telemetryMessage: + "versions upload binding depends on unprovisioned export", + }); + } + + const message = await helpIfErrorIsSizeOrScriptStartup( + err, + dependencies, + workerBundle, + projectRoot, + callbacks.analyseBundle + ); + if (message) { + logger.error(message); + } + + handleMissingSecretsError(err, config, { type: "upload" }); + + // Apply source mapping to validation startup errors if possible + if ( + err instanceof ParseError && + "code" in err && + err.code === 10021 /* validation error */ && + err.notes.length > 0 + ) { + const maybeNameToFilePath = (moduleName: string) => { + // If this is a service worker, always return the entrypoint path. + // Service workers can't have additional JavaScript modules. + if (bundleType === "commonjs") { + return resolvedEntryPointPath; + } + // Similarly, if the name matches the entrypoint, return its path + if (moduleName === entryPointName) { + return resolvedEntryPointPath; + } + // Otherwise, return the file path of the matching module (if any) + for (const module of modules) { + if (moduleName === module.name) { + return module.filePath; + } + } + }; + const retrieveSourceMap: RetrieveSourceMapFunction = (moduleName) => + maybeRetrieveFileSourceMap(maybeNameToFilePath(moduleName)); + + err.notes[0].text = getSourceMappedString( + err.notes[0].text, + retrieveSourceMap + ); + } + + throw err; + } + + // Update service and environment tags when using environments + + const nextTags = applyServiceAndEnvironmentTags(config, tags); + if (!tagsAreEqual(tags, nextTags)) { + try { + await patchNonVersionedScriptSettings(config, accountId, scriptName, { + tags: nextTags, + }); + } catch { + warnOnErrorUpdatingServiceAndEnvironmentTags(); + } + } + } + if (props.outfile) { + // we're using a custom output file, + // so let's first ensure it's parent directory exists + mkdirSync(path.dirname(props.outfile), { recursive: true }); + + const serializedFormData = await new Response(workerBundle).arrayBuffer(); + + writeFileSync(props.outfile, Buffer.from(serializedFormData)); + } + + if (props.dryRun) { + logger.log(`--dry-run: exiting now.`); + return { versionId, workerTag }; + } + assert(accountId); + + const uploadMs = Date.now() - start; + + logger.log("Uploaded", workerName, formatTime(uploadMs)); + logger.log("Worker Version ID:", versionId); + + let versionPreviewUrl: string | undefined = undefined; + let versionPreviewAliasUrl: string | undefined = undefined; + + if (versionId && hasPreview) { + const { previews_enabled: previews_available_on_subdomain } = + await fetchResult<{ + previews_enabled: boolean; + }>(config, `${workerUrl}/subdomain`); + + if (previews_available_on_subdomain) { + const userSubdomain = await getWorkersDevSubdomain(config, accountId, { + configPath: config.configPath, + }); + const shortVersion = versionId.slice(0, 8); + versionPreviewUrl = `https://${shortVersion}-${workerName}.${userSubdomain}`; + logger.log(`Version Preview URL: ${versionPreviewUrl}`); + + if (props.previewAlias) { + versionPreviewAliasUrl = `https://${props.previewAlias}-${workerName}.${userSubdomain}`; + logger.log(`Version Preview Alias URL: ${versionPreviewAliasUrl}`); + } + } + } + + const cmdVersionsDeploy = blue("wrangler versions deploy"); + const cmdTriggersDeploy = blue("wrangler triggers deploy"); + logger.info( + gray(` +To deploy this version to production traffic use the command ${cmdVersionsDeploy} + +Changes to non-versioned settings (config properties 'logpush' or 'tail_consumers') take effect after your next deployment using the command ${cmdVersionsDeploy} + +Changes to triggers (routes, custom domains, cron schedules, etc) must be applied with the command ${cmdTriggersDeploy} +`) + ); + + return { + versionId, + workerTag, + assetUploadStats: assetsUploadResult?.assetUploadStats, + versionPreviewUrl, + versionPreviewAliasUrl, + }; +} diff --git a/packages/deploy-helpers/src/index.ts b/packages/deploy-helpers/src/index.ts new file mode 100644 index 0000000..a79bdd4 --- /dev/null +++ b/packages/deploy-helpers/src/index.ts @@ -0,0 +1,50 @@ +export * from "./shared/types"; +export * from "./config"; +export { initDeployHelpersContext } from "./shared/context"; +export { default as deploy } from "./deploy/deploy"; +export type { DeployCallbacks } from "./deploy/deploy"; +export { default as versionsUpload } from "./deploy/versions-upload"; +export type { VersionsUploadCallbacks } from "./deploy/versions-upload"; +export * from "./triggers/deploy"; +export * from "./triggers/subdomain"; +export * from "./triggers/zones"; +export * from "./triggers/publish-routes"; +export * from "./triggers/queue-consumers"; +export * from "./deploy/helpers/placement"; +export * from "./deploy/helpers/worker-not-found-error"; +export * from "./deploy/helpers/error-codes"; +export * from "./deploy/helpers/secrets-validation"; +export * from "./deploy/helpers/diff-json"; +export * from "./deploy/helpers/config-diffs"; +export * from "./deploy/helpers/parse-bulk-input"; +export * from "./deploy/helpers/binding-utils"; +export * from "./deploy/helpers/capnp"; +export * from "./deploy/helpers/create-worker-upload-form"; +export * from "./deploy/helpers/workers-sites-bindings"; +export * from "./deploy/helpers/deploy-wfp"; +export * from "./deploy/helpers/deploy-confirm"; +export * from "./deploy/helpers/preview-alias"; +export * from "./deploy/helpers/sourcemap"; +export * from "./deploy/helpers/bundle-reporter"; +export * from "./deploy/helpers/node-compat"; +export * from "./deploy/helpers/validate-routes"; +export * from "./deploy/helpers/environments"; +export * from "./deploy/helpers/friendly-validator-errors"; +export * from "./deploy/helpers/versions-types"; +export * from "./deploy/helpers/versions-api"; +export * from "./deploy/helpers/check-workflow-conflicts"; +export * from "./deploy/helpers/download-worker-config"; +export * from "./deploy/helpers/durable"; +export * from "./deploy/helpers/exports"; +export * from "./deploy/helpers/exports-reconciliation"; +export * from "./deploy/helpers/inconsistent-exports"; +export * from "./deploy/helpers/binding-depends-on-export"; +export * from "./deploy/helpers/match-tag"; +export * from "./deploy/helpers/check-remote-secrets-override"; +export * from "./deploy/helpers/confirm-latest-deployment-overwrite"; +export * from "./deploy/helpers/print-bindings"; +export * from "./deploy/helpers/assets"; +export * from "./deploy/helpers/hash"; +export * from "./deploy/helpers/jwt"; +export * from "./deploy/helpers/package-dependencies"; +export * from "./deploy/helpers/provision-bindings"; diff --git a/packages/deploy-helpers/src/shared/context.ts b/packages/deploy-helpers/src/shared/context.ts new file mode 100644 index 0000000..5e3220c --- /dev/null +++ b/packages/deploy-helpers/src/shared/context.ts @@ -0,0 +1,46 @@ +import type { DeployHelpersContext } from "./types"; +import type { + FetchKVGetValueFetcher, + FetchListResultFetcher, + FetchPagedListResultFetcher, + FetchResultFetcher, + Logger, +} from "@cloudflare/workers-utils"; + +/** + * Module-level context globals for deploy-helpers. + * + * These are typed as non-nullable but are undefined until initDeployHelpersContext() + * is called. Consumers must import the live binding directly (e.g. `import { logger }`) + * and NOT destructure or cache the value at module-load time, otherwise they will + * capture `undefined` before init runs. + * + * Example: + * import { logger } from "./context"; // correct: live binding + * const { logger } = await import("./context"); // WRONG: captures undefined + */ +export let logger: Logger; +export let fetchResult: FetchResultFetcher; +export let fetchListResult: FetchListResultFetcher; +export let fetchPagedListResult: FetchPagedListResultFetcher; +export let fetchKVGetValue: FetchKVGetValueFetcher; +export let confirm: DeployHelpersContext["confirm"]; +export let prompt: DeployHelpersContext["prompt"]; +export let select: DeployHelpersContext["select"]; +export let isNonInteractiveOrCI: () => boolean; + +/** + * Set the global context for deploy-helpers. Must be called once at + * startup before any deploy-helpers function that needs these values. + */ +export function initDeployHelpersContext(ctx: DeployHelpersContext): void { + logger = ctx.logger; + fetchResult = ctx.fetchResult; + fetchListResult = ctx.fetchListResult; + fetchPagedListResult = ctx.fetchPagedListResult; + fetchKVGetValue = ctx.fetchKVGetValue; + confirm = ctx.confirm; + prompt = ctx.prompt; + select = ctx.select; + isNonInteractiveOrCI = ctx.isNonInteractiveOrCI; +} diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts new file mode 100644 index 0000000..5477f2a --- /dev/null +++ b/packages/deploy-helpers/src/shared/types.ts @@ -0,0 +1,169 @@ +import type { + ValidatedAssetsOptions, + LegacyAssetPaths, + CfModule, + CfModuleType, + CfWorkerSourceMap, + Config, + FetchKVGetValueFetcher, + FetchResultFetcher, + FetchListResultFetcher, + FetchPagedListResultFetcher, + Logger, + Route, + Entry, +} from "@cloudflare/workers-utils"; + +/** + * client needs to handle logger and fetch/auth implementation + * these are passed into this package to handle any API requests/logs + */ +export type DeployHelpersContext = { + fetchResult: FetchResultFetcher; + fetchListResult: FetchListResultFetcher; + fetchPagedListResult: FetchPagedListResultFetcher; + fetchKVGetValue: FetchKVGetValueFetcher; + logger: Logger; + confirm: ( + text: string, + options?: { defaultValue?: boolean; fallbackValue?: boolean } + ) => Promise; + prompt: ( + text: string, + options?: { defaultValue?: string } + ) => Promise; + select: ( + text: string, + options: { + choices: { + title: string; + description?: string; + value: Values; + }[]; + defaultOption?: number; + fallbackOption?: number; + } + ) => Promise; + isNonInteractiveOrCI: () => boolean; +}; + +/** + * Shared fields produced by merging CLI args with wrangler config. + * After this point, no raw config/arg merging should happen. + * + * Use props for all resolved/merged values. Only access config directly + * for raw values that aren't merged with CLI args (e.g., config.durable_objects, + * config.unsafe, config.tail_consumers). + */ +export type SharedDeployVersionsProps = { + /** Merged from args.script/config.main/config.site.entry-point/config.assets. */ + entry: Entry; + /** Merged: --name arg ?? config.name, with CI override applied. Validated as required separately. */ + name: string | undefined; + /** Merged: --compatibility-date arg ?? config.compatibility_date. Still optional — validated as required later. */ + compatibilityDate: string | undefined; + /** Merged: --compatibility-flags arg ?? config.compatibility_flags. */ + compatibilityFlags: string[]; + /** + * Validated/resolved assets directory, merged from --assets arg and + * config.assets. The full AssetsOptions are resolved later via + * `resolveAssetOptions`. + */ + assetsDir: ValidatedAssetsOptions | undefined; + /** + * The user Worker entry, merged: --script arg ?? config.main. Undefined for + * assets-only Workers. Drives `has_user_worker` when resolving assets. + */ + main: string | undefined; + /** Merged: --keep-vars arg || config.keep_vars. */ + keepVars: boolean; + /** Merged from --site arg and config.site. */ + isWorkersSite: boolean; + /** From --dry-run arg. */ + dryRun: boolean; + /** From --env arg. */ + env: string | undefined; + /** From --outfile arg. */ + outfile: string | undefined; + /** From --tag arg. */ + tag: string | undefined; + /** From --message arg. */ + message: string | undefined; + /** From --secrets-file arg. */ + secretsFile: string | undefined; + /** From collectKeyValues(--var arg). CLI-only vars; config vars flow separately via getBindings(config). */ + cliVars: Record; + /** From --experimental-auto-create arg. */ + experimentalAutoCreate: boolean; + /** Resolved from requireAuth() before calling deploy-helpers. undefined only in dry-run. */ + accountId: string | undefined; + /** Resolved from getMetricsUsageHeaders() / config.send_metrics. Controls whether usage metrics headers are sent with upload requests. */ + sendMetrics: boolean; + /** Resolved from getFlag("RESOURCES_PROVISION"). Controls whether bindings are auto-provisioned before upload. */ + resourcesProvision: boolean; + /** Controls whether provisioned resource IDs are written back to the config file. */ + skipProvisioningConfigWriteback: boolean; + /** From --strict arg. In strict mode, conflicting pre-upload checks abort instead of auto-continuing. */ + strict: boolean; +}; + +export type DeployProps = SharedDeployVersionsProps & { + /** Discriminant for DeployProps vs VersionsUploadProps */ + command: "deploy"; + /** Merged from --site arg and config.site. */ + legacyAssetPaths: LegacyAssetPaths | undefined; + /** Merged: --triggers arg ?? config.triggers.crons. */ + triggers: string[] | undefined; + /** Merged: --routes arg ?? config.routes ?? config.route. AND --domains and custom_domains*/ + routes: Route[]; + /** Merged: --logpush arg ?? config.logpush. */ + logpush: boolean | undefined; + /** From --dispatch-namespace arg. Deploy-only (Workers for Platforms). */ + dispatchNamespace: string | undefined; + /** From --old-asset-ttl arg. Deploy-only. */ + oldAssetTtl: number | undefined; + /** From --containers-rollout arg. Deploy-only. */ + containersRollout: "immediate" | "gradual" | "none" | undefined; + /** + * When true, an existing Worker with the same name aborts the deploy instead + * of updating it, because this run cannot confirm the local project owns the + * remote Worker. Set for non-interactive deploys with no pre-existing config + * file when either the name was generated automatically (no user-supplied + * name) or the deploy is the Pages-to-Workers delegation (where the name is a + * Pages project name carried across, not proof of Worker ownership). Deploys + * that carry a config file naming the Worker leave this false and update it as + * normal. + */ + failIfWorkerNameTaken?: boolean; +}; + +export type VersionsUploadProps = SharedDeployVersionsProps & { + /** Discriminant for DeployProps vs VersionsUploadProps */ + command: "versions upload"; + /** CLI-only (--preview-alias), or auto-generated from CI branch name. */ + previewAlias: string | undefined; +}; + +export type WorkerBuildResult = { + modules: CfModule[]; + sourceMaps: CfWorkerSourceMap[] | undefined; + dependencies: Record; + resolvedEntryPointPath: string; + bundleType: CfModuleType; + content: string; +}; + +export interface TriggerDeployment { + targets: string[]; + error?: Error; +} + +export type TriggerProps = { + config: Config; + accountId: string; + scriptName: string; + env: string | undefined; + crons: string[] | undefined; + routes: Route[]; + firstDeploy: boolean; +}; diff --git a/packages/deploy-helpers/src/triggers/deploy.ts b/packages/deploy-helpers/src/triggers/deploy.ts new file mode 100644 index 0000000..5405e9d --- /dev/null +++ b/packages/deploy-helpers/src/triggers/deploy.ts @@ -0,0 +1,614 @@ +import { + APIError, + formatTime, + getSubdomainMixedStateCheckDisabled, + retryOnAPIFailure, + UserError, +} from "@cloudflare/workers-utils"; +import chalk from "chalk"; +import PQueue from "p-queue"; +import { WORKFLOW_CRON_REQUIRES_PAID_PLAN_CODE } from "../deploy/helpers/error-codes"; +import { + fetchListResult, + fetchResult, + isNonInteractiveOrCI, + logger, +} from "../shared/context"; +import { + publishCustomDomains, + publishRoutes, + renderRoute, +} from "./publish-routes"; +import { updateQueueConsumers } from "./queue-consumers"; +import { getWorkersDevSubdomain } from "./subdomain"; +import { getZoneForRoute } from "./zones"; +import type { TriggerDeployment, TriggerProps } from "../shared/types"; +import type { RouteObject } from "./publish-routes"; +import type { Config, Route } from "@cloudflare/workers-utils"; + +export async function triggersDeploy( + props: TriggerProps +): Promise { + const { config, accountId, scriptName, routes, crons } = props; + + const routesOnly: Array = []; + const customDomainsOnly: Array = []; + + for (const route of routes) { + if (typeof route !== "string" && route.custom_domain) { + customDomainsOnly.push(route); + } else { + routesOnly.push(route); + } + } + + const start = Date.now(); + + const workerUrl = `/accounts/${accountId}/workers/scripts/${scriptName}`; + + const uploadMs = Date.now() - start; + const deployments: Promise[] = []; + const hasWorkflowsDefinedInThisScript = config.workflows.some((workflow) => + isWorkflowDefinedInThisScript(workflow, scriptName) + ); + + const { wantWorkersDev, workersDevInSync } = await subdomainDeploy( + props, + accountId, + scriptName, + workerUrl, + routes, + deployments, + props.firstDeploy + ); + + if (!wantWorkersDev && workersDevInSync && routes.length !== 0) { + // TODO is this true? How does last subdomain status affect route confict?? + // Why would we only need to validate route conflicts if didn't need to + // disable the subdomain deployment? + + // if you get to this point it's because + // you're trying to deploy a worker to a route + // that's already bound to another worker. + // so this thing is about finding workers that have + // bindings to the routes you're trying to deploy to. + // + // the logic is kinda similar (read: duplicated) from publishRoutesFallback, + // except here we know we have a good API token or whatever so we don't need + // to bother with all the error handling tomfoolery. + const routesWithOtherBindings: Record = {}; + + /** + * This queue ensures we limit how many concurrent fetch + * requests we're making to the Zones API. + */ + const queue = new PQueue({ concurrency: 10 }); + const queuePromises: Array> = []; + const zoneRoutesCache = new Map< + string, + Promise> + >(); + + const zoneIdCache = new Map(); + for (const route of routes) { + queuePromises.push( + queue.add(async () => { + const zone = await getZoneForRoute( + config, + { route, accountId }, + zoneIdCache + ); + if (!zone) { + return; + } + + const routePattern = + typeof route === "string" ? route : route.pattern; + + let routesInZone = zoneRoutesCache.get(zone.id); + if (!routesInZone) { + routesInZone = retryOnAPIFailure( + () => + fetchListResult<{ + pattern: string; + script: string; + }>(config, `/zones/${zone.id}/workers/routes`), + logger + ); + zoneRoutesCache.set(zone.id, routesInZone); + } + + (await routesInZone).forEach(({ script, pattern }) => { + if (pattern === routePattern && script !== scriptName) { + if (!(script in routesWithOtherBindings)) { + routesWithOtherBindings[script] = []; + } + + routesWithOtherBindings[script].push(pattern); + } + }); + }) + ); + } + // using Promise.all() here instead of queue.onIdle() to ensure + // we actually throw errors that occur within queued promises. + await Promise.all(queuePromises); + + if (Object.keys(routesWithOtherBindings).length > 0) { + let errorMessage = + "Can't deploy routes that are assigned to another worker.\n"; + + for (const worker in routesWithOtherBindings) { + const assignedRoutes = routesWithOtherBindings[worker]; + errorMessage += `"${worker}" is already assigned to routes:\n${assignedRoutes.map( + (r) => ` - ${chalk.underline(r)}\n` + )}`; + } + + const resolution = + "Unassign other workers from the routes you want to deploy to, and then try again."; + const dashHref = chalk.blue.underline( + `https://dash.cloudflare.com/${accountId}/workers/overview` + ); + const dashLink = `Visit ${dashHref} to unassign a worker from a route.`; + + throw new UserError(`${errorMessage}\n${resolution}\n${dashLink}`, { + telemetryMessage: "triggers deploy routes assigned", + }); + } + } + + if (!wantWorkersDev && hasWorkflowsDefinedInThisScript) { + await getWorkersDevSubdomain(config, accountId, { + configPath: config.configPath, + registrationContext: "workflows", + }); + } + + // Update routing table for the script. + if (routesOnly.length > 0) { + deployments.push( + publishRoutes(config, routesOnly, { + workerUrl, + scriptName, + accountId, + }).then( + () => { + if (routesOnly.length > 10) { + return { + targets: routesOnly + .slice(0, 9) + .map((route) => renderRoute(route)) + .concat([`...and ${routesOnly.length - 9} more routes`]), + }; + } + return { targets: routesOnly.map((route) => renderRoute(route)) }; + }, + (error) => ({ targets: [], error }) + ) + ); + } + + // Update custom domains for the script + if (customDomainsOnly.length > 0) { + deployments.push( + publishCustomDomains( + config, + workerUrl, + accountId, + customDomainsOnly + ).catch((error) => ({ targets: [], error })) + ); + } + + // Configure any schedules for the script. + // If schedules is not defined then we just leave whatever is previously deployed alone. + // If it is an empty array we will remove all schedules. + if (crons) { + deployments.push( + fetchResult(config, `${workerUrl}/schedules`, { + // Note: PUT will override previous schedules on this script. + method: "PUT", + body: JSON.stringify(crons.map((cron) => ({ cron }))), + headers: { + "Content-Type": "application/json", + }, + }).then( + () => ({ + targets: crons.map((trigger) => `schedule: ${trigger}`), + }), + (error) => ({ targets: [], error }) + ) + ); + } + + if (config.queues.producers && config.queues.producers.length) { + deployments.push( + ...config.queues.producers.map((producer) => + Promise.resolve({ targets: [`Producer for ${producer.queue}`] }) + ) + ); + } + + if (config.queues.consumers && config.queues.consumers.length) { + const consumerUpdates = await updateQueueConsumers( + config, + accountId, + scriptName, + config + ); + deployments.push(...consumerUpdates); + } + + if (config.workflows?.length) { + // NOTE: if the user provides a script_name thats not this script (aka bounds to another worker) + // we don't want to send this worker's config. + // TODO: move this earlier. + for (const workflow of config.workflows) { + if (!isWorkflowDefinedInThisScript(workflow, scriptName)) { + if (workflow.limits) { + throw new UserError( + `Workflow "${workflow.name}" has "limits" configured but references external script "${workflow.script_name}". ` + + `Configure limits on the worker that defines the workflow.`, + { + telemetryMessage: + "triggers deploy workflow limits external script", + } + ); + } + if (workflow.schedules) { + throw new UserError( + `Workflow "${workflow.name}" has "schedules" configured but references external script "${workflow.script_name}". ` + + `Configure schedules on the worker that defines the workflow.`, + { + telemetryMessage: + "triggers deploy workflow schedules external script", + } + ); + } + continue; + } + + deployments.push( + fetchResult( + config, + `/accounts/${accountId}/workflows/${workflow.name}`, + { + method: "PUT", + body: JSON.stringify({ + script_name: scriptName, + class_name: workflow.class_name, + ...(workflow.limits && { limits: workflow.limits }), + ...(workflow.schedules && { + schedules: (Array.isArray(workflow.schedules) + ? workflow.schedules + : [workflow.schedules] + ).map((cron) => ({ cron })), + }), + }), + headers: { + "Content-Type": "application/json", + }, + } + ).then( + () => ({ targets: [`workflow: ${workflow.name}`] }), + (error) => { + if ( + error instanceof APIError && + error.code === WORKFLOW_CRON_REQUIRES_PAID_PLAN_CODE && + workflow.schedules + ) { + error.preventReport(); + return { + targets: [], + error: new UserError( + `Workflow "${workflow.name}" has "schedules" configured, but scheduled Workflows require a paid Workers plan.`, + { + cause: error, + telemetryMessage: + "triggers deploy workflow cron requires paid plan", + } + ), + }; + } + + return { targets: [], error }; + } + ) + ); + } + } + + const completedDeployments = await Promise.all(deployments); + const deployMs = Date.now() - start - uploadMs; + + const workerName = scriptName; + + const targets = completedDeployments + .flatMap((deployment) => deployment.targets) + .map( + // Append protocol only on workers.dev domains + (target) => (target.endsWith("workers.dev") ? "https://" : "") + target + ); + if (targets.length > 0) { + logger.log(`Deployed ${workerName} triggers`, formatTime(deployMs)); + for (const target of targets) { + logger.log(" ", target); + } + } else { + logger.log("No targets deployed for", workerName, formatTime(deployMs)); + } + + const errors = completedDeployments + .map((deployment) => deployment.error) + .filter((error): error is Error => error !== undefined); + + if (errors.length > 0) { + throw new UserError( + `Some triggers failed to deploy for ${workerName}:\n` + + errors.map((error) => ` - ${error.message}`).join("\n"), + { + // Preserve the original errors (with stacks and subclass info) for + // debugging, while still presenting a single aggregated message. + cause: new AggregateError(errors), + // Aggregate the inner telemetry labels into a single deterministic, + // low-cardinality label so failures still group meaningfully. Non- + // UserError causes contribute a generic "non-user error" marker. + telemetryMessage: `triggers deploy partial failure: ${aggregateTelemetryMessages(errors)}`, + } + ); + } + + return targets; +} + +/** + * Collapse the telemetry labels of a set of inner errors into a single sorted, + * deduplicated string. Each `UserError` contributes its own `telemetryMessage`; + * anything else contributes `"non-user error"`. + */ +function aggregateTelemetryMessages(errors: Error[]): string { + const labels = errors.map((error) => + error instanceof UserError && error.telemetryMessage + ? error.telemetryMessage + : "non-user error" + ); + return Array.from(new Set(labels)).sort().join(", "); +} + +// getSubdomainValues returns the values for workers_dev and preview_urls. +// Defaults are computed at the API level. +export function getSubdomainValues( + config_workers_dev: boolean | undefined, + config_preview_urls: boolean | undefined, + routes: Route[] +): { + workers_dev: boolean; + preview_urls?: boolean; +} { + const defaultWorkersDev = routes.length === 0; // Default to true only if there aren't any routes defined. + const workers_dev = config_workers_dev ?? defaultWorkersDev; + const defaultPreviewUrls = undefined; // Undefined lets the API compute the default. + const preview_urls = config_preview_urls ?? defaultPreviewUrls; + return { + workers_dev, + preview_urls, + }; +} + +// getSubdomainValuesAPIMock returns the values for workers_dev and preview_urls. +// Mimics the logic in the API, ideally we would obtain defaults from the API. +export function getSubdomainValuesAPIMock( + config_workers_dev: boolean | undefined, + config_preview_urls: boolean | undefined, + routes: Route[] +): { + workers_dev: boolean; + preview_urls: boolean; +} { + const defaultWorkersDev = routes.length === 0; // Default to true only if there aren't any routes defined. + const workers_dev = config_workers_dev ?? defaultWorkersDev; + const defaultPreviewUrls = defaultWorkersDev; // Default to workers_dev status. + const preview_urls = config_preview_urls ?? defaultPreviewUrls; + return { + workers_dev, + preview_urls, + }; +} + +async function validateSubdomainMixedState( + props: TriggerProps, + accountId: string, + scriptName: string, + before: { workers_dev: boolean; preview_urls: boolean }, + after: { workers_dev: boolean; preview_urls: boolean }, + firstDeploy: boolean +): Promise<{ + workers_dev: boolean; + preview_urls: boolean; +}> { + const { config } = props; + + const changed = + after.workers_dev !== before.workers_dev || + after.preview_urls !== before.preview_urls; + + // Early return if config values are the same as remote values (so we only warn on change) + if (!changed) { + return after; + } + + // Early return if check disabled through environment variable. + if (getSubdomainMixedStateCheckDisabled()) { + return after; + } + + // Early return if non-interactive or CI + if (isNonInteractiveOrCI()) { + return after; + } + + // Early return if this is the first deploy + if (firstDeploy) { + return after; + } + + // Early return if config values are the same (e.g. both true or both false, not in mixed state) + if (after.workers_dev === after.preview_urls) { + return after; + } + + const userSubdomain = await getWorkersDevSubdomain(config, accountId, { + configPath: config.configPath, + }); + const previewUrl = `https://-${scriptName}.${userSubdomain}`; + + // Scenario 1: User disables workers.dev while having preview URLs enabled + if (!after.workers_dev && after.preview_urls) { + logger.warn( + [ + "You are disabling the 'workers.dev' subdomain for this Worker, but Preview URLs are still enabled.", + "Preview URLs will automatically generate a unique, shareable link for each new version which will be accessible at:", + ` ${previewUrl}`, + "", + "To prevent this Worker from being unintentionally public, you may want to disable the Preview URLs as well by setting `preview_urls = false` in your Wrangler config file.", + ].join("\n") + ); + } + + // Scenario 2: User enables workers.dev when Preview URLs are off + if (after.workers_dev && !after.preview_urls) { + logger.warn( + [ + "You are enabling the 'workers.dev' subdomain for this Worker, but Preview URLs are still disabled.", + "Preview URLs will automatically generate a unique, shareable link for each new version which will be accessible at:", + ` ${previewUrl}`, + "", + "You may want to enable the Preview URLs as well by setting `preview_urls = true` in your Wrangler config file.", + ].join("\n") + ); + } + + return after; +} + +async function subdomainDeploy( + props: TriggerProps, + accountId: string, + scriptName: string, + workerUrl: string, + routes: Route[], + deployments: Promise[], + firstDeploy: boolean +) { + const { config } = props; + + // Get desired subdomain enablement status. + + const { workers_dev: wantWorkersDev, preview_urls: wantPreviews } = + getSubdomainValues(config.workers_dev, config.preview_urls, routes); + + // workers.dev URL is only set if we want to deploy to workers.dev. + if (wantWorkersDev) { + const userSubdomain = await getWorkersDevSubdomain(config, accountId, { + configPath: config.configPath, + }); + const workersDevURL = `${scriptName}.${userSubdomain}`; + deployments.push(Promise.resolve({ targets: [workersDevURL] })); + } + + // Get current subdomain enablement status. + const before = await fetchResult<{ + enabled: boolean; + previews_enabled: boolean; + }>(config, `${workerUrl}/subdomain`); + + // Update subdomain status. + // Occasionally this update to the subdomain endpoint fails due to some internal API error, + // we retry this request a few times to mitigate that. + const after = await retryOnAPIFailure( + async () => + fetchResult<{ + enabled: boolean; + previews_enabled: boolean; + }>(config, `${workerUrl}/subdomain`, { + method: "POST", + body: JSON.stringify({ + enabled: wantWorkersDev, + previews_enabled: wantPreviews, + }), + headers: { + "Content-Type": "application/json", + "Cloudflare-Workers-Script-Api-Date": "2025-08-01", + }, + }), + logger + ); + + // Warn about mismatching config and current values. + if ( + !firstDeploy && + config.workers_dev == undefined && + after.enabled !== before.enabled + ) { + const status = (enabled: boolean, past: boolean) => { + if (past) { + return enabled ? "enabled" : "disabled"; + } else { + return enabled ? "enable" : "disable"; + } + }; + logger.warn( + [ + `Because 'workers_dev' is not in your Wrangler file, it will be ${status(after.enabled, true)} for this deployment by default.`, + `To override this setting, you can ${status(before.enabled, false)} workers.dev by explicitly setting 'workers_dev = ${before.enabled}' in your Wrangler file.`, + ].join("\n") + ); + } + + if ( + !firstDeploy && + config.preview_urls == undefined && + after.previews_enabled !== before.previews_enabled + ) { + const status = (enabled: boolean, past: boolean) => { + if (past) { + return enabled ? "enabled" : "disabled"; + } else { + return enabled ? "enable" : "disable"; + } + }; + logger.warn( + [ + `Because your 'workers.dev' route is ${status(after.enabled, true)} and your 'preview_urls' setting is not in your Wrangler file, Preview URLs will be ${status(after.previews_enabled, true)} for this deployment by default.`, + `To override this setting, you can ${status(before.previews_enabled, false)} Preview URLs by explicitly setting 'preview_urls = ${before.previews_enabled}' in your Wrangler file.`, + ].join("\n") + ); + } + + // Warn about mixed status. + await validateSubdomainMixedState( + props, + accountId, + scriptName, + { workers_dev: before.enabled, preview_urls: before.previews_enabled }, + { workers_dev: after.enabled, preview_urls: after.previews_enabled }, + firstDeploy + ); + + return { + wantWorkersDev, + wantPreviews, + workersDevInSync: before.enabled === after.enabled, + previewsInSync: before.previews_enabled === after.previews_enabled, + }; +} + +function isWorkflowDefinedInThisScript( + workflow: Config["workflows"][number], + scriptName: string +): boolean { + return ( + workflow.script_name === undefined || workflow.script_name === scriptName + ); +} diff --git a/packages/deploy-helpers/src/triggers/publish-routes.ts b/packages/deploy-helpers/src/triggers/publish-routes.ts new file mode 100644 index 0000000..8910a6f --- /dev/null +++ b/packages/deploy-helpers/src/triggers/publish-routes.ts @@ -0,0 +1,355 @@ +import { ParseError, UserError } from "@cloudflare/workers-utils"; +import PQueue from "p-queue"; +import { + confirm, + fetchListResult, + fetchResult, + logger, +} from "../shared/context"; +import { getZoneForRoute } from "./zones"; +import type { TriggerDeployment } from "../shared/types"; +import type { + ComplianceConfig, + CustomDomainRoute, + Route, + ZoneIdRoute, + ZoneNameRoute, +} from "@cloudflare/workers-utils"; + +export type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute; + +export type CustomDomain = { + id: string; + zone_id: string; + zone_name: string; + hostname: string; + service: string; + environment: string; + enabled: boolean; + previews_enabled: boolean; +}; + +type UpdatedCustomDomain = CustomDomain & { modified: boolean }; +type ConflictingCustomDomain = CustomDomain & { + external_dns_record_id?: string | null; + external_cert_id?: string; +}; + +export type CustomDomainChangeset = { + added: CustomDomain[]; + removed: CustomDomain[]; + updated: UpdatedCustomDomain[]; + conflicting: ConflictingCustomDomain[]; +}; + +export function renderRoute(route: Route): string { + let result = ""; + if (typeof route === "string") { + result = route; + } else { + result = route.pattern; + const isCustomDomain = Boolean( + "custom_domain" in route && route.custom_domain + ); + if (isCustomDomain && "zone_id" in route) { + result += ` (custom domain - zone id: ${route.zone_id})`; + } else if (isCustomDomain && "zone_name" in route) { + result += ` (custom domain - zone name: ${route.zone_name})`; + } else if (isCustomDomain) { + result += ` (custom domain)`; + } else if ("zone_id" in route) { + result += ` (zone id: ${route.zone_id})`; + } else if ("zone_name" in route) { + result += ` (zone name: ${route.zone_name})`; + } + + if (isCustomDomain) { + const flags: string[] = []; + if ("enabled" in route && route.enabled !== undefined) { + flags.push(route.enabled ? "enabled" : "disabled"); + } + if ("previews_enabled" in route && route.previews_enabled !== undefined) { + flags.push( + route.previews_enabled ? "previews: enabled" : "previews: disabled" + ); + } + if (flags.length > 0) { + result += ` [${flags.join(", ")}]`; + } + } + } + return result; +} + +function isAuthenticationError(e: unknown): e is ParseError { + return e instanceof ParseError && (e as { code?: number }).code === 10000; +} + +/** + * Associate the newly deployed Worker with the given routes. + */ +export async function publishRoutes( + complianceConfig: ComplianceConfig, + routes: Route[], + { + workerUrl, + scriptName, + accountId, + }: { + workerUrl: string; + scriptName: string; + accountId: string; + } +): Promise { + try { + return await fetchResult(complianceConfig, `${workerUrl}/routes`, { + // Note: PUT will delete previous routes on this script. + method: "PUT", + body: JSON.stringify( + routes.map((route) => + typeof route !== "object" ? { pattern: route } : route + ) + ), + headers: { + "Content-Type": "application/json", + }, + }); + } catch (e) { + if (isAuthenticationError(e)) { + // An authentication error is probably due to a known issue, + // where the user is logged in via an API token that does not have "All Zones". + return await publishRoutesFallback(complianceConfig, routes, { + scriptName, + accountId, + }); + } else { + throw e; + } + } +} +/** + * Try updating routes for the Worker using a less optimal zone-based API. + * + * Compute match zones to the routes, then for each route attempt to connect it to the Worker via the zone. + */ +async function publishRoutesFallback( + complianceConfig: ComplianceConfig, + routes: Route[], + { scriptName, accountId }: { scriptName: string; accountId: string } +) { + logger.info( + "The current authentication token does not have 'All Zones' permissions.\n" + + "Falling back to using the zone-based API endpoint to update each route individually.\n" + + "Note that there is no access to routes associated with zones that the API token does not have permission for.\n" + + "Existing routes for this Worker in such zones will not be deleted." + ); + + const deployedRoutes: string[] = []; + + const queue = new PQueue({ concurrency: 10 }); + const queuePromises: Array> = []; + const zoneIdCache = new Map(); + + // Collect the routes (and their zones) that will be deployed. + const activeZones = new Map(); + const routesToDeploy = new Map(); + for (const route of routes) { + queuePromises.push( + queue.add(async () => { + const zone = await getZoneForRoute( + complianceConfig, + { route, accountId }, + zoneIdCache + ); + if (zone) { + activeZones.set(zone.id, zone.host); + routesToDeploy.set( + typeof route === "string" ? route : route.pattern, + zone.id + ); + } + }) + ); + } + await Promise.all(queuePromises.splice(0, queuePromises.length)); + + // Collect the routes that are already deployed. + const allRoutes = new Map(); + const alreadyDeployedRoutes = new Set(); + for (const [zone, host] of activeZones) { + queuePromises.push( + queue.add(async () => { + try { + for (const { pattern, script } of await fetchListResult<{ + pattern: string; + script: string; + }>(complianceConfig, `/zones/${zone}/workers/routes`)) { + allRoutes.set(pattern, script); + if (script === scriptName) { + alreadyDeployedRoutes.add(pattern); + } + } + } catch (e) { + if (isAuthenticationError(e)) { + e.notes.push({ + text: `This could be because the API token being used does not have permission to access the zone "${host}" (${zone}).`, + }); + } + throw e; + } + }) + ); + } + // using Promise.all() here instead of queue.onIdle() to ensure + // we actually throw errors that occur within queued promises. + await Promise.all(queuePromises); + + // Deploy each route that is not already deployed. + for (const [routePattern, zoneId] of routesToDeploy.entries()) { + if (allRoutes.has(routePattern)) { + const knownScript = allRoutes.get(routePattern); + if (knownScript === scriptName) { + // This route is already associated with this worker, so no need to hit the API. + alreadyDeployedRoutes.delete(routePattern); + continue; + } else { + throw new UserError( + `The route with pattern "${routePattern}" is already associated with another worker called "${knownScript}".`, + { telemetryMessage: "route already associated with another worker" } + ); + } + } + + const { pattern } = await fetchResult<{ pattern: string }>( + complianceConfig, + `/zones/${zoneId}/workers/routes`, + { + method: "POST", + body: JSON.stringify({ + pattern: routePattern, + script: scriptName, + }), + headers: { + "Content-Type": "application/json", + }, + } + ); + + deployedRoutes.push(pattern); + } + + if (alreadyDeployedRoutes.size) { + logger.warn( + "Previously deployed routes:\n" + + "The following routes were already associated with this worker, and have not been deleted:\n" + + [...alreadyDeployedRoutes.values()].map((route) => ` - "${route}"\n`) + + "If these routes are not wanted then you can remove them in the dashboard." + ); + } + + return deployedRoutes; +} + +export async function publishCustomDomains( + complianceConfig: ComplianceConfig, + workerUrl: string, + accountId: string, + domains: Array +): Promise { + const options = { + override_scope: true, + override_existing_origin: false, + override_existing_dns_record: false, + }; + const origins = domains.map((domainRoute) => { + return { + hostname: domainRoute.pattern, + zone_id: "zone_id" in domainRoute ? domainRoute.zone_id : undefined, + zone_name: "zone_name" in domainRoute ? domainRoute.zone_name : undefined, + enabled: "enabled" in domainRoute ? domainRoute.enabled : undefined, + previews_enabled: + "previews_enabled" in domainRoute + ? domainRoute.previews_enabled + : undefined, + }; + }); + + const fail = (): TriggerDeployment => { + return { + targets: [], + error: new UserError( + domains.length > 1 + ? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again` + : `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`, + { telemetryMessage: "deploy custom domains skipped" } + ), + }; + }; + + if (!process.stdout.isTTY) { + options.override_existing_origin = true; + options.override_existing_dns_record = true; + } else { + const changeset = await fetchResult( + complianceConfig, + `${workerUrl}/domains/changeset?replace_state=true`, + { + method: "POST", + body: JSON.stringify(origins), + headers: { + "Content-Type": "application/json", + }, + } + ); + + const updatesRequired = changeset.updated.filter( + (domain) => domain.modified + ); + if (updatesRequired.length > 0) { + const existing = await Promise.all( + updatesRequired.map((domain) => + fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/domains/records/${domain.id}` + ) + ) + ); + const existingRendered = existing + .map( + (domain) => + `\t• ${domain.hostname} (used as a domain for "${domain.service}")` + ) + .join("\n"); + const message = `Custom Domains already exist for these domains: +${existingRendered} +Update them to point to this script instead?`; + if (!(await confirm(message))) { + return fail(); + } + options.override_existing_origin = true; + } + + if (changeset.conflicting.length > 0) { + const conflicitingRendered = changeset.conflicting + .map((domain) => `\t• ${domain.hostname}`) + .join("\n"); + const message = `You already have DNS records that conflict for these Custom Domains: +${conflicitingRendered} +Update them to point to this script instead?`; + if (!(await confirm(message))) { + return fail(); + } + options.override_existing_dns_record = true; + } + } + + await fetchResult(complianceConfig, `${workerUrl}/domains/records`, { + method: "PUT", + body: JSON.stringify({ ...options, origins }), + headers: { + "Content-Type": "application/json", + }, + }); + + return { targets: domains.map((domain) => renderRoute(domain)) }; +} diff --git a/packages/deploy-helpers/src/triggers/queue-consumers.ts b/packages/deploy-helpers/src/triggers/queue-consumers.ts new file mode 100644 index 0000000..8968a90 --- /dev/null +++ b/packages/deploy-helpers/src/triggers/queue-consumers.ts @@ -0,0 +1,448 @@ +import { UserError } from "@cloudflare/workers-utils"; +import { fetchPagedListResult, fetchResult, logger } from "../shared/context"; +import type { TriggerDeployment } from "../shared/types"; +import type { Config, ComplianceConfig } from "@cloudflare/workers-utils"; + +export interface PostQueueBody { + queue_name: string; + settings?: QueueSettings; +} + +export interface QueueSettings { + delivery_delay?: number; + delivery_paused?: boolean; + message_retention_period?: number; +} + +export interface PostQueueResponse { + queue_id: string; + queue_name: string; + settings?: QueueSettings; + created_on: string; + modified_on: string; +} + +export interface QueueResponse { + queue_id: string; + queue_name: string; + created_on: string; + modified_on: string; + producers: Producer[]; + producers_total_count: number; + consumers: Consumer[]; + consumers_total_count: number; + settings?: QueueSettings; +} + +export interface ScriptReference { + namespace?: string; + script?: string; + service?: string; + environment?: string; +} + +export type Producer = ScriptReference & { + type: string; + bucket_name?: string; +}; + +export type Consumer = ScriptReference & { + dead_letter_queue?: string; + settings: ConsumerSettings; + consumer_id: string; + bucket_name?: string; + type: string; +}; + +export interface TypedConsumerResponse extends Consumer { + queue_name: string; + created_on: string; +} + +export interface PostTypedConsumerBody { + type: string; + script_name?: string; + environment_name?: string; + settings: ConsumerSettings; + dead_letter_queue?: string; +} + +export interface ConsumerSettings { + batch_size?: number; + max_retries?: number; + max_wait_time_ms?: number; + max_concurrency?: number | null; + visibility_timeout_ms?: number; + retry_delay?: number; +} + +export interface PurgeQueueBody { + delete_messages_permanently: boolean; +} + +export interface PurgeQueueResponse { + started_at: string; + complete: boolean; +} + +export async function listQueues( + complianceConfig: ComplianceConfig, + accountId: string, + page?: number, + name?: string +): Promise { + page = page ?? 1; + const params = new URLSearchParams({ page: page.toString() }); + + if (name) { + params.append("name", name); + } + + return fetchResult( + complianceConfig, + `/accounts/${accountId}/queues`, + {}, + params + ); +} + +export async function getQueue( + complianceConfig: ComplianceConfig, + accountId: string, + queueName: string +): Promise { + const queues = await listQueues(complianceConfig, accountId, 1, queueName); + if (queues.length === 0) { + throw new UserError( + `Queue "${queueName}" does not exist. To create it, run: wrangler queues create ${queueName}`, + { telemetryMessage: "queues lookup missing queue" } + ); + } + return queues[0]; +} + +export async function postConsumer( + complianceConfig: ComplianceConfig, + accountId: string, + queueName: string, + body: PostTypedConsumerBody +): Promise { + const queue = await getQueue(complianceConfig, accountId, queueName); + return postConsumerById(complianceConfig, accountId, queue.queue_id, body); +} + +async function postConsumerById( + config: ComplianceConfig, + accountId: string, + queueId: string, + body: PostTypedConsumerBody +): Promise { + return fetchResult( + config, + `/accounts/${accountId}/queues/${queueId}/consumers`, + { + method: "POST", + body: JSON.stringify(body), + } + ); +} + +export async function putConsumerById( + complianceConfig: ComplianceConfig, + accountId: string, + queueId: string, + consumerId: string, + body: PostTypedConsumerBody +): Promise { + return fetchResult( + complianceConfig, + `/accounts/${accountId}/queues/${queueId}/consumers/${consumerId}`, + { + method: "PUT", + body: JSON.stringify(body), + } + ); +} + +export async function putConsumer( + complianceConfig: ComplianceConfig, + accountId: string, + queueName: string, + scriptName: string, + envName: string | undefined, + body: PostTypedConsumerBody +): Promise { + const queue = await getQueue(complianceConfig, accountId, queueName); + const targetConsumer = await resolveWorkerConsumerByName( + complianceConfig, + accountId, + scriptName, + envName, + queue + ); + return putConsumerById( + complianceConfig, + accountId, + queue.queue_id, + targetConsumer.consumer_id, + body + ); +} + +async function resolveWorkerConsumerByName( + complianceConfig: ComplianceConfig, + accountId: string, + consumerName: string, + envName: string | undefined, + queue: QueueResponse +): Promise { + const queueName = queue.queue_name; + const consumers = queue.consumers.filter( + (c) => + c.type === "worker" && + (c.script === consumerName || c.service === consumerName) + ); + + if (consumers.length === 0) { + throw new UserError( + `No worker consumer '${consumerName}' exists for queue ${queue.queue_name}`, + { telemetryMessage: "queues worker consumer missing" } + ); + } + + // If more than a consumer with the same name is found, it should be + // a service+environment combination + if (consumers.length > 1) { + const targetEnv = + envName ?? + (await getDefaultService(complianceConfig, accountId, consumerName)); + const targetConsumers = consumers.filter( + (c) => c.environment === targetEnv + ); + + if (targetConsumers.length === 0) { + throw new UserError( + `No worker consumer '${consumerName}' exists for queue ${queueName}`, + { telemetryMessage: "queues worker consumer missing environment" } + ); + } + return targetConsumers[0]; + } + + if (consumers[0].service) { + const targetEnv = + envName ?? + (await getDefaultService(complianceConfig, accountId, consumerName)); + if (targetEnv != consumers[0].environment) { + throw new UserError( + `No worker consumer '${consumerName}' exists for queue ${queueName}`, + { telemetryMessage: "queues worker consumer environment mismatch" } + ); + } + } + return consumers[0]; +} + +interface WorkerService { + id: string; + default_environment: { + environment: string; + }; +} + +async function getDefaultService( + complianceConfig: ComplianceConfig, + accountId: string, + serviceName: string +): Promise { + const service = await fetchResult( + complianceConfig, + `/accounts/${accountId}/workers/services/${serviceName}`, + { + method: "GET", + } + ); + + logger.info(service); + + return service.default_environment.environment; +} + +async function deleteConsumerById( + complianceConfig: ComplianceConfig, + accountId: string, + queueId: string, + consumerId: string +): Promise { + return fetchResult( + complianceConfig, + `/accounts/${accountId}/queues/${queueId}/consumers/${consumerId}`, + { + method: "DELETE", + } + ); +} + +export async function deletePullConsumer( + complianceConfig: ComplianceConfig, + accountId: string, + queueName: string +): Promise { + const queue = await getQueue(complianceConfig, accountId, queueName); + const consumer = queue.consumers[0]; + if (consumer?.type !== "http_pull") { + throw new UserError(`No http_pull consumer exists for queue ${queueName}`, { + telemetryMessage: "queues http pull consumer missing", + }); + } + return deleteConsumerById( + complianceConfig, + accountId, + queue.queue_id, + consumer.consumer_id + ); +} + +export async function listConsumers( + complianceConfig: ComplianceConfig, + accountId: string, + queueName: string +): Promise { + const queue = await getQueue(complianceConfig, accountId, queueName); + return queue.consumers; +} + +export async function deleteWorkerConsumer( + complianceConfig: ComplianceConfig, + accountId: string, + queueName: string, + scriptName: string, + envName: string | undefined +): Promise { + const queue = await getQueue(complianceConfig, accountId, queueName); + const targetConsumer = await resolveWorkerConsumerByName( + complianceConfig, + accountId, + scriptName, + envName, + queue + ); + return deleteConsumerById( + complianceConfig, + accountId, + queue.queue_id, + targetConsumer.consumer_id + ); +} + +export async function updateQueueConsumers( + complianceConfig: ComplianceConfig, + accountId: string, + scriptName: string, + config: Config +): Promise[]> { + const consumers = config.queues.consumers || []; + const updateConsumers: Promise[] = []; + for (const consumer of consumers) { + const queue = await getQueue(complianceConfig, accountId, consumer.queue); + + const body: PostTypedConsumerBody = { + type: "worker", + dead_letter_queue: consumer.dead_letter_queue, + script_name: scriptName, + settings: { + batch_size: consumer.max_batch_size, + max_retries: consumer.max_retries, + max_wait_time_ms: + consumer.max_batch_timeout !== undefined + ? 1000 * consumer.max_batch_timeout + : undefined, + max_concurrency: consumer.max_concurrency, + retry_delay: consumer.retry_delay, + }, + }; + + // Current script already assigned to queue? + const existingConsumer = + queue.consumers.filter( + (c) => c.script === scriptName || c.service === scriptName + ).length > 0; + const envName = undefined; // TODO: script environment for wrangler deploy? + if (existingConsumer) { + updateConsumers.push( + putConsumer( + complianceConfig, + accountId, + consumer.queue, + scriptName, + envName, + body + ).then( + () => ({ targets: [`Consumer for ${consumer.queue}`] }), + (error) => ({ targets: [], error }) + ) + ); + continue; + } + updateConsumers.push( + postConsumer(complianceConfig, accountId, consumer.queue, body).then( + () => ({ + targets: [`Consumer for ${consumer.queue}`], + }), + (error) => ({ targets: [], error }) + ) + ); + } + + return updateConsumers; +} + +const queuesUrl = (accountId: string, queueId?: string): string => { + let url = `/accounts/${accountId}/queues`; + if (queueId) { + url += `/${queueId}`; + } + return url; +}; + +export async function ensureQueuesExistByConfig( + config: Config, + accountId: string +) { + const producers = (config.queues.producers || []).map( + (producer) => producer.queue + ); + const consumers = (config.queues.consumers || []).map( + (consumer) => consumer.queue + ); + + const queueNames = producers.concat(consumers); + if (queueNames.length > 0) { + const params = new URLSearchParams(); + queueNames.forEach((e) => { + params.append("name", e); + }); + + const existingQueues = ( + await fetchPagedListResult( + config, + queuesUrl(accountId), + {}, + params + ) + ).map((q) => q.queue_name); + + if (queueNames.length !== existingQueues.length) { + const queueSet = new Set(existingQueues); + + for (const queue of queueNames) { + if (!queueSet.has(queue)) { + throw new UserError( + `Queue "${queue}" does not exist. To create it, run: wrangler queues create ${queue}`, + { telemetryMessage: "queues config missing queue" } + ); + } + } + } + } +} diff --git a/packages/deploy-helpers/src/triggers/subdomain.ts b/packages/deploy-helpers/src/triggers/subdomain.ts new file mode 100644 index 0000000..423accd --- /dev/null +++ b/packages/deploy-helpers/src/triggers/subdomain.ts @@ -0,0 +1,218 @@ +import { + configFileName, + getComplianceRegionSubdomain, + UserError, +} from "@cloudflare/workers-utils"; +import chalk from "chalk"; +import { confirm, fetchResult, logger, prompt } from "../shared/context"; +import type { ComplianceConfig } from "@cloudflare/workers-utils"; + +type WorkersDevSubdomainRegistrationContext = "workers_dev" | "workflows"; + +type GetWorkersDevSubdomainOptions = { + configPath?: string | undefined; + abortSignal?: AbortSignal | undefined; + registrationContext?: WorkersDevSubdomainRegistrationContext | undefined; +}; + +/** + * Gets the .(fed.)workers.dev URL for the given account. + */ +export async function getWorkersDevSubdomain( + complianceConfig: ComplianceConfig, + accountId: string, + options: GetWorkersDevSubdomainOptions = {} +): Promise { + const { + configPath, + abortSignal, + registrationContext = "workers_dev", + } = options; + + try { + // note: API docs say that this field is "name", but they're lying. + const { subdomain } = await fetchResult<{ subdomain: string }>( + complianceConfig, + `/accounts/${accountId}/workers/subdomain`, + undefined, + undefined, + abortSignal + ); + return `${subdomain}${getComplianceRegionSubdomain(complianceConfig)}.workers.dev`; + } catch (e) { + const error = e as { code?: number }; + if (typeof error !== "object" || !error || error.code !== 10007) { + throw e; + } + + // 10007 error code: not found + // https://api.cloudflare.com/#worker-subdomain-get-subdomain + logger.warn(getRegistrationWarning(registrationContext)); + + const wantsToRegister = await confirm( + "Would you like to register a workers.dev subdomain now?", + { fallbackValue: false } + ); + if (!wantsToRegister) { + throw getRegistrationDeclinedError( + registrationContext, + accountId, + configPath + ); + } + + return await registerSubdomain( + complianceConfig, + accountId, + configPath, + registrationContext + ); + } +} + +function getRegistrationWarning( + registrationContext: WorkersDevSubdomainRegistrationContext +): string { + switch (registrationContext) { + case "workflows": + return "You need to register a workers.dev subdomain before deploying Workflows"; + case "workers_dev": + return "You need to register a workers.dev subdomain before publishing to workers.dev"; + default: { + const _exhaustive: never = registrationContext; + return _exhaustive; + } + } +} + +function getRegistrationDeclinedError( + registrationContext: WorkersDevSubdomainRegistrationContext, + accountId: string, + configPath: string | undefined +): UserError { + const onboardingLink = `https://dash.cloudflare.com/${accountId}/workers/onboarding`; + switch (registrationContext) { + case "workflows": + return new UserError( + `Workflows require your account to have a workers.dev subdomain. Register a workers.dev subdomain here:\n${onboardingLink}`, + { + telemetryMessage: "workflows workers dev registration declined", + } + ); + case "workers_dev": { + const solutionMessage = `You can either deploy your worker to one or more routes by specifying them in your ${configFileName(configPath)} file, or register a workers.dev subdomain here:`; + return new UserError(`${solutionMessage}\n${onboardingLink}`, { + telemetryMessage: "routes workers dev registration declined", + }); + } + default: { + const _exhaustive: never = registrationContext; + return _exhaustive; + } + } +} + +async function registerSubdomain( + complianceConfig: ComplianceConfig, + accountId: string, + configPath: string | undefined, + registrationContext: WorkersDevSubdomainRegistrationContext +): Promise { + let subdomain: string | undefined; + + while (subdomain === undefined) { + const potentialName = await prompt( + "What would you like your workers.dev subdomain to be? It will be accessible at https://.workers.dev" + ); + + if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(potentialName)) { + logger.warn( + `${potentialName} is invalid, please choose another subdomain.` + ); + continue; + } + + try { + await fetchResult<{ subdomain: string }>( + complianceConfig, + `/accounts/${accountId}/workers/subdomains/${potentialName}` + ); + } catch (err) { + const subdomainAvailabilityCheckError = err as { code?: number }; + + if ( + typeof subdomainAvailabilityCheckError === "object" && + !!subdomainAvailabilityCheckError + ) { + if (subdomainAvailabilityCheckError.code === 10032) { + // oddly enough, this is a `subdomain_unavailable` error, meaning...that the subdomain + // doesn't exist. and we can register it. this is exactly how the dashboard does it. + } else if (subdomainAvailabilityCheckError.code === 10031) { + logger.error( + "Subdomain is unavailable, please try a different subdomain" + ); + continue; + } else { + logger.error("An unexpected error occurred, please try again."); + continue; + } + } + } + + const ok = await confirm( + `Creating a workers.dev subdomain for your account at ${chalk.blue( + chalk.underline( + `https://${potentialName}${getComplianceRegionSubdomain(complianceConfig)}.workers.dev` + ) + )}. Ok to proceed?` + ); + if (!ok) { + throw getRegistrationDeclinedError( + registrationContext, + accountId, + configPath + ); + } + + try { + const result = await fetchResult<{ subdomain: string }>( + complianceConfig, + `/accounts/${accountId}/workers/subdomain`, + { + method: "PUT", + body: JSON.stringify({ subdomain: potentialName }), + } + ); + subdomain = result.subdomain; + } catch (err) { + const subdomainCreationError = err as { code?: number }; + if ( + typeof subdomainCreationError === "object" && + !!subdomainCreationError && + subdomainCreationError.code !== undefined + ) { + switch (subdomainCreationError.code) { + case 10031: + logger.error( + "Subdomain is unavailable, please try a different subdomain." + ); + break; + default: + logger.error("An unexpected error occurred, please try again."); + break; + } + } + } + } + + logger.log("Success! It may take a few minutes for DNS records to update."); + logger.log( + `Visit ${chalk.blue( + chalk.underline( + `https://dash.cloudflare.com/${accountId}/workers/subdomain` + ) + )} to edit your workers.dev subdomain` + ); + + return `${subdomain}${getComplianceRegionSubdomain(complianceConfig)}.workers.dev`; +} diff --git a/packages/deploy-helpers/src/triggers/zones.ts b/packages/deploy-helpers/src/triggers/zones.ts new file mode 100644 index 0000000..ce27cef --- /dev/null +++ b/packages/deploy-helpers/src/triggers/zones.ts @@ -0,0 +1,97 @@ +import { + getHostFromRoute, + retryOnAPIFailure, + UserError, +} from "@cloudflare/workers-utils"; +import { fetchListResult, logger } from "../shared/context"; +import type { ComplianceConfig, Route } from "@cloudflare/workers-utils"; + +export interface Zone { + id: string; + host: string; +} + +export type ZoneIdCache = Map>; + +export async function getZoneForRoute( + complianceConfig: ComplianceConfig, + from: { + route: Route; + accountId: string; + }, + zoneIdCache: ZoneIdCache = new Map() +): Promise { + const { route, accountId } = from; + const host = getHostFromRoute(route); + let id: string | undefined; + + if (typeof route === "object" && "zone_id" in route) { + id = route.zone_id; + } else if (typeof route === "object" && "zone_name" in route) { + id = await getZoneIdFromHost( + complianceConfig, + { host: route.zone_name, accountId }, + zoneIdCache + ); + } else if (host) { + id = await getZoneIdFromHost( + complianceConfig, + { host, accountId }, + zoneIdCache + ); + } + + return id && host ? { id, host } : undefined; +} + +/** + * Given something that resembles a host, try to infer a zone id from it. + * + * It's hard to get a 'valid' domain from a string, so we don't even try to validate TLDs, etc. + * For each domain-like part of the host (e.g. w.x.y.z) try to get a zone id for it by + * lopping off subdomains until we get a hit from the API. + */ +export async function getZoneIdFromHost( + complianceConfig: ComplianceConfig, + from: { + host: string; + accountId: string; + }, + zoneIdCache: ZoneIdCache = new Map() +): Promise { + const hostPieces = from.host.split("."); + + while (hostPieces.length > 1) { + const cacheKey = `${from.accountId}:${hostPieces.join(".")}`; + if (!zoneIdCache.has(cacheKey)) { + zoneIdCache.set( + cacheKey, + retryOnAPIFailure( + () => + fetchListResult<{ id: string }>( + complianceConfig, + `/zones`, + {}, + new URLSearchParams({ + name: hostPieces.join("."), + "account.id": from.accountId, + }) + ), + logger + ).then((zones) => zones[0]?.id ?? null) + ); + } + + const cachedZone = await zoneIdCache.get(cacheKey); + if (cachedZone) { + return cachedZone; + } + + hostPieces.shift(); + } + + throw new UserError( + `Could not find zone for \`${from.host}\`. Make sure the domain is set up to be proxied by Cloudflare.\nFor more details, refer to https://developers.cloudflare.com/workers/configuration/routing/routes/#set-up-a-route`, + { telemetryMessage: "zones route zone not found" } + ); +} diff --git a/packages/deploy-helpers/tests/index.test.ts b/packages/deploy-helpers/tests/index.test.ts new file mode 100644 index 0000000..de82c52 --- /dev/null +++ b/packages/deploy-helpers/tests/index.test.ts @@ -0,0 +1,26 @@ +import { initDeployHelpersContext } from "@cloudflare/deploy-helpers"; +import { logger } from "@cloudflare/deploy-helpers/context"; +import { describe, it } from "vitest"; + +describe("context singleton", () => { + // Verifies that both package entry points (. and ./context) share the same + // context module. This only holds if tsup's splitting is enabled — if it's + // disabled, each entry bundles its own copy and this test will fail. + it("init from main entry propagates to context entry", ({ expect }) => { + const mockLogger = { debug: () => {}, log: () => {} }; + + initDeployHelpersContext({ + logger: mockLogger as never, + fetchResult: (() => {}) as never, + fetchListResult: (() => {}) as never, + fetchPagedListResult: (() => {}) as never, + fetchKVGetValue: (() => {}) as never, + confirm: (() => {}) as never, + prompt: (() => {}) as never, + select: (() => {}) as never, + isNonInteractiveOrCI: () => false, + }); + + expect(logger).toBe(mockLogger); + }); +}); diff --git a/packages/deploy-helpers/tests/package-dependencies.test.ts b/packages/deploy-helpers/tests/package-dependencies.test.ts new file mode 100644 index 0000000..0d8e953 --- /dev/null +++ b/packages/deploy-helpers/tests/package-dependencies.test.ts @@ -0,0 +1,343 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { describe, it } from "vitest"; +import { collectPackageDependencies } from "../src/deploy/helpers/package-dependencies"; + +describe("collectPackageDependencies", () => { + runInTempDir(); + + it("should return undefined when no package.json exists", async ({ + expect, + }) => { + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should return undefined when package.json has no dependencies", async ({ + expect, + }) => { + fs.writeFileSync( + "package.json", + JSON.stringify({ name: "test-project" }, null, 2) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should skip workspace dependencies", async ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "local-package": "workspace:*", + "another-local": "workspace:^", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should skip catalog dependencies", async ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "catalog-package": "catalog:", + "catalog-named": "catalog:default", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should collect public package dependencies", async ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + const packagePath = path.join(nodeModulesPath, "test-public-package"); + fs.mkdirSync(packagePath, { recursive: true }); + fs.writeFileSync(path.join(packagePath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(packagePath, "package.json"), + JSON.stringify( + { + name: "test-public-package", + version: "1.2.3", + }, + null, + 2 + ) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "test-public-package": "^1.0.0", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toEqual([ + { + name: "test-public-package", + packageJsonVersion: "^1.0.0", + installedVersion: "1.2.3", + }, + ]); + }); + + it("should collect from both dependencies and devDependencies", async ({ + expect, + }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + + // Create a regular dependency + const depPath = path.join(nodeModulesPath, "prod-dep"); + fs.mkdirSync(depPath, { recursive: true }); + fs.writeFileSync(path.join(depPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(depPath, "package.json"), + JSON.stringify({ name: "prod-dep", version: "1.0.0" }, null, 2) + ); + + // Create a dev dependency + const devDepPath = path.join(nodeModulesPath, "dev-dep"); + fs.mkdirSync(devDepPath, { recursive: true }); + fs.writeFileSync(path.join(devDepPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(devDepPath, "package.json"), + JSON.stringify({ name: "dev-dep", version: "2.0.0" }, null, 2) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "prod-dep": "^1.0.0", + }, + devDependencies: { + "dev-dep": "^2.0.0", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toEqual([ + { + name: "prod-dep", + packageJsonVersion: "^1.0.0", + installedVersion: "1.0.0", + }, + { + name: "dev-dep", + packageJsonVersion: "^2.0.0", + installedVersion: "2.0.0", + }, + ]); + }); + + it("should skip file: and link: dependencies", async ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "local-file-pkg": "file:../local-file-pkg", + "local-link-pkg": "link:../local-link-pkg", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should skip dependencies that cannot be resolved", async ({ expect }) => { + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "nonexistent-package": "^1.0.0", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeUndefined(); + }); + + it("should handle mixed dependencies correctly", async ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + + // Create an installed package + const publicPath = path.join(nodeModulesPath, "public-pkg"); + fs.mkdirSync(publicPath, { recursive: true }); + fs.writeFileSync(path.join(publicPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(publicPath, "package.json"), + JSON.stringify({ name: "public-pkg", version: "1.0.0" }, null, 2) + ); + + // Create another installed package + const anotherPath = path.join(nodeModulesPath, "another-pkg"); + fs.mkdirSync(anotherPath, { recursive: true }); + fs.writeFileSync(path.join(anotherPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(anotherPath, "package.json"), + JSON.stringify({ name: "another-pkg", version: "2.0.0" }, null, 2) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "public-pkg": "^1.0.0", + "another-pkg": "^2.0.0", + "workspace-pkg": "workspace:^", + "local-pkg": "file:../local-pkg", + "nonexistent-pkg": "^3.0.0", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toEqual([ + { + name: "public-pkg", + packageJsonVersion: "^1.0.0", + installedVersion: "1.0.0", + }, + { + name: "another-pkg", + packageJsonVersion: "^2.0.0", + installedVersion: "2.0.0", + }, + ]); + }); + + it("should cap at 200 entries", async ({ expect }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + const dependencies: Record = {}; + + // Create 210 packages + for (let i = 0; i < 210; i++) { + const pkgName = `pkg-${String(i).padStart(3, "0")}`; + const pkgPath = path.join(nodeModulesPath, pkgName); + fs.mkdirSync(pkgPath, { recursive: true }); + fs.writeFileSync(path.join(pkgPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(pkgPath, "package.json"), + JSON.stringify({ name: pkgName, version: `1.0.${i}` }, null, 2) + ); + dependencies[pkgName] = `^1.0.${i}`; + } + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + expect(result).toBeDefined(); + expect(result).toHaveLength(200); + }); + + it("should use devDependencies version when duplicate exists in dependencies", async ({ + expect, + }) => { + const nodeModulesPath = path.join(process.cwd(), "node_modules"); + + const pkgPath = path.join(nodeModulesPath, "shared-pkg"); + fs.mkdirSync(pkgPath, { recursive: true }); + fs.writeFileSync(path.join(pkgPath, "index.js"), "module.exports = {}"); + fs.writeFileSync( + path.join(pkgPath, "package.json"), + JSON.stringify({ name: "shared-pkg", version: "1.5.0" }, null, 2) + ); + + fs.writeFileSync( + "package.json", + JSON.stringify( + { + name: "test-project", + dependencies: { + "shared-pkg": "^1.0.0", + }, + devDependencies: { + "shared-pkg": "^1.5.0", + }, + }, + null, + 2 + ) + ); + + const result = await collectPackageDependencies(process.cwd()); + + // devDependencies spread over dependencies, so devDependencies version wins + expect(result).toEqual([ + { + name: "shared-pkg", + packageJsonVersion: "^1.5.0", + installedVersion: "1.5.0", + }, + ]); + }); +}); diff --git a/packages/deploy-helpers/tests/sourcemap.test.ts b/packages/deploy-helpers/tests/sourcemap.test.ts new file mode 100644 index 0000000..b4fb831 --- /dev/null +++ b/packages/deploy-helpers/tests/sourcemap.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from "vitest"; +import { getSourceMappedString } from "../src/deploy/helpers/sourcemap"; + +describe("getSourceMappedString", () => { + it("returns original value when source mapping throws", ({ expect }) => { + const value = `Error: test\n at Object. (/some/file.js:1:1)`; + + const result = getSourceMappedString(value, () => { + throw new Error("simulated source map failure"); + }); + + expect(result).toBe(value); + }); +}); diff --git a/packages/deploy-helpers/tests/tsconfig.json b/packages/deploy-helpers/tests/tsconfig.json new file mode 100644 index 0000000..094c387 --- /dev/null +++ b/packages/deploy-helpers/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "preserve", + "types": ["node"] + }, + "include": ["../*.d.ts", "**/*.ts", "**/*.js"] +} diff --git a/packages/deploy-helpers/tsconfig.json b/packages/deploy-helpers/tsconfig.json new file mode 100644 index 0000000..1fd9f9e --- /dev/null +++ b/packages/deploy-helpers/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["dist", "node_modules", "**/__tests__/**", "**/*.test.ts"] +} diff --git a/packages/deploy-helpers/tsup.config.ts b/packages/deploy-helpers/tsup.config.ts new file mode 100644 index 0000000..f1c8fb8 --- /dev/null +++ b/packages/deploy-helpers/tsup.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from "tsup"; + +export default defineConfig(() => [ + { + treeshake: true, + keepNames: true, + // Two entry points share context.ts as a singleton. esbuild's default + // `splitting: true` dedupes it into a shared chunk. If splitting is + // disabled, each entry bundles its own copy and init (via one entry) + // won't populate globals read via the other. Keep splitting enabled. + entry: { + index: "src/index.ts", + context: "src/shared/context.ts", + }, + platform: "node", + format: "esm", + dts: true, + outDir: "dist", + tsconfig: "tsconfig.json", + metafile: true, + sourcemap: process.env.SOURCEMAPS !== "false", + noExternal: [ + "@cloudflare/config", + "@cloudflare/containers-shared", + /^@cloudflare\/workers-shared(\/.*)?$/, + ], + external: [ + /^@cloudflare\//, + "blake3-wasm", + "miniflare", + "p-queue", + "pretty-bytes", + "undici", + "chalk", + "dotenv", + "command-exists", + "esbuild", + // Keep zod external so wrangler (the only consumer) bundles a single + // shared copy rather than inlining one here. + /^zod(\/.*)?$/, + ], + }, +]); diff --git a/packages/deploy-helpers/turbo.json b/packages/deploy-helpers/turbo.json new file mode 100644 index 0000000..a90e902 --- /dev/null +++ b/packages/deploy-helpers/turbo.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "!**/__tests__/**"], + "outputs": ["dist/**"], + "env": ["SOURCEMAPS"], + "passThroughEnv": ["PWD"] + }, + "test:ci": { + "dependsOn": ["build"], + "env": ["LC_ALL", "TZ"] + } + } +} diff --git a/packages/deploy-helpers/vitest.config.mts b/packages/deploy-helpers/vitest.config.mts new file mode 100644 index 0000000..c9db5d1 --- /dev/null +++ b/packages/deploy-helpers/vitest.config.mts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 15_000, + pool: "forks", + include: ["**/tests/**/*.test.ts"], + reporters: ["default"], + mockReset: true, + unstubEnvs: true, + }, +}); diff --git a/packages/devprod-status-bot/CHANGELOG.md b/packages/devprod-status-bot/CHANGELOG.md new file mode 100644 index 0000000..ec289d1 --- /dev/null +++ b/packages/devprod-status-bot/CHANGELOG.md @@ -0,0 +1,121 @@ +# devprod-status-bot + +## 1.5.0 + +### Minor Changes + +- [#13911](https://github.com/cloudflare/workers-sdk/pull/13911) [`2115e8a`](https://github.com/cloudflare/workers-sdk/commit/2115e8af048cd11bfb80008b2bf9ed49e9e52cc8) Thanks [@WillTaylorDev](https://github.com/WillTaylorDev)! - Post a Chat alert when an issue is opened with the `api` label, or when the `api` label is added to an existing issue + + Routes alerts to a new `API_ISSUES_WEBHOOK` so the team room watching the Workers SDK community channel gets realtime visibility into incoming api-tagged issues. + +## 1.4.2 + +### Patch Changes + +- [#13850](https://github.com/cloudflare/workers-sdk/pull/13850) [`1527003`](https://github.com/cloudflare/workers-sdk/commit/1527003a0f98f37eda3d9664a43e2c5ef87c515d) Thanks [@penalosa](https://github.com/penalosa)! - Migrate bot message generation off the deprecated `@cf/meta/llama-2-7b-chat-int8` Workers AI model + + Workers AI is deprecating Llama 2 7B chat (alongside several other older models) on May 30th 2026. The status bot now uses `@cf/google/gemma-4-26b-a4b-it` (Gemma 4) for generating its short, friendly chat messages. + +## 1.4.1 + +### Patch Changes + +- [#12435](https://github.com/cloudflare/workers-sdk/pull/12435) [`c2163df`](https://github.com/cloudflare/workers-sdk/commit/c2163df17f1c7b7fe96f10fbca35dc19ee65b7e2) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Simplify Version Packages PR CI failure alerts + + The bot now sends an alert for any failing CI job on the Version Packages PR, instead of first fetching the required status checks from GitHub's branch protection API and filtering. This removes unnecessary complexity and ensures all CI failures are reported. + +## 1.4.0 + +### Minor Changes + +- [#12371](https://github.com/cloudflare/workers-sdk/pull/12371) [`50ad9a9`](https://github.com/cloudflare/workers-sdk/commit/50ad9a9ad9581122f83b9da245f10c9d677cc848) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Send alert to ANT: Alerts chat on failed CI checks in Version Packages PRs + + When a required CI check fails or times out on the Version Packages PR (`changeset-release/main` branch), an alert is now sent to the ANT: Alerts Google Chat channel. This helps the team quickly identify and address CI failures that shouldn't occur since individual PRs have already passed before landing on main. + + Alerts for the same PR are grouped into the same chat thread using the PR number as the thread ID. + +## 1.3.0 + +### Minor Changes + +- [#11916](https://github.com/cloudflare/workers-sdk/pull/11916) [`0b249a1`](https://github.com/cloudflare/workers-sdk/commit/0b249a103e981b6f3f92290dbb65448ecd65739a) Thanks [@emily-shen](https://github.com/emily-shen)! - Notify when security advisories are submitted to workers-sdk + +## 1.2.3 + +### Patch Changes + +- [#11333](https://github.com/cloudflare/workers-sdk/pull/11333) [`474910a`](https://github.com/cloudflare/workers-sdk/commit/474910ac683bdb9b6c95fa4ab03623574d34ffc1) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - fix the security classification logic + +## 1.2.2 + +### Patch Changes + +- [#11315](https://github.com/cloudflare/workers-sdk/pull/11315) [`2309ec1`](https://github.com/cloudflare/workers-sdk/commit/2309ec1590490c803f254154bf731cdcd0eb3704) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Tighten up the security checks on issues to avoid false positives + +## 1.2.1 + +### Patch Changes + +- [#10270](https://github.com/cloudflare/workers-sdk/pull/10270) [`7caf49e`](https://github.com/cloudflare/workers-sdk/commit/7caf49ef5e25ec3326dfc46f157057ec109a3131) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - fix security alerts to avoid being in the same notification thread + +- [#10269](https://github.com/cloudflare/workers-sdk/pull/10269) [`469f6ee`](https://github.com/cloudflare/workers-sdk/commit/469f6eec9a2a2d29317720f0be6af5bd1fa02404) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Remove PR summary messages and PR update notifications from devprod-status-bot Worker + +## 1.2.0 + +### Minor Changes + +- [#10214](https://github.com/cloudflare/workers-sdk/pull/10214) [`b9d3174`](https://github.com/cloudflare/workers-sdk/commit/b9d317490a0236f3472646b175b75891290e41ef) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Add GitHub issue security analysis feature to detect and alert on potential vulnerability reports + +## 1.1.7 + +### Patch Changes + +- [#9649](https://github.com/cloudflare/workers-sdk/pull/9649) [`ec9b417`](https://github.com/cloudflare/workers-sdk/commit/ec9b417f8ed711e7b5044410e83d781f123a6a62) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - patch release to trigger a test release + +## 1.1.6 + +### Patch Changes + +- [#9033](https://github.com/cloudflare/workers-sdk/pull/9033) [`2c50115`](https://github.com/cloudflare/workers-sdk/commit/2c501151d3d1a563681cdb300a298b83862b60e2) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - chore: convert wrangler.toml files into wrangler.jsonc ones + +## 1.1.5 + +### Patch Changes + +- [`07613d3`](https://github.com/cloudflare/workers-sdk/commit/07613d3b231779466ca2528ce07385552ec73501) Thanks [@penalosa](https://github.com/penalosa)! - Trigger release after testing release process + +## 1.1.4 + +### Patch Changes + +- [#7486](https://github.com/cloudflare/workers-sdk/pull/7486) [`7e40e14`](https://github.com/cloudflare/workers-sdk/commit/7e40e145a8c3642c493a2f082eb32a1231c62ff8) Thanks [@penalosa](https://github.com/penalosa)! - Limit status bot repos + +## 1.1.3 + +### Patch Changes + +- [#7143](https://github.com/cloudflare/workers-sdk/pull/7143) [`4d7ce6f`](https://github.com/cloudflare/workers-sdk/commit/4d7ce6fd9fc80a0920a97dae14726c79012337b1) Thanks [@emily-shen](https://github.com/emily-shen)! - chore: enable observability on our internal infra Workers + bots + +## 1.1.2 + +### Patch Changes + +- [#6236](https://github.com/cloudflare/workers-sdk/pull/6236) [`26d0afc`](https://github.com/cloudflare/workers-sdk/commit/26d0afca80b2baf8ef79b1f4330e606b7d692adc) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - chore: add account_id to wrangler.toml to fix deployment + +## 1.1.1 + +### Patch Changes + +- [#6220](https://github.com/cloudflare/workers-sdk/pull/6220) [`c3cf009`](https://github.com/cloudflare/workers-sdk/commit/c3cf00984c16d3e865059fbd0f05fff28d8668cc) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - add Edmund and Emily to the contributors list + +## 1.1.0 + +### Minor Changes + +- [#5958](https://github.com/cloudflare/workers-sdk/pull/5958) [`93f7255`](https://github.com/cloudflare/workers-sdk/commit/93f725588a96a8baee408b273a57eb2dba9280d9) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - feat: add Andy to the list of internal contributors + +## 1.0.0 + +### Major Changes + +- [#5901](https://github.com/cloudflare/workers-sdk/pull/5901) [`ddf43da`](https://github.com/cloudflare/workers-sdk/commit/ddf43da91e4ec46249e38ccbb9498bb22085ebe1) Thanks [@penalosa](https://github.com/penalosa)! - feat: Add & release the initial version of the status bot diff --git a/packages/devprod-status-bot/package.json b/packages/devprod-status-bot/package.json new file mode 100644 index 0000000..127d5b1 --- /dev/null +++ b/packages/devprod-status-bot/package.json @@ -0,0 +1,22 @@ +{ + "name": "@cloudflare/devprod-status-bot", + "version": "1.5.0", + "private": true, + "scripts": { + "check:type": "tsc", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "@octokit/types": "^13.8.0", + "@octokit/webhooks-types": "^7.6.1", + "ts-dedent": "^2.2.0", + "wrangler": "workspace:*" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "deploy": true + } +} diff --git a/packages/devprod-status-bot/src/index.ts b/packages/devprod-status-bot/src/index.ts new file mode 100644 index 0000000..531b2c5 --- /dev/null +++ b/packages/devprod-status-bot/src/index.ts @@ -0,0 +1,866 @@ +import dedent from "ts-dedent"; +import type { + CheckRunCompletedEvent, + IssueCommentEvent, + IssuesEvent, + Schema, + WebhookEvent, +} from "@octokit/webhooks-types"; + +async function getBotMessage(ai: Ai, prompt: string) { + const chat = { + messages: [ + { + role: "system", + content: + "You are the ANT status bot, a helpful assistant who assists the ANT team by posting helpful updates in Google Chat", + }, + { + role: "user", + content: prompt, + }, + ], + } satisfies ChatCompletionsMessagesInput; + const message = (await ai.run("@cf/google/gemma-4-26b-a4b-it", chat)) as { + choices?: { message: { content: string } }[]; + }; + const content = message.choices?.[0]?.message.content; + if (!content) { + return "I'm feeling a bit poorly 🥲—try asking me for a message later!"; + } + return content; +} + +async function isWranglerTeamMember( + apiToken: string, + username: string +): Promise { + try { + const response = await fetch( + `https://api.github.com/orgs/cloudflare/teams/wrangler/memberships/${username}`, + { + headers: { + "User-Agent": "Cloudflare ANT Status bot", + Authorization: `Bearer ${apiToken}`, + Accept: "application/vnd.github+json", + }, + } + ); + + return response.status === 200; + } catch (error) { + // If there's an error checking membership, default to false + console.error("Error checking team membership:", error); + return false; + } +} + +async function checkForSecurityIssue( + ai: Ai, + apiToken: string, + message: Schema +): Promise { + const result = isIssueOrPREvent(message); + if (!result) { + return null; + } + + if (await isWranglerTeamMember(apiToken, result.event.issue.user.login)) { + return null; + } + + // Ignore dependabot updates + if (result.event.issue.user.login === "dependabot[bot]") { + return null; + } + + // Ignore our own bot's PRs (e.g. Version Packages) + if (result.event.issue.user.login === "workers-devprod") { + return null; + } + + const systemRole = dedent` + ## System Role: + You are an expert Security Triage Analyst and a software developer with deep knowledge of Common Weakness Enumeration (CWE) and security best practices. Your task is to analyze a GitHub Issue and determine the likelihood that it is reporting a genuine security vulnerability or exploit (not just a functional bug). + `; + const prompt = dedent` + ## Task + Analyze the provided GitHub Issue details (Title, Body, Comments, Labels) and classify it into one of two categories: "SECURITY VULNERABILITY" or "GENERAL BUG/FEATURE". + + ## Analysis Guidelines + Focus your analysis on identifying language, context, and details indicative of a security report. Key indicators include, but are not limited to: + - Impact: Does the issue describe a potential compromise of Confidentiality, Integrity, or Availability (CIA)? (e.g., unauthorized access, data loss, denial of service). + - Vulnerability Types: Mentions of common exploit classes (e.g., XSS, SQL Injection, Buffer Overflow, RCE, CSRF, insecure deserialization, broken access control, hardcoded secrets). + - Proof of Concept (PoC): Contains exploit steps, malicious input, stack traces, specific functions used to bypass security controls, or references to attack vectors. + - Terminology: Use of words like "exploit," "attack," "unauthorized," "bypass," "inject," "tainted," "secret," "leak," "data breach," or "DoS/DDoS." + - User/Privilege Context: Descriptions of an action an unprivileged user can take to affect privileged resources or other users. + + ## GitHub Issue Details: + Issue Title: ${result.event.issue.title} + Issue Body: ${result.event.issue.body || ""} + Changed Comment: ${"comment" in result.event ? result.event.comment.body : "N/A"} + + Look for keywords and patterns that suggest this is a security report, such as: + - Vulnerability, exploit, security flaw, CVE + - Authentication bypass, privilege escalation + - XSS, SQL injection, CSRF, RCE + - Unauthorized access, data exposure + - Security disclosure, responsible disclosure + + ## Output Format + Provide your response in the following structured Markdown format: + + \`\`\` + ## Triage Summary + Classification: [SECURITY VULNERABILITY or GENERAL BUG/FEATURE] + Confidence Level: [Low, Medium, or High] + + ## Rationale + [Explain in 2-3 concise sentences *why* you chose the classification. Highlight the specific keywords, behavior, or described impact that led to your decision.] + + ## Key Security Indicators Found + * [List specific keywords, code snippets, or user actions from the issue that suggest a vulnerability.] + * [Example: Describes using a special character in a username to execute a script (XSS).] + * [Example: Mentions an unauthenticated API endpoint that returns sensitive user data.] + \`\`\` + `; + + const { response } = await ai.run( + "@cf/mistralai/mistral-small-3.1-24b-instruct", + { + messages: [ + { role: "system", content: systemRole }, + { role: "user", content: prompt }, + ], + } + ); + + if (!response?.includes("SECURITY VULNERABILITY")) { + return null; + } else { + return { + type: result.type, + issueEvent: result.event, + reasoning: response, + }; + } +} + +type ProjectGQLResponse = { + data: { + organization: { + projectV2: { + id: string; + }; + }; + }; +}; +async function getProjectId(apiToken: string) { + const data = await fetch("https://api.github.com/graphql", { + headers: { + "User-Agent": "Cloudflare ANT Status bot", + Authorization: `Bearer ${apiToken}`, + }, + method: "POST", + body: JSON.stringify({ + query: `query{organization(login: "cloudflare") {projectV2(number: 1){id}}}`, + }), + }).then((r) => r.json()); + + return data.data.organization.projectV2.id; +} +type PRGQLResponse = { + data: { + repository: { + pullRequest: { + id: string; + }; + }; + }; +}; +async function getPRId(apiToken: string, repo: string, number: string) { + const data = await fetch("https://api.github.com/graphql", { + headers: { + "User-Agent": "Cloudflare ANT Status bot", + Authorization: `Bearer ${apiToken}`, + }, + method: "POST", + body: JSON.stringify({ + query: `query { + repository(owner: "cloudflare", name: "${repo}") { + pullRequest(number: ${number}) { + id + } + } + }`, + }), + }).then((r) => r.json()); + + return data.data.repository.pullRequest.id; +} + +async function addPRToProject(apiToken: string, repo: string, number: string) { + const projectId = await getProjectId(apiToken); + const prId = await getPRId(apiToken, repo, number); + return await fetch("https://api.github.com/graphql", { + headers: { + "User-Agent": "Cloudflare ANT Status bot", + Authorization: `Bearer ${apiToken}`, + }, + method: "POST", + body: JSON.stringify({ + query: `mutation { + addProjectV2ItemById(input: { + projectId: "${projectId}" + contentId: "${prId}" + }) { + item { + id + } + } + }`, + }), + }); +} + +function getThreadID(date?: string | Date, label = "-pull-requests") { + // Apply an offset so we rollover days at around 10am UTC + return ( + (date ? new Date(date) : new Date()) + .toLocaleString("en-GB", { timeZone: "Pacific/Kiritimati" }) + .split(",")[0] + label + ); +} + +async function sendMessage( + webhookUrl: string, + message: Record, + label?: string +) { + const threadId = getThreadID(new Date(), label); + + const url = new URL(webhookUrl); + + url.searchParams.set("threadKey", threadId); + url.searchParams.set( + "messageReplyOption", + "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" + ); + + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json; charset=utf-8" }, + body: JSON.stringify(message), + }); + + console.log(await response.json()); +} + +function isIssueOrPREvent( + message: WebhookEvent +): { type: "issue" | "pr"; event: IssuesEvent | IssueCommentEvent } | null { + if ( + "issue" in message && + (message.action === "opened" || + message.action === "reopened" || + message.action === "edited") + ) { + const isPR = "pull_request" in message.issue; + return { + type: isPR ? "pr" : "issue", + event: message as IssuesEvent | IssueCommentEvent, + }; + } + return null; +} + +// Repository advisory event type (not yet in @octokit/webhooks-types) +interface RepositoryAdvisoryEvent { + action: "reported" | "published"; + repository_advisory: { + ghsa_id: string; + html_url: string; + summary: string; + description: string; + }; +} + +function isRepositoryAdvisoryEvent( + message: WebhookEvent +): RepositoryAdvisoryEvent | null { + if ( + "repository_advisory" in message && + "action" in message && + message.action === "reported" + ) { + return message as RepositoryAdvisoryEvent; + } + return null; +} + +/** + * Returns the issue event if a new issue was opened with the `api` label, or + * if the `api` label was just added to an existing issue. Returns null + * otherwise. + */ +function isApiLabeledIssueEvent(message: WebhookEvent): IssuesEvent | null { + if (!("issue" in message) || !("action" in message)) { + return null; + } + if ("pull_request" in message.issue) { + return null; + } + + const event = message as IssuesEvent; + + if (event.action === "opened") { + const hasApiLabel = event.issue.labels?.some( + (label) => label.name === "api" + ); + return hasApiLabel ? event : null; + } + + if (event.action === "labeled" && event.label?.name === "api") { + return event; + } + + return null; +} + +function isCheckRunCompleted( + message: WebhookEvent +): message is CheckRunCompletedEvent { + return ( + "action" in message && + message.action === "completed" && + "check_run" in message + ); +} + +/** + * Returns information about a failed Version Packages PR check run, or null if the event is not relevant. + */ +function isVersionPackagesPRCheckRun(message: WebhookEvent): { + checkRun: CheckRunCompletedEvent["check_run"]; + prNumber: number; + owner: string; + repo: string; +} | null { + if (!isCheckRunCompleted(message)) { + return null; + } + + const checkRun = message.check_run; + + // Only process failures and timeouts (not cancelled) + if ( + checkRun.conclusion !== "failure" && + checkRun.conclusion !== "timed_out" + ) { + return null; + } + + // Check if this is from the changeset-release/main branch + if (checkRun.check_suite.head_branch !== "changeset-release/main") { + return null; + } + + // Get the PR number from pull_requests array + const pr = checkRun.pull_requests[0]; + if (!pr) { + return null; + } + + return { + checkRun, + prNumber: pr.number, + owner: message.repository.owner.login, + repo: message.repository.name, + }; +} + +async function sendSecurityAlert( + webhookUrl: string, + { + type, + issueEvent, + reasoning, + }: { + type: "issue" | "pr"; + issueEvent: IssuesEvent | IssueCommentEvent; + reasoning: string; + } +) { + const itemType = type === "pr" ? "PR" : "Issue"; + + return sendMessage( + webhookUrl, + { + cardsV2: [ + { + cardId: "unique-card-id", + card: { + header: { + title: `🚨 Potential Security ${itemType} Detected`, + subtitle: `${itemType} #${issueEvent.issue.number} in ${issueEvent.repository.full_name}`, + imageUrl: issueEvent.issue.user.avatar_url, + imageType: "CIRCLE", + imageAltText: "Reporter Avatar", + }, + sections: [ + { + collapsible: false, + widgets: [ + { + textParagraph: { + text: `Title: ${issueEvent.issue.title}\n\nReporter: ${issueEvent.issue.user.login}`, + }, + }, + { + buttonList: { + buttons: [ + { + text: `View ${itemType}`, + onClick: { + openLink: { + url: issueEvent.issue.html_url, + }, + }, + }, + ], + }, + }, + ], + }, + { + collapsible: true, + uncollapsibleWidgetsCount: 0, + widgets: [ + { + textParagraph: { + text: reasoning, + }, + }, + ], + }, + ], + }, + }, + ], + }, + "-security-alert-" + issueEvent.issue.number + ); +} + +async function sendRepositoryAdvisoryAlert( + webhookUrl: string, + advisoryEvent: RepositoryAdvisoryEvent +) { + const advisory = advisoryEvent.repository_advisory; + + return sendMessage( + webhookUrl, + { + cardsV2: [ + { + cardId: "unique-card-id", + card: { + header: { + title: `🔐 Repository Security Advisory Reported`, + subtitle: advisory.summary, + }, + sections: [ + { + collapsible: true, + widgets: [ + { + textParagraph: { + text: advisory.description, + }, + }, + ], + }, + { + collapsible: false, + widgets: [ + { + buttonList: { + buttons: [ + { + text: "View Advisory", + onClick: { + openLink: { + url: advisory.html_url, + }, + }, + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + "-repository-advisory-" + advisory.ghsa_id + ); +} + +async function sendApiIssueAlert(webhookUrl: string, event: IssuesEvent) { + return sendMessage( + webhookUrl, + { + cardsV2: [ + { + cardId: "api-issue-" + event.issue.number, + card: { + header: { + title: `🏷️ New api-labeled issue`, + subtitle: `#${event.issue.number} in ${event.repository.full_name}`, + imageUrl: event.issue.user.avatar_url, + imageType: "CIRCLE", + imageAltText: "Reporter Avatar", + }, + sections: [ + { + collapsible: false, + widgets: [ + { + textParagraph: { + text: `Title: ${event.issue.title}\n\nReporter: ${event.issue.user.login}`, + }, + }, + { + buttonList: { + buttons: [ + { + text: "View Issue", + onClick: { + openLink: { + url: event.issue.html_url, + }, + }, + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + "-api-issue-" + event.issue.number + ); +} + +/** + * Formats a duration given start and end timestamps as a string in "Mins Secs" format. + */ +function formatDuration(startedAt: string, completedAt: string): string { + const start = new Date(startedAt).getTime(); + const end = new Date(completedAt).getTime(); + const durationMs = end - start; + const minutes = Math.floor(durationMs / 60000); + const seconds = Math.floor((durationMs % 60000) / 1000); + return `${minutes}m ${seconds}s`; +} + +async function sendVersionPackagesCIFailureAlert( + webhookUrl: string, + { + checkRun, + prNumber, + owner, + repo, + }: { + checkRun: CheckRunCompletedEvent["check_run"]; + prNumber: number; + owner: string; + repo: string; + } +) { + const conclusionEmoji = checkRun.conclusion === "timed_out" ? "⏱️" : "❌"; + const conclusionText = + checkRun.conclusion === "timed_out" ? "Timed out" : "Failed"; + + return sendMessage( + webhookUrl, + { + cardsV2: [ + { + cardId: `vp-ci-failure-${checkRun.id}`, + card: { + header: { + title: `${conclusionEmoji} CI Check ${conclusionText} on Version Packages PR`, + subtitle: `PR #${prNumber} in ${owner}/${repo}`, + }, + sections: [ + { + collapsible: false, + widgets: [ + { + textParagraph: { + text: `Check: ${checkRun.name}\nConclusion: ${conclusionText}\nDuration: ${formatDuration(checkRun.started_at, checkRun.completed_at)}`, + }, + }, + { + buttonList: { + buttons: [ + { + text: "View Check Run", + onClick: { + openLink: { + url: checkRun.html_url, + }, + }, + }, + { + text: "View PR", + onClick: { + openLink: { + url: `https://github.com/${owner}/${repo}/pull/${prNumber}`, + }, + }, + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + `-vp-ci-failure-pr-${prNumber}` + ); +} + +async function sendUpcomingMeetingMessage(webhookUrl: string, ai: Ai) { + const message = await getBotMessage( + ai, + "Write a very short informative message reminding team members to check the meeting notes and add anything they want to discuss to the agenda. Make it fun and quirky!" + ); + + await sendMessage( + webhookUrl, + { + cardsV2: [ + { + cardId: "unique-card-id", + card: { + header: { + title: "📋 Upcoming meeting!", + }, + sections: [ + { + widgets: [ + { + textParagraph: { + text: message, + }, + }, + { + columns: { + columnItems: [ + { + horizontalSizeStyle: "FILL_MINIMUM_SPACE", + horizontalAlignment: "START", + verticalAlignment: "TOP", + widgets: [ + { + buttonList: { + buttons: [ + { + text: "Open Meeting Notes", + onClick: { + openLink: { + url: "https://cflare.co/ant-meeting-notes", + }, + }, + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + "meeting-notes" + ); + await sendMessage( + webhookUrl, + { + text: "cc ", + }, + "meeting-notes" + ); +} + +export default { + async fetch(request, env): Promise { + const url = new URL(request.url); + if (url.pathname === "/release-failure") { + if (request.headers.get("X-Auth-Header") !== env.PRESHARED_SECRET) { + return new Response("Not allowed", { status: 401 }); + } + const body = await request.json<{ + status: { label: string; details: string }[]; + url: string; + }>(); + await sendMessage( + env.PROD_TEAM_ONLY_WEBHOOK, + { + cardsV2: [ + { + cardId: "unique-card-id", + card: { + header: { + title: "🚨 A workers-sdk release failed!", + }, + sections: [ + { + widgets: [ + { + columns: { + columnItems: [ + { + horizontalSizeStyle: "FILL_MINIMUM_SPACE", + horizontalAlignment: "START", + verticalAlignment: "TOP", + widgets: [ + { + buttonList: { + buttons: [ + { + text: "Open Workflow run", + onClick: { + openLink: { + url: body.url, + }, + }, + }, + ], + }, + }, + ], + }, + ], + }, + }, + ], + }, + { + collapsible: true, + uncollapsibleWidgetsCount: 3, + widgets: body.status.map(({ label, details }) => { + const emoji = "🔴"; + + return [ + { + columns: { + columnItems: [ + { + horizontalSizeStyle: "FILL_AVAILABLE_SPACE", + horizontalAlignment: "START", + verticalAlignment: "CENTER", + widgets: [ + { + textParagraph: { + text: `${emoji} ${label}: ${details}`, + }, + }, + ], + }, + ], + }, + }, + ]; + }), + }, + ], + }, + }, + ], + }, + crypto.randomUUID() + ); + } + + if (url.pathname === "/github") { + const body = await request.json(); + + const maybeSecurityIssue = await checkForSecurityIssue( + env.AI, + env.GITHUB_PAT, + body + ); + // Flags suspicious issues/PRs for review + if (maybeSecurityIssue) { + await sendSecurityAlert(env.ALERTS_WEBHOOK, maybeSecurityIssue); + } + // Notifies when a repository advisory is reported to workers-sdk + const maybeRepositoryAdvisory = isRepositoryAdvisoryEvent(body); + if (maybeRepositoryAdvisory) { + await sendRepositoryAdvisoryAlert( + env.ALERTS_WEBHOOK, + maybeRepositoryAdvisory + ); + } + // Notifies when any CI check fails on the Version Packages PR + const maybeVersionPackagesFailure = isVersionPackagesPRCheckRun(body); + if (maybeVersionPackagesFailure) { + await sendVersionPackagesCIFailureAlert( + env.ALERTS_WEBHOOK, + maybeVersionPackagesFailure + ); + } + // Notifies when an issue is opened with the `api` label, or the `api` label is added to an existing issue + const maybeApiIssue = isApiLabeledIssueEvent(body); + if (maybeApiIssue) { + await sendApiIssueAlert(env.API_ISSUES_WEBHOOK, maybeApiIssue); + } + } + + if (url.pathname.startsWith("/pr-project") && request.method === "POST") { + const [_, _prefix, _repo, prNumber] = url.pathname.split("/"); + return await addPRToProject( + env.GITHUB_PAT, + "workers-sdk", + prNumber.replaceAll(/[^0-9]/g, "-") + ); + } + + if (url.pathname === "/logo.png") { + return new Response(await (await import("./status-bot.png")).default, { + headers: { + "Content-Type": "image/png", + }, + }); + } + + return new Response(""); + }, + + async scheduled(controller, env): Promise { + if (controller.cron === "0 12 * * MON,WED,FRI") { + await sendUpcomingMeetingMessage(env.PROD_TEAM_ONLY_WEBHOOK, env.AI); + } + }, +} satisfies ExportedHandler; diff --git a/packages/devprod-status-bot/src/status-bot.png b/packages/devprod-status-bot/src/status-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..92cef918c81eeebaf868842724f2de8a39da41cc GIT binary patch literal 890516 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
kCKe$07RmwaAJ=4^) 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å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 z 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 oF=AZ&ahdOt{P(5Bbp*e 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

kl?QLLr9 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 +-Jh0xT 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%0DuN9TTEyGs`>Ss|+FhV==VS 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

O`PcRypd<0#{bdC>&^N% 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

b4T9hosoBd!4$KOG(JZRx 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 { + return {}; + } +} + +class NoExchangeUrl extends HttpError { + constructor() { + super("No exchange_url provided", 400, false); + } +} + +class ExchangeFailed extends HttpError { + constructor( + readonly url: string, + readonly exchangeStatus: number, + readonly body: string + ) { + super("Exchange failed", 400, true); + } + + get data(): { url: string; status: number; body: string } { + return { url: this.url, status: this.exchangeStatus, body: this.body }; + } +} + +class TokenUpdateFailed extends HttpError { + constructor() { + super("Provide token and remote", 400, false); + } +} + +class RawHttpFailed extends HttpError { + constructor() { + super("Provide token, and remote", 400, false); + } +} + +class PreviewRequestFailed extends HttpError { + constructor( + private tokenId: string, + reportable: boolean + ) { + super("Token and remote not found", 400, reportable); + } + get data(): { tokenId: string } { + return { tokenId: this.tokenId }; + } +} + +class InvalidURL extends HttpError { + constructor(private readonly url: string) { + super("Invalid URL", 400, false); + } + get data() { + return { url: this.url }; + } +} + +function assertValidURL(maybeUrl: string) { + if (!URL.canParse(maybeUrl)) { + throw new InvalidURL(maybeUrl); + } +} + +function switchRemote(url: URL, remote: string) { + const workerUrl = new URL(url); + const remoteUrl = new URL(remote); + workerUrl.hostname = remoteUrl.hostname; + workerUrl.protocol = remoteUrl.protocol; + workerUrl.port = remoteUrl.port; + return workerUrl; +} + +function isTokenExchangeRequest(request: Request, url: URL, env: Env) { + return ( + request.method === "POST" && + url.hostname === env.PREVIEW && + url.pathname === "/exchange" + ); +} + +function isPreviewUpdateRequest(request: Request, url: URL, env: Env) { + return ( + request.method === "GET" && + url.hostname.endsWith(env.PREVIEW) && + url.pathname === "/.update-preview-token" + ); +} + +function isRawHttpRequest(url: URL, env: Env) { + return url.hostname.endsWith(env.RAW_HTTP); +} + +async function handleRequest(request: Request, env: Env) { + const url = new URL(request.url); + + if (isTokenExchangeRequest(request, url, env)) { + return await handleTokenExchange(url); + } + + if (isPreviewUpdateRequest(request, url, env)) { + return await updatePreviewToken(url, env); + } + + if (isRawHttpRequest(url, env)) { + return await handleRawHttp(request, url); + } + + /** + * Finally, if no other conditions are met, make a preview request to the worker + * This must be called with the same subdomain as used in /.update-preview-token + * so that the cookie will be present. It will swap the host and inject the preview token + * but otherwise will pass the request through unchanged + */ + const parsedCookies = cookie.parse(request.headers.get("Cookie") ?? ""); + + const tokenId = parsedCookies?.token; + + const { token, remote } = JSON.parse( + (await env.TOKEN_LOOKUP.get(tokenId)) ?? "{}" + ); + if (!token || !remote) { + // Report this error if a tokenId was provided + throw new PreviewRequestFailed(tokenId, !!tokenId); + } + + const original = await fetch( + switchRemote(url, remote), + new Request(request, { + headers: { + ...Object.fromEntries(request.headers), + "cf-workers-preview-token": token, + }, + redirect: "manual", + }) + ); + const embeddable = new Response(original.body, original); + // This will be embedded in an iframe. In particular, the Cloudflare error page sets this header. + embeddable.headers.delete("X-Frame-Options"); + return embeddable; +} + +/** + * Given a preview token, this endpoint allows for raw http calls to be inspected + * It must be called with a random subdomain (i.e. some-random-data.rawhttp.cloudflarepreviews.com) + * for consistency with the preview endpoint. This is not currently used, but may be in future + * + * It requires two parameters, passed as headers: + * - `X-CF-Token` A preview token, as in /.update-preview-token + * - `X-CF-Remote` Which endpoint to hit with preview requests, as in /.update-preview-token + */ +async function handleRawHttp(request: Request, url: URL) { + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": request.headers.get("Origin") ?? "", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Headers": + request.headers.get("Access-Control-Request-Headers") ?? + "x-cf-token,x-cf-remote", + "Access-Control-Expose-Headers": "*", + Vary: "Origin, Access-Control-Request-Headers", + }, + }); + } + + const requestHeaders = new Headers(request.headers); + + const token = requestHeaders.get("X-CF-Token"); + const remote = requestHeaders.get("X-CF-Remote"); + + // Fallback to the request method for backward compatibility + const method = requestHeaders.get("X-CF-Http-Method") ?? request.method; + + if (!token || !remote) { + throw new RawHttpFailed(); + } + + // Reassign the token to a different header + requestHeaders.set("cf-workers-preview-token", token); + + // Delete these consumed headers so as not to bloat the request. + // Some tokens can be quite large and may cause nginx to reject the + // request due to exceeding size limits if the value is included twice. + requestHeaders.delete("X-CF-Token"); + requestHeaders.delete("X-CF-Remote"); + requestHeaders.delete("X-CF-Http-Method"); + + const headerEntries = [...requestHeaders.entries()]; + + for (const header of headerEntries) { + if (header[0].startsWith("cf-ew-raw-")) { + requestHeaders.set(header[0].split("cf-ew-raw-")[1], header[1]); + requestHeaders.delete(header[0]); + } + } + + const workerResponse = await fetch(switchRemote(url, remote), { + method, + headers: requestHeaders, + body: method === "GET" || method === "HEAD" ? null : request.body, + redirect: "manual", + }); + + const responseHeaders = new Headers(workerResponse.headers); + + const rawHeaders = new Headers({ + "Access-Control-Allow-Origin": request.headers.get("Origin") ?? "", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Credentials": "true", + "cf-ew-status": workerResponse.status.toString(), + "Access-Control-Expose-Headers": "*", + Vary: "Origin", + }); + + // Pass the raw content type back so that clients can decode the body correctly + const contentType = responseHeaders.get("Content-Type"); + if (contentType) { + rawHeaders.set("Content-Type", contentType); + } + const contentEncoding = responseHeaders.get("Content-Encoding"); + if (contentEncoding) { + rawHeaders.set("Content-Encoding", contentEncoding); + } + const transferEncoding = responseHeaders.get("Transfer-Encoding"); + if (transferEncoding) { + rawHeaders.set("Transfer-Encoding", transferEncoding); + } + + // The client needs the raw headers from the worker + // Prefix them with `cf-ew-raw-`, so that response headers from _this_ worker don't interfere + const setCookieHeader = responseHeaders.getSetCookie(); + for (const setCookie of setCookieHeader) { + rawHeaders.append("cf-ew-raw-set-cookie", setCookie); + } + responseHeaders.delete("Set-Cookie"); + for (const header of responseHeaders.entries()) { + rawHeaders.set(`cf-ew-raw-${header[0]}`, header[1]); + } + + return new Response(workerResponse.body, { + ...workerResponse, + headers: rawHeaders, + }); +} + +/** + * Given a preview session, the client should obtain a specific preview token + * This endpoint takes in the URL parameters: + * - `token` The preview token to authenticate future preview requests + * Crucially, this is _different_ from the session token obtained above + * - `remote` Which endpoint to hit with preview requests + * This should be the workers.dev deployment for the current worker + * - `prewarm` A fire-and-forget prewarm endpoint to hit to start up the preview + * - `suffix` (optional) The pathname + search to hit on the preview worker once redirected + * + * It must be called with a random subdomain (i.e. some-random-data.preview.cloudflarepreviews.com) + * to provide cookie isolation for the preview. + * + * It will redirect to the suffix provide, setting a cookie with the `token` and `remote` + * for future use. + */ +async function updatePreviewToken(url: URL, env: Env) { + const token = url.searchParams.get("token"); + const remote = url.searchParams.get("remote"); + if (!token || !remote) { + throw new TokenUpdateFailed(); + } + + assertValidURL(remote); + + // The token can sometimes be too large for a cookie (4096 bytes). + // Store the token in KV, and allow lookups + + const tokenId = crypto.randomUUID(); + + await env.TOKEN_LOOKUP.put(tokenId, JSON.stringify({ token, remote }), { + // A preview token should only be valid for an hour. + // Store it for 2 just in case + expirationTtl: 60 * 60 * 2, + }); + + return new Response(null, { + status: 307, + headers: { + Location: url.searchParams.get("suffix") ?? "/", + "Set-Cookie": cookie.serialize("token", tokenId, { + secure: true, + sameSite: "none", + httpOnly: true, + domain: url.hostname, + partitioned: true, + }), + }, + }); +} + +/** + * Request the preview session associated with a given exchange_url + * exchange_url comes from an authenticated core API call made in the client + */ +async function handleTokenExchange(url: URL) { + const exchangeUrl = url.searchParams.get("exchange_url"); + if (!exchangeUrl) { + throw new NoExchangeUrl(); + } + assertValidURL(exchangeUrl); + const exchangeRes = await fetch(exchangeUrl); + if (exchangeRes.status !== 200) { + const exchange = new URL(exchangeUrl); + // Clear sensitive token + exchange.search = ""; + + throw new ExchangeFailed( + exchange.href, + exchangeRes.status, + await exchangeRes.text() + ); + } + const session = await exchangeRes.json<{ + prewarm: string; + token: string; + }>(); + if ( + typeof session.token !== "string" || + typeof session.prewarm !== "string" + ) { + const exchange = new URL(exchangeUrl); + // Clear sensitive token + exchange.search = ""; + throw new ExchangeFailed( + exchange.href, + exchangeRes.status, + JSON.stringify(session) + ); + } + return Response.json(session, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST", + }, + }); +} + +// No ecosystem routers support hostname matching 😥 +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const registry = new MetricsRegistry(); + const requestCounter = registry.createCounter( + "devprod_edge_preview_authenticated_proxy_request_total", + "Request counter for DevProd's edge-preview-authenticated-proxy service" + ); + requestCounter.inc(); + + const sentry = new Toucan({ + dsn: env.SENTRY_DSN, + context: ctx, + request, + requestDataOptions: { + allowedHeaders: [ + "user-agent", + "accept-encoding", + "accept-language", + "cf-ray", + "content-length", + "content-type", + "host", + ], + }, + transportOptions: { + headers: { + "CF-Access-Client-ID": env.SENTRY_ACCESS_CLIENT_ID, + "CF-Access-Client-Secret": env.SENTRY_ACCESS_CLIENT_SECRET, + }, + }, + }); + + try { + return await handleRequest(request, env); + } catch (e) { + console.error(e); + if (e instanceof HttpError) { + if (e.reportable) { + sentry.setContext("Details", e.data); + sentry.captureException(e); + } + return e.toResponse(); + } else { + sentry.captureException(e); + const errorCounter = registry.createCounter( + "devprod_edge_preview_authenticated_proxy_error_total", + "Error counter for DevProd's edge-preview-authenticated-proxy service" + ); + errorCounter.inc(); + + return Response.json( + { + error: "UnexpectedError", + message: "Something went wrong", + }, + { + status: 500, + } + ); + } + } finally { + ctx.waitUntil( + fetch("https://workers-logging.cfdata.org/prometheus", { + method: "POST", + headers: { + Authorization: `Bearer ${env.PROMETHEUS_TOKEN}`, + }, + body: registry.metrics(), + }) + ); + } + }, +}; diff --git a/packages/edge-preview-authenticated-proxy/tests/index.test.ts b/packages/edge-preview-authenticated-proxy/tests/index.test.ts new file mode 100644 index 0000000..3774cf7 --- /dev/null +++ b/packages/edge-preview-authenticated-proxy/tests/index.test.ts @@ -0,0 +1,487 @@ +import { randomBytes } from "node:crypto"; +import { exports } from "cloudflare:workers"; +import { afterEach, beforeEach, describe, it, vi } from "vitest"; + +// Mock URL for the remote worker - all outbound fetches will be intercepted +const MOCK_REMOTE_URL = "http://mock-remote.test"; + +function removeUUID(str: string) { + return str.replace( + /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/g, + "00000000-0000-0000-0000-000000000000" + ); +} + +// Mock implementation for the remote worker +function createMockFetchImplementation() { + return async (input: Request | string | URL, init?: RequestInit) => { + const request = new Request(input, init); + const url = new URL(request.url); + + if (url.origin === "https://workers-logging.cfdata.org") { + // Ignore prometheus logging requests + return new Response("OK", { status: 200 }); + } + + // Only intercept requests to our mock remote URL + if (url.origin !== MOCK_REMOTE_URL) { + console.error(`Request to unexpected URL: ${request.url}`); + return new Response("BAD", { status: 500 }); + } + + if (url.pathname === "/exchange") { + return Response.json({ + token: "TEST_TOKEN", + prewarm: "TEST_PREWARM", + }); + } + if (url.pathname === "/redirect") { + // Use manual redirect to avoid trailing slash being added + return new Response(null, { + status: 302, + headers: { Location: "https://example.com" }, + }); + } + if (url.pathname === "/method") { + // HEAD requests should return empty body + const body = request.method === "HEAD" ? null : request.method; + return new Response(body, { + headers: { "Test-Http-Method": request.method }, + }); + } + if (url.pathname === "/status") { + return new Response("407"); + } + if (url.pathname === "/header") { + return new Response(request.headers.get("X-Custom-Header")); + } + if (url.pathname === "/cookies") { + const headers = new Headers(); + headers.append("Set-Cookie", "foo=1"); + headers.append("Set-Cookie", "bar=2"); + return new Response(undefined, { headers }); + } + return Response.json( + { + url: request.url, + headers: [...request.headers.entries()], + }, + { headers: { "Content-Encoding": "identity" } } + ); + }; +} + +beforeEach(() => { + vi.spyOn(globalThis, "fetch").mockImplementation( + createMockFetchImplementation() + ); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("Preview Worker", () => { + it("should obtain token from exchange_url", async ({ expect }) => { + const resp = await exports.default.fetch( + `https://preview.cloudflarepreviews.com/exchange?exchange_url=${encodeURIComponent( + `${MOCK_REMOTE_URL}/exchange` + )}`, + { + method: "POST", + } + ); + const text = await resp.json(); + expect(text).toMatchInlineSnapshot( + ` + { + "prewarm": "TEST_PREWARM", + "token": "TEST_TOKEN", + } + ` + ); + }); + it("should reject invalid exchange_url", async ({ expect }) => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const resp = await exports.default.fetch( + `https://preview.cloudflarepreviews.com/exchange?exchange_url=not_an_exchange_url`, + { method: "POST" } + ); + expect(resp.status).toBe(400); + expect(await resp.text()).toMatchInlineSnapshot( + `"{"error":"Error","message":"Invalid URL"}"` + ); + }); + it("should allow tokens > 4096 bytes", async ({ expect }) => { + // 4096 is the size limit for cookies + const token = randomBytes(4096).toString("hex"); + expect(token.length).toBe(8192); + + let resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=${encodeURIComponent( + token + )}&remote=${encodeURIComponent( + MOCK_REMOTE_URL + )}&suffix=${encodeURIComponent("/hello?world")}`, + { + method: "GET", + redirect: "manual", + } + ); + expect(await resp.text()).toMatchInlineSnapshot('""'); + expect(resp.headers.get("location")).toMatchInlineSnapshot( + '"/hello?world"' + ); + expect( + removeUUID(resp.headers.get("set-cookie") ?? "") + ).toMatchInlineSnapshot( + '"token=00000000-0000-0000-0000-000000000000; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None"' + ); + const tokenId = (resp.headers.get("set-cookie") ?? "") + .split(";")[0] + .split("=")[1]; + resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com`, + { + method: "GET", + headers: { + cookie: `token=${tokenId}`, + }, + } + ); + + const json = await resp.json(); + expect(json).toMatchObject({ + url: `${MOCK_REMOTE_URL}/`, + headers: expect.arrayContaining([["cf-workers-preview-token", token]]), + }); + }); + it("should be redirected with cookie", async ({ expect }) => { + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( + MOCK_REMOTE_URL + )}&suffix=${encodeURIComponent("/hello?world")}`, + { + method: "GET", + redirect: "manual", + } + ); + expect(resp.headers.get("location")).toMatchInlineSnapshot( + '"/hello?world"' + ); + expect( + removeUUID(resp.headers.get("set-cookie") ?? "") + ).toMatchInlineSnapshot( + '"token=00000000-0000-0000-0000-000000000000; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None"' + ); + }); + + async function getToken() { + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( + MOCK_REMOTE_URL + )}&suffix=${encodeURIComponent("/hello?world")}`, + { + method: "GET", + redirect: "manual", + } + ); + return (resp.headers.get("set-cookie") ?? "").split(";")[0].split("=")[1]; + } + it("should reject invalid remote url", async ({ expect }) => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/.update-preview-token?token=TEST_TOKEN&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}` + ); + expect(resp.status).toBe(400); + expect(await resp.text()).toMatchInlineSnapshot( + `"{"error":"Error","message":"Invalid URL"}"` + ); + }); + + it("should convert cookie to header", async ({ expect }) => { + const tokenId = await getToken(); + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com`, + { + method: "GET", + headers: { + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, + }, + } + ); + + const json = (await resp.json()) as { headers: string[][]; url: string }; + expect(json).toMatchObject({ + url: `${MOCK_REMOTE_URL}/`, + headers: expect.arrayContaining([ + ["cf-workers-preview-token", "TEST_TOKEN"], + ]), + }); + }); + it("should not follow redirects", async ({ expect }) => { + const tokenId = await getToken(); + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/redirect`, + { + method: "GET", + headers: { + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, + }, + redirect: "manual", + } + ); + + expect(resp.status).toMatchInlineSnapshot("302"); + expect(resp.headers.get("Location")).toMatchInlineSnapshot( + '"https://example.com"' + ); + expect(await resp.text()).toMatchInlineSnapshot('""'); + }); + it("should return method", async ({ expect }) => { + const tokenId = await getToken(); + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/method`, + { + method: "PUT", + headers: { + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, + }, + redirect: "manual", + } + ); + + expect(await resp.text()).toMatchInlineSnapshot('"PUT"'); + }); + it("should return header", async ({ expect }) => { + const tokenId = await getToken(); + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/header`, + { + method: "PUT", + headers: { + "X-Custom-Header": "custom", + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, + }, + redirect: "manual", + } + ); + + expect(await resp.text()).toMatchInlineSnapshot('"custom"'); + }); + it("should return status", async ({ expect }) => { + const tokenId = await getToken(); + const resp = await exports.default.fetch( + `https://random-data.preview.cloudflarepreviews.com/status`, + { + method: "PUT", + headers: { + cookie: `token=${tokenId}; Domain=random-data.preview.cloudflarepreviews.com; HttpOnly; Secure; Partitioned; SameSite=None`, + }, + redirect: "manual", + } + ); + + expect(await resp.text()).toMatchInlineSnapshot('"407"'); + }); +}); + +describe("Raw HTTP preview", () => { + it("should allow arbitrary headers in cross-origin requests", async ({ + expect, + }) => { + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com`, + { + method: "OPTIONS", + headers: { + "Access-Control-Request-Headers": "foo", + origin: "https://cloudflare.dev", + }, + } + ); + + expect(resp.headers.get("Access-Control-Allow-Headers")).toBe("foo"); + }); + + it("should allow arbitrary methods in cross-origin requests", async ({ + expect, + }) => { + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com`, + { + method: "OPTIONS", + headers: { + "Access-Control-Request-Method": "PUT", + origin: "https://cloudflare.dev", + }, + } + ); + + expect(resp.headers.get("Access-Control-Allow-Methods")).toBe("*"); + }); + + it("should preserve multiple cookies", async ({ expect }) => { + const token = randomBytes(4096).toString("hex"); + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com/cookies`, + { + method: "GET", + headers: { + "Access-Control-Request-Method": "GET", + origin: "https://cloudflare.dev", + "X-CF-Token": token, + "X-CF-Remote": MOCK_REMOTE_URL, + }, + } + ); + + expect(resp.headers.get("cf-ew-raw-set-cookie")).toMatchInlineSnapshot( + `"foo=1, bar=2"` + ); + }); + + it("should pass headers to the user-worker", async ({ expect }) => { + const token = randomBytes(4096).toString("hex"); + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com/`, + { + method: "GET", + headers: { + "Access-Control-Request-Method": "GET", + origin: "https://cloudflare.dev", + "X-CF-Token": token, + "X-CF-Remote": MOCK_REMOTE_URL, + "Some-Custom-Header": "custom", + Accept: "application/json", + }, + } + ); + + const body = (await resp.json()) as Record; + + const headers = (body.headers as [string, string][]).filter( + (h) => h[0] === "some-custom-header" || h[0] === "accept" + ); + + // This contains some-custom-header & accept, as expected + expect(headers).toMatchInlineSnapshot(` + [ + [ + "accept", + "application/json", + ], + [ + "some-custom-header", + "custom", + ], + ] + `); + }); + + it("should use the method specified on the X-CF-Http-Method header", async ({ + expect, + }) => { + const token = randomBytes(4096).toString("hex"); + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com/method`, + { + method: "POST", + headers: { + origin: "https://cloudflare.dev", + "X-CF-Token": token, + "X-CF-Remote": MOCK_REMOTE_URL, + "X-CF-Http-Method": "PUT", + }, + } + ); + + expect(await resp.text()).toEqual("PUT"); + }); + + it.for(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])( + "should support %s method specified on the X-CF-Http-Method header", + async (method, { expect }) => { + const token = randomBytes(4096).toString("hex"); + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com/method`, + { + method: "POST", + headers: { + origin: "https://cloudflare.dev", + "X-CF-Token": token, + "X-CF-Remote": MOCK_REMOTE_URL, + "X-CF-Http-Method": method, + }, + } + ); + + // HEAD request does not return any body. So we will confirm by asserting the response header + expect(await resp.text()).toEqual(method === "HEAD" ? "" : method); + // Header from the client response will be prefixed with "cf-ew-raw-" + expect(resp.headers.get("cf-ew-raw-Test-Http-Method")).toEqual(method); + } + ); + + it("should fallback to the request method if the X-CF-Http-Method header is missing", async ({ + expect, + }) => { + const token = randomBytes(4096).toString("hex"); + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com/method`, + { + method: "PUT", + headers: { + origin: "https://cloudflare.dev", + "X-CF-Token": token, + "X-CF-Remote": MOCK_REMOTE_URL, + }, + } + ); + + expect(await resp.text()).toEqual("PUT"); + }); + + it("should strip cf-ew-raw- prefix from headers which have it before hitting the user-worker", async ({ + expect, + }) => { + const token = randomBytes(4096).toString("hex"); + const resp = await exports.default.fetch( + `https://0000.rawhttp.cloudflarepreviews.com/`, + { + method: "GET", + headers: { + "Access-Control-Request-Method": "GET", + origin: "https://cloudflare.dev", + "X-CF-Token": token, + "X-CF-Remote": MOCK_REMOTE_URL, + "cf-ew-raw-Some-Custom-Header": "custom", + "cf-ew-raw-Accept": "application/json", + }, + } + ); + + const body = (await resp.json()) as Record; + + const headers = (body.headers as [string, string][]).filter( + (h) => + h[0] === "some-custom-header" || + h[0] === "accept" || + h[0].startsWith("cf-ew-raw-") + ); + + // 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", + ], + ] + `); + }); +}); diff --git a/packages/edge-preview-authenticated-proxy/tests/tsconfig.json b/packages/edge-preview-authenticated-proxy/tests/tsconfig.json new file mode 100644 index 0000000..73752a8 --- /dev/null +++ b/packages/edge-preview-authenticated-proxy/tests/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": [ + "@types/node", + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers/types" + ], + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["**/*.test.ts", "../worker-configuration.d.ts"] +} diff --git a/packages/edge-preview-authenticated-proxy/tsconfig.json b/packages/edge-preview-authenticated-proxy/tsconfig.json new file mode 100644 index 0000000..63a2337 --- /dev/null +++ b/packages/edge-preview-authenticated-proxy/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "es2022", + "moduleResolution": "bundler", + "types": ["@cloudflare/workers-types/experimental"], + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "exclude": ["**/*.test.ts", "vitest.config.mts"] +} diff --git a/packages/edge-preview-authenticated-proxy/vitest.config.mts b/packages/edge-preview-authenticated-proxy/vitest.config.mts new file mode 100644 index 0000000..e8bcc20 --- /dev/null +++ b/packages/edge-preview-authenticated-proxy/vitest.config.mts @@ -0,0 +1,17 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: "./wrangler.jsonc", + }, + }), + ], + test: { + reporters: ["default"], + retry: 2, + testTimeout: 50_000, + }, +}); diff --git a/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts b/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts new file mode 100644 index 0000000..8f809de --- /dev/null +++ b/packages/edge-preview-authenticated-proxy/worker-configuration.d.ts @@ -0,0 +1,19 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --include-runtime=false ./worker-configuration.d.ts` (hash: c8a899098b01428f56aa03eeb3586f5d) +interface __BaseEnv_Env { + TOKEN_LOOKUP: KVNamespace; + SENTRY_DSN: "https://1ff9df95733c4e7d9c31dc13ab05d44a@sentry10.cfdata.org/891"; + PREVIEW: "preview.cloudflarepreviews.com"; + RAW_HTTP: "rawhttp.cloudflarepreviews.com"; + // Secrets + PROMETHEUS_TOKEN: string; + SENTRY_ACCESS_CLIENT_SECRET: string; + SENTRY_ACCESS_CLIENT_ID: string; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/packages/edge-preview-authenticated-proxy/wrangler.jsonc b/packages/edge-preview-authenticated-proxy/wrangler.jsonc new file mode 100644 index 0000000..d78d8e1 --- /dev/null +++ b/packages/edge-preview-authenticated-proxy/wrangler.jsonc @@ -0,0 +1,30 @@ +{ + "name": "edge-preview-authenticated-proxy", + "main": "src/index.ts", + "compatibility_date": "2023-03-28", + "workers_dev": false, + "account_id": "e35fd947284363a46fd7061634477114", + "routes": [ + "preview.cloudflarepreviews.com/*", + "*.preview.cloudflarepreviews.com/*", + "*.rawhttp.cloudflarepreviews.com/*", + ], + "vars": { + "SENTRY_DSN": "https://1ff9df95733c4e7d9c31dc13ab05d44a@sentry10.cfdata.org/891", + "PREVIEW": "preview.cloudflarepreviews.com", + "RAW_HTTP": "rawhttp.cloudflarepreviews.com", + }, + "dev": { + "host": "preview.cloudflarepreviews.com", + }, + "kv_namespaces": [ + { + "binding": "TOKEN_LOOKUP", + "id": "76afd4161617490b9d2addc59b1e22e4", + "preview_id": "76afd4161617490b9d2addc59b1e22e4", + }, + ], + "observability": { + "enabled": true, + }, +} diff --git a/packages/format-errors/CHANGELOG.md b/packages/format-errors/CHANGELOG.md new file mode 100644 index 0000000..74ad1a1 --- /dev/null +++ b/packages/format-errors/CHANGELOG.md @@ -0,0 +1,55 @@ +# format-errors + +## 0.0.8 + +### Patch Changes + +- [#12753](https://github.com/cloudflare/workers-sdk/pull/12753) [`ea8b1a4`](https://github.com/cloudflare/workers-sdk/commit/ea8b1a4619b663598f03d72760090f5e67827d05) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Replace deprecated `promjs` library with `MetricsRegistry` from `@cloudflare/workers-utils/prometheus-metrics` + + The `promjs` library has been unmaintained since 2022 and has a broken `package.json` requiring workarounds. It has been replaced with a lightweight `MetricsRegistry` class in `@cloudflare/workers-utils/prometheus-metrics` that produces byte-identical Prometheus text exposition format output. + +## 0.0.7 + +### Patch Changes + +- [#12756](https://github.com/cloudflare/workers-sdk/pull/12756) [`c7d0d18`](https://github.com/cloudflare/workers-sdk/commit/c7d0d189a40bea786e0425f25d9aa15686f40e92) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Fix error formatting to reliably return fallback responses on failure + + Previously, if something went wrong while formatting a pretty error page, the failure could go unhandled, resulting in no response being returned to the user. Now, errors during formatting are properly caught, ensuring users always receive a 500 JSON fallback response. + +## 0.0.6 + +### Patch Changes + +- [#11217](https://github.com/cloudflare/workers-sdk/pull/11217) [`9ed1542`](https://github.com/cloudflare/workers-sdk/commit/9ed1542e854be65f7c03ed9596ec36767ce8aa36) Thanks [@penalosa](https://github.com/penalosa)! - Use toucan-js v4 + +## 0.0.5 + +### Patch Changes + +- [#9649](https://github.com/cloudflare/workers-sdk/pull/9649) [`ec9b417`](https://github.com/cloudflare/workers-sdk/commit/ec9b417f8ed711e7b5044410e83d781f123a6a62) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - patch release to trigger a test release + +## 0.0.4 + +### Patch Changes + +- [#9033](https://github.com/cloudflare/workers-sdk/pull/9033) [`2c50115`](https://github.com/cloudflare/workers-sdk/commit/2c501151d3d1a563681cdb300a298b83862b60e2) Thanks [@dario-piotrowicz](https://github.com/dario-piotrowicz)! - chore: convert wrangler.toml files into wrangler.jsonc ones + +## 0.0.3 + +### Patch Changes + +- [#7143](https://github.com/cloudflare/workers-sdk/pull/7143) [`4d7ce6f`](https://github.com/cloudflare/workers-sdk/commit/4d7ce6fd9fc80a0920a97dae14726c79012337b1) Thanks [@emily-shen](https://github.com/emily-shen)! - chore: enable observability on our internal infra Workers + bots + +## 0.0.2 + +### Patch Changes + +- [#6046](https://github.com/cloudflare/workers-sdk/pull/6046) [`c643a81`](https://github.com/cloudflare/workers-sdk/commit/c643a8193a3c0739b33d3c0072ae716bc8f1565b) Thanks [@threepointone](https://github.com/threepointone)! - chore: Normalize more dependencies. + + Follow up to https://github.com/cloudflare/workers-sdk/pull/6029, this normalizes some more dependencies : `get-port`, `chalk`, `yargs`, `toucan-js`, `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, `esbuild-register`, `hono`, `glob-to-regexp`, `@cloudflare/workers-types` + +## 0.0.1 + +### Patch Changes + +- [#5482](https://github.com/cloudflare/workers-sdk/pull/5482) [`1b7739e`](https://github.com/cloudflare/workers-sdk/commit/1b7739e0af99860aa063f01c0a6e7712ac072fdb) Thanks [@DaniFoldi](https://github.com/DaniFoldi)! - docs: show new Discord url everywhere for consistency. The old URL still works, but https://discord.cloudflare.com is preferred. diff --git a/packages/format-errors/README.md b/packages/format-errors/README.md new file mode 100644 index 0000000..7c68e4c --- /dev/null +++ b/packages/format-errors/README.md @@ -0,0 +1,91 @@ +# format-errors + +A Worker that will nicely format errors, optionally inflating with source context if available. + + + +
+ +## Why is this needed? + +In development, it's nice to have good error messages to help when debugging, and ideally the logic for that shouldn't be bundled into your user Worker. This Worker is ~50kb when gzipped, which doesn't add _that_ much, but there's still a benefit to keeping it separate from the user Worker. Additionally, in environments where we're unable to bundle complex Workers, this error formatting Worker allows for the display of useful errors where otherwise only a stack trace would be available. + +## How does it work? + +Fundamentally, this is based on [Youch](https://github.com/poppinss/youch) and [StackTracey](https://github.com/xpl/stacktracey). Neither of these projects are designed to be run in the Workers environment (although StackTracey does have support for running in the browser), and so both have been vendored in and modified. These modifications aren't upstreamable, since they involve things like changing how Youch detects node-internal modules and changing how sources are loaded. + +### Youch changes: + +- **"internal" module detection:** By default, Youch recognises modules with a prefix of `node:` to be internal modules. Instead, `format-error`s recognises modules with a prefix of `.internal` as internal modules. +- **source loading:** Youch loads sources at error-page generation time, `format-error`s loads sources at error-page viewing time. +- **cookies:** `format-error`s doesn't handle cookies. + +### Stacktracey changes: + +- **browser detection:** `format-error`s always reports itself to be running inside a browser, but removes references to browser globals like `window`. +- **table rendering:** To remove the `as-table` dependency, `format-error` doesn't implement Stacktracey's `asTable` method, which Youch doesn't use. + +## How can you get a formatted error page? + +The first thing you'll need is an error. `format-errors` requires input conforming to the `Payload` interface below, and will reject invalid input at runtime: + +```ts +export interface JsonError { + message?: string; + name?: string; + stack?: string; + cause?: JsonError; +} +export interface Payload { + url?: string; + method?: string; + headers?: Record; + error?: JsonError; +} +``` + +To call `format-errors` (assuming it's hosted at `https://format-errors.example.com`), send a `POST` request to `https://format-errors.example.com` with a body containing a serialised `Payload` object. `format-errors` will parse your input, pass it to Youch, and generate an HTML page which will be sent back as the response to the `POST` request. + +If you don't need source context, you can stop there! However, if you'd like to get rid of the "Missing sources" message, there's a bit more complexity. + +> _Why can't I just pass sources to `format-error` in the request body?_ +> +> Often your sources won't be available at the location where your error is thrown. You may have access to a sourcemap, but your running code probably doesn't have access to your source code in serialised format. As such, this package is geared towards supporting use cases where you have access to your sources at the time of _viewing_ the generated error page (i.e. in-browser editors). + +A requirement of passing sources to `format-errors` is that the generated HTML error page needs to be embedded in an iframe. Once you've done that, you can start passing sources from the embedder of the iframe into the generated HTML error page. + +Assuming `iframeEl` is a reference to the iframe which embeds the generated error page, here's an example of how you could pass sources to the generated error page: + +```ts +const encoder = new TextEncoder(); +const channel = new MessageChannel(); + +iframeEl.addEventListener("load", () => { + iframeEl.contentWindow?.postMessage("PORT", "*", [channel.port2]); +}); + +channel.port1.onmessage = (data) => { + if (data.type === "RequestSources") { + const message = { + type: "SourcesLoaded", + body: { + files: [ + { + path: "index.js", + contents: encoder.encode("console.log('hi')"), + }, + ], + internalLines: 0, + }, + }; + channel.port1.postMessage( + message, + message.body.files.map((f) => f.contents.buffer) + ); + } +}; +``` + +The interface is fairly simple. The embedder passes a `MessagePort` to the iframe on load, over which the error page requests sources. Of note: this does not currently support sourcemaps, and it's assumed that the lines within your error stack trace map to the exact line in your source. + +There's one exception to that, to support _very_ limited source transformations, in the `internalLines` property. If you pass a value for `internalLines`, the error page will act as if any error messages from line numbers _below_ `internalLines` are internal, and shouldn't be surfaced to the user. Line numbers in error messages will be remapped so that it appears the user-visible source starts at line `0`, rather than line `internalLines`. diff --git a/packages/format-errors/images/modules-worker.png b/packages/format-errors/images/modules-worker.png new file mode 100644 index 0000000000000000000000000000000000000000..7fcf5f87307d61b622259118b583a28fb391cfb9 GIT binary patch literal 169192 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%0DddI=dc0$eM*AdM+oT29jRpQTn~7Wv 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 M3YzlOZ!CiU7q_!SsQ>@~ literal 0 HcmV?d00001 diff --git a/packages/format-errors/images/service-worker.png b/packages/format-errors/images/service-worker.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ac317aea210b34a1b4e46a461c50f4116aa4ab GIT binary patch literal 140634 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%0D2@O%$0DG51PW42wgJnx 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≠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 literal 0 HcmV?d00001 diff --git a/packages/format-errors/package.json b/packages/format-errors/package.json new file mode 100644 index 0000000..618e604 --- /dev/null +++ b/packages/format-errors/package.json @@ -0,0 +1,28 @@ +{ + "name": "@cloudflare/format-errors", + "version": "0.0.8", + "private": true, + "scripts": { + "build": "wrangler build", + "check:type": "tsc", + "deploy": "wrangler deploy", + "start": "wrangler dev", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:default", + "@cloudflare/workers-utils": "workspace:*", + "mustache": "^4.2.0", + "toucan-js": "4.0.0", + "tsconfig": "*", + "vitest": "catalog:default", + "wrangler": "workspace:*", + "zod": "^3.22.3" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "deploy": true + } +} diff --git a/packages/format-errors/src/Stacktracey.js b/packages/format-errors/src/Stacktracey.js new file mode 100644 index 0000000..a574822 --- /dev/null +++ b/packages/format-errors/src/Stacktracey.js @@ -0,0 +1,335 @@ +"use strict"; + +/** + * Adapted from Stacktracey (https://github.com/xpl/stacktracey). + * + * For the original license, see https://github.com/xpl/stacktracey/blob/master/LICENSE + * + */ +import partition from "./impl/partition"; + +const O = Object, + isBrowser = true, + nodeRequire = null, + lastOf = (x) => x[x.length - 1], + getSource = null, + nixSlashes = (x) => x.replace(/\\/g, "/"), + pathRoot = "/"; + +/* ------------------------------------------------------------------------ */ + +export default class StackTracey { + constructor(input, offset) { + const originalInput = input, + isParseableSyntaxError = + input && input instanceof SyntaxError && !isBrowser; + + /* new StackTracey () */ + + if (!input) { + input = new Error(); + offset = offset === undefined ? 1 : offset; + } + + /* new StackTracey (Error) */ + + if (input instanceof Error) { + input = input.stack || ""; + } + + /* new StackTracey (string) */ + + if (typeof input === "string") { + input = this.rawParse(input) + .slice(offset) + .map((x) => this.extractEntryMetadata(x)); + } + + /* new StackTracey (array) */ + + if (Array.isArray(input)) { + if (isParseableSyntaxError) { + const rawLines = nodeRequire("util").inspect(originalInput).split("\n"), + fileLine = rawLines[0].split(":"), + line = fileLine.pop(), + file = fileLine.join(":"); + + if (file) { + input.unshift({ + file: nixSlashes(file), + line: line, + column: (rawLines[2] || "").indexOf("^") + 1, + sourceLine: rawLines[1], + callee: "(syntax error)", + syntaxError: true, + }); + } + } + + this.items = input; + } else { + this.items = []; + } + } + + extractEntryMetadata(e) { + const decomposedPath = this.decomposePath(e.file || ""); + const fileRelative = decomposedPath[0]; + const externalDomain = decomposedPath[1]; + + return O.assign(e, { + calleeShort: e.calleeShort || lastOf((e.callee || "").split(".")), + fileRelative: fileRelative, + fileShort: this.shortenPath(fileRelative), + fileName: lastOf((e.file || "").split("/")), + thirdParty: this.isThirdParty(fileRelative, externalDomain) && !e.index, + externalDomain: externalDomain, + }); + } + + shortenPath(relativePath) { + return relativePath + .replace(/^node_modules\//, "") + .replace(/^webpack\/bootstrap\//, "") + .replace(/^__parcel_source_root\//, ""); + } + + decomposePath(fullPath) { + let result = fullPath; + + if (isBrowser) result = result.replace(pathRoot, ""); + + const externalDomainMatch = result.match( + /^(http|https)\:\/\/?([^\/]+)\/(.*)/ + ); + const externalDomain = externalDomainMatch + ? externalDomainMatch[2] + : undefined; + result = externalDomainMatch ? externalDomainMatch[3] : result; + + if (!isBrowser) result = nodeRequire("path").relative(pathRoot, result); + + return [ + nixSlashes(result).replace(/^.*\:\/\/?\/?/, ""), // cut webpack:/// and webpack:/ things + externalDomain, + ]; + } + + isThirdParty(relativePath, externalDomain) { + return ( + externalDomain || + relativePath[0] === "~" || // webpack-specific heuristic + relativePath[0] === "/" || // external source + relativePath.indexOf("node_modules") === 0 || + relativePath.indexOf("webpack/bootstrap") === 0 + ); + } + + rawParse(str) { + const lines = (str || "").split("\n"); + + const entries = lines.map((line) => { + line = line.trim(); + + let callee, + fileLineColumn = [], + native, + planA, + planB; + + if ( + (planA = line.match(/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls + (planA = line.match(/at (.+) \((.+)\)/)) || + (line.slice(0, 3) !== "at " && (planA = line.match(/(.*)@(.*)/))) + ) { + callee = planA[1]; + native = planA[2] === "native"; + fileLineColumn = ( + planA[2].match(/(.*):(\d+):(\d+)/) || + planA[2].match(/(.*):(\d+)/) || + [] + ).slice(1); + } else if ((planB = line.match(/^(at\s+)*(.+):(\d+):(\d+)/))) { + fileLineColumn = planB.slice(2); + } else { + return undefined; + } + + /* Detect things like Array.reduce + TODO: detect more built-in types */ + + if (callee && !fileLineColumn[0]) { + const type = callee.split(".")[0]; + if (type === "Array") { + native = true; + } + } + + return { + beforeParse: line, + callee: callee || "", + index: isBrowser && fileLineColumn[0] === "/", + native: native || false, + file: nixSlashes(fileLineColumn[0] || ""), + line: parseInt(fileLineColumn[1] || "", 10) || undefined, + column: parseInt(fileLineColumn[2] || "", 10) || undefined, + }; + }); + + return entries.filter((x) => x !== undefined); + } + + withSourceAt(i) { + return this.items[i] && this.withSource(this.items[i]); + } + + withSourceAsyncAt(i) { + return this.items[i] && this.withSourceAsync(this.items[i]); + } + + withSource(loc) { + if (this.shouldSkipResolving(loc)) { + return loc; + } else { + let resolved = getSource(loc.file || "").resolve(loc); + + if (!resolved.sourceFile) { + return loc; + } + + return this.withSourceResolved(loc, resolved); + } + } + + withSourceAsync(loc) { + if (this.shouldSkipResolving(loc)) { + return Promise.resolve(loc); + } else { + return getSource + .async(loc.file || "") + .then((x) => x.resolve(loc)) + .then((resolved) => this.withSourceResolved(loc, resolved)) + .catch((e) => + this.withSourceResolved(loc, { error: e, sourceLine: "" }) + ); + } + } + + shouldSkipResolving(loc) { + return ( + loc.sourceFile || loc.error || (loc.file && loc.file.indexOf("<") >= 0) + ); // skip things like and stuff that was already fetched + } + + withSourceResolved(loc, resolved) { + if (resolved.sourceFile && !resolved.sourceFile.error) { + resolved.file = nixSlashes(resolved.sourceFile.path); + resolved = this.extractEntryMetadata(resolved); + } + + if (resolved.sourceLine.includes("// @hide")) { + resolved.sourceLine = resolved.sourceLine.replace("// @hide", ""); + resolved.hide = true; + } + + if ( + resolved.sourceLine.includes("__webpack_require__") || // webpack-specific heuristics + resolved.sourceLine.includes("/******/ ({") + ) { + resolved.thirdParty = true; + } + + return O.assign({ sourceLine: "" }, loc, resolved); + } + + withSources() { + return this.map((x) => this.withSource(x)); + } + + withSourcesAsync() { + return Promise.all(this.items.map((x) => this.withSourceAsync(x))).then( + (items) => new StackTracey(items) + ); + } + + mergeRepeatedLines() { + return new StackTracey( + partition(this.items, (e) => e.file + e.line).map((group) => { + return group.items.slice(1).reduce( + (memo, entry) => { + memo.callee = + (memo.callee || "") + + " → " + + (entry.callee || ""); + memo.calleeShort = + (memo.calleeShort || "") + + " → " + + (entry.calleeShort || ""); + return memo; + }, + O.assign({}, group.items[0]) + ); + }) + ); + } + + clean() { + const s = this.withSources().mergeRepeatedLines(); + return s.filter(s.isClean.bind(s)); + } + + cleanAsync() { + return this.withSourcesAsync().then((s) => { + s = s.mergeRepeatedLines(); + return s.filter(s.isClean.bind(s)); + }); + } + + isClean(entry, index) { + return index === 0 || !(entry.thirdParty || entry.hide || entry.native); + } + + at(i) { + return O.assign( + { + beforeParse: "", + callee: "", + index: false, + native: false, + file: "", + line: 0, + column: 0, + }, + this.items[i] + ); + } + + maxColumnWidths() { + return { + callee: 30, + file: 60, + sourceLine: 80, + }; + } + + static resetCache() { + getSource.resetCache(); + getSource.async.resetCache(); + } + + static locationsEqual(a, b) { + return a.file === b.file && a.line === b.line && a.column === b.column; + } +} + +/* Array methods + ------------------------------------------------------------------------ */ + +["map", "filter", "slice", "concat"].forEach((method) => { + StackTracey.prototype[method] = function (/*...args */) { + // no support for ...args in Node v4 :( + return new StackTracey(this.items[method].apply(this.items, arguments)); + }; +}); + +/* ------------------------------------------------------------------------ */ diff --git a/packages/format-errors/src/Youch.js b/packages/format-errors/src/Youch.js new file mode 100644 index 0000000..62062f6 --- /dev/null +++ b/packages/format-errors/src/Youch.js @@ -0,0 +1,314 @@ +"use strict"; + +/* + * Adapted from Youch (https://github.com/poppinss/youch). See below for original license: + * + * (c) Harminder Virk + * + * For the full copyright and license information, see https://github.com/poppinss/youch/blob/develop/LICENSE.md + */ +import Mustache from "mustache"; +import viewTemplate from "./error.compiled.mustache"; +import StackTracey from "./Stacktracey"; + +export default class Youch { + constructor(error, request, options = {}) { + this.options = options; + this.options.postLines = options.postLines || 5; + this.options.preLines = options.preLines || 5; + + this._filterHeaders = ["cookie", "connection"]; + this.error = error; + this.request = request; + this.links = []; + this.showAllFrames = false; + } + + /** + * Returns the source code for a given file. It unable to + * read file it resolves the promise with a null. + * + * @param {Object} frame + * @return {Promise} + */ + _getFrameSource(frame) { + return Promise.resolve(null); + } + + /** + * Parses the error stack and returns serialized + * frames out of it. + * + * @return {Object} + */ + _parseError() { + return new Promise((resolve, reject) => { + const stack = new StackTracey(this.error); + Promise.all( + stack.items.map(async (frame) => { + if (this._isNode(frame)) { + return Promise.resolve(frame); + } + return this._getFrameSource(frame).then((context) => { + frame.context = context; + return frame; + }); + }) + ) + .then(resolve) + .catch(reject); + }); + } + + /** + * Returns the context with code for a given + * frame. + * + * @param {Object} + * @return {Object} + */ + _getContext(frame) { + if (!frame.context) { + return {}; + } + + return { + start: frame.line - (frame.context.pre || []).length, + pre: frame.context.pre.join("\n"), + line: frame.context.line, + post: frame.context.post.join("\n"), + }; + } + + /** + * Returns classes to be used inside HTML when + * displaying the frames list. + * + * @param {Object} + * @param {Number} + * + * @return {String} + */ + _getDisplayClasses(frame, index) { + const classes = []; + if (index === 0) { + classes.push("active"); + } + + if (!frame.isApp) { + classes.push("native-frame"); + } + + return classes.join(" "); + } + + /** + * Compiles the view using HTML + * + * @param {String} + * @param {Object} + * + * @return {String} + */ + _compileView(view, data) { + return Mustache.render(view, data); + } + + /** + * Serializes frame to a usable error object. + * + * @param {Object} + * + * @return {Object} + */ + _serializeFrame(frame) { + return { + file: frame.file, + filePath: frame.file, + line: frame.line, + callee: frame.callee, + calleeShort: frame.calleeShort, + column: frame.column, + context: this._getContext(frame), + isModule: frame.thirdParty, + isNative: frame.native, + isApp: this._isApp(frame), + }; + } + + /** + * Returns whether frame belongs to nodejs + * or not. + * + * @return {Boolean} [description] + */ + _isNode(frame) { + if (frame.native) { + return true; + } + + const filename = frame.file || ""; + if (filename.startsWith(".internal")) { + return true; + } + return false; + + // return !path.isAbsolute(filename) && filename[0] !== '.' + } + + /** + * Returns whether code belongs to the app + * or not. + * + * @return {Boolean} [description] + */ + _isApp(frame) { + return !this._isNode(frame) && !this._isNodeModule(frame); + } + + /** + * Returns whether frame belongs to a node_module or + * not + * + * @method _isNodeModule + * + * @param {Object} frame + * + * @return {Boolean} + * + * @private + */ + _isNodeModule(frame) { + return (frame.file || "").indexOf("node_modules/") > -1; + } + + /** + * Serializes stack to Mustache friendly object to + * be used within the view. Optionally can pass + * a callback to customize the frames output. + * + * @param {Object} + * @param {Function} [callback] + * + * @return {Object} + */ + _serializeData(stack, callback) { + callback = callback || this._serializeFrame.bind(this); + return { + message: this.error.message, + help: this.error.help, + cause: this.error.cause, + name: this.error.name, + status: this.error.status, + frames: + stack instanceof Array === true + ? stack.filter((frame) => frame.file).map(callback) + : [], + }; + } + + /** + * Returns a serialized object with important + * information. + * + * @return {Object} + */ + _serializeRequest() { + const headers = []; + + Object.keys(this.request.headers).forEach((key) => { + if (this._filterHeaders.indexOf(key) > -1) { + return; + } + headers.push({ + key: key.toUpperCase(), + value: this.request.headers[key], + }); + }); + + return { + url: this.request.url, + httpVersion: this.request.httpVersion, + method: this.request.method, + connection: this.request.headers.connection, + headers, + }; + } + + /** + * Stores the link `callback` which + * will be processed when rendering + * the HTML view. + * + * @param {Function} callback + * + * @returns {Object} + */ + addLink(callback) { + if (typeof callback === "function") { + this.links.push(callback); + return this; + } + + throw new Error('Pass a callback function to "addLink"'); + } + + /** + * Toggle the state of showing all frames by default + */ + toggleShowAllFrames() { + this.showAllFrames = !this.showAllFrames; + return this; + } + + /** + * Returns error stack as JSON. + * + * @return {Promise} + */ + toJSON() { + return new Promise((resolve, reject) => { + this._parseError() + .then((stack) => { + resolve({ + error: this._serializeData(stack), + }); + }) + .catch(reject); + }); + } + + /** + * Returns HTML representation of the error stack + * by parsing the stack into frames and getting + * important info out of it. + * + * @return {Promise} + */ + toHTML() { + return new Promise((resolve, reject) => { + this._parseError() + .then((stack) => { + const data = this._serializeData(stack, (frame, index) => { + const serializedFrame = this._serializeFrame(frame); + serializedFrame.classes = this._getDisplayClasses( + serializedFrame, + index + ); + return serializedFrame; + }); + + if (this.request) { + data.request = this._serializeRequest(); + } + + data.links = this.links.map((renderLink) => renderLink(data)); + data.loadFA = !!data.links.find((link) => link.includes("fa-")); + data.showAllFrames = this.showAllFrames; + + return resolve(this._compileView(viewTemplate, data)); + }) + .catch(reject); + }); + } +} diff --git a/packages/format-errors/src/__tests__/index.test.ts b/packages/format-errors/src/__tests__/index.test.ts new file mode 100644 index 0000000..c06b29c --- /dev/null +++ b/packages/format-errors/src/__tests__/index.test.ts @@ -0,0 +1,101 @@ +import { describe, it, vi } from "vitest"; +import type { Payload } from "../index"; + +vi.mock("toucan-js", () => { + return { + Toucan: vi.fn().mockImplementation(() => ({ + captureException: vi.fn(), + })), + }; +}); + +describe("handlePrettyErrorRequest", () => { + it("should propagate async rejections to callers", async ({ expect }) => { + // Mock Youch to throw asynchronously + vi.doMock("../Youch", () => { + return { + default: vi.fn().mockImplementation(function () { + return { + addLink: vi.fn(), + toHTML: vi.fn().mockRejectedValue(new Error("Youch async error")), + }; + }), + }; + }); + + // handlePrettyErrorRequest is async — if it's not awaited, async + // rejections (e.g. from Youch.toHTML()) won't be caught by a + // surrounding try/catch, leading to unhandled promise rejections. + // This test verifies that the function properly rejects so that + // callers who `await` it can catch the error. + const { handlePrettyErrorRequest } = await import("../index"); + + const payload: Payload = { + url: "https://example.com", + method: "GET", + headers: { "content-type": "text/html" }, + error: { + message: "Test error", + name: "Error", + stack: "Error: Test error\n at test:1:1", + }, + }; + + await expect(handlePrettyErrorRequest(payload)).rejects.toThrow( + "Youch async error" + ); + }); +}); + +describe("reviveError", () => { + it("should revive a plain Error", async ({ expect }) => { + const { reviveError } = await import("../index"); + const error = reviveError({ + message: "test", + name: "Error", + stack: "Error: test\n at foo:1:1", + }); + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe("test"); + expect(error.name).toBe("Error"); + expect(error.stack).toBe("Error: test\n at foo:1:1"); + }); + + it("should revive a TypeError", async ({ expect }) => { + const { reviveError } = await import("../index"); + const error = reviveError({ + message: "x is not a function", + name: "TypeError", + }); + expect(error).toBeInstanceOf(TypeError); + expect(error.message).toBe("x is not a function"); + }); + + it("should revive an error with a cause", async ({ expect }) => { + const { reviveError } = await import("../index"); + const error = reviveError({ + message: "outer", + name: "Error", + cause: { + message: "inner", + name: "RangeError", + }, + }); + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe("outer"); + expect(error.cause).toBeInstanceOf(RangeError); + expect((error.cause as Error).message).toBe("inner"); + }); + + it("should fall back to Error for unknown error names", async ({ + expect, + }) => { + const { reviveError } = await import("../index"); + const error = reviveError({ + message: "custom", + name: "CustomError", + }); + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe("CustomError"); + }); +}); diff --git a/packages/format-errors/src/error.compiled.mustache b/packages/format-errors/src/error.compiled.mustache new file mode 100644 index 0000000..4ed05cb --- /dev/null +++ b/packages/format-errors/src/error.compiled.mustache @@ -0,0 +1,805 @@ + + + + + + + {{#loadFA}} + + + {{/loadFA}} + + + + + +

+
+

{{ status }}

+ +
+

{{ name }}

+

{{ message }}

+ +
+ +
+ {{!-- Filled using frontend code, based upon the active frame --}} + + +
+ +
+ {{#frames}} + {{ index }} +
+
+ {{ file }}:{{ line }}:{{ column }} +
+
+ {{ callee }} +
+
{{ context.pre }} +{{ context.line }} +{{ context.post }} +
+
+ {{/frames}} +
+
+
+
+ + {{#request}} +
+

Request Details

+ + + + + + + + + + + + + + + + + + + + + +
URI {{ request.url }}
Request Method {{ request.method }}
HTTP Version {{ request.httpVersion }}
Connection {{ request.connection }}
+ +

Headers

+ + + {{#request.headers}} + + + + + {{/request.headers}} +
{{ key }} {{ value }}
+ +
+ {{/request}} +
+ + + + \ No newline at end of file diff --git a/packages/format-errors/src/impl/partition.js b/packages/format-errors/src/impl/partition.js new file mode 100644 index 0000000..b953fab --- /dev/null +++ b/packages/format-errors/src/impl/partition.js @@ -0,0 +1,20 @@ +"use strict"; + +module.exports = (arr_, pred) => { + const arr = arr_ || [], + spans = []; + + let span = { label: undefined, items: [arr.first] }; + + arr.forEach((x) => { + const label = pred(x); + + if (span.label !== label && span.items.length) { + spans.push((span = { label: label, items: [x] })); + } else { + span.items.push(x); + } + }); + + return spans; +}; diff --git a/packages/format-errors/src/index.ts b/packages/format-errors/src/index.ts new file mode 100644 index 0000000..33abfde --- /dev/null +++ b/packages/format-errors/src/index.ts @@ -0,0 +1,199 @@ +import { MetricsRegistry } from "@cloudflare/workers-utils/prometheus-metrics"; +import { Toucan } from "toucan-js"; +import { z } from "zod"; +import Youch from "./Youch"; + +export interface Env { + PROMETHEUS_TOKEN: string; + SENTRY_ACCESS_CLIENT_SECRET: string; + SENTRY_ACCESS_CLIENT_ID: string; + SENTRY_DSN: string; +} +export interface JsonError { + message?: string; + name?: string; + stack?: string; + cause?: JsonError; +} +export interface Payload { + url?: string; + method?: string; + headers?: Record; + error?: JsonError; +} +export const JsonErrorSchema: z.ZodType = z.lazy(() => + z.object({ + message: z.string().optional(), + name: z.string().optional(), + stack: z.string().optional(), + cause: JsonErrorSchema.optional(), + }) +); + +export const PayloadSchema = z.object({ + url: z.string().optional(), + method: z.string().optional(), + headers: z.record(z.string()), + error: JsonErrorSchema.optional(), +}); + +interface StandardErrorConstructor { + new (message?: string, options?: { cause?: Error }): Error; +} +const ALLOWED_ERROR_SUBCLASS_CONSTRUCTORS: StandardErrorConstructor[] = [ + EvalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError, +]; +export function reviveError(jsonError: JsonError): Error { + // At a high level, this function takes a JSON-serialisable representation of + // an `Error`, and converts it to an `Error`. `Error`s may have `cause`s, so + // we need to do this recursively. + let cause: Error | undefined; + if (jsonError.cause !== undefined) { + cause = reviveError(jsonError.cause); + } + + // If this is one of the built-in error types, construct an instance of that. + // For example, if we threw a `TypeError` in the Worker, we'd like to + // construct a `TypeError` here, so it looks like the error has been thrown + // through a regular function call, not an HTTP request (i.e. we want + // `instanceof TypeError` to pass in Node for `TypeError`s thrown in Workers). + let ctor: StandardErrorConstructor = Error; + if (jsonError.name !== undefined && jsonError.name in globalThis) { + const maybeCtor = (globalThis as Record)[ + jsonError.name + ] as StandardErrorConstructor; + if (ALLOWED_ERROR_SUBCLASS_CONSTRUCTORS.includes(maybeCtor)) { + ctor = maybeCtor; + } + } + + // Construct the error, copying over the correct name and stack trace. + // Because constructing an `Error` captures the stack trace at point of + // construction, we override the stack trace to the one from the Worker in the + // JSON-serialised error. + const error = new ctor(jsonError.message, { cause }); + if (jsonError.name !== undefined) { + error.name = jsonError.name; + } + error.stack = jsonError.stack; + + return error; +} + +export async function handlePrettyErrorRequest({ + error: jsonError, + url, + method, + headers, +}: Payload): Promise { + // Parse and validate the error we've been given from user code + const caught = JsonErrorSchema.parse(jsonError); + + // Convert the error into a regular `Error` object and try to source-map it. + // We need to give `name`, `message` and `stack` to Youch, but StackTracy, + // Youch's dependency for parsing `stack`s, will only extract `stack` from + // an object if it's an `instanceof Error`. + const error = reviveError(caught); + + // Log source-mapped error to console if logging enabled + + // Lazily import `youch` when required + // `cause` is usually more useful than the error itself, display that instead + // TODO(someday): would be nice if we could display both + const youch = new Youch(error.cause ?? error, { + url, + method, + headers, + }); + youch.addLink(() => { + return [ + '📚 Workers Docs', + '💬 Workers Discord', + ].join(""); + }); + return new Response(await youch.toHTML(), { + headers: { "Content-Type": "text/html;charset=utf-8" }, + }); +} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + const registry = new MetricsRegistry(); + const requestCounter = registry.createCounter( + "devprod_format_errors_request_total", + "Request counter for DevProd's format-errors service" + ); + requestCounter.inc(); + + const sentry = new Toucan({ + dsn: env.SENTRY_DSN, + context: ctx, + request, + requestDataOptions: { + allowedHeaders: [ + "user-agent", + "accept-encoding", + "accept-language", + "cf-ray", + "content-length", + "content-type", + "host", + ], + }, + transportOptions: { + headers: { + "CF-Access-Client-ID": env.SENTRY_ACCESS_CLIENT_ID, + "CF-Access-Client-Secret": env.SENTRY_ACCESS_CLIENT_SECRET, + }, + }, + }); + + // Validate payload outside of Sentry/metrics reporting + let payload: Payload; + try { + payload = PayloadSchema.parse(await request.json()); + } catch { + return new Response("Invalid payload", { status: 400 }); + } + + try { + return await handlePrettyErrorRequest(payload); + } catch (e) { + sentry.captureException(e); + const errorCounter = registry.createCounter( + "devprod_format_errors_error_total", + "Error counter for DevProd's format-errors service" + ); + errorCounter.inc(); + + return Response.json( + { + error: "UnexpectedError", + message: "Something went wrong", + }, + { + status: 500, + } + ); + } finally { + ctx.waitUntil( + fetch("https://workers-logging.cfdata.org/prometheus", { + method: "POST", + headers: { + Authorization: `Bearer ${env.PROMETHEUS_TOKEN}`, + }, + body: registry.metrics(), + }) + ); + } + }, +}; diff --git a/packages/format-errors/tsconfig.json b/packages/format-errors/tsconfig.json new file mode 100644 index 0000000..a68ad77 --- /dev/null +++ b/packages/format-errors/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "es2022" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "jsx": "react-jsx" /* Specify what JSX code is generated. */, + + /* Modules */ + "module": "es2022" /* Specify what module code is generated. */, + "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + "types": [ + "@cloudflare/workers-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + "resolveJsonModule": true /* Enable importing .json files */, + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, + "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, + /* Emit */ + "noEmit": true /* Disable emitting files from a compilation. */, + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + /* Completeness */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/packages/format-errors/turbo.json b/packages/format-errors/turbo.json new file mode 100644 index 0000000..6556dcf --- /dev/null +++ b/packages/format-errors/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/format-errors/vitest.config.mts b/packages/format-errors/vitest.config.mts new file mode 100644 index 0000000..8ae1797 --- /dev/null +++ b/packages/format-errors/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineProject, mergeConfig } from "vitest/config"; +import configShared from "../../vitest.shared"; + +export default mergeConfig( + configShared, + defineProject({ + test: { + include: ["src/__tests__/**/*.{test,spec}.{ts,js}"], + }, + }) +); diff --git a/packages/format-errors/wrangler.jsonc b/packages/format-errors/wrangler.jsonc new file mode 100644 index 0000000..36a6683 --- /dev/null +++ b/packages/format-errors/wrangler.jsonc @@ -0,0 +1,26 @@ +{ + "name": "format-errors", + "main": "src/index.ts", + "compatibility_date": "2023-03-30", + "workers_dev": false, + "account_id": "e35fd947284363a46fd7061634477114", + "rules": [ + { + "type": "Text", + "globs": ["**/*.mustache"], + "fallthrough": false, + }, + ], + "routes": [ + { + "pattern": "format-errors.devprod.cloudflare.dev", + "custom_domain": true, + }, + ], + "vars": { + "SENTRY_DSN": "https://1ff9df95733c4e7d9c31dc13ab05d44a@sentry10.cfdata.org/891", + }, + "observability": { + "enabled": true, + }, +} diff --git a/packages/kv-asset-handler/CHANGELOG.md b/packages/kv-asset-handler/CHANGELOG.md new file mode 100644 index 0000000..dffe83f --- /dev/null +++ b/packages/kv-asset-handler/CHANGELOG.md @@ -0,0 +1,587 @@ +# Changelog + +## 0.5.0 + +### Minor Changes + +- [#13726](https://github.com/cloudflare/workers-sdk/pull/13726) [`b5ac54b`](https://github.com/cloudflare/workers-sdk/commit/b5ac54baa4a6e40b7352f7d3ed0d3531a37a5e8f) Thanks [@penalosa](https://github.com/penalosa)! - Hard fail on Node.js < 22 + + Wrangler no longer supports Node.js 20.x, as it reached end-of-life on 2026-04-30. The minimum supported Node.js version is now 22.0.0. See https://github.com/nodejs/release?tab=readme-ov-file#end-of-life-releases. + +## 0.4.2 + +### Patch Changes + +- [#11898](https://github.com/cloudflare/workers-sdk/pull/11898) [`c17e971`](https://github.com/cloudflare/workers-sdk/commit/c17e971af01a9bcead0aca409666e29417f4636a) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - Bundle more third-party dependencies to reduce supply chain risk + + Previously, several small utility packages were listed as runtime dependencies and + installed separately. These are now bundled directly into the published packages, + reducing the number of external dependencies users need to trust. + + Bundled dependencies: + + - **miniflare**: `acorn`, `acorn-walk`, `exit-hook`, `glob-to-regexp`, `stoppable` + - **kv-asset-handler**: `mime` + - **vite-plugin-cloudflare**: `@remix-run/node-fetch-server`, `defu`, `get-port`, `picocolors`, `tinyglobby` + - **vitest-pool-workers**: `birpc`, `devalue`, `get-port`, `semver` + +## 0.4.1 + +### Patch Changes + +- [#11348](https://github.com/cloudflare/workers-sdk/pull/11348) [`4d61fae`](https://github.com/cloudflare/workers-sdk/commit/4d61faed1c0c5cb0f7a7f085d31c3dca9a83c802) Thanks [@petebacondarwin](https://github.com/petebacondarwin)! - refactor to generate ESM modules and support Node 22+ + +## 0.4.0 + +### Minor Changes + +- [#7334](https://github.com/cloudflare/workers-sdk/pull/7334) [`869ec7b`](https://github.com/cloudflare/workers-sdk/commit/869ec7b916487ec43b958a27bdfea13588c5685f) Thanks [@penalosa](https://github.com/penalosa)! - Packages in Workers SDK now support the versions of Node that Node itself supports (Current, Active, Maintenance). Currently, that includes Node v18, v20, and v22. + +## 0.3.4 + +### Patch Changes + +- [#6102](https://github.com/cloudflare/workers-sdk/pull/6102) [`d4e1e9f`](https://github.com/cloudflare/workers-sdk/commit/d4e1e9fc3439c3d6bd2d1d145d3edc85b551f276) Thanks [@threepointone](https://github.com/threepointone)! - chore: pass lint/typechecks for kv-asset-handler + + Followup from https://github.com/cloudflare/workers-sdk/pull/6090, this enables typechecking and linting in kv-asset-handler, and fixes any failures. + +## 0.3.3 + +### Patch Changes + +- [#6029](https://github.com/cloudflare/workers-sdk/pull/6029) [`f5ad1d3`](https://github.com/cloudflare/workers-sdk/commit/f5ad1d3e562ce63b59f6ab136f1cdd703605bca4) Thanks [@threepointone](https://github.com/threepointone)! - chore: Normalize some dependencies in workers-sdk + + This is the first of a few expected patches that normalize dependency versions, This normalizes `undici`, `concurrently`, `@types/node`, `react`, `react-dom`, `@types/react`, `@types/react-dom`, `eslint`, `typescript`. There are no functional code changes (but there are a couple of typecheck fixes). + +## 0.3.2 + +### Patch Changes + +- [#5482](https://github.com/cloudflare/workers-sdk/pull/5482) [`1b7739e`](https://github.com/cloudflare/workers-sdk/commit/1b7739e0af99860aa063f01c0a6e7712ac072fdb) Thanks [@DaniFoldi](https://github.com/DaniFoldi)! - docs: show new Discord url everywhere for consistency. The old URL still works, but https://discord.cloudflare.com is preferred. + +## 0.3.1 + +- ## Maintenance + + - **Remove tests from npm package to reduce npm package size - [boidolr], [pull/388]** + + This PR removes the tests from the npm package, reducing the size of the package by about half. + + [boidolr]: https://github.com/boidolr + [pull/388]: https://github.com/cloudflare/kv-asset-handler/pull/388 + + - **Bump dependencies - [Cherry], [pull/367] [pull/361] [pull/362] [pull/359] [pull/390]** + + These PRs bump dependencies of the project to their latest versions. + + [pull/367]: https://github.com/cloudflare/kv-asset-handler/pull/367 + [pull/361]: https://github.com/cloudflare/kv-asset-handler/pull/361 + [pull/362]: https://github.com/cloudflare/kv-asset-handler/pull/362 + [pull/359]: https://github.com/cloudflare/kv-asset-handler/pull/359 + [pull/390]: https://github.com/cloudflare/kv-asset-handler/pull/390 + + - **Fix README anchor links - [johtso] [pull/372]** + + This PR fixes the anchor links in the README. + + [johtso]: https://github.com/johtso + [pull/372]: https://github.com/cloudflare/kv-asset-handler/pull/372 + +## 0.3.0 + +- ### Features + + - **Allow configurable downgrade of ETag validator strength - [awwong1], [pull/315]** + + This allows users to override the default strong ETag validator behaviour to use weak ETag validators. This change allows the developer to use weak ETags and preserve 304 responses (e.g. on \*.workers.dev domains). + +- ### Fixes + + - **Fix length property call on ArrayBuffer instance - [philipatkinson], [pull/295**] + + Previously when edge cached was enabled, the `content-length` of the response was not being set correctly. This was due to the `length` property of the `ArrayBuffer` instance being called instead of the `byteLength` property. This PR fixes this issue. + +- ### Maintenance + + - **chore(ci): bump node versions in actions - [KianNH], [pull/354]** + + This bumps the Node versions used in the CI actions to the latest LTS versions. + + - **chore: use tabs for indentation - [Cherry], [pull/355]** + + This PR changes the indentation of the project to use tabs instead of spaces, falling more in line with other Cloudflare JavaScript projects like wrangler. + + - **chore: bump dependencies - [Cherry], [pull/356]** + + This bumps many dependencies of the project to their latest versions. + +## 0.2.0 + +- ### Features + + - **Allow changing pathIsEncoded through options - [JackPriceBurns], [pull/243]** + + When using `mapRequestToAsset`, it encodes the URL / key and will never check the KV store for the decoded key. + + This adds the ability to set `pathIsEncoded` to true, which will decode the URL before getting it from the KV. + + [jackpriceburns]: https://github.com/JackPriceBurns + [pull/243]: https://github.com/cloudflare/kv-asset-handler/pull/243 + + - **Support ES Modules. - [threepointone], [pull/261]** + + This PR provides a possible solution for getting Workers Sites working with ES Module workers. This approach is not as invasive as other approaches, so isn't as risky either. + + Usage: + + ```jsx + import manifestJSON from "__STATIC_CONTENT_MANIFEST"; + const manifest = JSON.parse(manifestJSON); + + export default { + fetch(request, env, ctx) { + return await getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise); + }, + }, + { + ASSET_NAMESPACE: env.ASSET_NAMESPACE, + ASSET_MANIFEST: manifest, + } + ); + // ... + }, + }; + ``` + + [threepointone]: https://github.com/threepointone + [pull/261]: https://github.com/cloudflare/kv-asset-handler/pull/261 + +- ### Fixes + + - **fix: default ASSET_MANIFEST to empty object - [Cherry], [pull/254]** + + As per [discussion in Discord](https://canary.discord.com/channels/595317990191398933/831143699999752262/898392183999197184) and the repo at [https://github.com/Erisa-bits/getassetfromkv-undefined-error], allowing `ASSET_MANIFEST` to be optional got lost somewhere along the years and errors when attempted to be used without it. This PR restores this functionality by setting it to an empty object (instead of `undefined`), which allows fall-through to the standard `mapRequestToAsset` function. + + chore: bump dependencies - This updates a few dependencies and also pins `@types/node` to `15.x` since `16.x` has some incompatible types. + feat: generate more modern code - This removes the unnecessary async/await polyfill added by TypeScript + + [cherry]: https://github.com/Cherry + [pull/254]: https://github.com/cloudflare/kv-asset-handler/pull/254 + +- ### Maintenance + + - **chore: remove debug logs around `response.body.cancel` support - [Cherry], [pull/249]** + + Fixes [issues/248] + + [cherry]: https://github.com/Cherry + [pull/249]: https://github.com/cloudflare/kv-asset-handler/pull/249 + [issues/248]: https://github.com/cloudflare/kv-asset-handler/issue/248 + +## 0.1.3 + +- ### Performance + + - **Only parse `ASSET_MANIFEST` once on startup - [Cherry], [pull/185]** + + This PR improves performance of the `getAssetFromKV` function by only parsing the asset manifest once on startup, instead of on each request. This can have a significant improvement in response times for larger sites. An example of the performance improvement with an asset manifest of over 50k files: + + > Before change: + > 100 iterations: Done. Mean kv response time is 16.61 + > 1000 iterations: Done. Mean kv response time is 17.798 + > After change: + > 100 iterations: Done. Mean kv response time is 6.62 + > 1000 iterations: Done. Mean kv response time is 7.296 + + Initial work and credit to [groenlid] in [pull/143]. + + [cherry]: https://github.com/Cherry + [groenlid]: https://github.com/groenlid + [pull/185]: https://github.com/cloudflare/kv-asset-handler/pull/185 + [pull/143]: https://github.com/cloudflare/kv-asset-handler/pull/143 + +- ### Fixes + + - **ESM compatibility: fix crash on missing global environment variables - [ttraenkler], [pull/188]** + + This PR fixes the library from crashing when global environment variables such as `__STATIC_CONTENT` and `__STATIC_CONTENT_MANIFEST` are missing, which is currently the case when using the new ESM module syntax. + + Note that whilst this partially resolves the issue discussed in [issue/174], it does not provide full ESM compatibility yet. Please see [issue/174] for further discussion. + + [ttraenkler]: https://github.com/ttraenkler + [pull/188]: https://github.com/cloudflare/kv-asset-handler/pull/188 + [issue/174]: https://github.com/cloudflare/kv-asset-handler/issues/174 + +- ### Maintenance + + - **Tweak GitHub Actions Workflow for proper PR testing - [Cherry], [pull/185]** + + This PR tweaks the GitHub Actions Workflow to test PRs properly, both in terms of linting and the repository tests. It runs `prettier` to maintain code quality and style, and all unit tests on every PR to ensure no regressions occur. + + [pull/183]: https://github.com/cloudflare/kv-asset-handler/pull/185 + [cherry]: https://github.com/Cherry + + - **Add test for `mapRequestToAsset` asset override - [Cherry], [pull/186]** + + This PR adds a test for the functionality added in [pull/159]. This tests that when overriding the `mapRequestToAsset` function in its entirety, this function is always run. + + [pull/159]: https://github.com/cloudflare/kv-asset-handler/pull/159 + [pull/186]: https://github.com/cloudflare/kv-asset-handler/pull/186 + [cherry]: https://github.com/Cherry + + - **Dependabot updates** + + A number of dependabot patch-level updates have been merged: + + - Bump @types/node from 15.3.1 to 15.6.0 ([pull/183]) + - Bump @types/node from 15.6.0 to 15.6.1 ([pull/184]) + - Bump @types/node from 15.6.1 to 15.9.0 ([pull/189]) + - Bump @types/node from 15.9.0 to 15.12.0 ([pull/190]) + - Bump @types/node from 15.12.0 to 15.12.1 ([pull/191]) + - Bump @types/node from 15.12.1 to 15.12.2 ([pull/193]) + - Bump typescript from 4.2.4 to 4.3.2 ([pull/187]) + - Bump prettier from 2.3.0 to 2.3.1 ([pull/192]) + + [pull/183]: https://github.com/cloudflare/kv-asset-handler/pull/183 + [pull/184]: https://github.com/cloudflare/kv-asset-handler/pull/184 + [pull/189]: https://github.com/cloudflare/kv-asset-handler/pull/189 + [pull/190]: https://github.com/cloudflare/kv-asset-handler/pull/190 + [pull/191]: https://github.com/cloudflare/kv-asset-handler/pull/191 + [pull/193]: https://github.com/cloudflare/kv-asset-handler/pull/193 + [pull/187]: https://github.com/cloudflare/kv-asset-handler/pull/187 + [pull/192]: https://github.com/cloudflare/kv-asset-handler/pull/192 + +## 0.1.2 + +- ### Features + + - **Support for `defaultDocument` configuration - [boemekeld], [pull/161]** + + This PR adds support for customizing the `defaultDocument` option in `getAssetFromKV`. In situations where a project does not use `index.html` as the default document for a path, this can now be customized to values like `index.shtm`: + + ```js + return getAssetFromKV(event, { + defaultDocument: "index.shtm", + }); + ``` + + [boemekeld]: https://github.com/boemekeld + [pull/161]: https://github.com/cloudflare/kv-asset-handler/pull/161 + +- ### Fixes + + - **Fire `mapRequestToAsset` for all requests, if explicitly defined - [Cherry], [pull/159]** + + This PR fixes an issue where a custom `mapRequestToAsset` handler weren't fired if a matching asset path was found in `ASSET_MANIFEST` data. By correctly checking for this handler, we can conditionally handle any assets with this handler _even_ if they exist in the `ASSET_MANIFEST`. + + **Note that this is a breaking change**, as previously, the mapRequestToAsset function was ignored if you set it, and an exact match was found in the `ASSET_MANIFEST`. That being said, this behavior was a bug, and unexpected behavior, as documented in [issue/158]. + + [cherry]: https://github.com/Cherry + [issue/158]: https://github.com/cloudflare/kv-asset-handler/pull/158 + [pull/159]: https://github.com/cloudflare/kv-asset-handler/pull/159 + + - **Etag logic refactor - [shagamemnon], [pull/133]** + + This PR refactors a great deal of the Etag functionality introduced in [0.0.11](https://github.com/cloudflare/kv-asset-handler/milestone/7?closed=1). `kv-asset-handler` will now correctly set [strong and weak Etags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) both to the Cloudflare CDN and to client eyeballs, allowing for higher cache percentages with Workers Sites projects. + + [pull/133]: https://github.com/cloudflare/kv-asset-handler/pull/133 + [shagamemnon]: https://github.com/shagamemnon + + - **Fix path decoding issue - [xiaolanglanglang], [pull/142]** + + This PR improves support for non-alphanumeric character paths in `kv-asset-handler`, for instance, if the path requested is in Chinese. + + [xiaolanglanglang]: https://github.com/xiaolanglanglang + [pull/142]: https://github.com/cloudflare/kv-asset-handler/pull/142 + + - **Check HTTP method after mapRequestToAsset - [oliverpool], [pull/178]** + + This PR fixes an issue where the HTTP method for an asset is checked before the `mapRequestToAsset` handler is called. This has caused issues for users in the past, where they need to generate a `requestKey` based on an asset path, even if the request method is not `GET`. This fixes [issue/151]. + + [oliverpool]: https://github.com/oliverpool + [pull/178]: https://github.com/cloudflare/kv-asset-handler/pull/178 + [issue/151]: https://github.com/cloudflare/kv-asset-handler/issues/151 + +- ### Maintenance + + - **Add Markdown linting workflow to GitHub Actions - [jbampton], [pull/135]** + + Our GitHub Actions workflow now includes a linting workflow for Markdown in the project, including the README, this CHANGELOG, and any other `.md` files in the source code. + + [jbampton]: https://github.com/jbampton + [pull/135]: https://github.com/cloudflare/kv-asset-handler/pull/135 + + - **Dependabot updates** + + A number of dependabot patch-level updates have been merged since our last release: + + - Bump @types/node from 15.30.0 to 15.30.1 ([pull/180]) + - Bump hosted-git-info from 2.8.8 to 2.8.9 ([pull/176]) + - Bump ini from 1.3.5 to 1.3.8 ([pull/160]) + - Bump lodash from 4.17.19 to 4.17.21 ([pull/175]) + - Bump urijs from 1.19.2 to 1.19.6 ([pull/168]) + - Bump y18n from 4.0.0 to 4.0.1 ([pull/173]) + + [pull/160]: https://github.com/cloudflare/kv-asset-handler/pull/160 + [pull/168]: https://github.com/cloudflare/kv-asset-handler/pull/168 + [pull/173]: https://github.com/cloudflare/kv-asset-handler/pull/173 + [pull/175]: https://github.com/cloudflare/kv-asset-handler/pull/175 + [pull/176]: https://github.com/cloudflare/kv-asset-handler/pull/176 + [pull/180]: https://github.com/cloudflare/kv-asset-handler/pull/180 + + - **Repository maintenance - [Cherry], [pull/179]** + + New project maintainer Cherry did a ton of maintenance in this release, improving workflows, code quality, and more. Check out the full list in [the PR][pull/179]. + + [cherry]: https://github.com/Cherry + [pull/179]: https://github.com/cloudflare/kv-asset-handler/pull/179 + +- ### Documentation + + - **Update README.md - [signalnerve], [pull/177]** + + This PR adds context to our README, with mentions about _what_ this project is, how to use it, and some new things since the last version of this package: namely, [Cloudflare Pages](https://pages.dev) and the new [Cloudflare Workers Discord server](https://discord.gg/cloudflaredev) + + [signalnerve]: https://github.com/signalnerve + [pull/177]: https://github.com/cloudflare/kv-asset-handler/pull/177 + + - **Add instructions for updating version in related repos - [caass], [pull/171]** + + This PR adds instructions for updating the `kv-asset-handler` version in related repositories, such as our templates, that use `kv-asset-handler` and are exposed to end-users of Wrangler and Workers. + + [caass]: https://github.com/caass + [pull/177]: https://github.com/cloudflare/kv-asset-handler/pull/171 + +## 0.1.1 + +- ### Fixes + + - **kv-asset-handler can translate 206 responses to 200 - [harrishancock], [pull/166]** + + Fixes [wrangler#1746](https://github.com/cloudflare/wrangler/issues/1746) + + [harrishancock](https://github.com/harrishancock) + [pull/166](https://github.com/cloudflare/kv-asset-handler/pull/166) + +## 0.0.12 + +- ### Features + + - **Add defaultMimeType option to getAssetFromKV - [mgrahamjo], [pull/121]** + + Some static website owners prefer not to create all of their web routes as directories containing index.html files. Instead, they prefer to create pages as extensionless HTML files. Providing a defaultMimeType option will allow users to set the Content-Type header for extensionless files to text/html, which will enable this use case. + + [mgrahamjo]: https://github.com/mgrahamjo + [pull/121]: https://github.com/cloudflare/kv-asset-handler/pull/121 + + - **Add defaultMimeType to types - [shagamemnon], [pull/132]** + + Adds the newly added defaultMimeType to the exported types for this package. + + [pull/132]: https://github.com/cloudflare/kv-asset-handler/pull/132 + +- ### Fixes + + - **Fix text/\* charset - [EatonZ], [pull/130]** + + Adds a missing `-` to the `utf-8` charset value in response mime types. + + [eatonz]: https://github.com/EatonZ + [pull/130]: https://github.com/cloudflare/kv-asset-handler/pull/130 + + - **Cache handling for HEAD requests - [klittlepage], [pull/141]** + + This PR skips caching for incoming HEAD requests, as they should not be able to be edge cached. + + [klittlepage]: https://github.com/klittlepage + [pull/141]: https://github.com/cloudflare/kv-asset-handler/pull/141 + +- ### Maintenance + + - **Markdown linting/typos - [jbampton], [pull/123], [pull/125], [pull/126], [pull/127], [pull/128], [pull/129], [pull/131], [pull/134]** + + These PRs contain various typo fixes and linting of existing Markdown files in our documentation and CHANGELOG. + + [jbampton]: https://github.com/jbampton + [pull/123]: https://github.com/cloudflare/kv-asset-handler/pull/123 + [pull/125]: https://github.com/cloudflare/kv-asset-handler/pull/125 + [pull/126]: https://github.com/cloudflare/kv-asset-handler/pull/126 + [pull/127]: https://github.com/cloudflare/kv-asset-handler/pull/127 + [pull/128]: https://github.com/cloudflare/kv-asset-handler/pull/128 + [pull/129]: https://github.com/cloudflare/kv-asset-handler/pull/129 + [pull/131]: https://github.com/cloudflare/kv-asset-handler/pull/131 + [pull/134]: https://github.com/cloudflare/kv-asset-handler/pull/134 + +## 0.0.11 + +- ### Features + + - **Support cache revalidation using ETags and If-None-Match - [shagamemnon], [issue/62] [pull/94] [pull/113]** + + Previously, cacheable resources were not looked up from the browser cache because `getAssetFromKV` would never return a `304 Not Modified` response. + + Now, `getAssetFromKV` sets an `ETag` header on all cacheable assets before putting them in the Cache API, and therefore will return a `304` response when appropriate. + + [shagamemnon]: https://github.com/shagamemnon + [pull/94]: https://github.com/cloudflare/kv-asset-handler/pull/94 + [pull/113]: https://github.com/cloudflare/kv-asset-handler/issues/113 + [issue/62]: https://github.com/cloudflare/kv-asset-handler/issues/62 + + - **Export TypeScript types - [ispivey], [issue/43] [pull/106]** + + [ispivey]: https://github.com/ispivey + [pull/106]: https://github.com/cloudflare/kv-asset-handler/pull/106 + [issue/43]: https://github.com/cloudflare/kv-asset-handler/issues/43 + +- ### Fixes + + - **Support non-ASCII characters in paths - [SukkaW], [issue/99] [pull/105]** + + Fixes an issue where non-ASCII paths were not URI-decoded before being looked up, causing non-ASCII paths to 404. + + [sukkaw]: https://github.com/SukkaW + [pull/105]: https://github.com/cloudflare/kv-asset-handler/pull/105 + [issue/99]: https://github.com/cloudflare/kv-asset-handler/issues/99 + + - **Support `charset=utf8` in MIME type - [theromis], [issue/92] [pull/97]** + + Fixes an issue where `Content-Type: text/*` was never appended with `; charset=utf8`, meaning clients would not render non-ASCII characters properly. + + [theromis]: https://github.com/theromis + [pull/97]: https://github.com/cloudflare/kv-asset-handler/pull/97 + [issue/92]: https://github.com/cloudflare/kv-asset-handler/issues/92 + + - **Fix bugs in README examples - [kentonv] [bradyjoslin], [issue/93] [pull/102] [issue/88] [pull/116]** + + [kentonv]: https://github.com/kentonv + [bradyjoslin]: https://github.com/bradyjoslin + [pull/102]: https://github.com/cloudflare/kv-asset-handler/pull/102 + [pull/116]: https://github.com/cloudflare/kv-asset-handler/pull/116 + [issue/93]: https://github.com/cloudflare/kv-asset-handler/issues/93 + [issue/88]: https://github.com/cloudflare/kv-asset-handler/issues/88 + +- ### Maintenance + + - **Make `@cloudflare/workers-types` a dependency and update deps - [ispivey], [pull/107]** + + [ispivey]: https://github.com/ispivey + [pull/107]: https://github.com/cloudflare/kv-asset-handler/pull/107 + + - **Add Code of Conduct - [EverlastingBugstopper], [pull/101]** + + [everlastingbugstopper]: https://github.com/EverlastingBugstopper + [pull/101]: https://github.com/cloudflare/kv-asset-handler/pull/101 + +## 0.0.10 + +- ### Features + + - **Allow extensionless files to be served - [victoriabernard92], [cloudflare/wrangler/issues/980], [pull/73]** + + Prior to this PR, `getAssetFromKv` assumed extensionless requests (e.g. `/some-path`) would be set up to be served as the corresponding HTML file in storage (e.g. `some-path.html`). + This fix checks the `ASSET_MANIFEST` for the extensionless file name _before_ appending the HTML extension. If the extensionless file exists (e.g. `some-path` exists as a key in the ASSET_MANIFEST) then we serve that file first. If the extensionless file does not exist, then the behavior does not change (e.g. it still looks for `some-path.html`). + + [victoriabernard92]: https://github.com/victoriabernard92 + [cloudflare/wrangler/issues/980]: https://github.com/cloudflare/wrangler/issues/980 + [pull/73]: https://github.com/cloudflare/kv-asset-handler/pull/73 + +- ### Fixes + + - **Fix URL parsing in serveSinglePageApp - [signalnerve],[sgiacosa], [issue/72], [pull/82]** + + This fixes an issue in `serveSinglePageApp` where the request.url is used as a string to retrieve static content. For example, + if a query parameter was set, the URL lookup would break. This fix uses a parsed URL instead of the string and adjusts the README. + + [signalnerve]: https://github.com/signalnerve + [sgiacosa]: https://github.com/sgiacosa + [issue/72]: https://github.com/cloudflare/kv-asset-handler/issue/72 + [pull/82]: https://github.com/cloudflare/kv-asset-handler/pull/82 + +## 0.0.9 + +- ### Fixes + + - **Building and publishing to npm - [victoriabernard92], [pull/78], [pull/79]** + + Added a `prepack` step that builds JavaScript files from the TypeScript source. This fixes previously broken `npm` publishes. + + [victoriabernard92]: https://github.com/victoriabernard92 + [issue/78]: https://github.com/cloudflare/kv-asset-handler/issue/78 + [pull/79]: https://github.com/cloudflare/kv-asset-handler/pull/79 + +## 0.0.8 + +- ### Features + + - **Support a variety of errors thrown from `getAssetFromKV` - [victoriabernard92], [issue/59] [pull/64]** + + Previously, `getAssetFromKv` would throw the same error type if anything went wrong. Now it will throw different error types so that clients can catch and differentiate them. + For example, a 404 `NotFoundError` error implies nothing went wrong, the asset just didn't exist while + a 500 `InternalError` means an expected variable was undefined. + + [victoriabernard92]: https://github.com/victoriabernard92 + [issue/44]: https://github.com/cloudflare/kv-asset-handler/issues/44 + [issue/59]: https://github.com/cloudflare/kv-asset-handler/issues/59 + [pull/47]: https://github.com/cloudflare/kv-asset-handler/pull/47 + +- ### Fixes + + - **Range Issue with Safari and videos - [victoriabernard92], [issue/60] [pull/66]** + + Previously, if you wanted to serve a video from Workers KV using `kv-asset-handler`, it would be broken on Safari due to its requirement that all videos support the [`Content-Range` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range). Cloudflare already has a feature that will handle these headers automatically, we just needed to take advantage of it by passing in a `Request` object to the [Cache API](https://developers.cloudflare.com/workers/reference/apis/cache/) rather than a URL string. + videos from not including the range headers. + + [victoriabernard92]: https://github.com/victoriabernard92 + [shagamemnon]: https://github.com/shagamemnon + [issue/60]: https://github.com/cloudflare/kv-asset-handler/issues/60 + [issue/63]: https://github.com/cloudflare/kv-asset-handler/issues/63 + [pull/47]: https://github.com/cloudflare/kv-asset-handler/pull/52 + [pull/66]: https://github.com/cloudflare/kv-asset-handler/pull/66 + + - **Support custom asset namespaces passed into `getAssetFromKV` - [victoriabernard92], [issue/67] [pull/68]** + + This functionality was documented but not properly supported. Tests and implementation fixes applied. + + [victoriabernard92]: https://github.com/victoriabernard92 + [issue/67]: https://github.com/cloudflare/kv-asset-handler/issues/67 + [pull/68]: https://github.com/cloudflare/kv-asset-handler/pull/68 + +## 0.0.7 + +- ### Features + + - **Add handler for SPAs - [ashleymichal], [issue/46] [pull/47]** + + Some browser applications employ client-side routers that handle navigation in the browser rather than on the server. These applications will work as expected until a non-root URL is requested from the server. This PR adds a special handler, `serveSinglePageApp`, that maps all HTML requests to the root index.html. This is similar to setting a static asset route pattern in an Express.js app. + + [ashleymichal]: https://github.com/ashleymichal + [issue/46]: https://github.com/cloudflare/kv-asset-handler/issues/46 + [pull/47]: https://github.com/cloudflare/kv-asset-handler/pull/47 + +- ### Documentation + + - **Add function API for `getAssetFromKV` to README.md - [ashleymichal], [issue/48] [pull/52]** + + This function, used to abstract away the implementation for retrieving static assets from a Workers KV namespace, includes a lot of great options for configuring your own, bespoke "Workers Sites" implementation. This PR adds documentation to the README for use by those who would like to tinker with these options. + + [ashleymichal]: https://github.com/ashleymichal + [issue/46]: https://github.com/cloudflare/kv-asset-handler/issues/48 + [pull/47]: https://github.com/cloudflare/kv-asset-handler/pull/52 + +## 0.0.6 + +- ### Fixes + + - **Improve caching - [victoriabernard92], [issue/38] [pull/37]** + + - Don't use browser cache by default: Previously, `kv-asset-handler` would set a `Cache-Control` header on the response sent back from the Worker to the client. After this fix, the `Cache-Control` header will only be set if `options.cacheControl.browserTTL` is set by the caller. + + - Set default edge caching to 2 days: Previously the default cache time for static assets was 100 days. This PR sets the default to 2 days. This can be overridden with `options.cacheControl.edgeTTL`. + + [victoriabernard92]: https://github.com/victoriabernard92 + [issue/38]: https://github.com/cloudflare/kv-asset-handler/issues/38 + [pull/37]: https://github.com/cloudflare/kv-asset-handler/pull/37 diff --git a/packages/kv-asset-handler/README.md b/packages/kv-asset-handler/README.md new file mode 100644 index 0000000..cd7acdb --- /dev/null +++ b/packages/kv-asset-handler/README.md @@ -0,0 +1,346 @@ +# @cloudflare/kv-asset-handler + +[![npm](https://img.shields.io/npm/v/@cloudflare/kv-asset-handler.svg)](https://www.npmjs.com/package/@cloudflare/kv-asset-handler)   +[![Run npm tests](https://github.com/cloudflare/kv-asset-handler/actions/workflows/test.yml/badge.svg)](https://github.com/cloudflare/kv-asset-handler/actions/workflows/test.yml)   +[![Lint Markdown](https://github.com/cloudflare/kv-asset-handler/actions/workflows/lint.yml/badge.svg)](https://github.com/cloudflare/kv-asset-handler/actions/workflows/lint.yml)   + +`kv-asset-handler` is an open-source library for managing the retrieval of static assets from [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv) inside of a [Cloudflare Workers](https://workers.dev) function. `kv-asset-handler` is designed for use with [Workers Sites](https://developers.cloudflare.com/workers/platform/sites), a feature included in [Wrangler](https://github.com/cloudflare/wrangler), our command-line tool for deploying Workers projects. + +`kv-asset-handler` runs as part of a Cloudflare Workers function, so it allows you to customize _how_ and _when_ your static assets are loaded, as well as customize how those assets behave before they're sent to the client. + +Most users and sites will not need these customizations, and just want to serve their statically built applications. For that simple use-case, you can check out [Cloudflare Pages](https://pages.cloudflare.com), our batteries-included approach to deploying static sites on Cloudflare's edge network. Workers Sites was designed as a precursor to Cloudflare Pages, so check out Pages if you just want to deploy your static site without any special customizations! + +For users who _do_ want to customize their assets, and want to build complex experiences on top of their static assets, `kv-asset-handler` (and the default [Workers Sites template](https://github.com/cloudflare/worker-sites-template), which is provided for use with Wrangler + Workers Sites) allows you to customize caching behavior, headers, and anything else about how your Workers function loads the static assets for your site stored in Workers KV. + +The Cloudflare Workers Discord server is an active place where Workers users get help, share feedback, and collaborate on making our platform better. The `#workers` channel in particular is a great place to chat about `kv-asset-handler`, and building cool experiences for your users using these tools! If you have questions, want to share what you're working on, or give feedback, [join us in Discord and say hello](https://discord.cloudflare.com)! + +- [Installation](#installation) +- [Usage](#usage) +- [`getAssetFromKV`](#getassetfromkv) + - [Example](#example) + * [Return](#return) + * [Optional Arguments](#optional-arguments) + - [`mapRequestToAsset`](#maprequesttoasset) + - [Example](#example-1) + - [`cacheControl`](#cachecontrol) + - [`browserTTL`](#browserttl) + - [`edgeTTL`](#edgettl) + - [`bypassCache`](#bypasscache) + - [`ASSET_NAMESPACE` (required for ES Modules)](#asset_namespace-required-for-es-modules) + - [`ASSET_MANIFEST` (required for ES Modules)](#asset_manifest-required-for-es-modules) + - [`defaultETag`](#defaultetag-optional) + +* [Helper functions](#helper-functions) + - [`mapRequestToAsset`](#maprequesttoasset-1) + - [`serveSinglePageApp`](#servesinglepageapp) +* [Cache revalidation and etags](#cache-revalidation-and-etags) + +## Installation + +Add this package to your `package.json` by running this in the root of your +project's directory: + +``` +npm i @cloudflare/kv-asset-handler +``` + +## Usage + +This package was designed to work with [Worker Sites](https://workers.cloudflare.com/sites). + +## `getAssetFromKV` + +getAssetFromKV(Evt) => Promise + +`getAssetFromKV` is an async function that takes an `Evt` object (containing a `Request` and a [`waitUntil`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#waituntil)) and returns a `Response` object if the request matches an asset in KV, otherwise it will throw a `KVError`. + +#### Example + +This example checks for the existence of a value in KV, and returns it if it's there, and returns a 404 if it is not. It also serves index.html from `/`. + +### Return + +`getAssetFromKV` returns a `Promise` with `Response` being the body of the asset requested. + +Known errors to be thrown are: + +- MethodNotAllowedError +- NotFoundError +- InternalError + +#### ES Modules + +```js +import manifestJSON from "__STATIC_CONTENT_MANIFEST"; +import { + getAssetFromKV, + MethodNotAllowedError, + NotFoundError, +} from "@cloudflare/kv-asset-handler"; + +const assetManifest = JSON.parse(manifestJSON); + +export default { + async fetch(request, env, ctx) { + if (request.url.includes("/docs")) { + try { + return await getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise); + }, + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: assetManifest, + } + ); + } catch (e) { + if (e instanceof NotFoundError) { + // ... + } else if (e instanceof MethodNotAllowedError) { + // ... + } else { + return new Response("An unexpected error occurred", { status: 500 }); + } + } + } else return fetch(request); + }, +}; +``` + +#### Service Worker + +```js +import { + getAssetFromKV, + MethodNotAllowedError, + NotFoundError, +} from "@cloudflare/kv-asset-handler"; + +addEventListener("fetch", (event) => { + event.respondWith(handleEvent(event)); +}); + +async function handleEvent(event) { + if (event.request.url.includes("/docs")) { + try { + return await getAssetFromKV(event); + } catch (e) { + if (e instanceof NotFoundError) { + // ... + } else if (e instanceof MethodNotAllowedError) { + // ... + } else { + return new Response("An unexpected error occurred", { status: 500 }); + } + } + } else return fetch(event.request); +} +``` + +### Optional Arguments + +You can customize the behavior of `getAssetFromKV` by passing the following properties as an object into the second argument. + +``` +getAssetFromKV(event, { mapRequestToAsset: ... }) +``` + +#### `mapRequestToAsset` + +mapRequestToAsset(Request) => Request + +Maps the incoming request to the value that will be looked up in Cloudflare's KV + +By default, mapRequestToAsset is set to the exported function [`mapRequestToAsset`](#maprequesttoasset-1). This works for most static site generators, but you can customize this behavior by passing your own function as `mapRequestToAsset`. The function should take a `Request` object as its only argument, and return a new `Request` object with an updated path to be looked up in the asset manifest/KV. + +For SPA mapping pass in the [`serveSinglePageApp`](#servesinglepageapp) function + +#### Example + +Strip `/docs` from any incoming request before looking up an asset in Cloudflare's KV. + +```js +import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler' +... +const customKeyModifier = request => { + let url = request.url + //custom key mapping optional + url = url.replace('/docs', '').replace(/^\/+/, '') + return mapRequestToAsset(new Request(url, request)) +} +let asset = await getAssetFromKV(event, { mapRequestToAsset: customKeyModifier }) +``` + +#### `cacheControl` + +type: object + +`cacheControl` allows you to configure options for both the Cloudflare Cache accessed by your Worker, and the browser cache headers sent along with your Workers' responses. The default values are as follows: + +```javascript +let cacheControl = { + browserTTL: null, // do not set cache control ttl on responses + edgeTTL: 2 * 60 * 60 * 24, // 2 days + bypassCache: false, // do not bypass Cloudflare's cache +}; +``` + +##### `browserTTL` + +type: number | null +nullable: true + +Sets the `Cache-Control: max-age` header on the response returned from the Worker. By default set to `null` which will not add the header at all. + +##### `edgeTTL` + +type: number | null +nullable: true + +Sets the `Cache-Control: max-age` header on the response used as the edge cache key. By default set to 2 days (`2 * 60 * 60 * 24`). When null will not cache on the edge at all. + +##### `bypassCache` + +type: boolean + +Determines whether to cache requests on Cloudflare's edge cache. By default set to `false` (recommended for production builds). Useful for development when you need to eliminate the cache's effect on testing. + +#### `ASSET_NAMESPACE` (required for ES Modules) + +type: KV Namespace Binding + +The binding name to the KV Namespace populated with key/value entries of files for the Worker to serve. By default, Workers Sites uses a [binding to a Workers KV Namespace](https://developers.cloudflare.com/workers/reference/storage/api/#namespaces) named `__STATIC_CONTENT`. + +It is further assumed that this namespace consists of static assets such as HTML, CSS, JavaScript, or image files, keyed off of a relative path that roughly matches the assumed URL pathname of the incoming request. + +In ES Modules format, this argument is required, and can be gotten from `env`. + +##### ES Module + +```js +return getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise); + }, + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + } +); +``` + +##### Service Worker + +``` +return getAssetFromKV(event, { ASSET_NAMESPACE: MY_NAMESPACE }) +``` + +#### `ASSET_MANIFEST` (required for ES Modules) + +type: text blob (JSON formatted) or object + +The mapping of requested file path to the key stored on Cloudflare. + +Workers Sites with Wrangler bundles up a text blob that maps request paths to content-hashed keys that are generated by Wrangler as a cache-busting measure. If this option/binding is not present, the function will fallback to using the raw pathname to look up your asset in KV. If, for whatever reason, you have rolled your own implementation of this, you can include your own by passing a stringified JSON object where the keys are expected paths, and the values are the expected keys in your KV namespace. + +In ES Modules format, this argument is required, and can be imported. + +##### ES Module + +```js +import manifestJSON from "__STATIC_CONTENT_MANIFEST"; +let manifest = JSON.parse(manifestJSON); +manifest["index.html"] = "index.special.html"; + +return getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise); + }, + }, + { + ASSET_MANIFEST: manifest, + // ... + } +); +``` + +##### Service Worker + +``` +let assetManifest = { "index.html": "index.special.html" } +return getAssetFromKV(event, { ASSET_MANIFEST: assetManifest }) +``` + +#### `defaultMimeType` (optional) + +type: string + +This is the mime type that will be used for files with unrecognized or missing extensions. The default value is `'text/plain'`. + +If you are serving a static site and would like to use extensionless HTML files instead of index.html files, set this to `'text/html'`. + +#### `defaultDocument` (optional) + +type: string + +This is the default document that will be concatenated for requests ends in `'/'` or without a valid mime type like `'/about'` or `'/about.me'`. The default value is `'index.html'`. + +#### `defaultETag` (optional) + +type: `'strong' | 'weak'` + +This determines the format of the response [ETag header](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag). If the resource is in the cache, the ETag will always be weakened before being returned. +The default value is `'strong'`. + +# Helper functions + +## `mapRequestToAsset` + +mapRequestToAsset(Request) => Request + +The default function for mapping incoming requests to keys in Cloudflare's KV. + +Takes any path that ends in `/` or evaluates to an HTML file and appends `index.html` or `/index.html` for lookup in your Workers KV namespace. + +## `serveSinglePageApp` + +serveSinglePageApp(Request) => Request + +A custom handler for mapping requests to a single root: `index.html`. The most common use case is single-page applications - frameworks with in-app routing - such as React Router, VueJS, etc. It takes zero arguments. + +```js +import { getAssetFromKV, serveSinglePageApp } from '@cloudflare/kv-asset-handler' +... +let asset = await getAssetFromKV(event, { mapRequestToAsset: serveSinglePageApp }) +``` + +# Cache revalidation and etags + +All responses served from cache (including those with `cf-cache-status: MISS`) include an `etag` response header that identifies the version of the resource. The `etag` value is identical to the path key used in the `ASSET_MANIFEST`. It is updated each time an asset changes and looks like this: `etag: ..` (ex. `etag: index.54321.html`). + +Resources served with an `etag` allow browsers to use the `if-none-match` request header to make conditional requests for that resource in the future. This has two major benefits: + +- When a request's `if-none-match` value matches the `etag` of the resource in Cloudflare cache, Cloudflare will send a `304 Not Modified` response without a body, saving bandwidth. +- Changes to a file on the server are immediately reflected in the browser - even when the cache control directive `max-age` is unexpired. + +#### Disable the `etag` + +To turn `etags` **off**, you must bypass cache: + +```js +/* Turn etags off */ +let cacheControl = { + bypassCache: true, +}; +``` + +#### Syntax and comparison context + +`kv-asset-handler` sets and evaluates etags as [strong validators](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#Strong_validation). To preserve `etag` integrity, the format of the value deviates from the [RFC2616 recommendation to enclose the `etag` with quotation marks](https://tools.ietf.org/html/rfc2616#section-3.11). This is intentional. Cloudflare cache applies the `W/` prefix to all `etags` that use quoted-strings -- a process that converts the `etag` to a "weak validator" when served to a client. diff --git a/packages/kv-asset-handler/package.json b/packages/kv-asset-handler/package.json new file mode 100644 index 0000000..9e5580f --- /dev/null +++ b/packages/kv-asset-handler/package.json @@ -0,0 +1,59 @@ +{ + "name": "@cloudflare/kv-asset-handler", + "version": "0.5.0", + "description": "Routes requests to KV assets", + "keywords": [ + "assets", + "cloudflare", + "kv", + "workers", + "wrangler" + ], + "homepage": "https://github.com/cloudflare/workers-sdk#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "MIT OR Apache-2.0", + "author": "wrangler@cloudflare.com", + "repository": { + "type": "git", + "url": "git+https://github.com/cloudflare/workers-sdk.git", + "directory": "packages/kv-asset-handler" + }, + "files": [ + "src", + "dist" + ], + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "check:type": "tsc", + "prepack": "npm run build", + "pretest": "npm run build", + "test": "vitest", + "test:ci": "vitest run" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "catalog:default", + "@cloudflare/workers-types": "catalog:default", + "@types/mime": "^3.0.4", + "@types/node": "catalog:default", + "mime": "^3.0.0", + "tsup": "8.3.0", + "vitest": "catalog:default" + }, + "engines": { + "node": ">=22.0.0" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "prerelease": true + } +} diff --git a/packages/kv-asset-handler/src/index.ts b/packages/kv-asset-handler/src/index.ts new file mode 100644 index 0000000..7b2c64b --- /dev/null +++ b/packages/kv-asset-handler/src/index.ts @@ -0,0 +1,353 @@ +import * as mime from "mime"; +import { + CacheControl, + InternalError, + MethodNotAllowedError, + NotFoundError, + Options, +} from "./types"; +import type { AssetManifestType } from "./types"; + +const defaultCacheControl: CacheControl = { + browserTTL: null, + edgeTTL: 2 * 60 * 60 * 24, // 2 days + bypassCache: false, // do not bypass Cloudflare's cache +}; + +const parseStringAsObject = (maybeString: string | T): T => + typeof maybeString === "string" + ? (JSON.parse(maybeString) as T) + : maybeString; + +function getAssetFromKVDefaultOptions(): Partial { + return { + ASSET_NAMESPACE: + typeof __STATIC_CONTENT !== "undefined" ? __STATIC_CONTENT : undefined, + ASSET_MANIFEST: + typeof __STATIC_CONTENT_MANIFEST !== "undefined" + ? parseStringAsObject(__STATIC_CONTENT_MANIFEST) + : {}, + cacheControl: defaultCacheControl, + defaultMimeType: "text/plain", + defaultDocument: "index.html", + pathIsEncoded: false, + defaultETag: "strong", + }; +} + +function assignOptions(options?: Partial): Options { + // Assign any missing options passed in to the default + // options.mapRequestToAsset is handled manually later + return Object.assign({}, getAssetFromKVDefaultOptions(), options); +} + +/** + * maps the path of incoming request to the request pathKey to look up + * in bucket and in cache + * e.g. for a path '/' returns '/index.html' which serves + * the content of bucket/index.html + * @param {Request} request incoming request + */ +const mapRequestToAsset = (request: Request, options?: Partial) => { + options = assignOptions(options); + + const parsedUrl = new URL(request.url); + let pathname = parsedUrl.pathname; + + if (pathname.endsWith("/")) { + // If path looks like a directory append options.defaultDocument + // e.g. If path is /about/ -> /about/index.html + pathname = pathname.concat(options.defaultDocument); + } else if (!mime.getType(pathname)) { + // If path doesn't look like valid content + // e.g. /about.me -> /about.me/index.html + pathname = pathname.concat("/" + options.defaultDocument); + } + + parsedUrl.pathname = pathname; + return new Request(parsedUrl.toString(), request); +}; + +/** + * maps the path of incoming request to /index.html if it evaluates to + * any HTML file. + * @param {Request} request incoming request + */ +function serveSinglePageApp( + request: Request, + options?: Partial +): Request { + options = assignOptions(options); + + // First apply the default handler, which already has logic to detect + // paths that should map to HTML files. + request = mapRequestToAsset(request, options); + + const parsedUrl = new URL(request.url); + + // Detect if the default handler decided to map to + // a HTML file in some specific directory. + if (parsedUrl.pathname.endsWith(".html")) { + // If expected HTML file was missing, just return the root index.html (or options.defaultDocument) + return new Request( + `${parsedUrl.origin}/${options.defaultDocument}`, + request + ); + } else { + // The default handler decided this is not an HTML page. It's probably + // an image, CSS, or JS file. Leave it as-is. + return request; + } +} + +/** + * takes the path of the incoming request, gathers the appropriate content from KV, and returns + * the response + * + * @param {FetchEvent} event the fetch event of the triggered request + * @param {{mapRequestToAsset: (string: Request) => Request, cacheControl: {bypassCache:boolean, edgeTTL: number, browserTTL:number}, ASSET_NAMESPACE: any, ASSET_MANIFEST:any}} [options] configurable options + * @param {CacheControl} [options.cacheControl] determine how to cache on Cloudflare and the browser + * @param {typeof(options.mapRequestToAsset)} [options.mapRequestToAsset] maps the path of incoming request to the request pathKey to look up + * @param {Object | string} [options.ASSET_NAMESPACE] the binding to the namespace that script references + * @param {any} [options.ASSET_MANIFEST] the map of the key to cache and store in KV + * */ + +type Evt = { + request: Request; + waitUntil: (promise: Promise) => void; +}; + +const getAssetFromKV = async ( + event: Evt, + options?: Partial +): Promise => { + options = assignOptions(options); + + const request = event.request; + const ASSET_NAMESPACE = options.ASSET_NAMESPACE; + const ASSET_MANIFEST = parseStringAsObject( + options.ASSET_MANIFEST + ); + + if (typeof ASSET_NAMESPACE === "undefined") { + throw new InternalError(`there is no KV namespace bound to the script`); + } + + const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, ""); // strip any preceding /'s + let pathIsEncoded = options.pathIsEncoded; + let requestKey; + // if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions + // otherwise handle request as normal, with default mapRequestToAsset below + if (options.mapRequestToAsset) { + requestKey = options.mapRequestToAsset(request); + } else if (ASSET_MANIFEST[rawPathKey]) { + requestKey = request; + } else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { + pathIsEncoded = true; + requestKey = request; + } else { + const mappedRequest = mapRequestToAsset(request); + const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace( + /^\/+/, + "" + ); + if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) { + pathIsEncoded = true; + requestKey = mappedRequest; + } else { + // use default mapRequestToAsset + requestKey = mapRequestToAsset(request, options); + } + } + + const SUPPORTED_METHODS = ["GET", "HEAD"]; + if (!SUPPORTED_METHODS.includes(requestKey.method)) { + throw new MethodNotAllowedError( + `${requestKey.method} is not a valid request method` + ); + } + + const parsedUrl = new URL(requestKey.url); + const pathname = pathIsEncoded + ? decodeURIComponent(parsedUrl.pathname) + : parsedUrl.pathname; // decode percentage encoded path only when necessary + + // pathKey is the file path to look up in the manifest + let pathKey = pathname.replace(/^\/+/, ""); // remove prepended / + + // @ts-expect-error we should pick cf types here + const cache = caches.default; + let mimeType = mime.getType(pathKey) || options.defaultMimeType; + if (mimeType.startsWith("text") || mimeType === "application/javascript") { + mimeType += "; charset=utf-8"; + } + + let shouldEdgeCache = false; // false if storing in KV by raw file path i.e. no hash + // check manifest for map from file path to hash + if (typeof ASSET_MANIFEST !== "undefined") { + if (ASSET_MANIFEST[pathKey]) { + pathKey = ASSET_MANIFEST[pathKey]; + // if path key is in asset manifest, we can assume it contains a content hash and can be cached + shouldEdgeCache = true; + } + } + + // TODO this excludes search params from cache, investigate ideal behavior + const cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request); + + // if argument passed in for cacheControl is a function then + // evaluate that function. otherwise return the Object passed in + // or default Object + const evalCacheOpts = (() => { + switch (typeof options.cacheControl) { + case "function": + return options.cacheControl(request); + case "object": + return options.cacheControl; + default: + return defaultCacheControl; + } + })(); + + // formats the etag depending on the response context. if the entityId + // is invalid, returns an empty string (instead of null) to prevent the + // the potentially disastrous scenario where the value of the Etag resp + // header is "null". Could be modified in future to base64 encode etc + const formatETag = ( + entityId: string = pathKey, + validatorType: string = options.defaultETag + ) => { + if (!entityId) { + return ""; + } + switch (validatorType) { + case "weak": + if (!entityId.startsWith("W/")) { + if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) { + return `W/${entityId}`; + } + return `W/"${entityId}"`; + } + return entityId; + case "strong": + if (entityId.startsWith(`W/"`)) { + entityId = entityId.replace("W/", ""); + } + if (!entityId.endsWith(`"`)) { + entityId = `"${entityId}"`; + } + return entityId; + default: + return ""; + } + }; + + options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts); + + // override shouldEdgeCache if options say to bypassCache + if ( + options.cacheControl.bypassCache || + options.cacheControl.edgeTTL === null || + request.method == "HEAD" + ) { + shouldEdgeCache = false; + } + // only set max-age if explicitly passed in a number as an arg + const shouldSetBrowserCache = + typeof options.cacheControl.browserTTL === "number"; + + let response = null; + if (shouldEdgeCache) { + response = await cache.match(cacheKey); + } + + if (response) { + if (response.status > 300 && response.status < 400) { + if (response.body && "cancel" in Object.getPrototypeOf(response.body)) { + // Body exists and environment supports readable streams + response.body.cancel(); + } else { + // Environment doesnt support readable streams, or null repsonse body. Nothing to do + } + response = new Response(null, response); + } else { + // fixes #165 + const opts = { + headers: new Headers(response.headers), + status: 0, + statusText: "", + }; + + opts.headers.set("cf-cache-status", "HIT"); + + if (response.status) { + opts.status = response.status; + opts.statusText = response.statusText; + } else if (opts.headers.has("Content-Range")) { + opts.status = 206; + opts.statusText = "Partial Content"; + } else { + opts.status = 200; + opts.statusText = "OK"; + } + response = new Response(response.body, opts); + } + } else { + const body = await ASSET_NAMESPACE.get(pathKey, "arrayBuffer"); + if (body === null) { + throw new NotFoundError( + `could not find ${pathKey} in your content namespace` + ); + } + response = new Response(body); + + if (shouldEdgeCache) { + response.headers.set("Accept-Ranges", "bytes"); + response.headers.set("Content-Length", String(body.byteLength)); + // set etag before cache insertion + if (!response.headers.has("etag")) { + response.headers.set("etag", formatETag(pathKey)); + } + // determine Cloudflare cache behavior + response.headers.set( + "Cache-Control", + `max-age=${options.cacheControl.edgeTTL}` + ); + event.waitUntil(cache.put(cacheKey, response.clone())); + response.headers.set("CF-Cache-Status", "MISS"); + } + } + response.headers.set("Content-Type", mimeType); + + if (response.status === 304) { + const etag = formatETag(response.headers.get("etag")); + const ifNoneMatch = cacheKey.headers.get("if-none-match"); + const proxyCacheStatus = response.headers.get("CF-Cache-Status"); + if (etag) { + if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === "MISS") { + response.headers.set("CF-Cache-Status", "EXPIRED"); + } else { + response.headers.set("CF-Cache-Status", "REVALIDATED"); + } + response.headers.set("etag", formatETag(etag, "weak")); + } + } + if (shouldSetBrowserCache) { + response.headers.set( + "Cache-Control", + `max-age=${options.cacheControl.browserTTL}` + ); + } else { + response.headers.delete("Cache-Control"); + } + return response; +}; + +export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp }; +export { + Options, + CacheControl, + MethodNotAllowedError, + NotFoundError, + InternalError, +}; diff --git a/packages/kv-asset-handler/src/types.ts b/packages/kv-asset-handler/src/types.ts new file mode 100644 index 0000000..6ab7453 --- /dev/null +++ b/packages/kv-asset-handler/src/types.ts @@ -0,0 +1,57 @@ +declare global { + const __STATIC_CONTENT: KVNamespace | undefined; + const __STATIC_CONTENT_MANIFEST: Record | undefined; +} + +export type CacheControl = { + browserTTL: number; + edgeTTL: number; + bypassCache: boolean; +}; + +export type AssetManifestType = Record; + +export type Options = { + cacheControl: + | ((req: Request) => Partial) + | Partial; + ASSET_NAMESPACE: KVNamespace; + ASSET_MANIFEST: AssetManifestType | string; + mapRequestToAsset?: (req: Request, options?: Partial) => Request; + defaultMimeType: string; + defaultDocument: string; + pathIsEncoded: boolean; + defaultETag: "strong" | "weak"; +}; + +export class KVError extends Error { + constructor(message?: string, status: number = 500) { + super(message); + // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + this.name = KVError.name; // stack traces display correctly now + this.status = status; + } + status: number; +} +export class MethodNotAllowedError extends KVError { + constructor( + message: string = `Not a valid request method`, + status: number = 405 + ) { + super(message, status); + } +} +export class NotFoundError extends KVError { + constructor(message: string = `Not Found`, status: number = 404) { + super(message, status); + } +} +export class InternalError extends KVError { + constructor( + message: string = `Internal Error in KV Asset Handler`, + status: number = 500 + ) { + super(message, status); + } +} diff --git a/packages/kv-asset-handler/test/getAssetFromKV-optional.test.ts b/packages/kv-asset-handler/test/getAssetFromKV-optional.test.ts new file mode 100644 index 0000000..2f6dffe --- /dev/null +++ b/packages/kv-asset-handler/test/getAssetFromKV-optional.test.ts @@ -0,0 +1,22 @@ +import { test, vi } from "vitest"; +import { getAssetFromKV } from "../src/index"; +import { getEvent, mockGlobalScope, mockRequestScope } from "./mocks"; + +test("getAssetFromKV return correct val from KV without manifest", async ({ + expect, +}) => { + mockRequestScope(); + mockGlobalScope(); + // manually reset manifest global, to test optional behaviour + vi.stubGlobal("__STATIC_CONTENT_MANIFEST", undefined); + + const event = getEvent(new Request("https://blah.com/key1.123HASHBROWN.txt")); + const res = await getAssetFromKV(event); + + if (res) { + expect(await res.text()).toBe("val1"); + expect(res.headers.get("content-type")).toContain("text"); + } else { + expect.fail("Response was undefined"); + } +}); diff --git a/packages/kv-asset-handler/test/getAssetFromKV.test.ts b/packages/kv-asset-handler/test/getAssetFromKV.test.ts new file mode 100644 index 0000000..f3af2c7 --- /dev/null +++ b/packages/kv-asset-handler/test/getAssetFromKV.test.ts @@ -0,0 +1,574 @@ +import { beforeEach, test } from "vitest"; +import { getAssetFromKV, mapRequestToAsset } from "../src/index"; +import { + getEvent, + mockGlobalScope, + mockKV, + mockManifest, + mockRequestScope, + sleep, +} from "./mocks"; + +beforeEach(async () => { + mockGlobalScope(); + mockRequestScope(); +}); + +test("getAssetFromKV return correct val from KV and default caching", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/key1.txt")); + const res = await getAssetFromKV(event); + + if (res) { + expect(res.headers.get("cache-control")).toBe(null); + expect(res.headers.get("cf-cache-status")).toBe("MISS"); + expect(await res.text()).toBe("val1"); + expect(res.headers.get("content-type")).toContain("text"); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client", async ({ + expect, +}) => { + const event = getEvent(new Request(`https://foo.com/client/`)); + const res = await getAssetFromKV(event); + expect(await res.text()).toBe("important file"); + expect(res.headers.get("content-type")).toContain("text"); +}); +test("getAssetFromKV evaluated the file matching the extensionless path first /client -> client", async ({ + expect, +}) => { + const event = getEvent(new Request(`https://foo.com/client`)); + const res = await getAssetFromKV(event); + expect(await res.text()).toBe("important file"); + expect(res.headers.get("content-type")).toContain("text"); +}); + +test("getAssetFromKV if not in asset manifest still returns nohash.txt", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/nohash.txt")); + const res = await getAssetFromKV(event); + + if (res) { + expect(await res.text()).toBe("no hash but still got some result"); + expect(res.headers.get("content-type")).toContain("text"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV if no asset manifest /client -> client fails", async ({ + expect, +}) => { + const event = getEvent(new Request(`https://foo.com/client`)); + await expect(() => + getAssetFromKV(event, { ASSET_MANIFEST: {} }) + ).rejects.toThrow(expect.objectContaining({ status: 404 })); +}); + +test("getAssetFromKV if sub/ -> sub/index.html served", async ({ expect }) => { + const event = getEvent(new Request(`https://foo.com/sub`)); + const res = await getAssetFromKV(event); + if (res) { + expect(await res.text()).toBe("picturedis"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV gets index.html by default for / requests", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/")); + const res = await getAssetFromKV(event); + + if (res) { + expect(await res.text()).toBe("index.html"); + expect(res.headers.get("content-type")).toContain("html"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV non ASCII path support", async ({ expect }) => { + const event = getEvent(new Request("https://blah.com/测试.html")); + const res = await getAssetFromKV(event); + + if (res) { + expect(await res.text()).toBe("My filename is non-ascii"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV supports browser percent encoded URLs", async ({ + expect, +}) => { + const event = getEvent( + new Request("https://example.com/%not-really-percent-encoded.html") + ); + const res = await getAssetFromKV(event); + + if (res) { + expect(await res.text()).toBe("browser percent encoded"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV supports user percent encoded URLs", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/%2F.html")); + const res = await getAssetFromKV(event); + + if (res) { + expect(await res.text()).toBe("user percent encoded"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV only decode URL when necessary", async ({ expect }) => { + const event1 = getEvent( + new Request("https://blah.com/%E4%BD%A0%E5%A5%BD.html") + ); + const event2 = getEvent(new Request("https://blah.com/你好.html")); + const res1 = await getAssetFromKV(event1); + const res2 = await getAssetFromKV(event2); + + if (res1 && res2) { + expect(await res1.text()).toBe("Im important"); + expect(await res2.text()).toBe("Im important"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV Support for user decode url path", async ({ expect }) => { + const event1 = getEvent(new Request("https://blah.com/%E4%BD%A0%E5%A5%BD/")); + const event2 = getEvent(new Request("https://blah.com/你好/")); + const res1 = await getAssetFromKV(event1); + const res2 = await getAssetFromKV(event2); + + if (res1 && res2) { + expect(await res1.text()).toBe("My path is non-ascii"); + expect(await res2.text()).toBe("My path is non-ascii"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV custom key modifier", async ({ expect }) => { + const event = getEvent(new Request("https://blah.com/docs/sub/blah.png")); + + const customRequestMapper = (request: Request) => { + const defaultModifiedRequest = mapRequestToAsset(request); + + const url = new URL(defaultModifiedRequest.url); + url.pathname = url.pathname.replace("/docs", ""); + return new Request(url.toString(), request); + }; + + const res = await getAssetFromKV(event, { + mapRequestToAsset: customRequestMapper, + }); + + if (res) { + expect(await res.text()).toBe("picturedis"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV request override with existing manifest file", async ({ + expect, +}) => { + // see https://github.com/cloudflare/kv-asset-handler/pull/159 for more info + const event = getEvent(new Request("https://blah.com/image.png")); // real file in manifest + + const customRequestMapper = (request: Request) => { + const defaultModifiedRequest = mapRequestToAsset(request); + + const url = new URL(defaultModifiedRequest.url); + url.pathname = "/image.webp"; // other different file in manifest + return new Request(url.toString(), request); + }; + + const res = await getAssetFromKV(event, { + mapRequestToAsset: customRequestMapper, + }); + + if (res) { + expect(await res.text()).toBe("imagewebp"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV when setting browser caching", async ({ expect }) => { + const event = getEvent(new Request("https://blah.com/")); + + const res = await getAssetFromKV(event, { cacheControl: { browserTTL: 22 } }); + + if (res) { + expect(res.headers.get("cache-control")).toBe("max-age=22"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV when setting custom cache setting", async ({ expect }) => { + const event1 = getEvent(new Request("https://blah.com/")); + const event2 = getEvent(new Request("https://blah.com/key1.png?blah=34")); + const cacheOnlyPngs = (req: Request) => { + if (new URL(req.url).pathname.endsWith(".png")) { + return { + browserTTL: 720, + edgeTTL: 720, + }; + } else { + return { + bypassCache: true, + }; + } + }; + + const res1 = await getAssetFromKV(event1, { cacheControl: cacheOnlyPngs }); + const res2 = await getAssetFromKV(event2, { cacheControl: cacheOnlyPngs }); + + if (res1 && res2) { + expect(res1.headers.get("cache-control")).toBe(null); + expect(res2.headers.get("content-type")).toContain("png"); + expect(res2.headers.get("cache-control")).toBe("max-age=720"); + expect(res2.headers.get("cf-cache-status")).toBe("MISS"); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV caches on two sequential requests", async ({ expect }) => { + const resourceKey = "cache.html"; + const resourceVersion = JSON.parse(mockManifest())[resourceKey]; + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)); + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}"`, + }, + }) + ); + + const res1 = await getAssetFromKV(event1, { + cacheControl: { edgeTTL: 720, browserTTL: 720 }, + }); + await sleep(1); + const res2 = await getAssetFromKV(event2); + + if (res1 && res2) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res1.headers.get("cache-control")).toBe("max-age=720"); + expect(res2.headers.get("cf-cache-status")).toBe("REVALIDATED"); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV does not store max-age on two sequential requests", async ({ + expect, +}) => { + const resourceKey = "cache.html"; + const resourceVersion = JSON.parse(mockManifest())[resourceKey]; + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)); + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}"`, + }, + }) + ); + + const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }); + await sleep(100); + const res2 = await getAssetFromKV(event2); + + if (res1 && res2) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res1.headers.get("cache-control")).toBe(null); + expect(res2.headers.get("cf-cache-status")).toBe("REVALIDATED"); + expect(res2.headers.get("cache-control")).toBe(null); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV does not cache on Cloudflare when bypass cache set", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/")); + + const res = await getAssetFromKV(event, { + cacheControl: { bypassCache: true }, + }); + + if (res) { + expect(res.headers.get("cache-control")).toBe(null); + expect(res.headers.get("cf-cache-status")).toBe(null); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV with no trailing slash on root", async ({ expect }) => { + const event = getEvent(new Request("https://blah.com")); + const res = await getAssetFromKV(event); + if (res) { + expect(await res.text()).toBe("index.html"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV with no trailing slash on a subdirectory", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/sub/blah.png")); + const res = await getAssetFromKV(event); + if (res) { + expect(await res.text()).toBe("picturedis"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV no result throws an error", async ({ expect }) => { + const event = getEvent(new Request("https://blah.com/random")); + await expect(getAssetFromKV(event)).rejects.toThrow( + expect.objectContaining({ status: 404 }) + ); +}); +test("getAssetFromKV TTls set to null should not cache on browser or edge", async ({ + expect, +}) => { + const event = getEvent(new Request("https://blah.com/")); + + const res1 = await getAssetFromKV(event, { + cacheControl: { browserTTL: null, edgeTTL: null }, + }); + await sleep(100); + const res2 = await getAssetFromKV(event, { + cacheControl: { browserTTL: null, edgeTTL: null }, + }); + + if (res1 && res2) { + expect(res1.headers.get("cf-cache-status")).toBe(null); + expect(res1.headers.get("cache-control")).toBe(null); + expect(res2.headers.get("cf-cache-status")).toBe(null); + expect(res2.headers.get("cache-control")).toBe(null); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV passing in a custom NAMESPACE serves correct asset", async ({ + expect, +}) => { + const CUSTOM_NAMESPACE = mockKV({ + "key1.123HASHBROWN.txt": "val1", + }); + Object.assign(globalThis, { CUSTOM_NAMESPACE }); + const event = getEvent(new Request("https://blah.com/")); + const res = await getAssetFromKV(event); + if (res) { + expect(await res.text()).toBe("index.html"); + expect(res.headers.get("content-type")).toContain("html"); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV when custom namespace without the asset should fail", async ({ + expect, +}) => { + const CUSTOM_NAMESPACE = mockKV({ + "key5.123HASHBROWN.txt": "customvalu", + }); + + const event = getEvent(new Request("https://blah.com")); + await expect( + getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE }) + ).rejects.toThrow(expect.objectContaining({ status: 404 })); +}); +test("getAssetFromKV when namespace not bound fails", async ({ expect }) => { + const MY_CUSTOM_NAMESPACE: KVNamespace = undefined; + Object.assign(globalThis, { MY_CUSTOM_NAMESPACE }); + + const event = getEvent(new Request("https://blah.com/")); + await expect( + getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE }) + ).rejects.toThrow(expect.objectContaining({ status: 500 })); +}); + +test("getAssetFromKV when if-none-match === active resource version, should revalidate", async ({ + expect, +}) => { + const resourceKey = "key1.png"; + const resourceVersion = JSON.parse(mockManifest())[resourceKey]; + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)); + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { + "if-none-match": `W/"${resourceVersion}"`, + }, + }) + ); + + const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }); + await sleep(100); + const res2 = await getAssetFromKV(event2); + + if (res1 && res2) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res2.headers.get("cf-cache-status")).toBe("REVALIDATED"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV when if-none-match equals etag of stale resource then should bypass cache", async ({ + expect, +}) => { + const resourceKey = "key1.png"; + const resourceVersion = JSON.parse(mockManifest())[resourceKey]; + const req1 = new Request(`https://blah.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}"`, + }, + }); + const req2 = new Request(`https://blah.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}-another-version"`, + }, + }); + const event = getEvent(req1); + const event2 = getEvent(req2); + const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }); + const res2 = await getAssetFromKV(event); + const res3 = await getAssetFromKV(event2); + if (res1 && res2 && res3) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res2.headers.get("etag")).toBe( + `W/${req1.headers.get("if-none-match")}` + ); + expect(res2.headers.get("cf-cache-status")).toBe("REVALIDATED"); + expect(res3.headers.get("etag")).not.toBe( + req2.headers.get("if-none-match") + ); + expect(res3.headers.get("cf-cache-status")).toBe("MISS"); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV when resource in cache, etag should be weakened before returned to eyeball", async ({ + expect, +}) => { + const resourceKey = "key1.png"; + const resourceVersion = JSON.parse(mockManifest())[resourceKey]; + const req1 = new Request(`https://blah.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}"`, + }, + }); + const event = getEvent(req1); + const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }); + const res2 = await getAssetFromKV(event); + if (res1 && res2) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res2.headers.get("etag")).toBe( + `W/${req1.headers.get("if-none-match")}` + ); + } else { + expect.fail("Response was undefined"); + } +}); +test("getAssetFromKV should support weak etag override of resource", async ({ + expect, +}) => { + const resourceKey = "key1.png"; + const resourceVersion = JSON.parse(mockManifest())[resourceKey]; + const req1 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + "if-none-match": `W/"${resourceVersion}"`, + }, + }); + const req2 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}"`, + }, + }); + const req3 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + "if-none-match": `"${resourceVersion}-another-version"`, + }, + }); + const event1 = getEvent(req1); + const event2 = getEvent(req2); + const event3 = getEvent(req3); + const res1 = await getAssetFromKV(event1, { defaultETag: "weak" }); + const res2 = await getAssetFromKV(event2, { defaultETag: "weak" }); + const res3 = await getAssetFromKV(event3, { defaultETag: "weak" }); + if (res1 && res2 && res3) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res1.headers.get("etag")).toBe(req1.headers.get("if-none-match")); + expect(res2.headers.get("cf-cache-status")).toBe("REVALIDATED"); + expect(res2.headers.get("etag")).toBe( + `W/${req2.headers.get("if-none-match")}` + ); + expect(res3.headers.get("cf-cache-status")).toBe("MISS"); + expect(res3.headers.get("etag")).not.toBe( + req2.headers.get("if-none-match") + ); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV if-none-match not sent but resource in cache, should return cache hit 200 OK", async ({ + expect, +}) => { + const resourceKey = "cache.html"; + const event = getEvent(new Request(`https://blah.com/${resourceKey}`)); + const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }); + await sleep(1); + const res2 = await getAssetFromKV(event); + if (res1 && res2) { + expect(res1.headers.get("cf-cache-status")).toBe("MISS"); + expect(res1.headers.get("cache-control")).toBe(null); + expect(res2.status).toBe(200); + expect(res2.headers.get("cf-cache-status")).toBe("HIT"); + } else { + expect.fail("Response was undefined"); + } +}); + +test("getAssetFromKV if range request submitted and resource in cache, request fulfilled", async ({ + expect, +}) => { + const resourceKey = "cache.html"; + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)); + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { range: "bytes=0-10" }, + }) + ); + const res1 = getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }); + await res1; + await sleep(2); + const res2 = await getAssetFromKV(event2); + if (res2.headers.has("content-range")) { + expect(res2.status).toBe(206); + } else { + expect.fail("Response was undefined"); + } +}); + +test.todo("getAssetFromKV when body not empty, should invoke .cancel()"); diff --git a/packages/kv-asset-handler/test/mapRequestToAsset.test.ts b/packages/kv-asset-handler/test/mapRequestToAsset.test.ts new file mode 100644 index 0000000..398ba57 --- /dev/null +++ b/packages/kv-asset-handler/test/mapRequestToAsset.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, test } from "vitest"; +import { mapRequestToAsset } from "../src/index"; +import { mockGlobalScope, mockRequestScope } from "./mocks"; + +beforeEach(() => { + mockGlobalScope(); + mockRequestScope(); +}); + +test("mapRequestToAsset() correctly changes /about -> /about/index.html", async ({ + expect, +}) => { + const path = "/about"; + const request = new Request(`https://foo.com${path}`); + const newRequest = mapRequestToAsset(request); + expect(newRequest.url).toBe(request.url + "/index.html"); +}); + +test("mapRequestToAsset() correctly changes /about/ -> /about/index.html", async ({ + expect, +}) => { + const path = "/about/"; + const request = new Request(`https://foo.com${path}`); + const newRequest = mapRequestToAsset(request); + expect(newRequest.url).toBe(request.url + "index.html"); +}); + +test("mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html", async ({ + expect, +}) => { + const path = "/about.me/"; + const request = new Request(`https://foo.com${path}`); + const newRequest = mapRequestToAsset(request); + expect(newRequest.url).toBe(request.url + "index.html"); +}); + +test("mapRequestToAsset() correctly changes /about -> /about/default.html", async ({ + expect, +}) => { + const path = "/about"; + const request = new Request(`https://foo.com${path}`); + const newRequest = mapRequestToAsset(request, { + defaultDocument: "default.html", + }); + expect(newRequest.url).toBe(request.url + "/default.html"); +}); diff --git a/packages/kv-asset-handler/test/mocks.ts b/packages/kv-asset-handler/test/mocks.ts new file mode 100644 index 0000000..4815a8a --- /dev/null +++ b/packages/kv-asset-handler/test/mocks.ts @@ -0,0 +1,158 @@ +import { vi } from "vitest"; + +const HASH = "123HASHBROWN"; + +export const getEvent = ( + request: Request +): Pick => { + return { + request, + async waitUntil(maybePromise: unknown) { + await maybePromise; + }, + }; +}; +const store = { + "key1.123HASHBROWN.txt": "val1", + "key1.123HASHBROWN.png": "val1", + "index.123HASHBROWN.html": "index.html", + "cache.123HASHBROWN.html": "cache me if you can", + "测试.123HASHBROWN.html": "My filename is non-ascii", + "%not-really-percent-encoded.123HASHBROWN.html": "browser percent encoded", + "%2F.123HASHBROWN.html": "user percent encoded", + "你好.123HASHBROWN.html": "I shouldnt be served", + "%E4%BD%A0%E5%A5%BD.123HASHBROWN.html": "Im important", + "nohash.txt": "no hash but still got some result", + "sub/blah.123HASHBROWN.png": "picturedis", + "sub/index.123HASHBROWN.html": "picturedis", + "client.123HASHBROWN": "important file", + "client.123HASHBROWN/index.html": "Im here but serve my big bro above", + "image.123HASHBROWN.png": "imagepng", + "image.123HASHBROWN.webp": "imagewebp", + "你好/index.123HASHBROWN.html": "My path is non-ascii", +}; +export const mockKV = (kvStore: Record) => { + return { + get: async (path: string) => kvStore[path] || null, + } as KVNamespace; +}; + +export const mockManifest = () => { + return JSON.stringify({ + "key1.txt": `key1.${HASH}.txt`, + "key1.png": `key1.${HASH}.png`, + "cache.html": `cache.${HASH}.html`, + "测试.html": `测试.${HASH}.html`, + "你好.html": `你好.${HASH}.html`, + "%not-really-percent-encoded.html": `%not-really-percent-encoded.${HASH}.html`, + "%2F.html": `%2F.${HASH}.html`, + "%E4%BD%A0%E5%A5%BD.html": `%E4%BD%A0%E5%A5%BD.${HASH}.html`, + "index.html": `index.${HASH}.html`, + "sub/blah.png": `sub/blah.${HASH}.png`, + "sub/index.html": `sub/index.${HASH}.html`, + client: `client.${HASH}`, + "client/index.html": `client.${HASH}`, + "image.png": `image.${HASH}.png`, + "image.webp": `image.${HASH}.webp`, + "你好/index.html": `你好/index.${HASH}.html`, + }); +}; + +interface CacheKey { + url: string; + headers: HeadersInit; +} +export const mockDefaultCache = () => { + const cacheStore = new Map(); + // @ts-expect-error we should pick cf types here + vi.spyOn(caches.default, "match").mockImplementation(async (key: Request) => { + const cacheKey: CacheKey = { + url: key.url, + headers: {}, + }; + let response; + if (key.headers.has("if-none-match")) { + const makeStrongEtag = key.headers.get("if-none-match").replace("W/", ""); + Reflect.set(cacheKey.headers, "etag", makeStrongEtag); + response = cacheStore.get(JSON.stringify(cacheKey)); + } else { + // if client doesn't send if-none-match, we need to iterate through these keys + // and just test the URL + const activeCacheKeys: Array = Array.from(cacheStore.keys()); + for (const cacheStoreKey of activeCacheKeys) { + if (JSON.parse(cacheStoreKey).url === key.url) { + response = cacheStore.get(cacheStoreKey); + } + } + } + if (!response) { + return undefined; + } + + const headers = new Headers(response?.headers); + let status = response?.status; + + // TODO: write test to accommodate for rare scenarios with where range requests accommodate etags + if (!key.headers.has("if-none-match")) { + // this appears overly verbose, but is necessary to document edge cache behavior + // The Range request header triggers the response header Content-Range ... + const range = key.headers.get("range"); + if (range) { + headers.set( + "content-range", + `bytes ${range.split("=").pop()}/${headers.get("content-length")}` + ); + } + // ... which we are using in this repository to set status 206 + if (headers.has("content-range")) { + status = 206; + } else { + status = 200; + } + const etag = headers.get("etag"); + if (etag && !etag.includes("W/")) { + headers.set("etag", `W/${etag}`); + } + } + return new Response(response.body, { ...response, headers, status }); + }); + + // @ts-expect-error we should pick cf types here + vi.spyOn(caches.default, "put").mockImplementation( + async (key: Request, val: Response) => { + const headers = new Headers(val.headers); + const url = new URL(key.url); + const resWithBody = new Response(val.body, { headers, status: 200 }); + const resNoBody = new Response(null, { headers, status: 304 }); + const cacheKey: CacheKey = { + url: key.url, + headers: { + etag: `"${url.pathname.replace("/", "")}"`, + }, + }; + cacheStore.set(JSON.stringify(cacheKey), resNoBody); + cacheKey.headers = {}; + cacheStore.set(JSON.stringify(cacheKey), resWithBody); + return; + } + ); +}; + +// mocks functionality used inside worker request +export function mockRequestScope() { + // const serviceWorkerEnv = makeServiceWorkerEnv(); + // for (const key of Object.keys(serviceWorkerEnv)) { + // vi.stubGlobal(key, serviceWorkerEnv[key as keyof WorkerGlobalScope]); + // } + mockDefaultCache(); +} + +// mocks functionality used on global isolate scope. such as the KV namespace bind +export function mockGlobalScope() { + vi.stubGlobal("__STATIC_CONTENT_MANIFEST", mockManifest()); + vi.stubGlobal("__STATIC_CONTENT", mockKV(store)); +} + +export const sleep = (milliseconds: number) => { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +}; diff --git a/packages/kv-asset-handler/test/serveSinglePageApp.test.ts b/packages/kv-asset-handler/test/serveSinglePageApp.test.ts new file mode 100644 index 0000000..c088298 --- /dev/null +++ b/packages/kv-asset-handler/test/serveSinglePageApp.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, test } from "vitest"; +import { serveSinglePageApp } from "../src/index"; +import { mockGlobalScope, mockRequestScope } from "./mocks"; + +beforeEach(() => { + mockGlobalScope(); + mockRequestScope(); +}); + +function testRequest(path: string) { + const url = new URL("https://example.com"); + url.pathname = path; + const request = new Request(url.toString()); + + return request; +} + +test("serveSinglePageApp returns root asset path when request path ends in .html", async ({ + expect, +}) => { + const path = "/foo/thing.html"; + const request = testRequest(path); + + const expected_request = testRequest("/index.html"); + const actual_request = serveSinglePageApp(request); + + expect(expected_request.url).toEqual(actual_request.url); +}); + +test("serveSinglePageApp returns root asset path when request path does not have extension", async ({ + expect, +}) => { + const path = "/foo/thing"; + const request = testRequest(path); + + const expected_request = testRequest("/index.html"); + const actual_request = serveSinglePageApp(request); + + expect(expected_request.url).toEqual(actual_request.url); +}); + +test("serveSinglePageApp returns requested asset when request path has non-html extension", async ({ + expect, +}) => { + const path = "/foo/thing.js"; + const request = testRequest(path); + + const expected_request = request; + const actual_request = serveSinglePageApp(request); + + expect(expected_request.url).toEqual(actual_request.url); +}); diff --git a/packages/kv-asset-handler/tsconfig.json b/packages/kv-asset-handler/tsconfig.json new file mode 100644 index 0000000..7dc8b75 --- /dev/null +++ b/packages/kv-asset-handler/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "target": "ES2022", + "allowJs": false, + "types": ["@cloudflare/workers-types"], + "module": "es2022", + "moduleResolution": "bundler", + "skipLibCheck": true, + "noEmit": true + }, + "include": ["./src", "./test"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/kv-asset-handler/tsup.config.ts b/packages/kv-asset-handler/tsup.config.ts new file mode 100644 index 0000000..cba60d9 --- /dev/null +++ b/packages/kv-asset-handler/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; + +export default defineConfig(() => [ + { + clean: true, + treeshake: true, + keepNames: true, + entry: ["src/index.ts"], + platform: "node", + format: "esm", + dts: true, + outDir: "dist", + tsconfig: "tsconfig.json", + metafile: true, + sourcemap: process.env.SOURCEMAPS !== "false", + define: { + "process.env.NODE_ENV": `'${"production"}'`, + }, + }, +]); diff --git a/packages/kv-asset-handler/turbo.json b/packages/kv-asset-handler/turbo.json new file mode 100644 index 0000000..987b69a --- /dev/null +++ b/packages/kv-asset-handler/turbo.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"], + "env": ["SOURCEMAPS"] + } + } +} diff --git a/packages/kv-asset-handler/vitest.config.mts b/packages/kv-asset-handler/vitest.config.mts new file mode 100644 index 0000000..15d16da --- /dev/null +++ b/packages/kv-asset-handler/vitest.config.mts @@ -0,0 +1,16 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + miniflare: { + compatibilityDate: "2025-02-04", + }, + }), + ], + test: { + reporters: ["default"], + testTimeout: 50_000, + }, +}); diff --git a/packages/lint-config-shared/CHANGELOG.md b/packages/lint-config-shared/CHANGELOG.md new file mode 100644 index 0000000..92f2f1c --- /dev/null +++ b/packages/lint-config-shared/CHANGELOG.md @@ -0,0 +1,30 @@ +# @cloudflare/lint-config-shared + +## 1.2.1 + +### Patch Changes + +- [#12347](https://github.com/cloudflare/workers-sdk/pull/12347) [`1a1f9e4`](https://github.com/cloudflare/workers-sdk/commit/1a1f9e4802e9c524cc8c59cca6340c995ae8fa12) Thanks [@vicb](https://github.com/vicb)! - Add an ESLint rule checking that `expect` is not imported from `vitest`. + + Retrieving `expect` from the test context is safer for concurrent tests, + so we will standardize on using that. + +## 1.2.0 + +### Minor Changes + +- [#11902](https://github.com/cloudflare/workers-sdk/pull/11902) [`2aa769c`](https://github.com/cloudflare/workers-sdk/commit/2aa769c8730a0ef99f315f9a1dd5d25d52c51974) Thanks [@emily-shen](https://github.com/emily-shen)! - Add a custom eslint rule that checks for unsafe command execution + +## 1.1.0 + +### Minor Changes + +- [#4139](https://github.com/cloudflare/workers-sdk/pull/4139) [`884e4188`](https://github.com/cloudflare/workers-sdk/commit/884e41881687c34957bf22f97fb12a127707aef9) Thanks [@1000hz](https://github.com/1000hz)! - `import/order` rule has been removed. + +## 1.0.0 + +### Major Changes + +- [#3833](https://github.com/cloudflare/workers-sdk/pull/3833) [`c2f1b689`](https://github.com/cloudflare/workers-sdk/commit/c2f1b6897ad6ccdb2d5fe8fc24fa5ea645723bf1) Thanks [@1000hz](https://github.com/1000hz)! - Updated `import/order` and `unused-imports/no-unused-vars` to raise errors. + + This change updates all rules currently raising warnings to instead raise errors. Our lint philosophy should not allow problems to be merged without being explicitly ignored. diff --git a/packages/lint-config-shared/oxlint-plugin.mjs b/packages/lint-config-shared/oxlint-plugin.mjs new file mode 100644 index 0000000..67fe604 --- /dev/null +++ b/packages/lint-config-shared/oxlint-plugin.mjs @@ -0,0 +1,13 @@ +import noDirectRecursiveRm from "./rules/no-direct-recursive-rm.mjs"; +import noUnsafeCommandExecution from "./rules/no-unsafe-command-execution.mjs"; +import noWranglerNamedImports from "./rules/no-wrangler-named-imports.mjs"; +import requireDescriptionWhenDisabling from "./rules/require-description-when-disabling.mjs"; + +export default { + rules: { + "no-direct-recursive-rm": noDirectRecursiveRm, + "no-unsafe-command-execution": noUnsafeCommandExecution, + "no-wrangler-named-imports": noWranglerNamedImports, + "require-description-when-disabling": requireDescriptionWhenDisabling, + }, +}; diff --git a/packages/lint-config-shared/package.json b/packages/lint-config-shared/package.json new file mode 100644 index 0000000..45d6bc6 --- /dev/null +++ b/packages/lint-config-shared/package.json @@ -0,0 +1,22 @@ +{ + "name": "@cloudflare/lint-config-shared", + "version": "1.2.1", + "private": true, + "description": "Custom lint rules for workers-sdk (used by oxlint jsPlugins)", + "type": "module", + "exports": { + ".": "./oxlint-plugin.mjs" + }, + "scripts": { + "deploy": "echo 'no deploy'", + "test": "node rules/__tests__/no-unsafe-command-execution.test.mjs && node rules/__tests__/no-direct-recursive-rm.test.mjs", + "test:ci": "node rules/__tests__/no-unsafe-command-execution.test.mjs && node rules/__tests__/no-direct-recursive-rm.test.mjs" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "eslint": "^9.39.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/lint-config-shared/rules/__tests__/no-direct-recursive-rm.test.mjs b/packages/lint-config-shared/rules/__tests__/no-direct-recursive-rm.test.mjs new file mode 100644 index 0000000..120bfd2 --- /dev/null +++ b/packages/lint-config-shared/rules/__tests__/no-direct-recursive-rm.test.mjs @@ -0,0 +1,129 @@ +import { RuleTester } from "eslint"; +import rule from "../no-direct-recursive-rm.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 2022, sourceType: "module" }, +}); + +ruleTester.run("no-direct-recursive-rm", rule, { + valid: [ + // Single file removal (no recursive option) — should be allowed + { + code: 'import fs from "node:fs"; fs.rmSync("file.txt");', + }, + { + code: 'import fs from "node:fs"; fs.rmSync("file.txt", { force: true });', + }, + { + code: 'import fs from "node:fs/promises"; await fs.rm("file.txt");', + }, + { + code: 'import fs from "node:fs/promises"; await fs.rm("file.txt", { force: true });', + }, + // Named import without recursive + { + code: 'import { rmSync } from "node:fs"; rmSync("file.txt");', + }, + { + code: 'import { rm } from "node:fs/promises"; await rm("file.txt", { force: true });', + }, + // Named import rm from "node:fs" (callback-based) without recursive + { + code: 'import { rm } from "node:fs"; rm("file.txt", (err) => {});', + }, + // Using the correct helpers + { + code: 'import { removeDir } from "@cloudflare/workers-utils"; await removeDir("dir");', + }, + { + code: 'import { removeDirSync } from "@cloudflare/workers-utils"; removeDirSync("dir");', + }, + // recursive: false is fine + { + code: 'import fs from "node:fs"; fs.rmSync("dir", { recursive: false });', + }, + // Unrelated function called rm + { + code: 'function rm(path, opts) {} rm("dir", { recursive: true });', + }, + // CommonJS single file removal + { + code: 'const fs = require("node:fs"); fs.rmSync("file.txt");', + }, + { + code: 'const fs = require("node:fs/promises"); fs.rm("file.txt", { force: true });', + }, + ], + + invalid: [ + // Default import from "node:fs" — fs.rmSync with recursive + { + code: 'import fs from "node:fs"; fs.rmSync("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Namespace import from "node:fs" — fs.rmSync with recursive + { + code: 'import * as fs from "node:fs"; fs.rmSync("dir", { recursive: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Named import from "node:fs" — rmSync with recursive + { + code: 'import { rmSync } from "node:fs"; rmSync("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Default import from "node:fs/promises" — fs.rm with recursive + { + code: 'import fs from "node:fs/promises"; await fs.rm("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Named import from "node:fs/promises" — rm with recursive + { + code: 'import { rm } from "node:fs/promises"; await rm("dir", { recursive: true, maxRetries: 10 });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // fs.promises.rm pattern + { + code: 'import fs from "node:fs"; fs.promises.rm("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Without "node:" prefix + { + code: 'import fs from "fs"; fs.rmSync("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + { + code: 'import fs from "fs/promises"; await fs.rm("dir", { force: true, recursive: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // CommonJS require — default + { + code: 'const fs = require("node:fs"); fs.rmSync("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // CommonJS require — promises + { + code: 'const fs = require("node:fs/promises"); fs.rm("dir", { recursive: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // CommonJS require — fs.promises.rm + { + code: 'const fs = require("node:fs"); fs.promises.rm("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Callback-based fs.rm from "node:fs" with recursive in second arg + { + code: 'import fs from "node:fs"; fs.rm("dir", { recursive: true, force: true }, (err) => {});', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Named import rm from "node:fs" (callback-based) with recursive + { + code: 'import { rm } from "node:fs"; rm("dir", { recursive: true, force: true }, (err) => {});', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + // Named import rm from "node:fs" with recursive (no callback) + { + code: 'import { rm } from "node:fs"; rm("dir", { recursive: true, force: true });', + errors: [{ messageId: "noDirectRecursiveRm" }], + }, + ], +}); diff --git a/packages/lint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs b/packages/lint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs new file mode 100644 index 0000000..b18bee0 --- /dev/null +++ b/packages/lint-config-shared/rules/__tests__/no-unsafe-command-execution.test.mjs @@ -0,0 +1,192 @@ +import { RuleTester } from "eslint"; +import rule from "../no-unsafe-command-execution.mjs"; + +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 2022, sourceType: "module" }, +}); + +ruleTester.run("no-unsafe-command-execution", rule, { + valid: [ + // Static strings are safe + { + code: 'const { execSync } = require("child_process"); execSync("ls -la");', + }, + { + code: 'require("child_process").execSync("git status");', + }, + // execFileSync with argument array is safe + { + code: 'const { execFileSync } = require("child_process"); execFileSync("git", ["show", "-s", commitHash]);', + }, + // spawn with argument array and no shell is safe + { + code: 'const { spawn } = require("child_process"); spawn("git", ["status"]);', + }, + // Template literals without expressions are safe + { + code: 'const { execSync } = require("child_process"); execSync(`ls -la`);', + }, + // Non-child_process functions are ignored + { + code: "const result = exec(`command ${variable}`);", + }, + // node:child_process also works + { + code: 'const { execSync } = require("node:child_process"); execSync("ls");', + }, + // ES module imports are safe with static strings + { + code: 'import { execSync } from "node:child_process"; execSync("git status");', + }, + { + code: 'import { execFileSync } from "node:child_process"; execFileSync("git", ["show", commitHash]);', + }, + // Aliased imports with static strings are safe + { + code: 'import { execSync as run } from "node:child_process"; run("git status");', + }, + // Namespaced imports with static strings are safe + { + code: 'import * as cp from "node:child_process"; cp.execSync("git status");', + }, + // Aliased CommonJS imports with static strings are safe + { + code: 'const { execSync: run } = require("node:child_process"); run("git status");', + }, + ], + + invalid: [ + // Template literal with expression in execSync + { + code: 'const { execSync } = require("child_process"); execSync(`git show ${commitHash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // String concatenation in execSync + { + code: 'const { execSync } = require("child_process"); execSync("git show " + commitHash);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Direct require pattern + { + code: 'require("child_process").execSync(`command ${variable}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // exec (not execSync) + { + code: 'const { exec } = require("child_process"); exec(`ls ${dir}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // spawn with shell: true and template literal + { + code: 'const { spawn } = require("child_process"); spawn(`ls ${dir}`, [], { shell: true });', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // execFile with template literal + { + code: 'const { execFile } = require("child_process"); execFile(`command ${arg}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // node:child_process variant + { + code: 'const { execSync } = require("node:child_process"); execSync(`git ${command}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // ES module import with template literal + { + code: 'import { execSync } from "node:child_process"; execSync(`git show ${commitHash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // ES module import with string concatenation + { + code: 'import { execSync } from "node:child_process"; execSync("git show " + commitHash);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // The actual vulnerability we found + { + code: ` +const { execSync } = require("child_process"); +if (!commitMessage) { + commitMessage = execSync(\`git show -s --format=%B \${commitHash}\`) + .toString() + .trim(); +}`, + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Aliased ES module import with template literal + { + code: 'import { execSync as run } from "node:child_process"; run(`git show ${hash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Namespaced ES module import with template literal + { + code: 'import * as cp from "node:child_process"; cp.execSync(`git show ${hash}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Aliased CommonJS import with template literal + { + code: 'const { execSync: run } = require("node:child_process"); run(`git ${cmd}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + // Namespaced CommonJS import with template literal + { + code: 'const cp = require("node:child_process"); cp.execSync(`git ${cmd}`);', + errors: [ + { + messageId: "unsafeCommandExecution", + }, + ], + }, + ], +}); diff --git a/packages/lint-config-shared/rules/no-direct-recursive-rm.mjs b/packages/lint-config-shared/rules/no-direct-recursive-rm.mjs new file mode 100644 index 0000000..63bc2f5 --- /dev/null +++ b/packages/lint-config-shared/rules/no-direct-recursive-rm.mjs @@ -0,0 +1,235 @@ +/** + * Module sources for Node.js filesystem APIs + */ +const FS_MODULES = ["fs", "node:fs"]; +const FS_PROMISES_MODULES = ["fs/promises", "node:fs/promises"]; + +/** + * Check if an options argument contains `recursive: true` + */ +function hasRecursiveOption(node) { + if (!node || node.type !== "ObjectExpression") { + return false; + } + + return node.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "recursive" && + prop.value.type === "Literal" && + prop.value.value === true + ); +} + +/** + * Resolve the import source for a given identifier by walking the scope chain. + * Returns the module specifier string (e.g. "node:fs") or null if not from an fs module. + */ +function resolveImportSource(name, scope) { + let currentScope = scope; + while (currentScope) { + const variable = currentScope.set.get(name); + if (variable && variable.defs.length > 0) { + const def = variable.defs[0]; + + // ES module import: import fs from "node:fs" / import { rmSync } from "node:fs" + if ( + def.type === "ImportBinding" && + def.parent?.type === "ImportDeclaration" + ) { + return def.parent.source?.value ?? null; + } + + // CommonJS require: const fs = require("node:fs") + if ( + def.type === "Variable" && + def.node?.init?.type === "CallExpression" && + def.node.init.callee.name === "require" && + def.node.init.arguments[0]?.type === "Literal" + ) { + return def.node.init.arguments[0].value; + } + } + currentScope = currentScope.upper; + } + return null; +} + +/** + * Get the scope for a node using the available API. + */ +function getScope(node, context) { + const sourceCode = context.sourceCode || context.getSourceCode(); + return sourceCode.getScope ? sourceCode.getScope(node) : context.getScope(); +} + +/** + * Check if a CallExpression is a recursive rm/rmSync call from an fs module. + * + * Supported patterns: + * - fs.rm(path, { recursive: true }) (fs from "node:fs/promises") + * - fs.rmSync(path, { recursive: true }) (fs from "node:fs") + * - fs.promises.rm(path, { recursive: true }) (fs from "node:fs") + * - rm(path, { recursive: true }) (named import from "node:fs/promises") + * - rmSync(path, { recursive: true }) (named import from "node:fs") + */ +function checkCall(node, context) { + const scope = getScope(node, context); + const callee = node.callee; + + // Pattern 1: Direct named import call — rm(...) or rmSync(...) + if (callee.type === "Identifier") { + const name = callee.name; + if (name !== "rm" && name !== "rmSync") { + return null; + } + + const source = resolveImportSource(name, scope); + if (!source) { + return null; + } + + // rmSync must come from "node:fs" / "fs" + if (name === "rmSync" && FS_MODULES.includes(source)) { + const options = node.arguments[1]; + if (hasRecursiveOption(options)) { + return name; + } + } + + // rm can come from "node:fs/promises" / "fs/promises" + if (name === "rm" && FS_PROMISES_MODULES.includes(source)) { + const options = node.arguments[1]; + if (hasRecursiveOption(options)) { + return name; + } + } + + // rm can also come from "node:fs" / "fs" (callback-based) + if (name === "rm" && FS_MODULES.includes(source)) { + const options = node.arguments[1]; + if (hasRecursiveOption(options)) { + return name; + } + // callback-based fs.rm: options may be in arg[2] if arg[1] is the callback + const options2 = node.arguments[2]; + if (hasRecursiveOption(options2)) { + return name; + } + } + + return null; + } + + // Pattern 2: Member expression — obj.rm(...) or obj.rmSync(...) or obj.promises.rm(...) + if (callee.type === "MemberExpression") { + const prop = callee.property; + if (prop.type !== "Identifier") { + return null; + } + + // Sub-pattern 2a: fs.rmSync(...) or fs.rm(...) + if ( + (prop.name === "rm" || prop.name === "rmSync") && + callee.object.type === "Identifier" + ) { + const source = resolveImportSource(callee.object.name, scope); + if (!source) { + return null; + } + + // fs.rmSync from "node:fs" + if (prop.name === "rmSync" && FS_MODULES.includes(source)) { + const options = node.arguments[1]; + if (hasRecursiveOption(options)) { + return prop.name; + } + } + + // fs.rm from "node:fs/promises" + if (prop.name === "rm" && FS_PROMISES_MODULES.includes(source)) { + const options = node.arguments[1]; + if (hasRecursiveOption(options)) { + return prop.name; + } + } + + // fs.rm from "node:fs" — this is fs.rm (callback-based) + if (prop.name === "rm" && FS_MODULES.includes(source)) { + const options = node.arguments[1]; + // callback-based fs.rm: the options may be arg[1] or arg[2] if there's a callback + if (hasRecursiveOption(options)) { + return prop.name; + } + const options2 = node.arguments[2]; + if (hasRecursiveOption(options2)) { + return prop.name; + } + } + + return null; + } + + // Sub-pattern 2b: fs.promises.rm(...) + if ( + prop.name === "rm" && + callee.object.type === "MemberExpression" && + callee.object.property.type === "Identifier" && + callee.object.property.name === "promises" && + callee.object.object.type === "Identifier" + ) { + const source = resolveImportSource(callee.object.object.name, scope); + if (source && FS_MODULES.includes(source)) { + const options = node.arguments[1]; + if (hasRecursiveOption(options)) { + return "promises.rm"; + } + } + return null; + } + } + + return null; +} + +export default { + meta: { + type: "suggestion", + docs: { + description: + "Disallow direct fs.rm/fs.rmSync with recursive option; use removeDir/removeDirSync from @cloudflare/workers-utils instead", + category: "Best Practices", + recommended: true, + }, + messages: { + noDirectRecursiveRm: + 'Use {{ replacement }} from "@cloudflare/workers-utils" instead of {{ method }} with { recursive: true }. ' + + "The helper provides sensible defaults for retries on Windows.", + }, + schema: [], // no options + }, + + create(context) { + return { + CallExpression(node) { + const method = checkCall(node, context); + if (!method) { + return; + } + + const isSync = method === "rmSync"; + const replacement = isSync ? "removeDirSync" : "removeDir"; + + context.report({ + node, + messageId: "noDirectRecursiveRm", + data: { + method, + replacement, + }, + }); + }, + }; + }, +}; diff --git a/packages/lint-config-shared/rules/no-unsafe-command-execution.mjs b/packages/lint-config-shared/rules/no-unsafe-command-execution.mjs new file mode 100644 index 0000000..92e1bd3 --- /dev/null +++ b/packages/lint-config-shared/rules/no-unsafe-command-execution.mjs @@ -0,0 +1,268 @@ +const UNSAFE_FUNCTIONS = [ + "exec", + "execSync", + "spawn", + "spawnSync", + "execFile", + "execFileSync", +]; + +/** + * Package names for child_process module + */ +const CHILD_PROCESS_MODULES = ["child_process", "node:child_process"]; + +/** + * Check if a node is a template literal with expressions + */ +function isTemplateLiteralWithExpressions(node) { + return node.type === "TemplateLiteral" && node.expressions.length > 0; +} + +/** + * Check if a node is a binary expression (string concatenation) + */ +function isBinaryExpression(node) { + return node.type === "BinaryExpression" && node.operator === "+"; +} + +/** + * Check if a call is to one of the unsafe functions from child_process + * Supports both CommonJS (require) and ES module (import) syntax + */ +function isUnsafeChildProcessCall(node, context) { + const sourceCode = context.sourceCode || context.getSourceCode(); + const scope = sourceCode.getScope + ? sourceCode.getScope(node) + : context.getScope(); + + // Check for direct require('child_process').exec(...) pattern + if ( + node.callee.type === "MemberExpression" && + node.callee.object.type === "CallExpression" && + node.callee.object.callee.name === "require" && + node.callee.object.arguments[0]?.type === "Literal" && + CHILD_PROCESS_MODULES.includes(node.callee.object.arguments[0].value) && + node.callee.property.type === "Identifier" && + UNSAFE_FUNCTIONS.includes(node.callee.property.name) + ) { + return node.callee.property.name; + } + + // Check for imported functions: const { exec } = require('child_process') or import { exec } from 'child_process' + if (node.callee.type === "Identifier") { + // Walk up the scope chain to find if this identifier is from child_process + let currentScope = scope; + while (currentScope) { + const variable = currentScope.set.get(node.callee.name); + if (variable && variable.defs.length > 0) { + const def = variable.defs[0]; + + // Check for ES module import: import { execSync } from 'node:child_process' + // or import { execSync as run } from 'node:child_process' + if ( + def.type === "ImportBinding" && + def.parent?.type === "ImportDeclaration" + ) { + const importSource = def.parent.source?.value; + if (CHILD_PROCESS_MODULES.includes(importSource)) { + // Check if it's one of the unsafe functions + // For aliased imports: import { execSync as run } - def.node.imported.name = "execSync" + // For regular imports: import { execSync } - def.node.local.name = "execSync" + const importedName = + def.node.imported?.name || def.node.local?.name; + if (importedName && UNSAFE_FUNCTIONS.includes(importedName)) { + return importedName; + } + } + } + + // Check for CommonJS require() + if ( + def.type === "Variable" && + def.parent?.type === "VariableDeclaration" && + def.node?.init + ) { + const init = def.node.init; + // Check if it's from require('child_process') + if ( + init.type === "CallExpression" && + init.callee.name === "require" && + init.arguments[0]?.type === "Literal" && + CHILD_PROCESS_MODULES.includes(init.arguments[0].value) + ) { + // For destructured requires, check the property name + // const { execSync: run } = require('child_process') + if (def.node.id?.type === "ObjectPattern") { + const prop = def.node.id.properties.find( + (p) => p.value?.name === node.callee.name + ); + if (prop && UNSAFE_FUNCTIONS.includes(prop.key?.name)) { + return prop.key.name; + } + } + // For direct import: const execSync = require('child_process').execSync + if (UNSAFE_FUNCTIONS.includes(node.callee.name)) { + return node.callee.name; + } + } + // Check if it's from destructuring: const { exec } = require('child_process') + if ( + init.type === "MemberExpression" && + init.object.type === "CallExpression" && + init.object.callee.name === "require" && + init.object.arguments[0]?.type === "Literal" && + CHILD_PROCESS_MODULES.includes(init.object.arguments[0].value) + ) { + if (UNSAFE_FUNCTIONS.includes(node.callee.name)) { + return node.callee.name; + } + } + } + } + currentScope = currentScope.upper; + } + } + + // Check for MemberExpression: childProcess.exec(...) + if ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + UNSAFE_FUNCTIONS.includes(node.callee.property.name) + ) { + // This is a heuristic - we check if the object might be child_process + // We return the function name to trigger checking, even if we're not 100% sure + return node.callee.property.name; + } + + return null; +} + +/** + * Check if the options object has shell: true + */ +function hasShellOption(node) { + if (!node || node.type !== "ObjectExpression") { + return false; + } + + return node.properties.some( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "shell" && + prop.value.type === "Literal" && + prop.value.value === true + ); +} + +/** + * Get a helpful suggestion message based on the function being used + */ +function getSuggestion(functionName, hasTemplate, hasConcatenation) { + const patterns = []; + if (hasTemplate) { + patterns.push("template literal with expressions"); + } + if (hasConcatenation) { + patterns.push("string concatenation"); + } + + const patternStr = patterns.join(" and "); + + if (functionName.includes("exec")) { + return `Detected ${patternStr} in ${functionName}(). Use execFileSync() with an argument array instead to prevent command injection: execFileSync("command", ["arg1", "arg2"])`; + } + + if (functionName.includes("spawn")) { + return `Detected ${patternStr} in ${functionName}(). Ensure you're using an argument array and avoid shell: true with user input.`; + } + + return `Detected ${patternStr} in ${functionName}(). This may allow command injection. Use argument arrays instead of string interpolation.`; +} + +export default { + meta: { + type: "error", + docs: { + description: + "Disallow template literals and string concatenation in shell command execution", + category: "Possible Security Vulnerability", + recommended: true, + url: "https://cwe.mitre.org/data/definitions/78.html", + }, + messages: { + unsafeCommandExecution: + "Potential command injection vulnerability: {{message}}", + }, + schema: [], // no options + }, + + create(context) { + return { + CallExpression(node) { + // Check if this is a call to a child_process function + const functionName = isUnsafeChildProcessCall(node, context); + if (!functionName) { + return; + } + + // Check first argument (command string) + if (node.arguments.length === 0) { + return; + } + + const commandArg = node.arguments[0]; + let hasTemplate = false; + let hasConcatenation = false; + + // Check for template literal with expressions + if (isTemplateLiteralWithExpressions(commandArg)) { + hasTemplate = true; + } + + // Check for string concatenation + if (isBinaryExpression(commandArg)) { + hasConcatenation = true; + } + + // For spawn/spawnSync, also check if shell: true is in options + if ( + (functionName === "spawn" || functionName === "spawnSync") && + node.arguments.length >= 3 + ) { + const options = node.arguments[2]; + if (hasShellOption(options) && (hasTemplate || hasConcatenation)) { + context.report({ + node, + messageId: "unsafeCommandExecution", + data: { + message: getSuggestion( + functionName, + hasTemplate, + hasConcatenation + ), + }, + }); + return; + } + } + + // Report if we found unsafe patterns + if (hasTemplate || hasConcatenation) { + context.report({ + node, + messageId: "unsafeCommandExecution", + data: { + message: getSuggestion( + functionName, + hasTemplate, + hasConcatenation + ), + }, + }); + } + }, + }; + }, +}; diff --git a/packages/lint-config-shared/rules/no-wrangler-named-imports.mjs b/packages/lint-config-shared/rules/no-wrangler-named-imports.mjs new file mode 100644 index 0000000..32c0e0d --- /dev/null +++ b/packages/lint-config-shared/rules/no-wrangler-named-imports.mjs @@ -0,0 +1,47 @@ +/** + * @type {import("eslint").Rule.RuleModule} + * + * Bans named (non-type) imports from "wrangler". Namespace imports + * (`import * as wrangler from "wrangler"`) and type-only imports + * (`import type { X } from "wrangler"`) are allowed. + */ +export default { + meta: { + type: "problem", + docs: { + description: + 'Disallow named imports from "wrangler". Use namespace imports instead.', + }, + messages: { + namedImport: + 'Use namespace import: `import * as wrangler from "wrangler"` instead of named imports.', + }, + schema: [], + }, + create(context) { + return { + ImportDeclaration(node) { + if (node.source.value !== "wrangler") { + return; + } + + // Allow type-only imports: import type { X } from "wrangler" + if (node.importKind === "type") { + return; + } + + // Check for named import specifiers (not namespace or default) + const namedSpecifiers = node.specifiers.filter( + (s) => s.type === "ImportSpecifier" && s.importKind !== "type" + ); + + if (namedSpecifiers.length > 0) { + context.report({ + node, + messageId: "namedImport", + }); + } + }, + }; + }, +}; diff --git a/packages/lint-config-shared/rules/require-description-when-disabling.mjs b/packages/lint-config-shared/rules/require-description-when-disabling.mjs new file mode 100644 index 0000000..144eb56 --- /dev/null +++ b/packages/lint-config-shared/rules/require-description-when-disabling.mjs @@ -0,0 +1,83 @@ +/** + * Directive prefixes recognised by both ESLint and oxlint. + * + * Each prefix may optionally be followed by rule names and must contain a + * description separated by ` -- ` to pass this rule. + * + * Covers: + * eslint-disable / eslint-disable-next-line + * oxlint-disable / oxlint-disable-next-line + * + * Note: eslint-enable / oxlint-enable are intentionally excluded — they + * re-enable rules rather than suppress them, so a justification is not needed. + */ +const DIRECTIVE_PATTERN = + /^\s*(?:eslint-disable(?:-next-line)?|oxlint-disable(?:-next-line)?)\b/; + +/** + * A description is everything after the first ` -- ` separator. It must + * contain at least one non-whitespace character to count. + */ +function hasDescription(text) { + const separatorIndex = text.indexOf("--"); + if (separatorIndex === -1) { + return false; + } + const description = text.slice(separatorIndex + 2); + return description.trim().length > 0; +} + +/** + * Extract a human-friendly directive name from the comment text so the + * error message is useful. e.g. "eslint-disable-next-line" + */ +function getDirectiveName(text) { + const match = text.match(/\b((?:eslint|oxlint)-disable(?:-next-line)?)\b/); + return match ? match[1] : "disable directive"; +} + +export default { + meta: { + type: "suggestion", + docs: { + description: + "Require a description after `--` in eslint/oxlint disable directive comments", + category: "Best Practices", + recommended: true, + }, + messages: { + missingDescription: + "Unexpected `{{ directive }}` comment without description. Include a description after ` -- ` to explain why the rule is disabled.", + }, + schema: [], + }, + + create(context) { + return { + Program() { + const sourceCode = context.sourceCode || context.getSourceCode(); + const comments = sourceCode.getAllComments + ? sourceCode.getAllComments() + : []; + + for (const comment of comments) { + const text = comment.value; + + if (!DIRECTIVE_PATTERN.test(text)) { + continue; + } + + if (!hasDescription(text)) { + context.report({ + loc: comment.loc, + messageId: "missingDescription", + data: { + directive: getDirectiveName(text), + }, + }); + } + } + }, + }; + }, +}; diff --git a/packages/local-explorer-ui/.gitignore b/packages/local-explorer-ui/.gitignore new file mode 100644 index 0000000..cacb12a --- /dev/null +++ b/packages/local-explorer-ui/.gitignore @@ -0,0 +1,2 @@ +# Generated API client files +src/api/generated/ diff --git a/packages/local-explorer-ui/CHANGELOG.md b/packages/local-explorer-ui/CHANGELOG.md new file mode 100644 index 0000000..36040eb --- /dev/null +++ b/packages/local-explorer-ui/CHANGELOG.md @@ -0,0 +1,255 @@ +# @cloudflare/local-explorer-ui + +## 0.14.0 + +### Minor Changes + +- [#14154](https://github.com/cloudflare/workers-sdk/pull/14154) [`204f90a`](https://github.com/cloudflare/workers-sdk/commit/204f90a39000e54a9387eb45b54b2a6d894d8dcd) Thanks [@pombosilva](https://github.com/pombosilva)! - Add a restart-from-step button to each row in the workflow instance step list + +## 0.13.3 + +### Patch Changes + +- [#13654](https://github.com/cloudflare/workers-sdk/pull/13654) [`6d27479`](https://github.com/cloudflare/workers-sdk/commit/6d2747962be5ed8707840ce3cf138d1e2c434d10) Thanks [@pombosilva](https://github.com/pombosilva)! - fix: Preserve expanded workflow steps across polling and improve instance page UX + + Expanded steps no longer collapse when new steps arrive via polling. Expansion state is lifted into the parent component and tracked by stable step keys, and polling skips state updates when data hasn't changed. + + Also: navigate to instance page after triggering, distinct refresh/restart icons, toolbar layout below stats strip, always-visible stats, unified status colors, and visual-only stripping of internal step name suffixes. + +## 0.13.2 + +### Patch Changes + +- [#13562](https://github.com/cloudflare/workers-sdk/pull/13562) [`78a252d`](https://github.com/cloudflare/workers-sdk/commit/78a252dbf7fc818b56db2f0870c60d4f49e2f4c5) Thanks [@emily-shen](https://github.com/emily-shen)! - Local Explorer UI refinements + + - Add scrolling to the D1 table selector, instead of cutting off the table list + - Show table headers in R2 empty states + - Persist the `delimiter` search param when navigating to R2 object details + - Hide breadcrumb path segments when viewing R2 objects in ungrouped mode + +## 0.13.1 + +### Patch Changes + +- [#13466](https://github.com/cloudflare/workers-sdk/pull/13466) [`c4461ff`](https://github.com/cloudflare/workers-sdk/commit/c4461ff25ec9cb6266ad0da240bed8a755de7d08) Thanks [@NuroDev](https://github.com/NuroDev)! - Fix local explorer KV bulk / get for large payloads. + + Fixes an issue where the local explorer UI would crash when fetching large KV payloads. + + Additionally, the local KV bulk get API endpoint now enforces a total 25MB payload limit, in alignment with the remote Cloudflare API. + +- [#13543](https://github.com/cloudflare/workers-sdk/pull/13543) [`39a5f04`](https://github.com/cloudflare/workers-sdk/commit/39a5f04792f5e0ec12ff727fe9cb07e1f4516094) Thanks [@SAY-5](https://github.com/SAY-5)! - Fix `occured` -> `occurred` typo in the `ResourceError` fallback message rendered by the local explorer UI when a worker resource fails to load. + +## 0.13.0 + +### Minor Changes + +- [#13429](https://github.com/cloudflare/workers-sdk/pull/13429) [`54ceb95`](https://github.com/cloudflare/workers-sdk/commit/54ceb950c38d9b09f18de2e7a3db18c8d9fa2827) Thanks [@NuroDev](https://github.com/NuroDev)! - Add shift-click multi-select to R2 object list + + Shift-clicking a checkbox in the R2 object list now selects or deselects a contiguous range of rows between the last individually clicked row (the anchor) and the shift-clicked row. This matches standard shift-select behavior in file managers and data tables. + +## 0.12.0 + +### Minor Changes + +- [#13330](https://github.com/cloudflare/workers-sdk/pull/13330) [`b30eb67`](https://github.com/cloudflare/workers-sdk/commit/b30eb67130e9b7f7ac30d4e62f16d27f4ea37c8e) Thanks [@NuroDev](https://github.com/NuroDev)! - Update local explorer sidebar with collapsible groups, theme persistence, and Kumo v1.17 + + Adds localStorage persistence for sidebar group expansion states and theme mode (light/dark/system). The sidebar now uses Kumo v1.17 primitives with collapsible groups and a theme toggle in the footer. + + Users can now cycle between light, dark, and system theme modes, and their preference will be persisted across sessions. + + Sidebar groups (D1, Durable Objects, KV, R2, Workflows) also remember their collapsed/expanded state. + +### Patch Changes + +- [#13361](https://github.com/cloudflare/workers-sdk/pull/13361) [`a4f1d5c`](https://github.com/cloudflare/workers-sdk/commit/a4f1d5cf9a49b45f1dcc054858f85e7c9bff85ff) Thanks [@NuroDev](https://github.com/NuroDev)! - Cleaned up local explorer workflows page design. + + The core design & layout of the workflows page(s) in the local explorer has been tweaked to make it more uniform and consistent with all other resource pages. + +- [#13407](https://github.com/cloudflare/workers-sdk/pull/13407) [`496c5d5`](https://github.com/cloudflare/workers-sdk/commit/496c5d5bab56b59d11fc02fa077c1a246681416c) Thanks [@NuroDev](https://github.com/NuroDev)! - Add new "Copy prompt for agent" button. + + This adds a clipboard copy field to the Local Explorer homepage for sharing an agent/LLM Local Explorer API prompt. + +- [#13158](https://github.com/cloudflare/workers-sdk/pull/13158) [`67be6b0`](https://github.com/cloudflare/workers-sdk/commit/67be6b0ab97b0b5f85f9fbae93655ab390e8dbf9) Thanks [@NuroDev](https://github.com/NuroDev)! - Improves local explorer invalid route error handling. + + Visiting a route either as a 404 or 500 error now has dedicated components to handle as such, rather than the generic TanStack error UI. + + Additionally, it also fixes route loaders to correctly throw a 404 error if a resource is not found, rather than showing a generic error. + +- [#13407](https://github.com/cloudflare/workers-sdk/pull/13407) [`496c5d5`](https://github.com/cloudflare/workers-sdk/commit/496c5d5bab56b59d11fc02fa077c1a246681416c) Thanks [@NuroDev](https://github.com/NuroDev)! - Updates the Local Explorer homepage prompt to use the current runtime origin for the Explorer API endpoint. + + This ensures copied prompt text points to the correct local URL instead of a placeholder localhost port. + +## 0.11.0 + +### Minor Changes + +- [#13331](https://github.com/cloudflare/workers-sdk/pull/13331) [`a066e24`](https://github.com/cloudflare/workers-sdk/commit/a066e245ef5357c33609d19797123ec9b6c294f7) Thanks [@NuroDev](https://github.com/NuroDev)! - Add animated Cloudflare logo to local explorer homepage + + The local explorer now displays an animated Cloudflare logo on the homepage, + providing a more engaging visual experience when viewing local resources. + +- [#13133](https://github.com/cloudflare/workers-sdk/pull/13133) [`42c7ef0`](https://github.com/cloudflare/workers-sdk/commit/42c7ef04385094c77f0c2830134fc38b2dc39b02) Thanks [@emily-shen](https://github.com/emily-shen)! - explorer: list DO instances with name where possible + + Note: The local explorer is a WIP experimental feature. + +- [#13336](https://github.com/cloudflare/workers-sdk/pull/13336) [`a42e0e8`](https://github.com/cloudflare/workers-sdk/commit/a42e0e8b52df128513f85025f50eb985bc7f5748) Thanks [@emily-shen](https://github.com/emily-shen)! - local explorer: fix handling on resources that are bound to multiple workers + + Note the local explorer is a experimental feature still. + +## 0.10.0 + +### Minor Changes + +- [#13161](https://github.com/cloudflare/workers-sdk/pull/13161) [`f071008`](https://github.com/cloudflare/workers-sdk/commit/f07100810d6d8c00e7d1977f0b760b369b52aed0) Thanks [@NuroDev](https://github.com/NuroDev)! - Overhaul local explorer UI color palette. + + The core styles of the local explorer has been overhauled to remove all custom styles in favour of using Kumo styles / colors when possible. + + This is the first part of improving the local explorer UI to kumo-ify it all. + +## 0.9.0 + +### Minor Changes + +- [#12972](https://github.com/cloudflare/workers-sdk/pull/12972) [`cb71403`](https://github.com/cloudflare/workers-sdk/commit/cb714036d95ad0429f7e7a24c3c3a4317748ce22) Thanks [@NuroDev](https://github.com/NuroDev)! - Add worker filtering to the local explorer UI + + When multiple workers share a dev registry, all their bindings were previously shown together in a single flat list. The explorer now shows a worker selector dropdown, letting you inspect each worker's bindings independently. + + The selected worker is reflected in the URL as a `?worker=` search param, so deep links work correctly. By default the explorer selects the worker that is hosting the dashboard itself. + +- [#12888](https://github.com/cloudflare/workers-sdk/pull/12888) [`3a1c149`](https://github.com/cloudflare/workers-sdk/commit/3a1c149e1edf126ab072bf74ed624d3c42d561fb) Thanks [@emily-shen](https://github.com/emily-shen)! - Add R2 support to the local explorer. + + The local explorer now supports the following: + + - Viewing, modifying & deleting objects + - Uploading files + - Creating directories / prefixes + + Note: The local explorer is an experimental WIP feature that is now enabled by default. This can still be opt-ed out of by using `X_LOCAL_EXPLORER=false` to disable it. + +### Patch Changes + +- [#12918](https://github.com/cloudflare/workers-sdk/pull/12918) [`3de3ce5`](https://github.com/cloudflare/workers-sdk/commit/3de3ce519383b634bd1315eb94d789ec8def0670) Thanks [@NuroDev](https://github.com/NuroDev)! - Fixed listing internal Cloudflare Durable Object tables. + + The internal `_cf_KV` table that is used when using Durable Objects KV storage is now hidden from the table list dropdown in the local explorer as it is not accessible. + +## 0.8.2 + +### Patch Changes + +- [#12877](https://github.com/cloudflare/workers-sdk/pull/12877) [`7dc3fb3`](https://github.com/cloudflare/workers-sdk/commit/7dc3fb36b1af4740f14409d8cdf9c50d8942a4df) Thanks [@NuroDev](https://github.com/NuroDev)! - Fixed table selection dropdown incorrect z-index. + + Previously, the dropdown you used to select a table in the data studio had an incorrect or missing z-index, meanint it conflicted with the table row header & was partially cut off when you had too many tables. This change ensures that the dropdown is always "on top" and visible. + +## 0.8.1 + +### Patch Changes + +- [#12864](https://github.com/cloudflare/workers-sdk/pull/12864) [`ecc7f79`](https://github.com/cloudflare/workers-sdk/commit/ecc7f792f950fc786ff40fa140bd8907bd26ff31) Thanks [@NuroDev](https://github.com/NuroDev)! - Fix local explorer's sidebar header link to point to the correct `/cdn-cgi/explorer/` path rather than `/`. + +## 0.8.0 + +### Minor Changes + +- [#12754](https://github.com/cloudflare/workers-sdk/pull/12754) [`e4d9510`](https://github.com/cloudflare/workers-sdk/commit/e4d9510c3439d313ba0e0f78bf00d0726d5f67e9) Thanks [@emily-shen](https://github.com/emily-shen)! - Add cross-process support to the local explorer + + When running multiple miniflare processes, the local explorer will now be able to view and edit resources that are bound to workers in other miniflare instances. + +### Patch Changes + +- [#12828](https://github.com/cloudflare/workers-sdk/pull/12828) [`cb14820`](https://github.com/cloudflare/workers-sdk/commit/cb148200336ed57c56cb89028453ddd5fdef2e7b) Thanks [@jamesopstad](https://github.com/jamesopstad)! - Update `@hey-api/openapi-ts` to ^0.94.0 + +- [#12779](https://github.com/cloudflare/workers-sdk/pull/12779) [`b2f8b47`](https://github.com/cloudflare/workers-sdk/commit/b2f8b47b19ef2a2235130a681da002206ef4c4e6) Thanks [@NuroDev](https://github.com/NuroDev)! - Refactors KV & sidebar to use route loaders. + + This change improves the user experience of the Local Explorer dashboard by ensuring that the data used for the initial render is fetched server-side and passed down to the client. This avoids the initial flicker when loading in. Both D1 & Durable Object routes already incorporate this system. + +## 0.7.0 + +### Minor Changes + +- [#12453](https://github.com/cloudflare/workers-sdk/pull/12453) [`9764ea0`](https://github.com/cloudflare/workers-sdk/commit/9764ea09e0e106e96a403db8e99d41ad3f00ef98) Thanks [@NuroDev](https://github.com/NuroDev)! - Add initial data studio with D1 and Durable Objects support + + Adds a data studio interface to the local explorer UI, allowing you to browse and interact with D1 databases and Durable Objects during local development. The studio provides table browsing, query execution, and data editing capabilities. + +- [#12760](https://github.com/cloudflare/workers-sdk/pull/12760) [`fa88fef`](https://github.com/cloudflare/workers-sdk/commit/fa88fef992bd8e65d00d1e7d279bf62ee9120ce8) Thanks [@NuroDev](https://github.com/NuroDev)! - Add schema editor to data studio + + Adds a visual schema editor to the data studio that allows you to create new database tables and edit existing table schemas. The editor provides column management (add, edit, remove), constraint editing (primary keys, unique constraints), and generates the corresponding SQL statements for review before committing changes. + + This is a WIP experimental feature. + +## 0.6.0 + +### Minor Changes + +- [#12599](https://github.com/cloudflare/workers-sdk/pull/12599) [`3649d3e`](https://github.com/cloudflare/workers-sdk/commit/3649d3e408bf352468a59e47f05f42c9bd69c736) Thanks [@NuroDev](https://github.com/NuroDev)! - Adds the tab definition for the table explorer. + + This serves as another stepping stone for adding the complete data studio to the local explorer. + + This is a WIP experimental feature. + +## 0.5.0 + +### Minor Changes + +- [#12570](https://github.com/cloudflare/workers-sdk/pull/12570) [`01ded52`](https://github.com/cloudflare/workers-sdk/commit/01ded5289e744e6f604edcbffdd6b1c95e6339c0) Thanks [@NuroDev](https://github.com/NuroDev)! - Add the initial plumbing for data studio components. + + This serves as another stepping stone for adding the complete data studio to the local explorer. + + This is a WIP experimental feature. + +## 0.4.0 + +### Minor Changes + +- [#12518](https://github.com/cloudflare/workers-sdk/pull/12518) [`323f14e`](https://github.com/cloudflare/workers-sdk/commit/323f14e19605f3f0eb732992fa6d765657cb93ba) Thanks [@NuroDev](https://github.com/NuroDev)! - Implemented initial data studio driver support. + + This provides the initial plumbing needed to add the complete data studio component to the local explorer in a later PR. D1 databases will now appear in the sidebar alongside KV namespaces when running the local explorer. + + This is an experimental WIP feature. + +- [#12555](https://github.com/cloudflare/workers-sdk/pull/12555) [`2eeefeb`](https://github.com/cloudflare/workers-sdk/commit/2eeefeb01ddb6d44b99e3ea93a1b9faada925cb3) Thanks [@NuroDev](https://github.com/NuroDev)! - Add database table selection dropdown. + + As part of the ongoing work to implement the data studio into the local explorer, this change allows you to view and select which table for a database you want to inspect. + + This is an experimental WIP feature. + +## 0.3.0 + +### Minor Changes + +- [#12431](https://github.com/cloudflare/workers-sdk/pull/12431) [`7aaa2a5`](https://github.com/cloudflare/workers-sdk/commit/7aaa2a5aa93011bd03aa0998c7310fa6e1eaff41) Thanks [@emily-shen](https://github.com/emily-shen)! - Add ability to search KV keys by prefix + + The UI and list keys API now lets you search KV keys by prefix. + + This is an experimental WIP feature. + +## 0.2.0 + +### Minor Changes + +- [#12459](https://github.com/cloudflare/workers-sdk/pull/12459) [`e3e9ca9`](https://github.com/cloudflare/workers-sdk/commit/e3e9ca9453cbbbb5652f7798c8ad7b0897f5fa32) Thanks [@NuroDev](https://github.com/NuroDev)! - Add dark mode support to the local explorer UI + + This includes adding an initial implementation that uses your preferred system theme to select what theme to render the local explorer UI in. + + Additionally, this includes a number of minor style tweaks & fixes. + + This is an experimental WIP feature. + +- [#12391](https://github.com/cloudflare/workers-sdk/pull/12391) [`ce9dc01`](https://github.com/cloudflare/workers-sdk/commit/ce9dc01a4696e28bd9f3a900dd2f5a7783252906) Thanks [@emily-shen](https://github.com/emily-shen)! - Serve the local explorer UI from Miniflare + + This bundles the local explorer UI into Miniflare, and if enabled, Miniflare serves the UI at `/cdn-cgi/explorer`. + + This is an experimental, WIP feature. + +### Patch Changes + +- [#12414](https://github.com/cloudflare/workers-sdk/pull/12414) [`de473c2`](https://github.com/cloudflare/workers-sdk/commit/de473c2205250cbef2272d75e6dae50b064b1333) Thanks [@emily-shen](https://github.com/emily-shen)! - Change the favicon to a Cloudflare logo outline + + This is for an experimental WIP project. + +## 0.1.0 + +### Minor Changes + +- [#12288](https://github.com/cloudflare/workers-sdk/pull/12288) [`60eaf16`](https://github.com/cloudflare/workers-sdk/commit/60eaf16da41370b37ca215b9fcc8a8d9d0ea8171) Thanks [@emily-shen](https://github.com/emily-shen)! - Set up local explorer UI with a view for KV namespaces + + This is an experimental WIP package. diff --git a/packages/local-explorer-ui/README.md b/packages/local-explorer-ui/README.md new file mode 100644 index 0000000..2c20ce1 --- /dev/null +++ b/packages/local-explorer-ui/README.md @@ -0,0 +1,45 @@ +# Local explorer UI + +This package contains the UI for the local dev data explorer. +The built output will be bundled into Miniflare and served by Miniflare at `/cdn-cgi/explorer`. + +# API + +This fetches data from an API running in Miniflare at`/cdn-cgi/explorer/api`. + +The API client is generated from the OpenAPI spec in `packages/miniflare/src/workers/local-explorer/openapi.local.json`. + +## API Type Generation + +The API client code is auto-generated from the OpenAPI spec using [`@hey-api/openapi-ts`](https://heyapi.dev/). + +**Configuration:** `openapi-ts.config.ts` + +**Input:** `packages/miniflare/src/workers/local-explorer/openapi.local.json` + +**Output:** `src/api/generated/` + +The generated code includes: + +- TypeScript types for all API request/response schemas +- A type-safe SDK client for making API calls + +### When does generation run? + +| Command | Behavior | +| -------------- | ------------------------------------------------------ | +| `pnpm install` | Runs `openapi-ts` in `postinstall` to generate types | +| `pnpm build` | Runs `openapi-ts` before building | +| `pnpm dev` | Watches for spec changes and regenerates automatically | +| `pnpm dev:ui` | Same as `dev` - watches for spec changes | + +In development, the `openapi-ts --watch` process runs concurrently with Vite, automatically regenerating the client when the OpenAPI spec changes. + +# Developing + +`pnpm run dev` in this directory. + +Or to run a specific worker against the UI: + +1. Run a worker (`fixtures/worker-with-resources`) using Wrangler or Vite, making sure it is running at `localhost:8787`. +2. Run this package with `pnpm run dev:ui`, which will spin up a dev server for just the UI using Vite. diff --git a/packages/local-explorer-ui/index.html b/packages/local-explorer-ui/index.html new file mode 100644 index 0000000..264d6ac --- /dev/null +++ b/packages/local-explorer-ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Cloudflare Local Explorer + + +
+ + + diff --git a/packages/local-explorer-ui/openapi-ts.config.ts b/packages/local-explorer-ui/openapi-ts.config.ts new file mode 100644 index 0000000..d3b0528 --- /dev/null +++ b/packages/local-explorer-ui/openapi-ts.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "@hey-api/openapi-ts"; + +export default defineConfig({ + input: "../miniflare/src/workers/local-explorer/openapi.local.json", + output: "src/api/generated", + plugins: [ + "@hey-api/typescript", + { + name: "@hey-api/client-fetch", + runtimeConfigPath: "../client-config", + }, + "@hey-api/sdk", + ], +}); diff --git a/packages/local-explorer-ui/package.json b/packages/local-explorer-ui/package.json new file mode 100644 index 0000000..9cc567c --- /dev/null +++ b/packages/local-explorer-ui/package.json @@ -0,0 +1,63 @@ +{ + "name": "@cloudflare/local-explorer-ui", + "version": "0.14.0", + "private": true, + "type": "module", + "scripts": { + "build": "openapi-ts && vite build", + "check:type": "tsc --build", + "deploy": "echo 'no deploy'", + "predev": "pnpm -w build --filter wrangler", + "dev": "concurrently -n api,ui,worker -c green,blue,orange \"openapi-ts --watch\" \"vite\" \"pnpm --filter @fixture/worker-with-resources start\"", + "dev:ui": "concurrently -n api,ui -c green,blue \"openapi-ts --watch\" \"vite\"", + "postinstall": "openapi-ts", + "playwright:install": "pnpm playwright install chromium", + "preview": "vite preview", + "test": "vitest", + "test:ci": "vitest run", + "pretest:e2e": "pnpm playwright:install", + "test:e2e": "vitest run --config vitest.config.e2e.ts", + "test:e2e:watch": "vitest --config vitest.config.e2e.ts", + "test:watch": "vitest" + }, + "dependencies": { + "@base-ui/react": "^1.1.0", + "@cloudflare/kumo": "^1.18.0", + "@cloudflare/workers-editor-shared": "^0.1.1", + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/language": "^6.12.1", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.14", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@floating-ui/react": "^0.27.18", + "@lezer/common": "^1.5.1", + "@lezer/highlight": "^1.2.3", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-router": "^1.158.0", + "immer": "^11.1.4", + "pretty-bytes": "^7.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^4.0.15" + }, + "devDependencies": { + "@cloudflare/workers-tsconfig": "workspace:*", + "@hey-api/openapi-ts": "catalog:default", + "@tanstack/react-router-devtools": "^1.158.0", + "@tanstack/router-plugin": "^1.158.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.2.0", + "concurrently": "^9.0.0", + "playwright-chromium": "catalog:default", + "typescript": "catalog:default", + "vite": "catalog:default", + "vite-plugin-svgr": "^4.3.0", + "vitest": "catalog:default" + } +} diff --git a/packages/local-explorer-ui/public/favicon.svg b/packages/local-explorer-ui/public/favicon.svg new file mode 100644 index 0000000..cab10d9 --- /dev/null +++ b/packages/local-explorer-ui/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/local-explorer-ui/src/__e2e__/d1/d1-database.spec.ts b/packages/local-explorer-ui/src/__e2e__/d1/d1-database.spec.ts new file mode 100644 index 0000000..99acddf --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/d1/d1-database.spec.ts @@ -0,0 +1,270 @@ +import { beforeEach, describe, test } from "vitest"; +import { + clickButton, + isTextVisible, + navigateToD1, + openTableSelector, + page, + refreshTables, + runAllQueries, + runQuery, + seedD1, + typeInQueryEditor, + waitForDialog, + waitForQueryEditor, + waitForQueryResult, + waitForTableRows, + waitForText, +} from "../utils"; + +describe("D1 Database Studio", () => { + beforeEach(async () => { + await seedD1(); + }); + + describe("table list", () => { + test("displays available tables in the selector", async ({ expect }) => { + await navigateToD1("DB"); + await waitForText("D1"); + + const selectTableButton = page.getByRole("button", { + name: /Select a table|users/i, + }); + + if (!(await selectTableButton.isVisible())) { + return; + } + + await selectTableButton.click(); + + // Should show the users table from seed data + const usersOption = page.getByRole("menuitem", { name: "users" }); + const isUsersVisible = await usersOption.isVisible(); + expect(isUsersVisible).toBe(true); + }); + }); + + describe("table explorer", () => { + test("displays table data when table is selected", async () => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + await waitForText("Alice"); + await waitForText("Bob"); + }); + + test("shows table structure with `id` and `name` columns", async () => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + await waitForText("id"); + await waitForText("name"); + }); + }); + + describe("row operations", () => { + test("can click 'Add row' button", async ({ expect }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + const isAddRowVisible = await isTextVisible("Add row"); + expect(isAddRowVisible).toBe(true); + + await clickButton("Add row"); + + // After clicking, the table should still be visible + await waitForTableRows(1); + }); + + test("shows 'Commit' button after making changes", async ({ expect }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(2); + + await clickButton("Add row"); + + // Wait for the state to update - the Commit/Discard buttons appear when changes exist + await page.waitForTimeout(500); + + const commitButton = page.getByRole("button", { + name: /Commit|Delete \d+ row/i, + }); + const isCommitVisible = await commitButton.isVisible(); + expect(isCommitVisible).toBe(true); + + const isDiscardVisible = await isTextVisible("Discard"); + expect(isDiscardVisible).toBe(true); + }); + + test("discards changes when clicking 'Discard'", async ({ expect }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(2); + + await clickButton("Add row"); + await clickButton("Discard"); + + // After discard, the Commit/Discard buttons should disappear + await page.waitForTimeout(500); + const commitButton = page.getByRole("button", { name: /Commit/i }); + const isVisible = await commitButton.isVisible().catch(() => false); + expect(isVisible).toBe(false); + }); + }); + + describe("table schema management", () => { + test("'Edit Schema' button is visible when table selected", async ({ + expect, + }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + const editSchemaButton = page.getByRole("button", { + name: /Edit (table )?Schema/i, + }); + const isEditSchemaVisible = await editSchemaButton.isVisible(); + expect(isEditSchemaVisible).toBe(true); + }); + + test("'Delete Table' button is visible when table selected", async ({ + expect, + }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + const deleteTableButton = page.getByRole("button", { + name: /Delete (T|t)able/i, + }); + const isDeleteTableVisible = await deleteTableButton.isVisible(); + expect(isDeleteTableVisible).toBe(true); + }); + + test("shows confirmation when deleting table", async ({ expect }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + await page.getByRole("button", { name: /Delete (T|t)able/i }).click(); + + await waitForDialog(); + + const dialog = page.getByRole("dialog"); + const dialogText = await dialog.textContent(); + expect(dialogText).toContain("users"); + }); + }); + + describe("refresh functionality", () => { + test("refresh button is visible", async ({ expect }) => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + const refreshButton = page.getByRole("button", { + name: "Refresh tables", + }); + const isRefreshVisible = await refreshButton.isVisible(); + expect(isRefreshVisible).toBe(true); + }); + }); + + describe("navigation", () => { + test("breadcrumbs show D1 and database ID", async () => { + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + // Breadcrumbs should show D1 and the database ID + await waitForText("D1"); + await waitForText("DB"); + }); + }); + + describe("SQL queries", () => { + test("creates a table via SQL", async ({ expect }) => { + await navigateToD1("DB"); + await waitForText("D1"); + await waitForQueryEditor(); + + await typeInQueryEditor(` + DROP TABLE IF EXISTS products; + CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL); + `); + await runAllQueries(); + await waitForQueryResult(); + + await refreshTables(); + + await openTableSelector(); + + const productsOption = page.getByRole("menuitem", { + name: "products", + }); + const isProductsVisible = await productsOption.isVisible(); + expect(isProductsVisible).toBe(true); + }); + + test("inserts a row via SQL", async () => { + await navigateToD1("DB"); + await waitForText("D1"); + await waitForQueryEditor(); + + await typeInQueryEditor( + "INSERT INTO users (id, name) VALUES (3, 'Charlie');" + ); + await runQuery(); + await waitForQueryResult(); + + // Navigate to the users table and verify `Charlie` appears + await navigateToD1("DB", "users"); + await waitForTableRows(1); + await waitForText("Charlie"); + }); + + test("deletes a row via SQL", async ({ expect }) => { + await navigateToD1("DB"); + await waitForText("D1"); + await waitForQueryEditor(); + + await typeInQueryEditor("DELETE FROM users WHERE name = 'Bob';"); + await runQuery(); + await waitForQueryResult(); + + // Navigate to the users table and verify `Bob` is gone + await navigateToD1("DB", "users"); + await waitForTableRows(1); + + await waitForText("Alice"); + + const isBobVisible = await isTextVisible("Bob"); + expect(isBobVisible).toBe(false); + }); + + test("adds a column via SQL", async () => { + await navigateToD1("DB"); + await waitForText("D1"); + await waitForQueryEditor(); + + await typeInQueryEditor("ALTER TABLE users ADD COLUMN email TEXT;"); + await runQuery(); + await waitForQueryResult(); + + await navigateToD1("DB", "users"); + await waitForTableRows(1); + await waitForText("email"); + }); + + test("executes a SELECT query and displays results", async () => { + await navigateToD1("DB"); + await waitForText("D1"); + await waitForQueryEditor(); + + await typeInQueryEditor("SELECT * FROM users;"); + await runQuery(); + await waitForQueryResult(); + + // The result tab should show the table name and dimensions + // e.g., "users • 2 x 2" (2 columns x 2 rows) + await waitForText("users"); + + // Verify both rows appear in the results + await waitForText("Alice"); + await waitForText("Bob"); + }); + }); +}); diff --git a/packages/local-explorer-ui/src/__e2e__/do/durable-object.spec.ts b/packages/local-explorer-ui/src/__e2e__/do/durable-object.spec.ts new file mode 100644 index 0000000..677f90f --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/do/durable-object.spec.ts @@ -0,0 +1,179 @@ +import { beforeEach, describe, test } from "vitest"; +import { + isTextVisible, + navigateToDOClass, + navigateToDOObject, + navigateToDOObjectByName, + openTableSelector, + page, + refreshTables, + runAllQueries, + runQuery, + seedDO, + typeInQueryEditor, + waitForQueryEditor, + waitForQueryResult, + waitForTableRows, + waitForText, +} from "../utils"; + +describe("Durable Objects", () => { + beforeEach(async () => { + await seedDO("test-object"); + }); + + describe("class listing", () => { + test("displays DO class page", async () => { + await navigateToDOClass("MyDurableObject"); + + // Should show the class name in breadcrumbs + await waitForText("MyDurableObject"); + await waitForText("Durable Objects"); + }); + + test("shows 'Object ID' header in table", async () => { + await navigateToDOClass("MyDurableObject"); + + // The table should have an 'Object ID' column + await waitForText("Object ID"); + }); + + test("shows Open Studio link for objects if they exist", async ({ + expect, + }) => { + await navigateToDOClass("MyDurableObject"); + await waitForText("MyDurableObject"); + + await page.waitForTimeout(1_000); + + const openStudioLinks = page.locator('a:has-text("Open Studio")'); + const count = await openStudioLinks.count(); + expect(count).toBeGreaterThanOrEqual(1); + }); + }); + + describe("empty state", () => { + test("shows message when no DO objects exist or shows objects", async ({ + expect, + }) => { + await navigateToDOClass("MyDurableObject"); + + await page.waitForTimeout(1_000); + + const hasEmptyMessage = await isTextVisible( + "No Durable Objects with stored data" + ); + const hasObjectIdHeader = await isTextVisible("Object ID"); + const hasOpenStudioLink = await page + .locator('a:has-text("Open Studio")') + .first() + .isVisible() + .catch(() => false); + + expect(hasEmptyMessage || hasObjectIdHeader || hasOpenStudioLink).toBe( + true + ); + }); + }); + + describe("navigation", () => { + test("breadcrumbs show DO and class name", async () => { + await navigateToDOClass("MyDurableObject"); + + // Breadcrumbs should show the navigation path + await waitForText("Durable Objects"); + await waitForText("MyDurableObject"); + }); + }); + + describe("SQL queries", () => { + test("creates a table via SQL", async ({ expect }) => { + await navigateToDOObjectByName("MyDurableObject"); + await waitForText("Durable Objects"); + await waitForQueryEditor(); + + await typeInQueryEditor(` + DROP TABLE IF EXISTS products; + CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL); + `); + await runAllQueries(); + await waitForQueryResult(); + + await refreshTables(); + + await openTableSelector(); + + const productsOption = page.getByRole("menuitem", { + name: "products", + }); + const isProductsVisible = await productsOption.isVisible(); + expect(isProductsVisible).toBe(true); + }); + + test("inserts a row via SQL", async () => { + const objectId = await navigateToDOObjectByName("MyDurableObject"); + await waitForText("Durable Objects"); + await waitForQueryEditor(); + + await typeInQueryEditor( + "INSERT INTO users (id, name) VALUES (3, 'Charlie');" + ); + await runQuery(); + await waitForQueryResult(); + + await navigateToDOObject("MyDurableObject", objectId, "users"); + await waitForTableRows(1); + await waitForText("Charlie"); + }); + + test("deletes a row via SQL", async ({ expect }) => { + const objectId = await navigateToDOObjectByName("MyDurableObject"); + await waitForText("Durable Objects"); + await waitForQueryEditor(); + + await typeInQueryEditor("DELETE FROM users WHERE name = 'Bob';"); + await runQuery(); + await waitForQueryResult(); + + await navigateToDOObject("MyDurableObject", objectId, "users"); + await waitForTableRows(1); + + await waitForText("Alice"); + + const isBobVisible = await isTextVisible("Bob"); + expect(isBobVisible).toBe(false); + }); + + test("adds a column via SQL", async () => { + const objectId = await navigateToDOObjectByName("MyDurableObject"); + await waitForText("Durable Objects"); + await waitForQueryEditor(); + + await typeInQueryEditor("ALTER TABLE users ADD COLUMN email TEXT;"); + await runQuery(); + await waitForQueryResult(); + + await navigateToDOObject("MyDurableObject", objectId, "users"); + await waitForTableRows(1); + await waitForText("email"); + }); + + test("executes a SELECT query and displays results", async () => { + await navigateToDOObjectByName("MyDurableObject"); + await waitForText("Durable Objects"); + await waitForQueryEditor(); + + await typeInQueryEditor("SELECT * FROM users;"); + await runQuery(); + await waitForQueryResult(); + + // The result tab should show the table name and dimensions + // e.g., "users • 2 x 2" (2 columns x 2 rows) + await waitForText("users"); + + // Verify both rows appear in the results + await waitForText("Alice"); + await waitForText("Bob"); + }); + }); +}); diff --git a/packages/local-explorer-ui/src/__e2e__/global-setup.ts b/packages/local-explorer-ui/src/__e2e__/global-setup.ts new file mode 100644 index 0000000..c37be40 --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/global-setup.ts @@ -0,0 +1,71 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "playwright-chromium"; +import { createServer } from "vite"; +import { runWranglerDev } from "../../../../fixtures/shared/src/run-wrangler-long-lived"; +import type { BrowserServer } from "playwright-chromium"; +import type { ViteDevServer } from "vite"; +import type { TestProject } from "vitest/node"; + +let browserServer: BrowserServer | undefined; +let viteServer: ViteDevServer | undefined; +let stopWorker: (() => Promise) | undefined; + +const WORKER_PORT = 8787; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PACKAGE_DIR = path.resolve(__dirname, "../.."); +const ROOT_DIR = path.resolve(__dirname, "../../../.."); + +export async function setup({ provide }: TestProject): Promise { + console.log("Starting E2E test environment setup..."); + + // Start the worker fixture using the shared helper + console.log("Starting worker fixture..."); + const fixtureDir = path.resolve(ROOT_DIR, "fixtures/worker-with-resources"); + + const { ip, port, stop } = await runWranglerDev( + fixtureDir, + [`--port=${WORKER_PORT}`, "--inspector-port=0"], + { X_LOCAL_EXPLORER: "true" } + ); + stopWorker = stop; + + const workerUrl = `http://${ip}:${port}`; + console.log(`Worker fixture is ready at ${workerUrl}`); + + // Start the Vite dev server programmatically + console.log("Starting Vite dev server..."); + viteServer = await createServer({ + root: PACKAGE_DIR, + }); + await viteServer.listen(); + console.log( + `Vite dev server is ready at http://localhost:${viteServer.config.server.port}` + ); + + // Launch Playwright browser server + console.log("Launching browser server..."); + browserServer = await chromium.launchServer({ + args: process.env.CI + ? ["--no-sandbox", "--disable-setuid-sandbox"] + : undefined, + headless: true, + }); + + provide("viteUrl", `http://localhost:${viteServer.config.server.port}`); + provide("workerUrl", workerUrl); + provide("wsEndpoint", browserServer.wsEndpoint()); + + console.log("E2E test environment setup complete"); +} + +export async function teardown(): Promise { + console.log("Tearing down E2E test environment..."); + + await browserServer?.close(); + await viteServer?.close(); + await stopWorker?.(); + + console.log("E2E test teardown complete"); +} diff --git a/packages/local-explorer-ui/src/__e2e__/kv/kv-namespace.spec.ts b/packages/local-explorer-ui/src/__e2e__/kv/kv-namespace.spec.ts new file mode 100644 index 0000000..cf0ecc5 --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/kv/kv-namespace.spec.ts @@ -0,0 +1,266 @@ +import { beforeEach, describe, test } from "vitest"; +import { + clickButton, + fillByPlaceholder, + getTableRowCount, + isTextVisible, + navigateToKV, + page, + seedKV, + waitForSelector, + waitForTableRows, + waitForText, +} from "../utils"; + +describe("KV Namespace", () => { + beforeEach(async () => { + await seedKV(); + }); + + describe("viewing entries", () => { + test("displays entries in the table", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + await waitForText("greeting"); + + const greetingRow = page.locator("tr").filter({ hasText: "greeting" }); + const rowText = await greetingRow.textContent(); + expect(rowText).toContain("Hello, World!"); + }); + + test("loads namespace values when aggregate payload exceeds bulk limit", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + await waitForText("large-key-1"); + }); + + test("shows column headers", async () => { + await navigateToKV("KV"); + await waitForTableRows(1); + + await waitForText("Key"); + await waitForText("Value"); + }); + }); + + describe("searching entries", () => { + test("filters entries by prefix", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + + await fillByPlaceholder("Search keys by prefix...", "config:"); + await clickButton("Search"); + + await waitForText("config:theme"); + + const configRows = page.locator("tr").filter({ hasText: "config:" }); + const count = await configRows.count(); + expect(count).toEqual(5); + }); + + test("shows no results message when prefix matches nothing", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + + await fillByPlaceholder( + "Search keys by prefix...", + "nonexistent-prefix-xyz:" + ); + await clickButton("Search"); + + await waitForText("No keys found matching prefix"); + }); + }); + + describe("adding entries", () => { + test("adds a new key/value pair", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const mockData = { + key: `test-new-key-e2e-${Date.now()}`, + value: `test-new-value-e2e-${Date.now()}`, + }; + + await page.locator("#add-key").fill(mockData.key); + await page.locator("#add-value").fill(mockData.value); + await clickButton("Add entry"); + + await page.waitForTimeout(500); + + // Verify the new entry appears in the table (new entries are prepended at the top) + await waitForText(mockData.key, { timeout: 15_000 }); + + const newRow = page.locator("tr").filter({ hasText: mockData.key }); + const rowText = await newRow.textContent(); + expect(rowText).toContain(mockData.value); + }); + + test("shows overwrite confirmation when key exists", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + + await page.locator("#add-key").fill("greeting"); + await page.locator("#add-value").fill("new-greeting-value"); + await clickButton("Add entry"); + + await waitForSelector('[role="dialog"]', { timeout: 5_000 }); + await waitForText("Overwrite key?"); + }); + + test("confirms overwrite and updates value", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const mockValue = "overwritten-value-e2e"; + + await page.locator("#add-key").fill("greeting"); + await page.locator("#add-value").fill(mockValue); + await clickButton("Add entry"); + + await waitForSelector('[role="dialog"]', { timeout: 5_000 }); + await page + .getByRole("dialog") + .getByRole("button", { name: "Overwrite" }) + .click(); + + await waitForText(mockValue); + }); + }); + + describe("editing entries", () => { + test("opens edit mode via menu", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const row = page.locator("tr").filter({ hasText: "greeting" }); + await row.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Edit" }).click(); + + await waitForSelector('input[id^="edit-key-"]', { timeout: 5_000 }); + }); + + test("saves edited value", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const row = page.locator("tr").filter({ hasText: "greeting" }); + await row.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Edit" }).click(); + + await waitForSelector('textarea[id^="edit-value-"]', { timeout: 5_000 }); + + const updatedMockValue = "Updated greeting e2e"; + + const valueInput = page.locator('textarea[id^="edit-value-"]'); + await valueInput.fill(updatedMockValue); + await clickButton("Save"); + + await waitForText(updatedMockValue); + }); + + test("cancels editing without saving changes", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const row = page.locator("tr").filter({ hasText: "greeting" }); + await row.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Edit" }).click(); + + await waitForSelector('textarea[id^="edit-value-"]', { timeout: 5_000 }); + + const valueInput = page.locator('textarea[id^="edit-value-"]'); + await valueInput.fill("Should not be saved"); + await clickButton("Cancel"); + + // Original value should still be there + const greetingRow = page.locator("tr").filter({ hasText: "greeting" }); + const rowText = await greetingRow.textContent(); + expect(rowText).toContain("Hello, World!"); + }); + }); + + describe("deleting entries", () => { + test("shows delete confirmation dialog", async () => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const row = page.locator("tr").filter({ hasText: "greeting" }); + await row.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + + await waitForSelector('[role="dialog"]', { timeout: 5_000 }); + await waitForText("Delete key?"); + }); + + test("deletes entry after confirmation", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const row = page.locator("tr").filter({ hasText: "counter" }); + await row.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + + await waitForSelector('[role="dialog"]', { timeout: 5_000 }); + await page + .getByRole("dialog") + .getByRole("button", { name: "Delete" }) + .click(); + + await page.waitForSelector('[role="dialog"]', { + state: "hidden", + timeout: 5_000, + }); + + const counterCode = page.locator("code").filter({ hasText: "counter" }); + const count = await counterCode.count(); + expect(count).toBe(0); + }); + + test("cancels deletion when clicking 'Cancel'", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const row = page.locator("tr").filter({ hasText: "greeting" }); + await row.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + + await waitForSelector('[role="dialog"]', { timeout: 5_000 }); + await page + .getByRole("dialog") + .getByRole("button", { name: "Cancel" }) + .click(); + + const isVisible = await isTextVisible("greeting"); + expect(isVisible).toBe(true); + }); + }); + + describe("pagination", () => { + test("loads more entries when clicking Load More", async ({ expect }) => { + await navigateToKV("KV"); + await waitForTableRows(5); + + const initialRows = await getTableRowCount(); + + const loadMoreButton = page.getByRole("button", { name: "Load More" }); + if (!(await loadMoreButton.isVisible())) { + return; + } + + await loadMoreButton.click(); + + await page.waitForFunction( + (prevCount: number) => { + const rows = document.querySelectorAll("tbody tr"); + return rows.length > prevCount; + }, + initialRows, + { timeout: 10_000 } + ); + + const newRows = await getTableRowCount(); + expect(newRows).toBeGreaterThan(initialRows); + }); + }); +}); diff --git a/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts b/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts new file mode 100644 index 0000000..b046016 --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts @@ -0,0 +1,753 @@ +import { beforeEach, describe, test } from "vitest"; +import { + captureDownload, + cleanupDownload, + clickButton, + isTextVisible, + navigateToR2Bucket, + navigateToR2Object, + page, + seedR2, + waitForBreadcrumbText, + waitForDialog, + waitForTableRows, + waitForText, +} from "../utils"; + +describe("R2 Bucket", () => { + beforeEach(async () => { + await seedR2(); + }); + + describe("viewing objects", () => { + test("displays objects and directories in the table", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Column headers should be visible + await waitForText("Objects"); + await waitForText("Type"); + await waitForText("Size"); + await waitForText("Modified"); + }); + + test("shows root-level files in grouped mode", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Root-level files from seed data + await waitForText("readme.txt"); + await waitForText("config.json"); + }); + + test("shows directories in grouped mode", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Directories from seed data (grouped by delimiter "/") + await waitForText("images/"); + await waitForText("documents/"); + await waitForText("data/"); + await waitForText("logs/"); + await waitForText("assets/"); + + // Directories should show "Directory" as their type + const directoryRow = page.locator("tr").filter({ hasText: "images/" }); + const rowText = await directoryRow.textContent(); + expect(rowText).toContain("Directory"); + }); + + test("shows content type for files", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // File rows should display a MIME type in the Type column. + // The list API may not return the stored httpMetadata so the UI + // falls back to "application/octet-stream". + const configRow = page.locator("tr").filter({ hasText: "config.json" }); + const rowText = await configRow.textContent(); + expect(rowText).toMatch(/application\/\w+/); + }); + + test("shows empty state when bucket has no objects", async () => { + // Navigate to a non-existent prefix to simulate empty state + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Navigate into a prefix that doesn't exist by manipulating the URL + await page.goto( + `${page.url().split("/r2/")[0]}/r2/my-bucket?prefix=nonexistent-prefix/` + ); + await page.waitForLoadState("domcontentloaded"); + + await waitForText("No objects in this directory"); + }); + }); + + describe("breadcrumbs and navigation", () => { + test("shows R2 and bucket name in breadcrumbs", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await waitForBreadcrumbText("R2"); + await waitForBreadcrumbText("my-bucket"); + }); + + test("navigates into a directory and updates breadcrumbs", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Click on the "images/" directory + const imagesButton = page + .locator("button") + .filter({ hasText: "images/" }); + await imagesButton.click(); + + // Breadcrumbs should now include the directory + await waitForBreadcrumbText("images"); + + // Should show files inside the images directory + await waitForText("logo.svg"); + await waitForText("banner.svg"); + }); + + test("navigates into nested directories", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Navigate into "images/" + const imagesButton = page + .locator("button") + .filter({ hasText: "images/" }); + await imagesButton.click(); + await waitForBreadcrumbText("images"); + await waitForText("icons/"); + + // Navigate into "icons/" subdirectory + const iconsButton = page.locator("button").filter({ hasText: "icons/" }); + await iconsButton.click(); + await waitForBreadcrumbText("icons"); + + // Should show nested files + await waitForText("home.svg"); + await waitForText("settings.svg"); + }); + + test("navigates back to bucket root via breadcrumb", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Navigate into a directory + const imagesButton = page + .locator("button") + .filter({ hasText: "images/" }); + await imagesButton.click(); + await waitForBreadcrumbText("images"); + + // Click the bucket name breadcrumb to go back to root + const bucketBreadcrumb = page + .locator('nav[aria-label="breadcrumb"]') + .getByRole("link", { name: "my-bucket" }) + .first(); + await bucketBreadcrumb.click(); + + // Should be back at root showing directories + await waitForText("images/"); + await waitForText("documents/"); + }); + }); + + describe("grouped/ungrouped toggle", () => { + test("defaults to grouped mode", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // The "Grouped" button should be visible in the toolbar + await waitForText("Grouped"); + + // Directories should be visible (grouped mode uses delimiter) + await waitForText("images/"); + }); + + test("switches to ungrouped mode and shows flat object keys", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Open the view mode dropdown + await page.getByRole("button", { name: /Grouped|Ungrouped/ }).click(); + + // Click "Ungrouped" option (exact match to avoid matching "Grouped" substring) + await page + .getByRole("menuitem", { name: "Ungrouped", exact: true }) + .click(); + + // Wait for the page to reload with ungrouped view + await waitForTableRows(1); + + // In ungrouped mode, full object keys should be visible (no directories) + await waitForText("images/logo.svg"); + await waitForText("documents/report.txt"); + await waitForText("data/users.json"); + }); + + test("switches back to grouped mode from ungrouped", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Switch to ungrouped + await page.getByRole("button", { name: /Grouped|Ungrouped/ }).click(); + await page + .getByRole("menuitem", { name: "Ungrouped", exact: true }) + .click(); + await waitForTableRows(1); + + // Switch back to grouped (exact match to avoid matching "Ungrouped") + await page.getByRole("button", { name: /Grouped|Ungrouped/ }).click(); + await page + .getByRole("menuitem", { name: "Grouped", exact: true }) + .click(); + await waitForTableRows(1); + + // Directories should reappear + await waitForText("images/"); + await waitForText("documents/"); + }); + + test("delimiter param persists when navigating to an object and back", async ({ + expect, + }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Switch to ungrouped mode (sets delimiter=false in URL) + await page.getByRole("button", { name: /Grouped|Ungrouped/ }).click(); + await page + .getByRole("menuitem", { name: "Ungrouped", exact: true }) + .click(); + await waitForTableRows(1); + await waitForText("images/logo.svg"); + + // Navigate to an object + const objectLink = page + .getByRole("link", { name: "images/logo.svg" }) + .first(); + await objectLink.click(); + await waitForText("Object Details"); + + // URL should still contain delimiter=false + expect(page.url()).toContain("delimiter=false"); + + // Navigate back via the bucket breadcrumb + const bucketBreadcrumb = page + .locator('nav[aria-label="breadcrumb"]') + .getByRole("link", { name: "my-bucket" }) + .first(); + await bucketBreadcrumb.click(); + await waitForTableRows(1); + + // Should still be in ungrouped mode — full keys visible, no directories + await waitForText("images/logo.svg"); + + // The toolbar button should still show "Ungrouped" + await waitForText("Ungrouped"); + }); + }); + + describe("object detail page", () => { + test("navigates to object detail page from table", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Click on a root-level file to navigate to its detail page + const readmeLink = page.getByRole("link", { name: "readme.txt" }).first(); + await readmeLink.click(); + + // Should show the object key as the page title + await waitForText("readme.txt"); + + // Should show the "Object Details" card + await waitForText("Object Details"); + }); + + test("shows object metadata on detail page", async () => { + await navigateToR2Object("my-bucket", "config.json"); + + // Object Details card should show metadata labels and values + await waitForText("Object Details"); + await waitForText("Date Created"); + await waitForText("Type"); + await waitForText("Size"); + }); + + test("shows custom metadata when present", async () => { + // config.json has custom metadata: { author: "admin", created: "2025-01-15" } + await navigateToR2Object("my-bucket", "config.json"); + + await waitForText("Custom Metadata"); + await waitForText("author"); + await waitForText("admin"); + await waitForText("created"); + await waitForText("2025-01-15"); + }); + + test("shows 'No custom metadata' when none is set", async () => { + // readme.txt has no custom metadata + await navigateToR2Object("my-bucket", "readme.txt"); + + await waitForText("No custom metadata set"); + }); + + test("shows Download and Delete buttons on detail page", async () => { + await navigateToR2Object("my-bucket", "readme.txt"); + + const downloadButton = page.getByRole("button", { name: "Download" }); + const deleteButton = page.getByRole("button", { name: "Delete" }); + + await downloadButton.waitFor({ state: "visible" }); + await deleteButton.waitFor({ state: "visible" }); + }); + + test("breadcrumbs show full path for nested objects", async () => { + await navigateToR2Object("my-bucket", "documents/report.txt"); + + // Breadcrumbs should show: R2 > my-bucket > documents > report.txt + await waitForBreadcrumbText("R2"); + await waitForBreadcrumbText("my-bucket"); + await waitForBreadcrumbText("documents"); + await waitForBreadcrumbText("report.txt"); + }); + }); + + describe("deleting objects", () => { + test("shows delete confirmation dialog from table action menu", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Open the action menu on a file row + const readmeRow = page.locator("tr").filter({ hasText: "readme.txt" }); + await readmeRow.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + + // Delete confirmation dialog should appear + await waitForDialog(); + await waitForText("Delete object?"); + }); + + test("cancels deletion when clicking Cancel", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + const readmeRow = page.locator("tr").filter({ hasText: "readme.txt" }); + await readmeRow.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + + await waitForDialog(); + + // Click Cancel + await page + .getByRole("dialog") + .getByRole("button", { name: "Cancel" }) + .click(); + + // Dialog should close and object should still be visible + await page.waitForSelector('[role="dialog"]', { + state: "hidden", + timeout: 5_000, + }); + + const isVisible = await isTextVisible("readme.txt"); + expect(isVisible).toBe(true); + }); + + test("deletes object after confirmation from table", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Delete config.json (it has custom metadata, good to verify it's removed) + const configRow = page.locator("tr").filter({ hasText: "config.json" }); + await configRow.getByRole("button", { name: "Actions" }).click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + + await waitForDialog(); + + // Confirm deletion + await page + .getByRole("dialog") + .getByRole("button", { name: "Delete" }) + .click(); + + // Wait for dialog to close + await page.waitForSelector('[role="dialog"]', { + state: "hidden", + timeout: 5_000, + }); + + // config.json should no longer be visible in the table + const isVisible = await isTextVisible("config.json"); + expect(isVisible).toBe(false); + }); + + test("shows delete confirmation from object detail page", async () => { + await navigateToR2Object("my-bucket", "readme.txt"); + await waitForText("Object Details"); + + // Click the Delete button on the detail page + await page.getByRole("button", { name: "Delete" }).click(); + + await waitForDialog(); + await waitForText("Delete object?"); + + // "readme.txt" appears in multiple places (sidebar, breadcrumbs, heading), + // so scope the assertion to the dialog. + const dialog = page.getByRole("dialog"); + await dialog.getByText("readme.txt").waitFor({ state: "visible" }); + }); + + test("deletes object from detail page and navigates back", async () => { + await navigateToR2Object("my-bucket", "logs/access.log"); + await waitForText("Object Details"); + + await page.getByRole("button", { name: "Delete" }).click(); + await waitForDialog(); + + // Confirm deletion + await page + .getByRole("dialog") + .getByRole("button", { name: "Delete" }) + .click(); + + // Should navigate back to the bucket view (parent prefix) + await waitForText("R2"); + await waitForTableRows(1); + }); + }); + + describe("adding directories", () => { + test("opens add directory dialog", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await clickButton("Add directory"); + + await waitForDialog(); + await waitForText("Add directory"); + await waitForText("Directory name"); + }); + + test("creates a new directory", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await clickButton("Add directory"); + await waitForDialog(); + + // Fill in the directory name + const input = page.getByPlaceholder("my-directory"); + await input.fill(`test-dir-e2e-${Date.now()}`); + + // Click Create + await page + .getByRole("dialog") + .getByRole("button", { name: "Create" }) + .click(); + + // Dialog should close + await page.waitForSelector('[role="dialog"]', { + state: "hidden", + timeout: 5_000, + }); + }); + + test("cancels add directory dialog", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await clickButton("Add directory"); + await waitForDialog(); + + const input = page.getByPlaceholder("my-directory"); + await input.fill("should-not-be-created"); + + // Click Cancel + await page + .getByRole("dialog") + .getByRole("button", { name: "Cancel" }) + .click(); + + // Dialog should close + await page.waitForSelector('[role="dialog"]', { + state: "hidden", + timeout: 5_000, + }); + }); + + test("disables Create button when directory name is empty", async ({ + expect, + }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await clickButton("Add directory"); + await waitForDialog(); + + // Create button should be disabled when input is empty + const createButton = page + .getByRole("dialog") + .getByRole("button", { name: "Create" }); + expect(await createButton.isDisabled()).toBe(true); + }); + }); + + describe("upload dialog", () => { + test("opens upload dialog", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await clickButton("Upload"); + + await waitForDialog(); + await waitForText("Upload object"); + await waitForText("Drop a file here or click to browse"); + }); + + test("cancels upload dialog", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + await clickButton("Upload"); + await waitForDialog(); + + await page + .getByRole("dialog") + .getByRole("button", { name: "Cancel" }) + .click(); + + await page.waitForSelector('[role="dialog"]', { + state: "hidden", + timeout: 5_000, + }); + }); + }); + + describe("selection and bulk actions", () => { + test("selects an object via checkbox", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Click the checkbox on the readme.txt row + const readmeRow = page.locator("tr").filter({ hasText: "readme.txt" }); + const checkbox = readmeRow.getByRole("checkbox"); + await checkbox.click(); + + expect(await checkbox.isChecked()).toBe(true); + }); + + test("selects all objects via header checkbox", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Click the "Select all" checkbox in the header + const selectAllCheckbox = page.getByRole("checkbox", { + name: "Select all", + }); + await selectAllCheckbox.click(); + + expect(await selectAllCheckbox.isChecked()).toBe(true); + }); + + test("shows bulk action menu when items are selected", async ({ + expect, + }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Select an item + const readmeRow = page.locator("tr").filter({ hasText: "readme.txt" }); + await readmeRow.getByRole("checkbox").click(); + + // The bulk actions button should be enabled + const bulkActionsButton = page + .locator("thead") + .getByRole("button", { name: "Bulk actions" }); + expect(await bulkActionsButton.isDisabled()).toBe(false); + }); + + test("shift-click selects a contiguous range of rows", async ({ + expect, + }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(4); + + const checkboxes = page.locator("tbody tr").getByRole("checkbox"); + const count = await checkboxes.count(); + + expect(count).toBeGreaterThanOrEqual(4); + + await checkboxes.nth(0).click(); + expect(await checkboxes.nth(0).isChecked()).toBe(true); + + // Shift-click the fourth row checkbox + await checkboxes.nth(3).click({ modifiers: ["Shift"] }); + + expect(await checkboxes.nth(0).isChecked()).toBe(true); + expect(await checkboxes.nth(1).isChecked()).toBe(true); + expect(await checkboxes.nth(2).isChecked()).toBe(true); + expect(await checkboxes.nth(3).isChecked()).toBe(true); + }); + + test("shift-click deselects a contiguous range of rows", async ({ + expect, + }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(4); + + const checkboxes = page.locator("tbody tr").getByRole("checkbox"); + const count = await checkboxes.count(); + expect(count).toBeGreaterThanOrEqual(4); + + // Click the first row (sets anchor), then shift-click the fourth to select range + await checkboxes.nth(0).click(); + await checkboxes.nth(3).click({ modifiers: ["Shift"] }); + + expect(await checkboxes.nth(0).isChecked()).toBe(true); + expect(await checkboxes.nth(1).isChecked()).toBe(true); + expect(await checkboxes.nth(2).isChecked()).toBe(true); + expect(await checkboxes.nth(3).isChecked()).toBe(true); + + // Shift-click the fourth row again to deselect the range + await checkboxes.nth(3).click({ modifiers: ["Shift"] }); + + expect(await checkboxes.nth(0).isChecked()).toBe(false); + expect(await checkboxes.nth(1).isChecked()).toBe(false); + expect(await checkboxes.nth(2).isChecked()).toBe(false); + expect(await checkboxes.nth(3).isChecked()).toBe(false); + }); + }); + + describe("refresh", () => { + test("refresh button is visible", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + const refreshButton = page.getByRole("button", { name: "Refresh" }); + await refreshButton.waitFor({ state: "visible", timeout: 10_000 }); + }); + + test("refreshes the object list", async () => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Click refresh and verify the table still shows data + await page.getByRole("button", { name: "Refresh" }).click(); + + // After refresh, objects should still be visible + await waitForTableRows(1); + await waitForText("readme.txt"); + }); + }); + + describe("downloading objects", () => { + test("downloads file from object detail page", async ({ expect }) => { + await navigateToR2Object("my-bucket", "readme.txt"); + await waitForText("Object Details"); + + const { content, downloadPath, suggestedFilename } = + await captureDownload( + async () => + await page.getByRole("button", { name: "Download" }).click(), + "readme.txt" + ); + + expect(suggestedFilename).toBe("readme.txt"); + expect(content).toBe( + "Welcome to the R2 bucket! This is a sample readme file." + ); + + await cleanupDownload(downloadPath); + }); + + test("downloads nested file with correct filename", async ({ expect }) => { + await navigateToR2Object("my-bucket", "documents/report.txt"); + await waitForText("Object Details"); + + const { content, downloadPath, suggestedFilename } = + await captureDownload( + async () => + await page.getByRole("button", { name: "Download" }).click(), + "report.txt" + ); + + // Should extract filename from path (report.txt, not documents/report.txt) + expect(suggestedFilename).toBe("report.txt"); + expect(content).toContain("Annual Report 2024"); + + await cleanupDownload(downloadPath); + }); + + test("downloads JSON file with correct content", async ({ expect }) => { + await navigateToR2Object("my-bucket", "config.json"); + await waitForText("Object Details"); + + const { content, downloadPath, suggestedFilename } = + await captureDownload( + async () => + await page.getByRole("button", { name: "Download" }).click(), + "config.json" + ); + + expect(suggestedFilename).toBe("config.json"); + + // Verify JSON parses correctly and has expected structure + const parsed = JSON.parse(content); + expect(parsed.version).toBe("1.0.0"); + expect(parsed.environment).toBe("development"); + + await cleanupDownload(downloadPath); + }); + + test("downloads binary file (SVG) with correct content", async ({ + expect, + }) => { + await navigateToR2Object("my-bucket", "images/logo.svg"); + await waitForText("Object Details"); + + const { content, downloadPath, suggestedFilename } = + await captureDownload( + async () => + await page.getByRole("button", { name: "Download" }).click(), + "logo.svg" + ); + + expect(suggestedFilename).toBe("logo.svg"); + expect(content).toContain(""); + expect(content).toContain('xmlns="http://www.w3.org/2000/svg"'); + + await cleanupDownload(downloadPath); + }); + + test("downloads file from table row action menu", async ({ expect }) => { + await navigateToR2Bucket("my-bucket"); + await waitForTableRows(1); + + // Open the action menu on a file row + const readmeRow = page.locator("tr").filter({ hasText: "readme.txt" }); + await readmeRow.getByRole("button", { name: "Actions" }).click(); + + // Click download in the menu + const { content, downloadPath, suggestedFilename } = + await captureDownload( + async () => + await page.getByRole("menuitem", { name: "Download" }).click(), + "readme.txt" + ); + + expect(suggestedFilename).toBe("readme.txt"); + expect(content).toBe( + "Welcome to the R2 bucket! This is a sample readme file." + ); + + await cleanupDownload(downloadPath); + }); + }); +}); diff --git a/packages/local-explorer-ui/src/__e2e__/setup.ts b/packages/local-explorer-ui/src/__e2e__/setup.ts new file mode 100644 index 0000000..057e3e7 --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/setup.ts @@ -0,0 +1,49 @@ +import { chromium } from "playwright-chromium"; +import { beforeAll, inject } from "vitest"; +import type { Browser, ConsoleMessage, Page } from "playwright-chromium"; + +export let browser: Browser; +export let page: Page; +export let viteUrl: string; +export let workerUrl: string; + +const browserLogs = new Array(); +const browserErrors = new Array(); + +declare module "vitest" { + export interface ProvidedContext { + viteUrl: string; + workerUrl: string; + wsEndpoint: string; + } +} + +beforeAll(async () => { + viteUrl = inject("viteUrl"); + workerUrl = inject("workerUrl"); + + const wsEndpoint = inject("wsEndpoint"); + if (!wsEndpoint) { + throw new Error("`wsEndpoint` not provided by global setup"); + } + + browser = await chromium.connect(wsEndpoint); + page = await browser.newPage(); + + // Collect browser console logs + page.on("console", (msg: ConsoleMessage) => { + browserLogs.push(`[${msg.type()}] ${msg.text()}`); + }); + page.on("pageerror", (error: Error) => { + browserErrors.push(error); + }); + + await page.goto(viteUrl); + + return async () => { + await page?.close(); + await browser?.close(); + browserLogs.length = 0; + browserErrors.length = 0; + }; +}, 30_000); diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts new file mode 100644 index 0000000..0ed71ef --- /dev/null +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -0,0 +1,483 @@ +import { readFile, unlink } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { page, viteUrl, workerUrl } from "./setup"; + +export { page, viteUrl }; + +/** + * Navigate to a specific path in the app. + */ +async function navigateTo(path: string): Promise { + const url = new URL(path, viteUrl); + await page.goto(url.toString()); +} + +/** + * Wait for the page to finish loading. + */ +async function waitForPageLoad(): Promise { + await page.waitForLoadState("domcontentloaded"); +} + +/** + * Seed the KV namespace with test data. + */ +export async function seedKV(): Promise { + await fetch(`${workerUrl}/kv/seed`); +} + +/** + * Seed the R2 bucket with test data. + */ +export async function seedR2(): Promise { + await fetch(`${workerUrl}/r2/seed`); +} + +/** + * Seed the D1 database with test data. + */ +export async function seedD1(): Promise { + await fetch(`${workerUrl}/d1`); +} + +/** + * Seed a Durable Object with test data. + */ +export async function seedDO(objectId: string = "test-object"): Promise { + await fetch(`${workerUrl}/do?id=${objectId}`); +} + +/** + * Seed a Workflow instance by triggering via the explorer API. + */ +export async function seedWorkflow(workflowName: string): Promise<{ + id: string; +}> { + const workflowId = `e2e-test-${Date.now()}`; + + await fetch( + `${workerUrl}/cdn-cgi/explorer/api/workflows/${workflowName}/instances`, + { + body: JSON.stringify({ + id: workflowId, + params: { name: "E2E Test" }, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + } + ); + + return { + id: workflowId, + }; +} + +/** + * Delete all instances of a workflow via the explorer API. + */ +export async function cleanupWorkflow(workflowName: string): Promise { + await fetch(`${workerUrl}/cdn-cgi/explorer/api/workflows/${workflowName}`, { + method: "DELETE", + }); +} + +/** + * Common wait options for `vi.waitFor` + */ +const WAIT_OPTIONS = { + interval: 100, + timeout: 30_000, +}; + +/** + * Navigate to a KV namespace. + */ +export async function navigateToKV(namespaceId: string): Promise { + await navigateTo(`/cdn-cgi/explorer/kv/${namespaceId}`); + await waitForPageLoad(); +} + +/** + * Navigate to an R2 bucket. + */ +export async function navigateToR2Bucket(bucketName: string): Promise { + await navigateTo(`/cdn-cgi/explorer/r2/${bucketName}`); + await waitForPageLoad(); +} + +/** + * Navigate to an R2 object detail page. + */ +export async function navigateToR2Object( + bucketName: string, + objectKey: string +): Promise { + await navigateTo( + `/cdn-cgi/explorer/r2/${bucketName}/object/${encodeURIComponent(objectKey)}` + ); + await waitForPageLoad(); +} + +/** + * Navigate to a D1 database. + */ +export async function navigateToD1( + databaseId: string, + table?: string +): Promise { + let path = `/cdn-cgi/explorer/d1/${databaseId}`; + if (table) { + path += `?table=${encodeURIComponent(table)}`; + } + + await navigateTo(path); + await waitForPageLoad(); +} + +/** + * Navigate to a Durable Object class. + */ +export async function navigateToDOClass(className: string): Promise { + await navigateTo(`/cdn-cgi/explorer/do/${className}`); + await waitForPageLoad(); +} + +/** + * Navigate to a Workflow instances list page. + */ +export async function navigateToWorkflow(workflowName: string): Promise { + await navigateTo(`/cdn-cgi/explorer/workflows/${workflowName}`); + await waitForPageLoad(); +} + +/** + * Navigate to a specific Durable Object instance by hex ID. + * + * Note: The API requires the actual hex object ID, not a string name. + * If you only have a string name (e.g., "test-object"), use `navigateToDOObjectByName` instead. + */ +export async function navigateToDOObject( + className: string, + objectId: string, + table?: string +): Promise { + let path = `/cdn-cgi/explorer/do/${className}/${objectId}`; + if (table) { + path += `?table=${encodeURIComponent(table)}`; + } + + await navigateTo(path); + await waitForPageLoad(); +} + +/** + * Navigate to a Durable Object instance by going through the class page UI. + * This is necessary because the API requires hex object IDs, but tests often + * only know the string name used with `idFromName()`. + * + * This function: + * 1. Navigates to the DO class page + * 2. Finds the first "Open Studio" link + * 3. Extracts the hex object ID from the link href + * 4. Navigates to the object page (optionally with a table selected) + * + * @returns The hex object ID that was extracted from the UI + */ +export async function navigateToDOObjectByName( + className: string, + table?: string, + objectName: string = "test-object" +): Promise { + await navigateToDOClass(className); + await waitForText(className); + + await fillByPlaceholder("Enter instance name or hex ID...", objectName); + await page.getByRole("button", { name: "Open Studio" }).click(); + await waitForPageLoad(); + + // Extract the object ID from the current URL after navigation. + const objectPath = new URL(page.url()).pathname; + const match = objectPath.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([^/?#]+)/); + if (!match || !match[1]) { + throw new Error(`Could not extract object ID from URL path: ${objectPath}`); + } + + const objectId: string = match[1]; + + if (table) { + await navigateToDOObject(className, objectId, table); + } + + return objectId; +} + +/** + * Wait for text to appear on the page. + */ +export async function waitForText( + text: string, + options?: { + timeout?: number; + } +): Promise { + await page.waitForSelector(`text=${text}`, { + timeout: options?.timeout ?? WAIT_OPTIONS.timeout, + }); +} + +/** + * Wait for text to appear inside the breadcrumb navigation bar. + * + * Use this instead of `waitForText` when the same text also appears in the + * sidebar (e.g. bucket names, object keys) to avoid Playwright resolving + * to the wrong element. + */ +export async function waitForBreadcrumbText( + text: string, + options?: { + timeout?: number; + } +): Promise { + // The Kumo breadcrumb `

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

+ +