diff --git a/.changeset/old-boxes-find.md b/.changeset/old-boxes-find.md new file mode 100644 index 000000000000..1c216384df86 --- /dev/null +++ b/.changeset/old-boxes-find.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Wrangler will no longer try to add additional configuration to projects using `@cloudflare/vite-plugin` when deploying or running `wrangler setup` diff --git a/.changeset/petite-swans-train.md b/.changeset/petite-swans-train.md new file mode 100644 index 000000000000..3dd359de3fa9 --- /dev/null +++ b/.changeset/petite-swans-train.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +When a Vite project is detected, install `@cloudflare/vite-plugin` diff --git a/packages/create-cloudflare/e2e/helpers/framework-helpers.ts b/packages/create-cloudflare/e2e/helpers/framework-helpers.ts index fd7f05e22054..15479826d03d 100644 --- a/packages/create-cloudflare/e2e/helpers/framework-helpers.ts +++ b/packages/create-cloudflare/e2e/helpers/framework-helpers.ts @@ -355,6 +355,43 @@ export async function verifyTypes( } } +export async function verifyCloudflareVitePluginConfigured( + { verifyCloudflareVitePluginConfigured: verify }: FrameworkTestConfig, + projectPath: string, +) { + if (!verify) { + return; + } + + 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 prePackageJson = JSON.parse( + readFile(join(projectPath, "package.json")), + ) as { devDependencies: Record }; + + expect( + prePackageJson.devDependencies?.["@cloudflare/vite-plugin"], + ).not.toBeUndefined(); + + const viteConfig = readFile(viteConfigPath); + + expect(viteConfig).toContain( + 'import { cloudflare } from "@cloudflare/vite-plugin"', + ); + expect(viteConfig).toMatch(/plugins:\s*?\[.*?cloudflare.*?]/); +} + export function shouldRunTest(testConfig: FrameworkTestConfig) { return ( // Skip if the test is quarantined diff --git a/packages/create-cloudflare/e2e/helpers/run-c3.ts b/packages/create-cloudflare/e2e/helpers/run-c3.ts index 5bf6eab6f32f..0b00d4c38219 100644 --- a/packages/create-cloudflare/e2e/helpers/run-c3.ts +++ b/packages/create-cloudflare/e2e/helpers/run-c3.ts @@ -60,9 +60,13 @@ export type RunnerConfig = { }; }; /** - * Specifies whether to verify generated types for the project + * Specifies whether to verify generated types for the project. */ verifyTypes?: boolean; + /** + * Verifies whether the Cloudflare Vite plugin has been installed and configured. + */ + verifyCloudflareVitePluginConfigured?: boolean; }; export const runC3 = async ( diff --git a/packages/create-cloudflare/e2e/tests/frameworks/frameworks.test.ts b/packages/create-cloudflare/e2e/tests/frameworks/frameworks.test.ts index cfeb403258b9..37362559097e 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/frameworks.test.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/frameworks.test.ts @@ -16,6 +16,7 @@ import { runC3ForFrameworkTest, shouldRunTest, testGitCommitMessage, + verifyCloudflareVitePluginConfigured, verifyDeployment, verifyDevScript, verifyPreviewScript, @@ -150,6 +151,11 @@ describe ); await verifyTypes(testConfig, frameworkConfig, project.path); + + await verifyCloudflareVitePluginConfigured( + testConfig, + project.path, + ); } catch (e) { expect.fail( "Failed due to an exception while running C3. See logs for more details. Error: " + diff --git a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts index b5d3c5797718..71e756a05250 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts @@ -865,6 +865,7 @@ function getExperimentalFrameworkTestConfig( }, nodeCompat: false, verifyTypes: false, + verifyCloudflareVitePluginConfigured: true, }, { name: "react-router", diff --git a/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts index 58a84a998c68..8dc52c4a89e0 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts @@ -102,7 +102,7 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => { workerName: "my-astro-site", buildCommand: "astro build", framework: { - configured: false, + isConfigured: () => false, configure: () => ({ wranglerConfig: {} }), name: "astro", }, @@ -116,7 +116,7 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => { "configured": false, "framework": Object { "configure": [Function], - "configured": false, + "isConfigured": [Function], "name": "astro", }, "outputDir": "", diff --git a/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts index 641027f79e5a..f88905be2425 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/details/display-auto-config-details.test.ts @@ -37,7 +37,7 @@ describe("autoconfig details - displayAutoConfigDetails()", () => { workerName: "my-astro-app", framework: { name: "Astro", - configured: false, + isConfigured: () => false, configure: () => ({ wranglerConfig: {} }), }, buildCommand: "astro build", diff --git a/packages/wrangler/src/__tests__/autoconfig/run.test.ts b/packages/wrangler/src/__tests__/autoconfig/run.test.ts index 130c51542c69..8ccc9f5217ff 100644 --- a/packages/wrangler/src/__tests__/autoconfig/run.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/run.test.ts @@ -162,6 +162,7 @@ describe("autoconfig (deploy)", () => { framework: { name: "Fake", configure: configureSpy, + isConfigured: () => false, } as unknown as Framework, outputDir: "dist", packageJson: { diff --git a/packages/wrangler/src/autoconfig/details.ts b/packages/wrangler/src/autoconfig/details.ts index b3cff06d7291..f968a04bc826 100644 --- a/packages/wrangler/src/autoconfig/details.ts +++ b/packages/wrangler/src/autoconfig/details.ts @@ -162,7 +162,7 @@ export async function getDetailsForAutoConfig({ return { projectPath: projectPath, - configured: framework?.configured ?? false, + configured: framework?.isConfigured(projectPath) ?? false, framework, packageJson, buildCommand: detectedFramework?.buildCommand ?? packageJsonBuild, diff --git a/packages/wrangler/src/autoconfig/frameworks/get-framework.ts b/packages/wrangler/src/autoconfig/frameworks/get-framework.ts index 0fa636b3616f..f5b8232234a2 100644 --- a/packages/wrangler/src/autoconfig/frameworks/get-framework.ts +++ b/packages/wrangler/src/autoconfig/frameworks/get-framework.ts @@ -6,6 +6,7 @@ import { SolidStart } from "./solid-start"; import { Static } from "./static"; import { SvelteKit } from "./sveltekit"; import { TanstackStart } from "./tanstack"; +import { Vite } from "./vite"; import type { Framework } from "."; export function getFramework(detectedFramework?: { @@ -27,6 +28,8 @@ export function getFramework(detectedFramework?: { return new SolidStart(detectedFramework.name); case "qwik": return new Qwik(detectedFramework.name); + case "vite": + return new Vite(detectedFramework.name); default: return new Static(detectedFramework?.name); } diff --git a/packages/wrangler/src/autoconfig/frameworks/index.ts b/packages/wrangler/src/autoconfig/frameworks/index.ts index 395b9736b4c1..dad200bf8d61 100644 --- a/packages/wrangler/src/autoconfig/frameworks/index.ts +++ b/packages/wrangler/src/autoconfig/frameworks/index.ts @@ -24,8 +24,7 @@ export type ConfigurationResults = { export abstract class Framework { constructor(public name: string = "Static") {} - /** Some frameworks (i.e. Nuxt) don't need additional configuration */ - get configured() { + isConfigured(_projectPath: string): boolean { return false; } diff --git a/packages/wrangler/src/autoconfig/frameworks/react-router.ts b/packages/wrangler/src/autoconfig/frameworks/react-router.ts index a102928d0e69..2228ee7e86fc 100644 --- a/packages/wrangler/src/autoconfig/frameworks/react-router.ts +++ b/packages/wrangler/src/autoconfig/frameworks/react-router.ts @@ -8,123 +8,11 @@ import dedent from "ts-dedent"; import { logger } from "../../logger"; import { transformFile } from "../c3-vendor/codemod"; import { installPackages } from "../c3-vendor/packages"; +import { transformViteConfig } from "./utils/vite-config"; import { Framework, getInstalledPackageVersion } from "."; import type { ConfigurationOptions, ConfigurationResults } from "."; -import type { types } from "recast"; const b = recast.types.builders; -const t = recast.types.namedTypes; - -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 transformViteConfig(projectPath: 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"); - } - - 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' } })], - // }); - 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)); - - // 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.unshift( - b.callExpression(b.identifier("cloudflare"), [ - b.objectExpression([ - b.objectProperty( - b.identifier("viteEnvironment"), - b.objectExpression([ - b.objectProperty( - b.identifier("name"), - b.stringLiteral("ssr") - ), - ]) - ), - ]), - ]) - ); - } - - // Remove netlify plugins, as they conflict - // netlifyReactRouter(), netlify() - pluginsProp.value.elements = pluginsProp.value.elements.filter((el) => { - if ( - el?.type === "CallExpression" && - el.callee.type === "Identifier" && - ["netlifyReactRouter", "netlify"].includes(el.callee.name) - ) { - return false; - } - return true; - }); - return false; - }, - }); -} function transformReactRouterConfig( projectPath: string, @@ -358,7 +246,11 @@ export class ReactRouter extends Framework { ); } - transformViteConfig(projectPath); + transformViteConfig(projectPath, { + viteEnvironmentName: "ssr", + incompatibleVitePlugins: ["netlifyReactRouter"], + }); + transformReactRouterConfig(projectPath, viteEnvironmentKey); } diff --git a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts b/packages/wrangler/src/autoconfig/frameworks/tanstack.ts index 5cbd4ac2f479..aa8deac234f1 100644 --- a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts +++ b/packages/wrangler/src/autoconfig/frameworks/tanstack.ts @@ -1,127 +1,8 @@ -import assert from "node:assert"; -import { existsSync } from "node:fs"; -import path from "node:path"; import { brandColor, dim } from "@cloudflare/cli/colors"; -import * as recast from "recast"; -import { transformFile } from "../c3-vendor/codemod"; import { installPackages } from "../c3-vendor/packages"; +import { transformViteConfig } from "./utils/vite-config"; import { Framework } from "."; import type { ConfigurationOptions, ConfigurationResults } from "."; -import type { types } from "recast"; - -const b = recast.types.builders; -const t = recast.types.namedTypes; - -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 transformViteConfig(projectPath: string) { - const filePathTS = path.join(projectPath, `vite.config.ts`); - const filePathJS = path.join(projectPath, `vite.config.ts`); - - 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"); - } - - 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' } })], - // }); - 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)); - - // 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"), [ - b.objectExpression([ - b.objectProperty( - b.identifier("viteEnvironment"), - b.objectExpression([ - b.objectProperty( - b.identifier("name"), - b.stringLiteral("ssr") - ), - ]) - ), - ]), - ]) - ); - } - - // Remove nitro or netlify plugins, as they conflict - // nitro(), nitroV2Plugin(), netlify() - pluginsProp.value.elements = pluginsProp.value.elements.filter((el) => { - if ( - el?.type === "CallExpression" && - el.callee.type === "Identifier" && - ["nitro", "nitroV2Plugin", "netlify"].includes(el.callee.name) - ) { - return false; - } - return true; - }); - return false; - }, - }); -} export class TanstackStart extends Framework { async configure({ @@ -135,7 +16,7 @@ export class TanstackStart extends Framework { doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, }); - transformViteConfig(projectPath); + transformViteConfig(projectPath, { viteEnvironmentName: "ssr" }); } return { diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts b/packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts new file mode 100644 index 000000000000..a1aa1492c751 --- /dev/null +++ b/packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts @@ -0,0 +1,198 @@ +import assert from "node:assert"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import * as recast from "recast"; +import { transformFile } from "../../c3-vendor/codemod"; +import type { types } from "recast"; + +const b = recast.types.builders; +const t = recast.types.namedTypes; + +export function checkIfViteConfigUsesCloudflarePlugin( + projectPath: string +): 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 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)); + + 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; +} + +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' } })], + // }); + 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)); + + // 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/wrangler/src/autoconfig/frameworks/vite.ts b/packages/wrangler/src/autoconfig/frameworks/vite.ts new file mode 100644 index 000000000000..ba8a7da4680c --- /dev/null +++ b/packages/wrangler/src/autoconfig/frameworks/vite.ts @@ -0,0 +1,37 @@ +import { brandColor, dim } from "@cloudflare/cli/colors"; +import { installPackages } from "../c3-vendor/packages"; +import { + checkIfViteConfigUsesCloudflarePlugin, + transformViteConfig, +} from "./utils/vite-config"; +import { Framework } from "."; +import type { ConfigurationOptions, ConfigurationResults } from "."; + +export class Vite extends Framework { + isConfigured(projectPath: string): boolean { + return checkIfViteConfigUsesCloudflarePlugin(projectPath); + } + + async configure({ + dryRun, + projectPath, + }: ConfigurationOptions): Promise { + if (!dryRun) { + await installPackages(["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, + }); + + transformViteConfig(projectPath); + } + + return { + wranglerConfig: { + assets: { + not_found_handling: "single-page-application", + }, + }, + }; + } +} diff --git a/packages/wrangler/src/autoconfig/run.ts b/packages/wrangler/src/autoconfig/run.ts index 9bd91cc7582d..037d15608b6c 100644 --- a/packages/wrangler/src/autoconfig/run.ts +++ b/packages/wrangler/src/autoconfig/run.ts @@ -257,7 +257,7 @@ export async function buildOperationsSummary( if ( autoConfigDetails.framework && !(autoConfigDetails.framework instanceof Static) && - !autoConfigDetails.framework.configured + !autoConfigDetails.framework.isConfigured(autoConfigDetails.projectPath) ) { summary.frameworkConfiguration = autoConfigDetails.framework.configurationDescription ??