diff --git a/.changeset/fresh-dogs-configure.md b/.changeset/fresh-dogs-configure.md new file mode 100644 index 00000000000..5b3d0a9264e --- /dev/null +++ b/.changeset/fresh-dogs-configure.md @@ -0,0 +1,8 @@ +--- +"@cloudflare/autoconfig": minor +"wrangler": patch +--- + +Prepare autoconfig for multiple configuration targets + +Add target-specific configuration output while preserving Wrangler's existing setup and deployment behavior. diff --git a/.github/workflows/changeset-review.yml b/.github/workflows/changeset-review.yml deleted file mode 100644 index ac539636f97..00000000000 --- a/.github/workflows/changeset-review.yml +++ /dev/null @@ -1,92 +0,0 @@ -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/triage-issue.yml b/.github/workflows/triage-issue.yml deleted file mode 100644 index 10bcf761171..00000000000 --- a/.github/workflows/triage-issue.yml +++ /dev/null @@ -1,259 +0,0 @@ -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 report comment (identified by its 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, '')) - 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 - # Pin to a known-good version (matches the Bonk workflow). Newer - # versions (e.g. 1.17.x) changed the default model to ones that are not - # provisioned in our AI Gateway, which surfaced as "Invalid Anthropic - # API Key" errors. - run: | - npm install -g opencode-ai@1.15.13 - 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 \ - --model "cloudflare-ai-gateway/anthropic/claude-opus-4-8" \ - 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 "" - 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 "
" - echo "🤖 Automated triage report" - echo "" - cat "data/${ISSUE}/report.md" - echo "" - echo "
" - echo "" - echo "
" - echo "Structured summary (JSON)" - echo "" - echo '```json' - cat "data/${ISSUE}/summary.json" - echo "" - echo '```' - echo "" - echo "
" - } > "data/${ISSUE}/comment.md" - - # marocchino/sticky-pull-request-comment only supports PRs (it resolves - # the number as a PullRequest), so for issues we implement a "sticky" - # upsert with gh: find our previous comment by its marker and edit it, - # otherwise create a new one. gh is already used throughout this workflow. - - name: Post triage comment - env: - GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} - ISSUE: ${{ steps.issue.outputs.number }} - REPO: ${{ github.repository }} - run: | - MARKER="" - BODY_FILE="data/${ISSUE}/comment.md" - - COMMENT_ID=$(gh api --paginate "/repos/${REPO}/issues/${ISSUE}/comments" \ - --jq "map(select(.body | contains(\"${MARKER}\"))) | .[0].id // empty") - - if [ -n "$COMMENT_ID" ]; then - jq -n --rawfile body "$BODY_FILE" '{body: $body}' \ - | gh api --method PATCH "/repos/${REPO}/issues/comments/${COMMENT_ID}" --input - > /dev/null - echo "Updated existing triage comment ${COMMENT_ID}" - else - gh issue comment "$ISSUE" --repo "$REPO" --body-file "$BODY_FILE" - echo "Created new triage comment" - fi - - - 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/packages/autoconfig/package.json b/packages/autoconfig/package.json index a34b2972819..520c50cf11e 100644 --- a/packages/autoconfig/package.json +++ b/packages/autoconfig/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@cloudflare/cli-shared-helpers": "workspace:*", + "@cloudflare/config": "workspace:*", "@cloudflare/workers-utils": "workspace:*" }, "devDependencies": { diff --git a/packages/autoconfig/scripts/deps.ts b/packages/autoconfig/scripts/deps.ts index 6d7361e5358..01728ca8c86 100644 --- a/packages/autoconfig/scripts/deps.ts +++ b/packages/autoconfig/scripts/deps.ts @@ -8,5 +8,7 @@ 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", + // Its public types are exposed by autoconfig and must resolve for consumers. + "@cloudflare/config", "@cloudflare/workers-utils", ]; diff --git a/packages/autoconfig/src/context.ts b/packages/autoconfig/src/context.ts index 8c70cd987ed..e7301a8a731 100644 --- a/packages/autoconfig/src/context.ts +++ b/packages/autoconfig/src/context.ts @@ -15,6 +15,8 @@ export interface AutoConfigLogger { error(...args: unknown[]): void; } +export type AutoConfigTarget = "cf" | "wrangler"; + /** * Dialog interface for interactive prompts. * Callers provide their own implementation (e.g., using `prompts`, `inquirer`, or a custom UI). diff --git a/packages/autoconfig/src/details/framework-detection.ts b/packages/autoconfig/src/details/framework-detection.ts index 9835f23a9c9..2e3ba94af4e 100644 --- a/packages/autoconfig/src/details/framework-detection.ts +++ b/packages/autoconfig/src/details/framework-detection.ts @@ -32,8 +32,8 @@ import type { Settings } from "@netlify/build-info"; * @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. + * - `detectedFramework`: The matched framework, its build and development + * commands, and its 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). @@ -220,6 +220,7 @@ type DetectedFramework = { name: string; id: string; }; + devCommand?: string | undefined; buildCommand?: string | undefined; dist?: string; }; diff --git a/packages/autoconfig/src/details/index.ts b/packages/autoconfig/src/details/index.ts index 3ba589d68ad..6c255653397 100644 --- a/packages/autoconfig/src/details/index.ts +++ b/packages/autoconfig/src/details/index.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import { statSync } from "node:fs"; +import { existsSync, 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"; @@ -17,7 +17,7 @@ import { staticFramework, } from "../frameworks/all-frameworks"; import { detectFramework } from "./framework-detection"; -import type { AutoConfigContext } from "../context"; +import type { AutoConfigContext, AutoConfigTarget } from "../context"; import type { AutoConfigDetails, AutoConfigDetailsForNonConfiguredProject, @@ -25,6 +25,8 @@ import type { import type { PackageManager } from "@cloudflare/workers-utils"; import type { Config, PackageJSON } from "@cloudflare/workers-utils"; +const CLOUDFLARE_CONFIG_FILE = "cloudflare.config.ts"; + /** * Asserts that the current project being targeted for autoconfig is not already configured. * @@ -70,18 +72,12 @@ async function findAssetsDir(from: string): Promise { 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. + * output directory, worker name, commands, and whether the project is already configured. + * By default, the project is considered configured when `cloudflare.config.ts` + * exists. Passing `target: "wrangler"` opts into Wrangler configuration + * detection instead. * * @param options - Detection options including project path, wrangler config, and context. * @returns The detected project details. @@ -89,12 +85,15 @@ type DetectedFramework = { export async function getDetailsForAutoConfig({ projectPath = process.cwd(), wranglerConfig, + target = "cf", context, }: { /** The path to the project, defaults to cwd. */ projectPath?: string; /** The parsed wrangler configuration for the project (if any). */ wranglerConfig?: Config; + /** The configuration target, defaults to cf. */ + target?: AutoConfigTarget; /** The autoconfig context providing logger, dialogs, and other dependencies. */ context: AutoConfigContext; }): Promise { @@ -103,6 +102,19 @@ export async function getDetailsForAutoConfig({ logger.debug(`Running autoconfig detection in ${projectPath}...`); if ( + target === "cf" && + existsSync(resolve(projectPath, CLOUDFLARE_CONFIG_FILE)) + ) { + return { + configured: true, + projectPath, + workerName: getWorkerName(undefined, projectPath), + packageManager: NpmPackageManager, + }; + } + + if ( + target === "wrangler" && // 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 @@ -134,7 +146,7 @@ export async function getDetailsForAutoConfig({ logger.debug("No package.json found when running autoconfig"); } - const configured = framework.isConfigured(projectPath) ?? false; + const configured = framework.isConfigured(projectPath, { target }); const outputDir = detectedFramework?.dist ?? (await findAssetsDir(projectPath)); @@ -144,14 +156,11 @@ export async function getDetailsForAutoConfig({ framework, packageJson, packageManager, - ...(detectedFramework - ? { - buildCommand: getProjectBuildCommand( - detectedFramework, - packageManager - ), - } - : {}), + devCommand: getProjectCommand(detectedFramework.devCommand, packageManager), + buildCommand: getProjectCommand( + detectedFramework.buildCommand, + packageManager + ), workerName: getWorkerName(packageJson?.name, projectPath), }; @@ -185,34 +194,33 @@ export async function getDetailsForAutoConfig({ } /** - * 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. + * Converts a detected project command into one that can be run in the terminal + * (such as `npm run build` or `npx astro dev`). If no command is detected + * `undefined` is returned instead. * - * @param detectedFramework The detected framework (or settings) for the project + * @param command The detected project command * @param packageManager The package manager to use for command prefixes - * @returns A runnable command for the build process if detected, undefined otherwise + * @returns A runnable project command if detected, undefined otherwise */ -function getProjectBuildCommand( - detectedFramework: DetectedFramework, +function getProjectCommand( + command: string | undefined, packageManager: PackageManager ): string | undefined { - if (!detectedFramework.buildCommand) { + if (!command) { 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; + if (command.startsWith(packageManagerCommandPrefix)) { + // The command already includes a package-manager prefix. + return command; } } - // The command is something like `astro build` so we need to prefix it with `npx` and equivalents - return `${npx} ${detectedFramework.buildCommand}`; + // Framework executables such as `astro build` need the appropriate package-manager prefix. + return `${npx} ${command}`; } /** diff --git a/packages/autoconfig/src/frameworks/analog.ts b/packages/autoconfig/src/frameworks/analog.ts index c7932bd35c2..ef55cd72b2b 100644 --- a/packages/autoconfig/src/frameworks/analog.ts +++ b/packages/autoconfig/src/frameworks/analog.ts @@ -24,13 +24,14 @@ export class Analog extends Framework { } return { - wranglerConfig: { - main: "./dist/analog/server/index.mjs", - assets: { - binding: "ASSETS", - directory: "./dist/analog/public", + buildTool: "wrangler", + workerConfig: { + entrypoint: "./dist/analog/server/index.mjs", + env: { + ASSETS: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: "./dist/analog/public" }, }; } } diff --git a/packages/autoconfig/src/frameworks/angular.ts b/packages/autoconfig/src/frameworks/angular.ts index fdebfb1a01a..82082672013 100644 --- a/packages/autoconfig/src/frameworks/angular.ts +++ b/packages/autoconfig/src/frameworks/angular.ts @@ -34,23 +34,22 @@ export class Angular extends Framework { await installAdditionalDependencies(packageManager, isWorkspaceRoot); } return { - wranglerConfig: { - main: "./dist/server/server.mjs", - assets: { - binding: "ASSETS", - directory: `${outputDir}browser`, + buildTool: "wrangler", + workerConfig: { + entrypoint: "./dist/server/server.mjs", + env: { + ASSETS: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: `${outputDir}browser` }, }; } else { this.configurationDescription = "Configuring Angular SPA project (assets only)"; return { - wranglerConfig: { - assets: { - directory: outputDir, - }, - }, + buildTool: "wrangler", + workerConfig: {}, + buildConfig: { assetsDirectory: outputDir }, }; } } diff --git a/packages/autoconfig/src/frameworks/astro.ts b/packages/autoconfig/src/frameworks/astro.ts index 0c04ce98e98..9a705574b3d 100644 --- a/packages/autoconfig/src/frameworks/astro.ts +++ b/packages/autoconfig/src/frameworks/astro.ts @@ -66,20 +66,21 @@ export class Astro extends Framework { 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, + buildTool: "wrangler", + workerConfig: { + entrypoint: `${outputDir}/_worker.js/index.js`, + compatibilityFlags: ["global_fetch_strictly_public"], + env: { + ASSETS: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: outputDir }, }; } // From version 6 Astro doesn't need a wrangler config file but generates a redirected config on build return { - wranglerConfig: null, + workerConfig: null, }; } diff --git a/packages/autoconfig/src/frameworks/framework-class.ts b/packages/autoconfig/src/frameworks/framework-class.ts index 93f51529e88..4069e075e08 100644 --- a/packages/autoconfig/src/frameworks/framework-class.ts +++ b/packages/autoconfig/src/frameworks/framework-class.ts @@ -3,9 +3,9 @@ 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 { AutoConfigContext, AutoConfigTarget } from "../context"; +import type { WorkerConfigInput } from "@cloudflare/config"; import type { PackageManager } from "@cloudflare/workers-utils"; -import type { RawConfig } from "@cloudflare/workers-utils"; export abstract class Framework { readonly id: FrameworkInfo["id"]; @@ -25,7 +25,14 @@ export abstract class Framework { this.name = frameworkInfo.name; } - isConfigured(_projectPath: string): boolean { + isConfigured( + _projectPath: string, + { + target: _target = "cf", + }: { + target?: AutoConfigTarget; + } = {} + ): boolean { return false; } @@ -90,6 +97,7 @@ export abstract class Framework { } export type ConfigurationOptions = { + target: AutoConfigTarget; outputDir: string; projectPath: string; workerName: string; @@ -100,14 +108,22 @@ export type ConfigurationOptions = { }; export type PackageJsonScriptsOverrides = { - preview?: string; // default is `npm run build && wrangler dev` - deploy?: string; // default is `npm run build && wrangler deploy` + preview?: string; // legacy Wrangler preview command, default is `npm run build && wrangler dev` + deploy?: string; // legacy Wrangler deploy command, default is `npm run build && wrangler deploy` typegen?: string; // default is `wrangler types` }; +export type BuildConfig = { + assetsDirectory?: string; +}; + 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; + /** The tool that cf should delegate build and development commands to. */ + buildTool?: "vite" | "wrangler"; + /** Worker configuration generated by the framework. `null` if an external tool generates it. */ + workerConfig: Partial | null; + /** Build configuration that complements the Worker configuration. */ + buildConfig?: BuildConfig; // 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) diff --git a/packages/autoconfig/src/frameworks/next.ts b/packages/autoconfig/src/frameworks/next.ts index 70af2173cb1..fb7a8a10d4e 100644 --- a/packages/autoconfig/src/frameworks/next.ts +++ b/packages/autoconfig/src/frameworks/next.ts @@ -32,7 +32,7 @@ export class NextJs extends Framework { return { // `@opennextjs/cloudflare migrate` creates the wrangler config file - wranglerConfig: {}, + workerConfig: {}, packageJsonScriptsOverrides: { preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview", deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy", diff --git a/packages/autoconfig/src/frameworks/no-op.ts b/packages/autoconfig/src/frameworks/no-op.ts index 4746451b3c6..25155a9c8dd 100644 --- a/packages/autoconfig/src/frameworks/no-op.ts +++ b/packages/autoconfig/src/frameworks/no-op.ts @@ -4,7 +4,7 @@ import type { ConfigurationResults } from "./framework-class"; export class NoOpFramework extends Framework { async configure(): Promise { return { - wranglerConfig: {}, + workerConfig: {}, }; } } diff --git a/packages/autoconfig/src/frameworks/nuxt.ts b/packages/autoconfig/src/frameworks/nuxt.ts index aa536529ad4..3d9d63b765e 100644 --- a/packages/autoconfig/src/frameworks/nuxt.ts +++ b/packages/autoconfig/src/frameworks/nuxt.ts @@ -75,16 +75,17 @@ export class Nuxt extends Framework { } return { - wranglerConfig: { - main: "./.output/server/index.mjs", - assets: { - binding: "ASSETS", - directory: "./.output/public/", + buildTool: "wrangler", + workerConfig: { + entrypoint: "./.output/server/index.mjs", + env: { + ASSETS: { type: "assets" }, }, observability: { enabled: true, }, }, + buildConfig: { assetsDirectory: "./.output/public/" }, }; } diff --git a/packages/autoconfig/src/frameworks/qwik.ts b/packages/autoconfig/src/frameworks/qwik.ts index bcc07802cae..88d0dd90de6 100644 --- a/packages/autoconfig/src/frameworks/qwik.ts +++ b/packages/autoconfig/src/frameworks/qwik.ts @@ -40,14 +40,15 @@ export class Qwik extends Framework { addBindingsProxy(projectPath); } return { - wranglerConfig: { - main: "./dist/_worker.js", - compatibility_flags: ["global_fetch_strictly_public"], - assets: { - binding: "ASSET", - directory: "./dist", + buildTool: "wrangler", + workerConfig: { + entrypoint: "./dist/_worker.js", + compatibilityFlags: ["global_fetch_strictly_public"], + env: { + ASSET: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: "./dist" }, packageJsonScriptsOverrides: { preview: `${packageManager.type} run build && wrangler dev`, deploy: `${packageManager.type} run build && wrangler deploy`, diff --git a/packages/autoconfig/src/frameworks/react-router.ts b/packages/autoconfig/src/frameworks/react-router.ts index a4cddf5059b..eae05a72eb8 100644 --- a/packages/autoconfig/src/frameworks/react-router.ts +++ b/packages/autoconfig/src/frameworks/react-router.ts @@ -514,8 +514,9 @@ export class ReactRouter extends Framework { } return { - wranglerConfig: { - main: "./workers/app.ts", + buildTool: "vite", + workerConfig: { + entrypoint: "./workers/app.ts", }, }; } diff --git a/packages/autoconfig/src/frameworks/solid-start.ts b/packages/autoconfig/src/frameworks/solid-start.ts index 064106b7e32..679c9f1ed04 100644 --- a/packages/autoconfig/src/frameworks/solid-start.ts +++ b/packages/autoconfig/src/frameworks/solid-start.ts @@ -30,13 +30,14 @@ export class SolidStart extends Framework { } return { - wranglerConfig: { - main: "./.output/server/index.mjs", - assets: { - binding: "ASSETS", - directory: "./.output/public", + buildTool: "wrangler", + workerConfig: { + entrypoint: "./.output/server/index.mjs", + env: { + ASSETS: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: "./.output/public" }, }; } } diff --git a/packages/autoconfig/src/frameworks/static.ts b/packages/autoconfig/src/frameworks/static.ts index 0d1d7963797..2a106f960bd 100644 --- a/packages/autoconfig/src/frameworks/static.ts +++ b/packages/autoconfig/src/frameworks/static.ts @@ -7,11 +7,9 @@ import type { export class Static extends Framework { configure({ outputDir }: ConfigurationOptions): ConfigurationResults { return { - wranglerConfig: { - assets: { - directory: outputDir, - }, - }, + buildTool: "wrangler", + workerConfig: {}, + buildConfig: { assetsDirectory: outputDir }, }; } } diff --git a/packages/autoconfig/src/frameworks/sveltekit.ts b/packages/autoconfig/src/frameworks/sveltekit.ts index d37eeda483a..636434c9862 100644 --- a/packages/autoconfig/src/frameworks/sveltekit.ts +++ b/packages/autoconfig/src/frameworks/sveltekit.ts @@ -44,13 +44,14 @@ export class SvelteKit extends Framework { }); } return { - wranglerConfig: { - main: ".svelte-kit/cloudflare/_worker.js", - assets: { - binding: "ASSETS", - directory: ".svelte-kit/cloudflare", + buildTool: "wrangler", + workerConfig: { + entrypoint: ".svelte-kit/cloudflare/_worker.js", + env: { + ASSETS: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: ".svelte-kit/cloudflare" }, }; } diff --git a/packages/autoconfig/src/frameworks/tanstack.ts b/packages/autoconfig/src/frameworks/tanstack.ts index 5b8f2baa43d..f783c2f40df 100644 --- a/packages/autoconfig/src/frameworks/tanstack.ts +++ b/packages/autoconfig/src/frameworks/tanstack.ts @@ -24,8 +24,9 @@ export class TanstackStart extends Framework { } return { - wranglerConfig: { - main: "@tanstack/react-start/server-entry", + buildTool: "vite", + workerConfig: { + entrypoint: "@tanstack/react-start/server-entry", }, }; } diff --git a/packages/autoconfig/src/frameworks/vike.ts b/packages/autoconfig/src/frameworks/vike.ts index eccb0bcaeaf..8845011cbb3 100644 --- a/packages/autoconfig/src/frameworks/vike.ts +++ b/packages/autoconfig/src/frameworks/vike.ts @@ -60,8 +60,9 @@ export class Vike extends Framework { } return { - wranglerConfig: { - main: "virtual:photon:cloudflare:server-entry", + buildTool: "vite", + workerConfig: { + entrypoint: "virtual:photon:cloudflare:server-entry", }, packageJsonScriptsOverrides: { preview: "vike build && vike preview", diff --git a/packages/autoconfig/src/frameworks/vite.ts b/packages/autoconfig/src/frameworks/vite.ts index a8d7a834ff8..f427c7f9be2 100644 --- a/packages/autoconfig/src/frameworks/vite.ts +++ b/packages/autoconfig/src/frameworks/vite.ts @@ -8,13 +8,20 @@ import { transformViteConfig, } from "./utils/vite-config"; import { installCloudflareVitePlugin } from "./utils/vite-plugin"; +import type { AutoConfigTarget } from "../context"; import type { ConfigurationOptions, ConfigurationResults, } from "./framework-class"; export class Vite extends Framework { - isConfigured(projectPath: string): boolean { + isConfigured( + projectPath: string, + { target = "cf" }: { target?: AutoConfigTarget } = {} + ): boolean { + if (target !== "wrangler") { + return false; + } if (!hasViteConfig(projectPath)) { return false; } @@ -42,9 +49,10 @@ export class Vite extends Framework { } return { - wranglerConfig: { + buildTool: "vite", + workerConfig: { assets: { - not_found_handling: "single-page-application", + notFoundHandling: "single-page-application", }, }, }; diff --git a/packages/autoconfig/src/frameworks/waku.ts b/packages/autoconfig/src/frameworks/waku.ts index 08b5a241db4..9325265ddec 100644 --- a/packages/autoconfig/src/frameworks/waku.ts +++ b/packages/autoconfig/src/frameworks/waku.ts @@ -45,14 +45,17 @@ export class Waku extends Framework { } return { - wranglerConfig: { - main: "./src/waku.server", + buildTool: "wrangler", + workerConfig: { + entrypoint: "./src/waku.server", assets: { - binding: "ASSETS", - directory: "./dist/public", - html_handling: "drop-trailing-slash", + htmlHandling: "drop-trailing-slash", + }, + env: { + ASSETS: { type: "assets" }, }, }, + buildConfig: { assetsDirectory: "./dist/public" }, }; } } diff --git a/packages/autoconfig/src/index.ts b/packages/autoconfig/src/index.ts index a2f9df17f52..d925f29d321 100644 --- a/packages/autoconfig/src/index.ts +++ b/packages/autoconfig/src/index.ts @@ -1,5 +1,6 @@ export type { AutoConfigContext, + AutoConfigTarget, AutoConfigLogger, AutoConfigDialogs, } from "./context"; @@ -9,6 +10,7 @@ export { runAutoConfig, buildOperationsSummary } from "./run"; export { Framework } from "./frameworks/framework-class"; export type { + BuildConfig, ConfigurationOptions, ConfigurationResults, PackageJsonScriptsOverrides, diff --git a/packages/autoconfig/src/run.ts b/packages/autoconfig/src/run.ts index 803bb70e716..31ab8328d0d 100644 --- a/packages/autoconfig/src/run.ts +++ b/packages/autoconfig/src/run.ts @@ -6,7 +6,11 @@ import { maybeAppendWranglerToGitIgnoreLikeFile, maybeAppendWranglerToGitIgnore, } from "@cloudflare/cli-shared-helpers/gitignore"; -import { installWrangler } from "@cloudflare/cli-shared-helpers/packages"; +import { + installPackages, + installWrangler, +} from "@cloudflare/cli-shared-helpers/packages"; +import { convertToWranglerConfig, InputWorkerSchema } from "@cloudflare/config"; import { DEFAULT_COMPAT_DATE, FatalError, @@ -26,18 +30,23 @@ import { import { getFrameworkPackageInfo } from "./frameworks/all-frameworks"; import { Static } from "./frameworks/static"; import { usesTypescript } from "./uses-typescript"; -import type { AutoConfigContext } from "./context"; +import type { AutoConfigContext, AutoConfigTarget } from "./context"; +import type { + BuildConfig, + ConfigurationResults, +} from "./frameworks/framework-class"; import type { AutoConfigDetails, AutoConfigDetailsForNonConfiguredProject, AutoConfigOptions, AutoConfigSummary, } from "./types"; +import type { WorkerConfigInput } from "@cloudflare/config"; 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, + * validates the framework version, runs framework configuration, writes configuration, * updates package.json scripts, and optionally runs the build command. * * @param autoConfigDetails - The detected project details from `getDetailsForAutoConfig()`. @@ -49,13 +58,14 @@ export async function runAutoConfig( autoConfigOptions: AutoConfigOptions ): Promise { const { context } = autoConfigOptions; + const target = autoConfigOptions.target ?? "cf"; 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; + const enableTargetCliInstallation = + autoConfigOptions.enableTargetCliInstallation ?? true; assertNonConfigured(autoConfigDetails); @@ -95,17 +105,15 @@ export async function runAutoConfig( const compatibilityDate = DEFAULT_COMPAT_DATE; - const wranglerConfig: RawConfig = { - $schema: "node_modules/wrangler/config-schema.json", + const defaultWorkerConfig: WorkerConfigInput = { name: autoConfigDetails.workerName, - compatibility_date: compatibilityDate, + compatibilityDate, observability: { enabled: true, }, - } satisfies RawConfig; + }; const { packageManager } = autoConfigDetails; - const isWorkspaceRoot = autoConfigDetails.isWorkspaceRoot ?? false; const frameworkPackageInfo = getFrameworkPackageInfo( @@ -121,6 +129,7 @@ export async function runAutoConfig( const dryRunConfigurationResults = await autoConfigDetails.framework.configure({ + target, outputDir: autoConfigDetails.outputDir, projectPath: autoConfigDetails.projectPath, workerName: autoConfigDetails.workerName, @@ -129,28 +138,41 @@ export async function runAutoConfig( packageManager, context, }); + const dryRunWorkerConfig = mergeWorkerConfig( + defaultWorkerConfig, + dryRunConfigurationResults.workerConfig + ); + if ( + target === "cf" && + dryRunConfigurationResults.buildTool === "wrangler" && + dryRunConfigurationResults.buildConfig && + existsSync(resolve(autoConfigDetails.projectPath, "wrangler.config.ts")) + ) { + throw new FatalError( + "Cannot generate wrangler.config.ts because the file already exists. Remove or rename the existing file, then run autoconfig again.", + { telemetryMessage: "autoconfig wrangler config conflict" } + ); + } const { npx } = packageManager; const autoConfigSummary = await buildOperationsSummary( { ...autoConfigDetails, outputDir: autoConfigDetails.outputDir }, - dryRunConfigurationResults.wranglerConfig === null - ? null - : ensureNodejsCompatIsEnabled({ - ...wranglerConfig, - ...dryRunConfigurationResults.wranglerConfig, - }), + dryRunWorkerConfig, + dryRunConfigurationResults, { build: dryRunConfigurationResults.buildCommandOverride ?? autoConfigDetails.buildCommand, deploy: dryRunConfigurationResults.deployCommandOverride ?? - `${npx} wrangler deploy`, + `${npx} ${target} deploy`, version: - dryRunConfigurationResults?.versionCommandOverride ?? - `${npx} wrangler versions upload`, + dryRunConfigurationResults.versionCommandOverride ?? + `${npx} ${target} versions upload`, }, + enableTargetCliInstallation, + target, context, dryRunConfigurationResults.packageJsonScriptsOverrides ); @@ -179,11 +201,27 @@ export async function runAutoConfig( `Running autoconfig with:\n${JSON.stringify(autoConfigDetails, null, 2)}...` ); - if (autoConfigSummary.wranglerInstall && enableWranglerInstallation) { + if (autoConfigDetails.packageJson && enableTargetCliInstallation) { + if (target === "cf") { + await installPackages(packageManager.type, ["cf@latest"], { + dev: true, + isWorkspaceRoot, + }); + } else { + await installWrangler(packageManager.type, isWorkspaceRoot); + } + } + + if ( + autoConfigDetails.packageJson && + target === "cf" && + dryRunConfigurationResults.buildTool === "wrangler" + ) { await installWrangler(packageManager.type, isWorkspaceRoot); } const configurationResults = await autoConfigDetails.framework.configure({ + target, outputDir: autoConfigDetails.outputDir, projectPath: autoConfigDetails.projectPath, workerName: autoConfigDetails.workerName, @@ -192,6 +230,10 @@ export async function runAutoConfig( packageManager, context, }); + const workerConfig = mergeWorkerConfig( + defaultWorkerConfig, + configurationResults.workerConfig + ); if (autoConfigDetails.packageJson) { const packageJsonPath = resolve( @@ -218,13 +260,31 @@ export async function runAutoConfig( ); } - if (configurationResults.wranglerConfig !== null) { - // `saveWranglerJsonc()` reconciles the Node.js compatibility flags itself, - // once it has merged this with any config already on disk. - await saveWranglerJsonc(autoConfigDetails.projectPath, { - ...wranglerConfig, - ...configurationResults.wranglerConfig, - }); + if (target === "wrangler") { + const wranglerConfig = getWranglerConfig( + workerConfig, + configurationResults + ); + if (wranglerConfig !== null) { + // `saveWranglerJsonc()` reconciles the Node.js compatibility flags itself, + // once it has merged this with any config already on disk. + await saveWranglerJsonc(autoConfigDetails.projectPath, wranglerConfig); + } + } + + if (target === "cf") { + if (workerConfig !== null) { + await saveCloudflareConfig(autoConfigDetails.projectPath, workerConfig); + } + if ( + configurationResults.buildTool === "wrangler" && + configurationResults.buildConfig + ) { + await saveWranglerConfigTs( + autoConfigDetails.projectPath, + configurationResults.buildConfig + ); + } } maybeAppendWranglerToGitIgnore(autoConfigDetails.projectPath); @@ -250,6 +310,58 @@ export async function runAutoConfig( return autoConfigSummary; } +/** + * Applies the common Worker defaults to framework-provided configuration. + * + * @param defaultWorkerConfig The common Worker configuration defaults + * @param workerConfig The framework-provided Worker configuration + * @returns The merged Worker configuration, or `null` when an external tool owns it + */ +function mergeWorkerConfig( + defaultWorkerConfig: WorkerConfigInput, + workerConfig: Partial | null +): WorkerConfigInput | null { + return workerConfig === null + ? null + : { ...defaultWorkerConfig, ...workerConfig }; +} + +/** + * Converts framework configuration results into the legacy Wrangler format. + * + * @param workerConfig The resolved Worker configuration + * @param configurationResults The framework configuration results + * @returns The Wrangler configuration, or `null` when an external tool owns it + */ +function getWranglerConfig( + workerConfig: WorkerConfigInput | null, + configurationResults: ConfigurationResults +): RawConfig | null { + if (workerConfig === null) { + return null; + } + + const parsedWorkerConfig = InputWorkerSchema.parse({ + type: "worker", + ...workerConfig, + }); + const convertedWranglerConfig = convertToWranglerConfig(parsedWorkerConfig); + const wranglerConfig = ensureNodejsCompatIsEnabled({ + $schema: "node_modules/wrangler/config-schema.json", + ...convertedWranglerConfig, + ...(configurationResults.buildConfig?.assetsDirectory + ? { + assets: { + ...convertedWranglerConfig.assets, + directory: configurationResults.buildConfig.assetsDirectory, + }, + } + : {}), + }); + + return wranglerConfig; +} + /** * Given a wrangler config object this function makes sure that Node.js compatibility is enabled. * @@ -317,14 +429,79 @@ export async function saveWranglerJsonc( ); } +/** + * Writes the Worker configuration used by `cf`. + * + * @param projectPath The project directory + * @param workerConfig The Worker configuration to write + */ +async function saveCloudflareConfig( + projectPath: string, + workerConfig: WorkerConfigInput +): Promise { + await writeFile( + resolve(projectPath, "cloudflare.config.ts"), + renderCloudflareConfig(workerConfig) + ); +} + +/** + * Writes the Wrangler build-tool configuration used by `cf`. + * + * @param projectPath The project directory + * @param buildConfig The build-tool configuration to write + */ +async function saveWranglerConfigTs( + projectPath: string, + buildConfig: BuildConfig +): Promise { + await writeFile( + resolve(projectPath, "wrangler.config.ts"), + renderWranglerConfigTs(buildConfig) + ); +} + +/** + * Renders a Worker configuration as a `cloudflare.config.ts` module. + * + * @param workerConfig The Worker configuration to render + * @returns The configuration module source + */ +function renderCloudflareConfig(workerConfig: WorkerConfigInput): string { + return `import { defineWorker } from "cf/config";\n\nexport default defineWorker(${JSON.stringify(workerConfig, null, 2)});\n`; +} + +/** + * Renders build-tool configuration as a `wrangler.config.ts` module. + * + * @param buildConfig The build-tool configuration to render + * @returns The configuration module source + */ +function renderWranglerConfigTs(buildConfig: BuildConfig): string { + return `import { defineWranglerConfig } from "wrangler/experimental-config";\n\nexport default defineWranglerConfig(${JSON.stringify(buildConfig, null, 2)});\n`; +} + +/** + * Indents rendered file content for display in the operation summary. + * + * @param content The file content to indent + * @returns The indented content + */ +function indentFileContent(content: string): string { + return " " + content.trimEnd().replace(/\n/g, "\n "); +} + /** * Builds a summary of all operations that autoconfig will (or did) perform, - * including package installation, package.json script updates, wrangler config + * including package installation, package.json script updates, configuration * 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 workerConfig - The resolved Worker configuration. + * @param configurationResults - The framework configuration results. * @param projectCommands - The build, deploy, and version commands for the project. + * @param enableTargetCliInstallation - Whether to install the selected target CLI package. + * @param target - The configuration target. * @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. @@ -333,26 +510,33 @@ export async function buildOperationsSummary( autoConfigDetails: AutoConfigDetailsForNonConfiguredProject & { outputDir: NonNullable; }, - wranglerConfigToWrite: RawConfig | null, + workerConfig: WorkerConfigInput | null, + configurationResults: ConfigurationResults, projectCommands: { build?: string; deploy: string; version?: string; }, + enableTargetCliInstallation: boolean, + target: AutoConfigTarget, context: AutoConfigContext, packageJsonScriptsOverrides?: PackageJsonScriptsOverrides ): Promise { const { logger } = context; logger.log(""); + const wranglerConfig = + target === "wrangler" + ? getWranglerConfig(workerConfig, configurationResults) + : null; const summary: AutoConfigSummary = { - wranglerInstall: false, scripts: {}, - ...(wranglerConfigToWrite !== null - ? { - wranglerConfig: wranglerConfigToWrite, - } - : {}), + ...(target === "wrangler" + ? { wranglerConfig: wranglerConfig ?? undefined } + : { + workerConfig: workerConfig ?? undefined, + buildConfig: configurationResults.buildConfig, + }), outputDir: autoConfigDetails.outputDir, frameworkId: autoConfigDetails.framework.id, buildCommand: projectCommands.build, @@ -360,30 +544,49 @@ export async function buildOperationsSummary( versionCommand: projectCommands.version, }; + const packagesToInstall = new Set(); if (autoConfigDetails.packageJson) { - // If there is a package.json file we will want to install wrangler - summary.wranglerInstall = true; + if (enableTargetCliInstallation) { + packagesToInstall.add(target); + } + if (configurationResults.buildTool === "vite") { + packagesToInstall.add("@cloudflare/vite-plugin"); + } else if ( + target === "cf" && + configurationResults.buildTool === "wrangler" + ) { + packagesToInstall.add("wrangler"); + } + } + if (packagesToInstall.size > 0) { logger.log("📦 Install packages:"); - logger.log(` - wrangler (devDependency)`); + for (const packageName of packagesToInstall) { + logger.log(` - ${packageName} (devDependency)`); + } logger.log(""); + } + if (autoConfigDetails.packageJson) { + const scriptOverrides = + target === "wrangler" ? packageJsonScriptsOverrides : undefined; + const buildCommandPrefix = autoConfigDetails.buildCommand + ? `${autoConfigDetails.buildCommand} && ` + : ""; summary.scripts = { deploy: - packageJsonScriptsOverrides?.deploy ?? - (autoConfigDetails.buildCommand - ? `${autoConfigDetails.buildCommand} && wrangler deploy` - : `wrangler deploy`), + scriptOverrides?.deploy ?? + `${buildCommandPrefix}${target} deploy${ + target === "cf" && autoConfigDetails.buildCommand ? " --no-build" : "" + }`, preview: - packageJsonScriptsOverrides?.preview ?? - (autoConfigDetails.buildCommand - ? `${autoConfigDetails.buildCommand} && wrangler dev` - : `wrangler dev`), + scriptOverrides?.preview ?? + `${target === "wrangler" ? buildCommandPrefix : ""}${target} dev`, }; const containsServerSideCode = // If there is an entrypoint then we know that there is server side code - !!wranglerConfigToWrite?.main; + !!workerConfig?.entrypoint; if ( // If there is no server side code, then there is no need to add the cf-typegen script @@ -392,7 +595,7 @@ export async function buildOperationsSummary( !("cf-typegen" in (autoConfigDetails.packageJson.scripts ?? {})) ) { summary.scripts["cf-typegen"] = - packageJsonScriptsOverrides?.typegen ?? "wrangler types"; + scriptOverrides?.typegen ?? `${target} types`; } logger.log("📝 Update package.json scripts:"); @@ -402,7 +605,27 @@ export async function buildOperationsSummary( logger.log(""); } - if (wranglerConfigToWrite) { + if (target === "cf" && workerConfig) { + logger.log("📄 Create cloudflare.config.ts:"); + logger.log(indentFileContent(renderCloudflareConfig(workerConfig))); + logger.log(""); + } + + if ( + target === "cf" && + configurationResults.buildTool === "wrangler" && + configurationResults.buildConfig + ) { + logger.log("📄 Create wrangler.config.ts:"); + logger.log( + indentFileContent( + renderWranglerConfigTs(configurationResults.buildConfig) + ) + ); + logger.log(""); + } + + if (target === "wrangler" && wranglerConfig) { const wranglerConfigPath = resolve( autoConfigDetails.projectPath, "wrangler.jsonc" @@ -412,8 +635,7 @@ export async function buildOperationsSummary( configExists ? "📄 Update wrangler.jsonc:" : "📄 Create wrangler.jsonc:" ); logger.log( - " " + - JSON.stringify(wranglerConfigToWrite, null, 2).replace(/\n/g, "\n ") + " " + JSON.stringify(wranglerConfig, null, 2).replace(/\n/g, "\n ") ); logger.log(""); } @@ -421,7 +643,9 @@ export async function buildOperationsSummary( if ( autoConfigDetails.framework && !(autoConfigDetails.framework instanceof Static) && - !autoConfigDetails.framework.isConfigured(autoConfigDetails.projectPath) + !autoConfigDetails.framework.isConfigured(autoConfigDetails.projectPath, { + target, + }) ) { summary.frameworkConfiguration = autoConfigDetails.framework.configurationDescription ?? diff --git a/packages/autoconfig/src/types.ts b/packages/autoconfig/src/types.ts index 3cd97d09e3d..686cf4c10e6 100644 --- a/packages/autoconfig/src/types.ts +++ b/packages/autoconfig/src/types.ts @@ -1,5 +1,7 @@ -import type { AutoConfigContext } from "./context"; +import type { AutoConfigContext, AutoConfigTarget } from "./context"; import type { Framework } from "./frameworks/framework-class"; +import type { BuildConfig } from "./frameworks/framework-class"; +import type { WorkerConfigInput } from "@cloudflare/config"; import type { PackageManager } from "@cloudflare/workers-utils"; import type { PackageJSON, RawConfig } from "@cloudflare/workers-utils"; @@ -17,6 +19,8 @@ type AutoConfigDetailsBase = { 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 dev command used to run the project locally (if any) */ + devCommand?: string; /** 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) */ @@ -45,6 +49,8 @@ export type AutoConfigDetails = | AutoConfigDetailsForNonConfiguredProject; export type AutoConfigOptions = { + /** The configuration target, defaults to cf. */ + target?: AutoConfigTarget; /** The autoconfig context providing logger, dialogs, and other dependencies. */ context: AutoConfigContext; /** Whether to run autoconfig without actually applying any filesystem modification (default: false) */ @@ -62,14 +68,15 @@ export type AutoConfigOptions = { */ skipConfirmations?: boolean; /** - * Whether to install Wrangler during autoconfig + * Whether to install the selected target CLI during autoconfig. */ - enableWranglerInstallation?: boolean; + enableTargetCliInstallation?: boolean; }; export type AutoConfigSummary = { scripts: Record; - wranglerInstall: boolean; + workerConfig?: WorkerConfigInput; + buildConfig?: BuildConfig; wranglerConfig?: RawConfig; frameworkConfiguration?: string; outputDir: string; diff --git a/packages/autoconfig/tests/details/display-auto-config-details.test.ts b/packages/autoconfig/tests/details/display-auto-config-details.test.ts index 14dbff19a74..25183b31ce3 100644 --- a/packages/autoconfig/tests/details/display-auto-config-details.test.ts +++ b/packages/autoconfig/tests/details/display-auto-config-details.test.ts @@ -50,7 +50,7 @@ describe("autoconfig details - displayAutoConfigDetails()", () => { isConfigured: () => false, configure: () => ({ - wranglerConfig: {}, + workerConfig: {}, }) satisfies ReturnType, } as unknown as Framework, buildCommand: "astro build", 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 index 9d1190f6276..4dbeae3acdc 100644 --- a/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts +++ b/packages/autoconfig/tests/details/get-details-for-auto-config.test.ts @@ -20,17 +20,41 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { vi.unstubAllGlobals(); }); - it("should set configured: true if a configPath exists", async ({ + it("should set configured: true if a configPath exists during legacy Wrangler detection", async ({ expect, }) => { await expect( details.getDetailsForAutoConfig({ wranglerConfig: { configPath: "/tmp" } as Config, + target: "wrangler", context, }) ).resolves.toMatchObject({ configured: true }); }); + it("should migrate an existing Wrangler project by default", async ({ + expect, + }) => { + await writeFile("index.html", "

Hello World

"); + + await expect( + details.getDetailsForAutoConfig({ + wranglerConfig: { configPath: "/tmp" } as Config, + context, + }) + ).resolves.toMatchObject({ configured: false }); + }); + + it("should default to cloudflare.config.ts configuration detection", async ({ + expect, + }) => { + await writeFile("cloudflare.config.ts", "export default {};"); + + await expect( + details.getDetailsForAutoConfig({ 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)( @@ -59,6 +83,7 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { details.getDetailsForAutoConfig({ context }) ).resolves.toMatchObject({ buildCommand: pm === "pnpm" ? "pnpm astro build" : "npx astro build", + devCommand: pm === "pnpm" ? "pnpm astro dev" : "npx astro dev", configured: false, outputDir: "dist", packageJson: { diff --git a/packages/autoconfig/tests/frameworks/angular.test.ts b/packages/autoconfig/tests/frameworks/angular.test.ts index b5aa1cfda38..db1b275e18e 100644 --- a/packages/autoconfig/tests/frameworks/angular.test.ts +++ b/packages/autoconfig/tests/frameworks/angular.test.ts @@ -10,6 +10,7 @@ import type { AutoConfigFrameworkPackageInfo } from "../../src/frameworks"; const context = createMockContext(); const BASE_OPTIONS = { + target: "cf" as const, projectPath: process.cwd(), workerName: "my-angular-app", outputDir: "dist/my-angular-app/", @@ -72,16 +73,13 @@ describe("Angular framework configure()", () => { await mkdir(resolve("src"), { recursive: true }); }); - it("returns assets-only wranglerConfig", async ({ expect }) => { + it("returns assets-only configuration", 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"); + expect(result.workerConfig).toEqual({}); + expect(result.buildConfig?.assetsDirectory).toBe("dist/my-angular-app/"); + expect(result.workerConfig).not.toHaveProperty("entrypoint"); }); it("sets configurationDescription for SPA", async ({ expect }) => { @@ -134,9 +132,8 @@ describe("Angular framework configure()", () => { dryRun: true, }); - expect(result.wranglerConfig).toEqual({ - assets: { directory: "dist/my-angular-app/" }, - }); + expect(result.workerConfig).toEqual({}); + expect(result.buildConfig?.assetsDirectory).toBe("dist/my-angular-app/"); expect(installSpy).not.toHaveBeenCalled(); }); }); @@ -169,16 +166,15 @@ describe("Angular framework configure()", () => { await mkdir(resolve("src"), { recursive: true }); }); - it("returns assets-only wranglerConfig when ssr is false", async ({ + it("returns assets-only configuration 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"); + expect(result.workerConfig).toEqual({}); + expect(result.buildConfig?.assetsDirectory).toBe("dist/my-angular-app/"); + expect(result.workerConfig).not.toHaveProperty("entrypoint"); }); it("sets SPA configurationDescription when ssr is false", async ({ @@ -221,7 +217,7 @@ describe("Angular framework configure()", () => { await mkdir(resolve("src"), { recursive: true }); }); - it("returns SSR wranglerConfig without crashing", async ({ expect }) => { + it("returns SSR configuration without crashing", async ({ expect }) => { await mockAngularCoreVersion("21.0.0"); const framework = new Angular({ id: "angular", name: "Angular" }); framework.validateFrameworkVersion( @@ -231,13 +227,15 @@ describe("Angular framework configure()", () => { ); 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", + expect(result.workerConfig).toEqual({ + entrypoint: "./dist/server/server.mjs", + env: { + ASSETS: { type: "assets" }, }, }); + expect(result.buildConfig?.assetsDirectory).toBe( + "dist/my-angular-app/browser" + ); }); it("sets experimentalPlatform in angular.json when ssr was true (Angular <22)", async ({ @@ -316,7 +314,7 @@ describe("Angular framework configure()", () => { await mkdir(resolve("src"), { recursive: true }); }); - it("returns SSR wranglerConfig with main and assets", async ({ + it("returns SSR configuration with an entrypoint and assets", async ({ expect, }) => { await mockAngularCoreVersion("21.0.0"); @@ -328,13 +326,15 @@ describe("Angular framework configure()", () => { ); 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", + expect(result.workerConfig).toEqual({ + entrypoint: "./dist/server/server.mjs", + env: { + ASSETS: { type: "assets" }, }, }); + expect(result.buildConfig?.assetsDirectory).toBe( + "dist/my-angular-app/browser" + ); }); it("sets SSR configurationDescription", async ({ expect }) => { @@ -436,7 +436,7 @@ describe("Angular framework configure()", () => { }); // Config is still returned - expect(result.wranglerConfig).toHaveProperty("main"); + expect(result.workerConfig).toHaveProperty("entrypoint"); // But side effects are skipped expect(existsSync(resolve("src/server.ts"))).toBe(false); expect(installSpy).not.toHaveBeenCalled(); diff --git a/packages/autoconfig/tests/frameworks/react-router.test.ts b/packages/autoconfig/tests/frameworks/react-router.test.ts index 64717277b94..7f96ab764fb 100644 --- a/packages/autoconfig/tests/frameworks/react-router.test.ts +++ b/packages/autoconfig/tests/frameworks/react-router.test.ts @@ -33,6 +33,7 @@ const context = createMockContext(); function getBaseOptions() { return { + target: "cf" as const, projectPath: process.cwd(), outputDir: "build/", workerName: "my-react-router-app", @@ -422,8 +423,8 @@ describe("React Router framework configure()", () => { dryRun: true, }); - expect(result.wranglerConfig).toEqual({ - main: "./workers/app.ts", + expect(result.workerConfig).toEqual({ + entrypoint: "./workers/app.ts", }); expect(existsSync(resolve("workers/app.ts"))).toBe(false); expect(existsSync(resolve("app/entry.server.tsx"))).toBe(false); @@ -444,8 +445,8 @@ describe("React Router framework configure()", () => { const framework = createFramework("7.16.0"); const result = await framework.configure(getBaseOptions()); - expect(result.wranglerConfig).toEqual({ - main: "./workers/app.ts", + expect(result.workerConfig).toEqual({ + entrypoint: "./workers/app.ts", }); }); }); diff --git a/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts index 440aec9d23f..1bc851e533f 100644 --- a/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts +++ b/packages/autoconfig/tests/frameworks/validate-framework-version.test.ts @@ -15,7 +15,7 @@ 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 }; + return { workerConfig: null }; } } diff --git a/packages/autoconfig/tests/frameworks/vike.test.ts b/packages/autoconfig/tests/frameworks/vike.test.ts index 2cdaf3f62ce..18f1c25c203 100644 --- a/packages/autoconfig/tests/frameworks/vike.test.ts +++ b/packages/autoconfig/tests/frameworks/vike.test.ts @@ -19,6 +19,7 @@ const context = createMockContext(); function getBaseOptions() { return { + target: "cf" as const, projectPath: process.cwd(), workerName: "my-vike-app", outputDir: "", @@ -285,12 +286,12 @@ export default { ); }); - it("returns correct wranglerConfig", async ({ expect }) => { + it("returns the Worker config", 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", + expect(result.workerConfig).toEqual({ + entrypoint: "virtual:photon:cloudflare:server-entry", }); }); diff --git a/packages/autoconfig/tests/frameworks/vite.test.ts b/packages/autoconfig/tests/frameworks/vite.test.ts index 68757ca5ca2..3b074f6ecf3 100644 --- a/packages/autoconfig/tests/frameworks/vite.test.ts +++ b/packages/autoconfig/tests/frameworks/vite.test.ts @@ -10,6 +10,7 @@ import { createMockContext } from "../helpers/mock-context"; const context = createMockContext(); const BASE_OPTIONS = { + target: "cf" as const, projectPath: ".", workerName: "my-vite-app", outputDir: "dist", @@ -31,6 +32,21 @@ describe("Vite framework", () => { const framework = new Vite({ id: "vite", name: "Vite" }); expect(framework.isConfigured(".")).toBe(false); }); + + it("only treats the Cloudflare plugin as configured in legacy Wrangler mode", async ({ + expect, + }) => { + await writeFile( + "vite.config.ts", + `import { cloudflare } from "@cloudflare/vite-plugin"; + import { defineConfig } from "vite"; + export default defineConfig({ plugins: [cloudflare()] });` + ); + const framework = new Vite({ id: "vite", name: "Vite" }); + + expect(framework.isConfigured(".")).toBe(false); + expect(framework.isConfigured(".", { target: "wrangler" })).toBe(true); + }); }); describe("configure()", () => { @@ -47,11 +63,12 @@ describe("Vite framework", () => { ); expect(content).toContain("plugins: [cloudflare()]"); - expect(result.wranglerConfig).toEqual({ + expect(result.workerConfig).toEqual({ assets: { - not_found_handling: "single-page-application", + notFoundHandling: "single-page-application", }, }); + expect(result.buildTool).toBe("vite"); }); it("uses .ts extension when the project has a tsconfig.json", async ({ @@ -101,9 +118,9 @@ export default defineConfig({ expect(existsSync("vite.config.ts")).toBe(false); expect(existsSync("vite.config.js")).toBe(false); - expect(result.wranglerConfig).toEqual({ + expect(result.workerConfig).toEqual({ assets: { - not_found_handling: "single-page-application", + notFoundHandling: "single-page-application", }, }); }); diff --git a/packages/autoconfig/tests/run-summary.test.ts b/packages/autoconfig/tests/run-summary.test.ts index 8df4ca3b2e9..d6008f87e47 100644 --- a/packages/autoconfig/tests/run-summary.test.ts +++ b/packages/autoconfig/tests/run-summary.test.ts @@ -6,12 +6,11 @@ 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"; +import type { WorkerConfigInput } from "@cloudflare/config"; -const testRawConfig: RawConfig = { - $schema: "node_modules/wrangler/config-schema.json", +const testWorkerConfig: WorkerConfigInput = { name: "worker-name", - compatibility_date: "2025-01-01", + compatibilityDate: "2026-08-04", observability: { enabled: true, }, @@ -34,12 +33,15 @@ describe("autoconfig run - buildOperationsSummary()", () => { framework: new Static({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - testRawConfig, + testWorkerConfig, + { workerConfig: testWorkerConfig }, { build: "npm run build", deploy: "npx wrangler deploy", version: "npx wrangler versions upload", }, + false, + "wrangler", context ); @@ -49,7 +51,7 @@ describe("autoconfig run - buildOperationsSummary()", () => { { "$schema": "node_modules/wrangler/config-schema.json", "name": "worker-name", - "compatibility_date": "2025-01-01", + "compatibility_date": "2026-08-04", "observability": { "enabled": true } @@ -67,13 +69,12 @@ describe("autoconfig run - buildOperationsSummary()", () => { "versionCommand": "npx wrangler versions upload", "wranglerConfig": { "$schema": "node_modules/wrangler/config-schema.json", - "compatibility_date": "2025-01-01", + "compatibility_date": "2026-08-04", "name": "worker-name", "observability": { "enabled": true, }, }, - "wranglerInstall": false, } `); }); @@ -94,12 +95,15 @@ describe("autoconfig run - buildOperationsSummary()", () => { framework: new Static({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - testRawConfig, + testWorkerConfig, + { workerConfig: testWorkerConfig }, { build: "npm run build", deploy: "npx wrangler deploy", version: "npx wrangler versions upload", }, + true, + "wrangler", context ); @@ -123,13 +127,12 @@ describe("autoconfig run - buildOperationsSummary()", () => { "versionCommand": "npx wrangler versions upload", "wranglerConfig": { "$schema": "node_modules/wrangler/config-schema.json", - "compatibility_date": "2025-01-01", + "compatibility_date": "2026-08-04", "name": "worker-name", "observability": { "enabled": true, }, }, - "wranglerInstall": true, } `); }); @@ -152,12 +155,15 @@ describe("autoconfig run - buildOperationsSummary()", () => { framework: new Static({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - testRawConfig, + testWorkerConfig, + { workerConfig: testWorkerConfig }, { build: "npm run build", deploy: "npx wrangler deploy", version: "npx wrangler versions upload", }, + true, + "wrangler", context ); @@ -181,17 +187,69 @@ describe("autoconfig run - buildOperationsSummary()", () => { "versionCommand": "npx wrangler versions upload", "wranglerConfig": { "$schema": "node_modules/wrangler/config-schema.json", - "compatibility_date": "2025-01-01", + "compatibility_date": "2026-08-04", "name": "worker-name", "observability": { "enabled": true, }, }, - "wranglerInstall": true, } `); }); + test("uses cf scripts and previews package installation and generated config files", async ({ + expect, + }) => { + const buildConfig = { assetsDirectory: "dist" }; + const summary = await buildOperationsSummary( + { + workerName: "worker-name", + projectPath: "", + packageJson: { name: "my-project" }, + configured: false, + outputDir: "dist", + buildCommand: "npm run build", + framework: new Static({ id: "static", name: "Static" }), + packageManager: NpmPackageManager, + }, + testWorkerConfig, + { + buildTool: "wrangler", + workerConfig: testWorkerConfig, + buildConfig, + }, + { + build: "npm run build", + deploy: "cf deploy", + }, + true, + "cf", + context, + { + deploy: "wrangler deploy", + preview: "wrangler dev", + } + ); + + expect(summary.scripts).toEqual({ + deploy: "npm run build && cf deploy --no-build", + preview: "cf dev", + }); + expect(std.out).toContain( + dedent` + 📦 Install packages: + - cf (devDependency) + - wrangler (devDependency) + ` + ); + expect(std.out).toContain(' import { defineWorker } from "cf/config";'); + expect(std.out).toContain(" export default defineWorker({"); + expect(std.out).toContain( + ' import { defineWranglerConfig } from "wrangler/experimental-config";' + ); + expect(std.out).toContain(" export default defineWranglerConfig({"); + }); + test("shows that when needed a framework specific configuration will be run", async ({ expect, }) => { @@ -204,11 +262,14 @@ describe("autoconfig run - buildOperationsSummary()", () => { outputDir: "dist", packageManager: NpmPackageManager, }, - testRawConfig, + testWorkerConfig, + { workerConfig: testWorkerConfig }, { build: "npm run build", deploy: "npx wrangler deploy", }, + false, + "wrangler", context ); @@ -235,11 +296,14 @@ describe("autoconfig run - buildOperationsSummary()", () => { outputDir: "public", packageManager: NpmPackageManager, }, - testRawConfig, + testWorkerConfig, + { workerConfig: testWorkerConfig }, { build: "npm run build", deploy: "npx wrangler deploy", }, + false, + "wrangler", context ); diff --git a/packages/autoconfig/tests/run.test.ts b/packages/autoconfig/tests/run.test.ts new file mode 100644 index 00000000000..df1a3b74166 --- /dev/null +++ b/packages/autoconfig/tests/run.test.ts @@ -0,0 +1,244 @@ +import { existsSync, readFileSync } from "node:fs"; +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { + mockConsoleMethods, + runInTempDir, + seed, +} from "@cloudflare/workers-utils/test-helpers"; +import { describe, it, vi } from "vitest"; +import { Framework } from "../src/frameworks/framework-class"; +import { Static } from "../src/frameworks/static"; +import { runAutoConfig } from "../src/run"; +import { createMockContext } from "./helpers/mock-context"; +import type { ConfigurationResults } from "../src/frameworks/framework-class"; + +class ExternalWorkerConfigFramework extends Framework { + configure(): ConfigurationResults { + return { + buildTool: "wrangler", + workerConfig: null, + buildConfig: { assetsDirectory: "dist" }, + }; + } +} + +class ViteBuildToolFramework extends Framework { + configure(): ConfigurationResults { + return { + buildTool: "vite", + workerConfig: { entrypoint: "src/index.ts" }, + }; + } +} + +describe("runAutoConfig()", () => { + runInTempDir(); + mockConsoleMethods(); + + it("creates new configuration and cf scripts by default", async ({ + expect, + }) => { + const installWrangler = vi + .spyOn(cliPackages, "installWrangler") + .mockResolvedValue(); + const packageJson = { + name: "my-static-app", + scripts: { build: "generate && vite build" }, + }; + await seed({ + "package.json": JSON.stringify(packageJson), + "public/index.html": "

Hello World

", + }); + + const summary = await runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-static-app", + framework: new Static({ id: "static", name: "Static" }), + buildCommand: "npm run build", + outputDir: "public", + packageJson, + packageManager: NpmPackageManager, + }, + { + context: createMockContext(), + skipConfirmations: true, + runBuild: false, + enableTargetCliInstallation: false, + } + ); + + expect(summary.workerConfig).toMatchObject({ + name: "my-static-app", + observability: { enabled: true }, + }); + expect(summary.buildConfig).toEqual({ assetsDirectory: "public" }); + expect(summary.deployCommand).toBe("npx cf deploy"); + expect(summary.versionCommand).toBe("npx cf versions upload"); + expect(readFileSync("cloudflare.config.ts", "utf8")).toContain( + 'import { defineWorker } from "cf/config";\n\nexport default defineWorker({\n "name": "my-static-app"' + ); + expect(readFileSync("wrangler.config.ts", "utf8")).toContain( + 'import { defineWranglerConfig } from "wrangler/experimental-config";\n\nexport default defineWranglerConfig({\n "assetsDirectory": "public"' + ); + expect(existsSync("wrangler.jsonc")).toBe(false); + expect(installWrangler).toHaveBeenCalledWith("npm", false); + expect(JSON.parse(readFileSync("package.json", "utf8"))).toMatchObject({ + scripts: { + build: "generate && vite build", + deploy: "npm run build && cf deploy --no-build", + preview: "cf dev", + }, + }); + }); + + it("installs cf and Wrangler for cf projects", async ({ expect }) => { + const installPackages = vi + .spyOn(cliPackages, "installPackages") + .mockResolvedValue(); + const installWrangler = vi + .spyOn(cliPackages, "installWrangler") + .mockResolvedValue(); + const packageJson = { name: "my-static-app" }; + await seed({ + "package.json": JSON.stringify(packageJson), + "public/index.html": "

Hello World

", + }); + + await runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-static-app", + framework: new Static({ id: "static", name: "Static" }), + outputDir: "public", + packageJson, + packageManager: NpmPackageManager, + }, + { + context: createMockContext(), + skipConfirmations: true, + runBuild: false, + } + ); + + expect(installPackages).toHaveBeenCalledWith("npm", ["cf@latest"], { + dev: true, + isWorkspaceRoot: false, + }); + expect(installWrangler).toHaveBeenCalledWith("npm", false); + }); + + it("does not install Wrangler when cf delegates to Vite", async ({ + expect, + }) => { + const installPackages = vi + .spyOn(cliPackages, "installPackages") + .mockResolvedValue(); + const installWrangler = vi + .spyOn(cliPackages, "installWrangler") + .mockResolvedValue(); + const packageJson = { name: "my-vite-app" }; + await seed({ + "package.json": JSON.stringify(packageJson), + "dist/index.html": "

Hello World

", + "tsconfig.json": "{}", + }); + + await runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "my-vite-app", + framework: new ViteBuildToolFramework({ + id: "static", + name: "Static", + }), + outputDir: "dist", + packageJson, + packageManager: NpmPackageManager, + }, + { + context: createMockContext(), + skipConfirmations: true, + runBuild: false, + } + ); + + expect(installPackages).toHaveBeenCalledWith("npm", ["cf@latest"], { + dev: true, + isWorkspaceRoot: false, + }); + expect(installWrangler).not.toHaveBeenCalled(); + expect(JSON.parse(readFileSync("package.json", "utf8"))).toMatchObject({ + scripts: { + "cf-typegen": "cf types", + }, + }); + }); + + it("writes build configuration when an external tool owns the Worker configuration", async ({ + expect, + }) => { + await runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "external-config-app", + framework: new ExternalWorkerConfigFramework({ + id: "static", + name: "Static", + }), + outputDir: "dist", + packageManager: NpmPackageManager, + }, + { + context: createMockContext(), + skipConfirmations: true, + runBuild: false, + } + ); + + expect(existsSync("cloudflare.config.ts")).toBe(false); + expect(readFileSync("wrangler.config.ts", "utf8")).toContain( + '"assetsDirectory": "dist"' + ); + }); + + it("fails rather than overwriting existing Wrangler build configuration", async ({ + expect, + }) => { + const existingBuildConfig = "export default { minify: true };\n"; + await seed({ + "wrangler.config.ts": existingBuildConfig, + }); + + await expect( + runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "external-config-app", + framework: new ExternalWorkerConfigFramework({ + id: "static", + name: "Static", + }), + outputDir: "dist", + packageManager: NpmPackageManager, + }, + { + context: createMockContext(), + skipConfirmations: true, + runBuild: false, + } + ) + ).rejects.toThrow( + "Cannot generate wrangler.config.ts because the file already exists" + ); + expect(readFileSync("wrangler.config.ts", "utf8")).toBe( + existingBuildConfig + ); + }); +}); diff --git a/packages/wrangler/src/__tests__/autoconfig/index.test.ts b/packages/wrangler/src/__tests__/autoconfig/index.test.ts index 36fe3753053..774fcb3ad9d 100644 --- a/packages/wrangler/src/__tests__/autoconfig/index.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/index.test.ts @@ -47,7 +47,6 @@ const mockDetails = { /** Minimal mock satisfying {@link AutoConfigSummary}. */ const mockSummary = { scripts: {}, - wranglerInstall: false, outputDir: "dist", } as unknown as AutoConfigSummary; @@ -84,6 +83,7 @@ describe("autoconfig wrappers", () => { expect(getDetailsForAutoConfig).toHaveBeenCalledOnce(); expect(getDetailsForAutoConfig).toHaveBeenCalledWith({ wranglerConfig: mockConfig, + target: "wrangler", context: mockContext, }); expect(result).toBe(mockDetails); @@ -196,7 +196,10 @@ describe("autoconfig wrappers", () => { const result = await runAutoConfigLogic(mockDetails, options); expect(runAutoConfig).toHaveBeenCalledOnce(); - expect(runAutoConfig).toHaveBeenCalledWith(mockDetails, options); + expect(runAutoConfig).toHaveBeenCalledWith(mockDetails, { + ...options, + target: "wrangler", + }); expect(result).toBe(mockSummary); }); diff --git a/packages/wrangler/src/__tests__/autoconfig/run.test.ts b/packages/wrangler/src/__tests__/autoconfig/run.test.ts index 1644533e1ae..87dd6a52f03 100644 --- a/packages/wrangler/src/__tests__/autoconfig/run.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/run.test.ts @@ -44,9 +44,8 @@ import type { MockInstance } from "vitest"; class MockStaticFramework extends Framework { configure({ outputDir }: ConfigurationOptions): ConfigurationResults { return { - wranglerConfig: { - assets: { directory: outputDir }, - }, + workerConfig: {}, + buildConfig: { assetsDirectory: outputDir }, }; } } @@ -58,7 +57,7 @@ class MockStaticFramework extends Framework { */ class MockAstroFramework extends Framework { async configure(): Promise { - return { wranglerConfig: { assets: { directory: "dist" } } }; + return { workerConfig: {}, buildConfig: { assetsDirectory: "dist" } }; } } @@ -214,7 +213,7 @@ describe("autoconfig (deploy)", () => { framework: { id: "cloudflare-pages", name: "Cloudflare Pages", - configure: async () => ({ wranglerConfig: {} }), + configure: async () => ({ workerConfig: {} }), isConfigured: () => false, } as unknown as Framework, outputDir: "public", @@ -270,9 +269,8 @@ describe("autoconfig (deploy)", () => { const configureSpy = vi.fn( async ({ outputDir }) => ({ - wranglerConfig: { - assets: { directory: outputDir }, - }, + workerConfig: {}, + buildConfig: { assetsDirectory: outputDir }, }) satisfies ReturnType ); await autoconfig.runAutoConfig( @@ -298,7 +296,11 @@ describe("autoconfig (deploy)", () => { }, packageManager: NpmPackageManager, }, - { context, enableWranglerInstallation: false } + { + target: "wrangler", + context, + enableTargetCliInstallation: false, + } ); expect(std.out.replaceAll(DEFAULT_COMPAT_DATE, "")) @@ -311,9 +313,6 @@ describe("autoconfig (deploy)", () => { - Output Directory: dist - 📦 Install packages: - - wrangler (devDependency) - 📝 Update package.json scripts: - "deploy": "echo 'built' > build.txt && wrangler deploy" - "preview": "echo 'built' > build.txt && wrangler dev" @@ -368,9 +367,7 @@ describe("autoconfig (deploy)", () => { " `); - // Wrangler installation was disabled (enableWranglerInstallation: false) to avoid - // running the real installer in tests. The "📦 Install packages:" output in the - // snapshot above confirms the intent is recorded in the autoconfig summary. + // CLI installation was disabled to avoid running the real installer in tests. expect(installSpy).not.toHaveBeenCalled(); // The framework's configuration command should have been run @@ -407,7 +404,7 @@ describe("autoconfig (deploy)", () => { framework: new MockStaticFramework({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); expect(readFileSync(".gitignore")).toMatchInlineSnapshot(` @@ -444,7 +441,7 @@ describe("autoconfig (deploy)", () => { framework: new MockStaticFramework({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); // When gitignore pre-existed with trailing newline, one empty line is added as separator @@ -493,7 +490,7 @@ describe("autoconfig (deploy)", () => { outputDir: "dist", packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); expect(std.out.replaceAll(DEFAULT_COMPAT_DATE, "")) @@ -568,7 +565,7 @@ describe("autoconfig (deploy)", () => { framework: new MockStaticFramework({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); expect(readFileSync(".assetsignore")).toMatchInlineSnapshot(` @@ -604,7 +601,7 @@ describe("autoconfig (deploy)", () => { framework: new MockStaticFramework({ id: "static", name: "Static" }), packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); expect(readFileSync(".assetsignore")).toMatchInlineSnapshot(` @@ -641,7 +638,7 @@ describe("autoconfig (deploy)", () => { outputDir: "", packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[AssertionError: The Output Directory is unexpectedly missing]` @@ -664,14 +661,14 @@ describe("autoconfig (deploy)", () => { framework: { id: "cloudflare-pages", name: "Cloudflare Pages", - configure: async () => ({ wranglerConfig: {} }), + configure: async () => ({ workerConfig: {} }), isConfigured: () => false, } as unknown as Framework, workerName: "my-worker", outputDir: "dist", packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: The target project seems to be using Cloudflare Pages. Automatically migrating from a Pages project to Workers is not yet supported.]` @@ -694,14 +691,14 @@ describe("autoconfig (deploy)", () => { framework: { id: "hono", name: "Hono", - configure: async () => ({ wranglerConfig: {} }), + configure: async () => ({ workerConfig: {} }), isConfigured: () => false, } as unknown as Framework, workerName: "my-worker", outputDir: "dist", packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: The detected framework ("Hono") cannot be automatically configured.]` @@ -738,16 +735,14 @@ describe("autoconfig (deploy)", () => { id: "static", name: "Static", configure: async () => ({ - wranglerConfig: { - // No compatibility_flags specified - assets: { directory: "dist" }, - }, + workerConfig: {}, + buildConfig: { assetsDirectory: "dist" }, }), isConfigured: () => false, } as unknown as Framework, packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); @@ -777,16 +772,16 @@ describe("autoconfig (deploy)", () => { id: "static", name: "Static", configure: async () => ({ - wranglerConfig: { - compatibility_flags: ["global_fetch_strictly_public"], - assets: { directory: "dist" }, + workerConfig: { + compatibilityFlags: ["global_fetch_strictly_public"], }, + buildConfig: { assetsDirectory: "dist" }, }), isConfigured: () => false, } as unknown as Framework, packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); @@ -820,16 +815,16 @@ describe("autoconfig (deploy)", () => { id: "static", name: "Static", configure: async () => ({ - wranglerConfig: { - compatibility_flags: ["nodejs_compat"], - assets: { directory: "dist" }, + workerConfig: { + compatibilityFlags: ["nodejs_compat"], }, + buildConfig: { assetsDirectory: "dist" }, }), isConfigured: () => false, } as unknown as Framework, packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); @@ -858,16 +853,16 @@ describe("autoconfig (deploy)", () => { id: "static", name: "Nodejs Als Framework", configure: async () => ({ - wranglerConfig: { - compatibility_flags: ["nodejs_als", "some_other_flag"], - assets: { directory: "dist" }, + workerConfig: { + compatibilityFlags: ["nodejs_als", "some_other_flag"], }, + buildConfig: { assetsDirectory: "dist" }, }), isConfigured: () => false, } as unknown as Framework, packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); const wranglerConfig = JSON.parse(readFileSync("wrangler.jsonc")); @@ -900,7 +895,10 @@ describe("autoconfig (deploy)", () => { }); vi.spyOn(framework, "configure").mockImplementation(async () => { callOrder.push("configure"); - return { wranglerConfig: { assets: { directory: "dist" } } }; + return { + workerConfig: {}, + buildConfig: { assetsDirectory: "dist" }, + }; }); await autoconfig.runAutoConfig( @@ -912,7 +910,7 @@ describe("autoconfig (deploy)", () => { framework, packageManager: NpmPackageManager, }, - { context } + { target: "wrangler", context } ); // configure is called twice: once as a dry-run (to build the summary) and diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index 75c5a6c9326..a6bc9f6fbe5 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -622,7 +622,7 @@ describe("deploy", () => { framework: { id: "cloudflare-pages", name: "Cloudflare Pages", - configure: async () => ({ wranglerConfig: {} }), + configure: async () => ({ workerConfig: {} }), isConfigured: () => false, } as unknown as Framework, outputDir: "public", @@ -662,7 +662,7 @@ describe("deploy", () => { framework: { id: "cloudflare-pages", name: "Cloudflare Pages", - configure: async () => ({ wranglerConfig: {} }), + configure: async () => ({ workerConfig: {} }), isConfigured: () => false, } as unknown as Framework, outputDir: "public", @@ -1940,7 +1940,6 @@ describe("deploy", () => { scripts: { build: "npm run build-my-static-site", }, - wranglerInstall: true, wranglerConfig, outputDir: "public", }; @@ -1976,7 +1975,6 @@ describe("deploy", () => { "compatibility_date": "2025-12-02", "name": "my-site", }, - "wranglerInstall": true, } `); }); diff --git a/packages/wrangler/src/__tests__/setup.test.ts b/packages/wrangler/src/__tests__/setup.test.ts index 3e9d94b699e..c90df866d14 100644 --- a/packages/wrangler/src/__tests__/setup.test.ts +++ b/packages/wrangler/src/__tests__/setup.test.ts @@ -166,7 +166,7 @@ describe("wrangler setup", () => { assert(autoconfigOutputEntry); if (autoconfigOutputEntry.summary?.wranglerConfig) { - // Let's normalize the wrangler config values that are + // Let's normalize the Wrangler config values that are // randomly generated or change over time autoconfigOutputEntry.summary.wranglerConfig.name = "test-name"; autoconfigOutputEntry.summary.wranglerConfig.compatibility_date = @@ -194,7 +194,6 @@ describe("wrangler setup", () => { "enabled": true, }, }, - "wranglerInstall": true, } `); }); diff --git a/packages/wrangler/src/autoconfig/index.ts b/packages/wrangler/src/autoconfig/index.ts index 71696f46574..e6d9a74c830 100644 --- a/packages/wrangler/src/autoconfig/index.ts +++ b/packages/wrangler/src/autoconfig/index.ts @@ -52,6 +52,7 @@ export async function runAutoConfigDetection({ try { const details = await getDetailsForAutoConfig({ wranglerConfig, + target: "wrangler", context, }); @@ -114,7 +115,10 @@ export async function runAutoConfigLogic( ); try { - const summary = await runAutoConfig(details, options); + const summary = await runAutoConfig(details, { + ...options, + target: "wrangler", + }); sendMetricsEvent( "autoconfig_configuration_completed", diff --git a/packages/wrangler/src/setup.ts b/packages/wrangler/src/setup.ts index 2446df0020c..4861d00bbaf 100644 --- a/packages/wrangler/src/setup.ts +++ b/packages/wrangler/src/setup.ts @@ -93,7 +93,7 @@ export const setupCommand = createCommand({ runBuild: args.build, skipConfirmations: args.yes, dryRun: !!args.dryRun, - enableWranglerInstallation: args.installWrangler, + enableTargetCliInstallation: args.installWrangler, }); } catch (error) { sendAutoConfigProcessEndedMetricsEvent({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05514c02029..7723213403c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1692,6 +1692,9 @@ importers: '@cloudflare/cli-shared-helpers': specifier: workspace:* version: link:../cli + '@cloudflare/config': + specifier: workspace:* + version: link:../config '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils