From 6f6cd944480d6161c582c7af68f6c4205ddeaefe Mon Sep 17 00:00:00 2001 From: andrew martinez Date: Thu, 19 Feb 2026 15:31:20 -0600 Subject: [PATCH 1/3] amartinez/CC-6859 (#12591) Co-authored-by: amartinez Co-authored-by: Nikita Sharma <54369599+nikitassharma@users.noreply.github.com> --- .changeset/soft-apes-ask.md | 5 + .../__tests__/containers/registries.test.ts | 69 ++++- .../wrangler/src/containers/registries.ts | 256 +++++++++++++----- 3 files changed, 248 insertions(+), 82 deletions(-) create mode 100644 .changeset/soft-apes-ask.md diff --git a/.changeset/soft-apes-ask.md b/.changeset/soft-apes-ask.md new file mode 100644 index 000000000000..5ed03a889b2f --- /dev/null +++ b/.changeset/soft-apes-ask.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Implemented logic within `wrangler containers registries configure` to check if a specified secret name is already in-use and offer to reuse that secret. Also added `--skip-confirmation` flag to the command to skip all interactive prompts. diff --git a/packages/wrangler/src/__tests__/containers/registries.test.ts b/packages/wrangler/src/__tests__/containers/registries.test.ts index 36df31c58e54..24ab7652983c 100644 --- a/packages/wrangler/src/__tests__/containers/registries.test.ts +++ b/packages/wrangler/src/__tests__/containers/registries.test.ts @@ -48,7 +48,7 @@ describe("containers registries configure", () => { const domain = "123456789012.dkr.ecr.us-west-2.amazonaws.com"; await expect( runWrangler( - `containers registries configure ${domain} --public-credential=test-id --disableSecretsStore` + `containers registries configure ${domain} --public-credential=test-id --disable-secrets-store` ) ).rejects.toThrowErrorMatchingInlineSnapshot( `[Error: Secrets Store can only be disabled in FedRAMP compliance regions.]` @@ -61,23 +61,23 @@ describe("containers registries configure", () => { `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-store-id=storeid` ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Secrets Store is not supported in FedRAMP compliance regions. You must set --disableSecretsStore.]` + `[Error: Secrets Store is not supported in FedRAMP compliance regions. You must set --disable-secrets-store.]` ); await expect( runWrangler( - `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-store-id=storeid --disableSecretsStore` + `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-store-id=storeid --disable-secrets-store` ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Arguments secret-store-id and disableSecretsStore are mutually exclusive]` + `[Error: Arguments secret-store-id and disable-secrets-store are mutually exclusive]` ); await expect( runWrangler( - `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-name=secret-name --disableSecretsStore` + `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-name=secret-name --disable-secrets-store` ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Arguments secret-name and disableSecretsStore are mutually exclusive]` + `[Error: Arguments secret-name and disable-secrets-store are mutually exclusive]` ); }); @@ -123,7 +123,7 @@ describe("containers registries configure", () => { }); await runWrangler( - `containers registries configure ${awsEcrDomain} --aws-access-key-id=test-access-key-id --disableSecretsStore` + `containers registries configure ${awsEcrDomain} --aws-access-key-id=test-access-key-id --disable-secrets-store` ); expect(cliStd.stdout).toMatchInlineSnapshot(` @@ -161,7 +161,7 @@ describe("containers registries configure", () => { }); await runWrangler( - `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --disableSecretsStore` + `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --disable-secrets-store` ); }); }); @@ -192,6 +192,7 @@ describe("containers registries configure", () => { modified: "2024-01-01T00:00:00Z", }, ]); + mockListSecrets(storeId, []); mockCreateSecret(storeId); mockPutRegistry({ domain: "123456789012.dkr.ecr.us-west-2.amazonaws.com", @@ -235,6 +236,7 @@ describe("containers registries configure", () => { mockListSecretStores([]); mockCreateSecretStore(newStoreId); + mockListSecrets(newStoreId, []); mockCreateSecret(newStoreId); mockPutRegistry({ domain: awsEcrDomain, @@ -275,6 +277,7 @@ describe("containers registries configure", () => { result: "AWS_Secret_Access_Key", }); + mockListSecrets(providedStoreId, []); mockCreateSecret(providedStoreId); mockPutRegistry({ domain: awsEcrDomain, @@ -307,7 +310,7 @@ describe("containers registries configure", () => { │ │ │ - │ Container-scoped secret AWS_Secret_Access_Key created in Secrets Store. + │ Container-scoped secret "AWS_Secret_Access_Key" created in Secrets Store. │ ╰ Registry configuration completed @@ -336,6 +339,7 @@ describe("containers registries configure", () => { modified: "2024-01-01T00:00:00Z", }, ]); + mockListSecrets(storeId, []); mockCreateSecret(storeId); mockPutRegistry({ domain: awsEcrDomain, @@ -353,6 +357,53 @@ describe("containers registries configure", () => { `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --secret-name=AWS_Secret_Access_Key` ); }); + + it("should reuse existing secret with --skip-confirmation", async () => { + const storeId = "test-store-id-reuse"; + const secretName = "existing_secret"; + + mockStdIn.send("test-secret-value"); + mockListSecretStores([ + { + id: storeId, + account_id: "some-account-id", + name: "Default", + created: "2024-01-01T00:00:00Z", + modified: "2024-01-01T00:00:00Z", + }, + ]); + mockListSecrets(storeId, [ + { + id: "existing-secret-id", + store_id: storeId, + name: secretName, + comment: "", + scopes: ["containers"], + created: "2024-01-01T00:00:00Z", + modified: "2024-01-01T00:00:00Z", + status: "active", + }, + ]); + mockPutRegistry({ + domain: awsEcrDomain, + is_public: false, + auth: { + public_credential: "test-access-key-id", + private_credential: { + store_id: storeId, + secret_name: secretName, + }, + }, + kind: "ECR", + }); + + await runWrangler( + `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --secret-name=${secretName} --skip-confirmation` + ); + + // Should not contain "created" message since we reused existing secret + expect(cliStd.stdout).not.toContain("created in Secrets Store"); + }); }); }); }); diff --git a/packages/wrangler/src/containers/registries.ts b/packages/wrangler/src/containers/registries.ts index e0beae0209b9..49be21d42500 100644 --- a/packages/wrangler/src/containers/registries.ts +++ b/packages/wrangler/src/containers/registries.ts @@ -44,44 +44,58 @@ import type { ImageRegistryAuth } from "@cloudflare/containers-shared/src/client import type { Config } from "@cloudflare/workers-utils"; function _registryConfigureYargs(args: CommonYargsArgv) { - return ( - args - .positional("DOMAIN", { - describe: "Domain to configure for the registry", - type: "string", - demandOption: true, - }) - .option("public-credential", { - type: "string", - description: - "The public part of the registry credentials, e.g. `AWS_ACCESS_KEY_ID` for ECR", - demandOption: true, - alias: ["aws-access-key-id"], - }) - .option("secret-store-id", { - type: "string", - description: - "The ID of the secret store to use to store the registry credentials.", - demandOption: false, - conflicts: ["disableSecretsStore"], - }) - // TODO: allow users to provide an existing secret name - // but then we can't get secrets by name, only id, so we would need to list all secrets and find the right one - .option("secret-name", { - type: "string", - description: - "The name for the secret the private registry credentials should be stored under.", - demandOption: false, - conflicts: ["disableSecretsStore"], - }) - .option("disableSecretsStore", { - type: "boolean", - description: - "Whether to disable secrets store integration. This should be set iff the compliance region is FedRAMP High.", - demandOption: false, - conflicts: ["secret-store-id", "secret-name"], - }) - ); + return args + .positional("DOMAIN", { + describe: "Domain to configure for the registry", + type: "string", + demandOption: true, + }) + .option("public-credential", { + type: "string", + description: + "The public part of the registry credentials, e.g. `AWS_ACCESS_KEY_ID` for ECR", + demandOption: true, + alias: ["aws-access-key-id"], + }) + .option("secret-store-id", { + type: "string", + description: + "The ID of the secret store to use to store the registry credentials.", + demandOption: false, + conflicts: ["disable-secrets-store"], + }) + .option("secret-name", { + type: "string", + description: + "The name for the secret the private registry credentials should be stored under.", + demandOption: false, + conflicts: ["disable-secrets-store"], + }) + .option("disable-secrets-store", { + type: "boolean", + description: + "Whether to disable secrets store integration. This should be set iff the compliance region is FedRAMP High.", + demandOption: false, + conflicts: ["secret-store-id", "secret-name"], + }) + .option("skip-confirmation", { + type: "boolean", + description: "Skip confirmation prompt", + alias: "y", + default: false, + }) + .check((yargs) => { + if ( + yargs.skipConfirmation && + !yargs.secretName && + !yargs.disableSecretsStore + ) { + throw new Error( + "--secret-name is required when using --skip-confirmation" + ); + } + return true; + }); } async function registryConfigureCommand( @@ -107,7 +121,7 @@ async function registryConfigureCommand( if (isFedRAMPHigh) { if (!configureArgs.disableSecretsStore) { throw new UserError( - "Secrets Store is not supported in FedRAMP compliance regions. You must set --disableSecretsStore." + "Secrets Store is not supported in FedRAMP compliance regions. You must set --disable-secrets-store." ); } } else { @@ -125,7 +139,9 @@ async function registryConfigureCommand( } log(`Getting ${registryType.secretType}...\n`); - const secret = await getSecret(registryType.secretType); + const privateCredential = await promptForRegistryPrivateCredential( + registryType.secretType + ); // Secret Store is not available in FedRAMP High let private_credential: ImageRegistryAuth["private_credential"]; @@ -137,12 +153,15 @@ async function registryConfigureCommand( const stores = await listStores(config, accountId); if (stores.length === 0) { const defaultStoreName = "default_secret_store"; - const yes = await confirm( - `No existing Secret Stores found. Create a Secret Store to store your registry credentials?` - ); - if (!yes) { - endSection("Cancelled."); - return; + if (!configureArgs.skipConfirmation) { + const yes = await confirm( + `No existing Secret Stores found. Create a Secret Store to store your registry credentials?` + ); + + if (!yes) { + endSection("Cancelled."); + return; + } } const res = await promiseSpinner( createStore(config, accountId, { name: defaultStoreName }) @@ -164,36 +183,22 @@ async function registryConfigureCommand( log("\n"); - while (!secretName) { - try { - const res = await prompt(`Secret name:`, { - defaultValue: `${registryType.secretType?.replaceAll(" ", "_")}`, - }); - - validateSecretName(res); - secretName = res; - } catch (e) { - log((e as Error).message); - continue; - } - } + secretName = await getOrCreateSecret({ + configureArgs: configureArgs, + config: config, + accountId: accountId, + storeId: secretStoreId, + privateCredential, + secretType: registryType.secretType, + }); - await promiseSpinner( - createSecret(config, accountId, secretStoreId, { - name: secretName, - value: secret, - scopes: ["containers"], - comment: `Created by Wrangler: credentials for image registry ${configureArgs.DOMAIN}`, - }) - ); private_credential = { store_id: secretStoreId, secret_name: secretName, }; - log(`Container-scoped secret ${secretName} created in Secrets Store.\n`); } else { // If we are not using the secret store, we will be passing in the secret directly - private_credential = secret; + private_credential = privateCredential; } try { @@ -227,7 +232,95 @@ async function registryConfigureCommand( endSection("Registry configuration completed"); } -async function getSecret(secretType?: string): Promise { +async function promptForSecretName(secretType?: string): Promise { + while (true) { + try { + const res = await prompt(`Secret name:`, { + defaultValue: `${secretType?.replaceAll(" ", "_")}`, + }); + + validateSecretName(res); + return res; + } catch (e) { + log((e as Error).message); + continue; + } + } +} + +interface GetOrCreateSecretOptions { + configureArgs: StrictYargsOptionsToInterface; + config: Config; + accountId: string; + storeId: string; + privateCredential: string; + secretType?: string; +} + +async function getOrCreateSecret( + options: GetOrCreateSecretOptions +): Promise { + let secretName = + options.configureArgs.secretName ?? + (await promptForSecretName(options.secretType)); + + while (true) { + const existingSecretId = await getSecretByName( + options.config, + options.accountId, + options.storeId, + secretName + ); + + // secret doesn't exist - make a new one + if (!existingSecretId) { + await promiseSpinner( + createSecret(options.config, options.accountId, options.storeId, { + name: secretName, + value: options.privateCredential, + scopes: ["containers"], + comment: `Created by Wrangler: credentials for image registry ${options.configureArgs.DOMAIN}`, + }) + ); + + log( + `Container-scoped secret "${secretName}" created in Secrets Store.\n` + ); + + return secretName; + } + + // secret exists + skipConfirmation - default to reusing the secret + if (options.configureArgs.skipConfirmation) { + log( + `Using existing secret "${secretName}" from secret store with id: ${options.storeId}.\n` + ); + return secretName; + } + + // secret exists but not skipping confirmation - ask user if they want to reuse the secret + startSection( + `The provided secret name "${secretName}" is already in-use within the secret store. (Store ID: ${options.storeId})` + ); + + const reuseExisting = await confirm( + `Do you want to reuse the existing secret? If not, then you'll be prompted to pick a new name.` + ); + + if (reuseExisting) { + log( + `Using existing secret "${secretName}" from secret store with id: ${options.storeId}.\n` + ); + return secretName; + } + + secretName = await promptForSecretName(options.secretType); + } +} + +async function promptForRegistryPrivateCredential( + secretType?: string +): Promise { if (isNonInteractiveOrCI()) { // Non-interactive mode: expect JSON input via stdin const stdinInput = trimTrailingWhitespace(await readFromStdin()); @@ -422,24 +515,41 @@ export const containersRegistriesConfigureCommand = createCommand({ description: "The ID of the secret store to use to store the registry credentials.", demandOption: false, - conflicts: "disableSecretsStore", + conflicts: "disable-secrets-store", }, "secret-name": { type: "string", description: "The name for the secret the private registry credentials should be stored under.", demandOption: false, - conflicts: "disableSecretsStore", + conflicts: "disable-secrets-store", }, - disableSecretsStore: { + "disable-secrets-store": { type: "boolean", description: "Whether to disable secrets store integration. This should be set iff the compliance region is FedRAMP High.", demandOption: false, conflicts: ["secret-store-id", "secret-name"], }, + "skip-confirmation": { + type: "boolean", + description: "Skip confirmation prompts", + alias: "y", + default: false, + }, }, positionalArgs: ["DOMAIN"], + validateArgs(args) { + if ( + args.skipConfirmation && + !args.secretName && + !args.disableSecretsStore + ) { + throw new UserError( + "--secret-name is required when using --skip-confirmation" + ); + } + }, async handler(args, { config }) { await fillOpenAPIConfiguration(config, containersScope); await registryConfigureCommand(args, config); From ebdbe52c2bcd1b30758b54de57a046f3ab196f04 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:01:58 +0000 Subject: [PATCH 2/3] perf: remove `find-up` (#12601) --- .changeset/purple-queens-vanish.md | 6 ++++ packages/workers-utils/package.json | 2 +- .../src/config/config-helpers.ts | 10 +++--- packages/wrangler/package.json | 2 +- .../autoconfig/frameworks/utils/packages.ts | 6 ++-- packages/wrangler/src/config-cache.ts | 6 ++-- packages/wrangler/src/pages/utils.ts | 4 +-- .../wrangler/src/type-generation/index.ts | 4 +-- .../runtime/log-runtime-types-message.ts | 14 +++----- pnpm-lock.yaml | 35 +++++-------------- tools/package.json | 2 +- tools/test/run-test-file.ts | 4 +-- 12 files changed, 38 insertions(+), 57 deletions(-) create mode 100644 .changeset/purple-queens-vanish.md diff --git a/.changeset/purple-queens-vanish.md b/.changeset/purple-queens-vanish.md new file mode 100644 index 000000000000..c4175b1d5f0b --- /dev/null +++ b/.changeset/purple-queens-vanish.md @@ -0,0 +1,6 @@ +--- +"@cloudflare/workers-utils": patch +"wrangler": patch +--- + +Switch to `empathic` for file-system upwards traversal to reduce dependency bloat. diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json index cddd8e342d07..bfd6ed7294e1 100644 --- a/packages/workers-utils/package.json +++ b/packages/workers-utils/package.json @@ -45,8 +45,8 @@ "@vitest/ui": "catalog:default", "cloudflare": "^5.2.0", "concurrently": "^8.2.2", + "empathic": "^2.0.0", "eslint": "catalog:default", - "find-up": "^6.3.0", "jsonc-parser": "catalog:default", "smol-toml": "catalog:default", "ts-dedent": "^2.2.0", diff --git a/packages/workers-utils/src/config/config-helpers.ts b/packages/workers-utils/src/config/config-helpers.ts index 51576631b23f..3bb3964092f4 100644 --- a/packages/workers-utils/src/config/config-helpers.ts +++ b/packages/workers-utils/src/config/config-helpers.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import path from "node:path"; -import { findUpSync } from "find-up"; +import * as find from "empathic/find"; import dedent from "ts-dedent"; import { PATH_TO_DEPLOY_CONFIG } from "../constants"; import { UserError } from "../errors"; @@ -61,9 +61,9 @@ export function findWranglerConfig( { useRedirectIfAvailable = false } = {} ): ConfigPaths { const userConfigPath = - findUpSync(`wrangler.json`, { cwd: referencePath }) ?? - findUpSync(`wrangler.jsonc`, { cwd: referencePath }) ?? - findUpSync(`wrangler.toml`, { cwd: referencePath }); + find.file(`wrangler.json`, { cwd: referencePath }) ?? + find.file(`wrangler.jsonc`, { cwd: referencePath }) ?? + find.file(`wrangler.toml`, { cwd: referencePath }); if (!useRedirectIfAvailable) { return { @@ -99,7 +99,7 @@ function findRedirectedWranglerConfig( deployConfigPath: string | undefined; redirected: boolean; } { - const deployConfigPath = findUpSync(PATH_TO_DEPLOY_CONFIG, { cwd }); + const deployConfigPath = find.file(PATH_TO_DEPLOY_CONFIG, { cwd }); if (deployConfigPath === undefined) { return { configPath: userConfigPath, deployConfigPath, redirected: false }; } diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 1b771ad7f55f..d19b9468b33d 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -127,10 +127,10 @@ "devtools-protocol": "^0.0.1182435", "dotenv": "^16.3.1", "dotenv-expand": "^12.0.2", + "empathic": "^2.0.0", "eslint": "catalog:default", "esprima": "4.0.1", "execa": "^6.1.0", - "find-up": "^6.3.0", "get-port": "^7.0.0", "glob-to-regexp": "^0.4.1", "https-proxy-agent": "7.0.2", diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts b/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts index 7a0390ed3ead..5e721b02cf61 100644 --- a/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts +++ b/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts @@ -1,5 +1,5 @@ import { parsePackageJSON, readFileSync } from "@cloudflare/workers-utils"; -import { findUpSync } from "find-up"; +import * as find from "empathic/find"; /** * Checks wether a package is installed in a target project or not @@ -35,9 +35,9 @@ export function getInstalledPackageVersion( if (!packagePath) { return undefined; } - const packageJsonPath = findUpSync("package.json", { + const packageJsonPath = find.file("package.json", { cwd: packagePath, - stopAt: opts.stopAtProjectPath === true ? projectPath : undefined, + last: opts.stopAtProjectPath === true ? projectPath : undefined, }); if (!packageJsonPath) { return undefined; diff --git a/packages/wrangler/src/config-cache.ts b/packages/wrangler/src/config-cache.ts index c52eef4da70d..09152afb0108 100644 --- a/packages/wrangler/src/config-cache.ts +++ b/packages/wrangler/src/config-cache.ts @@ -7,7 +7,7 @@ import { } from "node:fs"; import * as path from "node:path"; import { getWranglerCacheDirFromEnv } from "@cloudflare/workers-utils"; -import { findUpSync } from "find-up"; +import * as find from "empathic/find"; import { isNonInteractiveOrCI } from "./is-interactive"; import { logger } from "./logger"; @@ -29,9 +29,7 @@ export function getCacheFolder(): string { } // Find node_modules using existing find-up logic - const closestNodeModulesDirectory = findUpSync("node_modules", { - type: "directory", - }); + const closestNodeModulesDirectory = find.dir("node_modules"); const nodeModulesCache = closestNodeModulesDirectory ? path.join(closestNodeModulesDirectory, ".cache", "wrangler") diff --git a/packages/wrangler/src/pages/utils.ts b/packages/wrangler/src/pages/utils.ts index e452bb856416..e14afaa2cfc9 100644 --- a/packages/wrangler/src/pages/utils.ts +++ b/packages/wrangler/src/pages/utils.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { findUpSync } from "find-up"; +import * as find from "empathic/find"; import { getWranglerTmpDir } from "../paths"; import type { BundleResult } from "../deployment-bundle/bundle"; @@ -47,7 +47,7 @@ export function getPagesProjectRoot(): string { if (projectRootCache !== undefined && projectRootCacheCwd === cwd) { return projectRootCache; } - const packagePath = findUpSync("package.json"); + const packagePath = find.file("package.json"); projectRootCache = packagePath ? path.dirname(packagePath) : process.cwd(); projectRootCacheCwd = cwd; return projectRootCache; diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index a842f4f7f760..e8611c779309 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -10,7 +10,7 @@ import { UserError, } from "@cloudflare/workers-utils"; import chalk from "chalk"; -import { findUpSync } from "find-up"; +import * as find from "empathic/find"; import { getNodeCompat } from "miniflare"; import { readConfig } from "../config"; import { createCommand } from "../core/create-command"; @@ -1257,7 +1257,7 @@ function generatePerEnvTypeStrings( * @throws {UserError} If a non-Wrangler .d.ts file already exists at the given path. */ const validateTypesFile = (path: string): void => { - const wranglerOverrideDTSPath = findUpSync(path); + const wranglerOverrideDTSPath = find.file(path); if (wranglerOverrideDTSPath === undefined) { return; } diff --git a/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts b/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts index 09b143292b60..03b09cdb401e 100644 --- a/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts +++ b/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts @@ -1,7 +1,6 @@ import { existsSync } from "node:fs"; -import { join } from "node:path"; import chalk from "chalk"; -import { findUpMultipleSync } from "find-up"; +import * as find from "empathic/find"; import { logger } from "../../logger"; /** @@ -42,14 +41,9 @@ export function logRuntimeTypesMessage( ); if (!isNodeTypesInstalled && isNodeCompat) { - const nodeModules = findUpMultipleSync("node_modules", { - type: "directory", - }); - for (const folder of nodeModules) { - if (nodeModules && existsSync(join(folder, "@types/node"))) { - isNodeTypesInstalled = true; - break; - } + const nodeModules = find.dir("node_modules/@types/node"); + if (nodeModules) { + isNodeTypesInstalled = true; } } if (isNodeCompat && !isNodeTypesInstalled) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d13eb814199b..d9a4a1626a6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4032,12 +4032,12 @@ importers: concurrently: specifier: ^8.2.2 version: 8.2.2 + empathic: + specifier: ^2.0.0 + version: 2.0.0 eslint: specifier: catalog:default version: 9.39.1(jiti@2.6.1) - find-up: - specifier: ^6.3.0 - version: 6.3.0 jsonc-parser: specifier: catalog:default version: 3.2.0 @@ -4290,6 +4290,9 @@ importers: dotenv-expand: specifier: ^12.0.2 version: 12.0.2 + empathic: + specifier: ^2.0.0 + version: 2.0.0 eslint: specifier: catalog:default version: 9.39.1(jiti@2.6.1)(supports-color@9.2.2) @@ -4299,9 +4302,6 @@ importers: execa: specifier: ^6.1.0 version: 6.1.0 - find-up: - specifier: ^6.3.0 - version: 6.3.0 get-port: specifier: ^7.0.0 version: 7.0.0 @@ -4449,12 +4449,12 @@ importers: '@typescript-eslint/parser': specifier: catalog:default version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + empathic: + specifier: ^2.0.0 + version: 2.0.0 eslint: specifier: catalog:default version: 9.39.1(jiti@2.6.1) - find-up: - specifier: ^6.3.0 - version: 6.3.0 glob: specifier: ^11.0.3 version: 11.0.3 @@ -10681,10 +10681,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - find-up@6.3.0: - resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - find-up@7.0.0: resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} engines: {node: '>=18'} @@ -11812,10 +11808,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - locate-path@7.1.0: - resolution: {integrity: sha512-HNx5uOnYeK4SxEoid5qnhRfprlJeGMzFRKPLCf/15N3/B4AiofNwC/yq7VBKdVk9dx7m+PiYCJOGg55JYTAqoQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - locate-path@7.2.0: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -22111,11 +22103,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - find-up@6.3.0: - dependencies: - locate-path: 7.1.0 - path-exists: 5.0.0 - find-up@7.0.0: dependencies: locate-path: 7.2.0 @@ -23192,10 +23179,6 @@ snapshots: dependencies: p-locate: 5.0.0 - locate-path@7.1.0: - dependencies: - p-locate: 6.0.0 - locate-path@7.2.0: dependencies: p-locate: 6.0.0 diff --git a/tools/package.json b/tools/package.json index 1c87ec716eaf..319b26c5ab79 100644 --- a/tools/package.json +++ b/tools/package.json @@ -17,8 +17,8 @@ "@types/semver": "^7.5.1", "@typescript-eslint/eslint-plugin": "catalog:default", "@typescript-eslint/parser": "catalog:default", + "empathic": "^2.0.0", "eslint": "catalog:default", - "find-up": "^6.3.0", "glob": "^11.0.3", "semver": "^7.7.1", "ts-dedent": "^2.2.0", diff --git a/tools/test/run-test-file.ts b/tools/test/run-test-file.ts index 00645992d605..ce437153f13c 100644 --- a/tools/test/run-test-file.ts +++ b/tools/test/run-test-file.ts @@ -1,12 +1,12 @@ import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { findUpSync } from "find-up"; +import * as find from "empathic/find"; const currentFile = process.argv[2]; const currentDirectory = path.dirname(currentFile); -const packageJsonPath = findUpSync("package.json", { cwd: currentDirectory }); +const packageJsonPath = find.file("package.json", { cwd: currentDirectory }); if (!packageJsonPath) { console.error("No package.json found."); From e93dc01839aee047b37188b850cac7f3465c7bd4 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 19 Feb 2026 22:30:46 +0000 Subject: [PATCH 3/3] Add a warning in the autoconfig logic letting users know that support for projects inside workspaces is limited (#12595) Co-authored-by: Pete Bacon Darwin --- .changeset/blue-dragons-wave.md | 5 ++++ .../confirm-auto-config-details.test.ts | 12 ++++++++++ .../get-details-for-auto-config.test.ts | 23 ++++++++++++++++++- packages/wrangler/src/__tests__/init.test.ts | 1 + packages/wrangler/src/autoconfig/details.ts | 15 ++++++++++++ packages/wrangler/src/package-manager.ts | 21 ++++++++++------- 6 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 .changeset/blue-dragons-wave.md diff --git a/.changeset/blue-dragons-wave.md b/.changeset/blue-dragons-wave.md new file mode 100644 index 000000000000..dae0642210d6 --- /dev/null +++ b/.changeset/blue-dragons-wave.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Add a warning in the autoconfig logic letting users know that support for projects inside workspaces is limited 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 9ea1aa95e7b1..573f9ce75642 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 @@ -56,6 +56,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => { "dlx": [ "npx", ], + "lockFiles": [ + "package-lock.json", + ], "npx": "npx", "type": "npm", }, @@ -115,6 +118,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => { "dlx": [ "npx", ], + "lockFiles": [ + "package-lock.json", + ], "npx": "npx", "type": "npm", }, @@ -174,6 +180,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => { "dlx": [ "npx", ], + "lockFiles": [ + "package-lock.json", + ], "npx": "npx", "type": "npm", }, @@ -255,6 +264,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => { "dlx": [ "npx", ], + "lockFiles": [ + "package-lock.json", + ], "npx": "npx", "type": "npm", }, diff --git a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts index 9db343668103..fdc4be0a7ed5 100644 --- a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts @@ -19,7 +19,7 @@ import type { Mock, MockInstance } from "vitest"; describe("autoconfig details - getDetailsForAutoConfig()", () => { runInTempDir(); const { setIsTTY } = useMockIsTTY(); - mockConsoleMethods(); + const std = mockConsoleMethods(); let isNonInteractiveOrCISpy: MockInstance; beforeEach(() => { @@ -122,6 +122,27 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => { ); }); + it("should warn when no lock file is detected (project may be inside a workspace)", async ({ + expect, + }) => { + // Create a project without a lock file - simulating a project inside a workspace + // where the lock file is at the workspace root + await seed({ + "package.json": JSON.stringify({ + name: "my-app", + dependencies: {}, + }), + "index.html": "

Hello World

", + }); + + await details.getDetailsForAutoConfig(); + + expect(std.warn).toContain( + "No lock file has been detected in the current working directory." + ); + expect(std.warn).toContain("project is part of a workspace"); + }); + it("should use npm build instead of framework build if present", async ({ expect, }) => { diff --git a/packages/wrangler/src/__tests__/init.test.ts b/packages/wrangler/src/__tests__/init.test.ts index 20eb84bfa34f..c2c6416f12de 100644 --- a/packages/wrangler/src/__tests__/init.test.ts +++ b/packages/wrangler/src/__tests__/init.test.ts @@ -97,6 +97,7 @@ describe("init", () => { type: "yarn", npx: "yarn", dlx: ["yarn", "dlx"], + lockFiles: ["yarn.lock"], }; (getPackageManager as Mock).mockResolvedValue(mockPackageManager); diff --git a/packages/wrangler/src/autoconfig/details.ts b/packages/wrangler/src/autoconfig/details.ts index 0275dcf5e914..30c5f2231e36 100644 --- a/packages/wrangler/src/autoconfig/details.ts +++ b/packages/wrangler/src/autoconfig/details.ts @@ -13,6 +13,7 @@ import { import { Project } from "@netlify/build-info"; import { NodeFS } from "@netlify/build-info/node"; import { captureException } from "@sentry/node"; +import chalk from "chalk"; import dedent from "ts-dedent"; import { getCacheFolder } from "../config-cache"; import { getErrorType } from "../core/handle-errors"; @@ -231,6 +232,20 @@ async function detectFramework( // This is populated after getBuildSettings() runs, which triggers the full detection chain. const packageManager = convertDetectedPackageManager(project.packageManager); + const lockFileExists = packageManager.lockFiles.some((lockFile) => + existsSync(join(projectPath, lockFile)) + ); + + if (!lockFileExists) { + logger.warn( + "No lock file has been detected in the current working directory." + + " This might indicate that the project is part of a workspace. Auto-configuration of " + + `projects inside workspaces is limited. See ${chalk.hex("#3B818D")( + "https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/#workspaces" + )}` + ); + } + if (await isPagesProject(projectPath, wranglerConfig, detectedFramework)) { return { detectedFramework: { diff --git a/packages/wrangler/src/package-manager.ts b/packages/wrangler/src/package-manager.ts index 32c92397e5a7..9bca8eed8e73 100644 --- a/packages/wrangler/src/package-manager.ts +++ b/packages/wrangler/src/package-manager.ts @@ -7,6 +7,7 @@ export interface PackageManager { type: "npm" | "yarn" | "pnpm" | "bun"; npx: string; dlx: string[]; + lockFiles: string[]; } export async function getPackageManager(): Promise { @@ -67,38 +68,42 @@ export function getPackageManagerName(packageManager: PackageManager): string { /** * Manage packages using npm */ -export const NpmPackageManager: PackageManager = { +export const NpmPackageManager = { type: "npm", npx: "npx", dlx: ["npx"], -}; + lockFiles: ["package-lock.json"], +} as const satisfies PackageManager; /** * Manage packages using pnpm */ -export const PnpmPackageManager: PackageManager = { +export const PnpmPackageManager = { type: "pnpm", npx: "pnpm", + lockFiles: ["pnpm-lock.yaml"], dlx: ["pnpm", "dlx"], -}; +} as const satisfies PackageManager; /** * Manage packages using yarn */ -export const YarnPackageManager: PackageManager = { +export const YarnPackageManager = { type: "yarn", npx: "yarn", dlx: ["yarn", "dlx"], -}; + lockFiles: ["yarn.lock"], +} as const satisfies PackageManager; /** * Manage packages using bun */ -export const BunPackageManager: PackageManager = { +export const BunPackageManager = { type: "bun", npx: "bunx", dlx: ["bunx"], -}; + lockFiles: ["bun.lockb", "bun.lock"], +} as const satisfies PackageManager; async function supports(name: string): Promise { try {