diff --git a/.yarn/versions/9c29ee99.yml b/.yarn/versions/9c29ee99.yml new file mode 100644 index 00000000000..fc9e06fc115 --- /dev/null +++ b/.yarn/versions/9c29ee99.yml @@ -0,0 +1,36 @@ +releases: + "@yarnpkg/cli": patch + "@yarnpkg/core": patch + +declined: + - "@yarnpkg/plugin-catalog" + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-exec" + - "@yarnpkg/plugin-file" + - "@yarnpkg/plugin-git" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-http" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-jsr" + - "@yarnpkg/plugin-link" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-npm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/doctor" + - "@yarnpkg/extensions" + - "@yarnpkg/nm" + - "@yarnpkg/pnpify" + - "@yarnpkg/sdks" diff --git a/packages/docusaurus/static/configuration/yarnrc.json b/packages/docusaurus/static/configuration/yarnrc.json index 97ed444e8d1..ad964640c0b 100644 --- a/packages/docusaurus/static/configuration/yarnrc.json +++ b/packages/docusaurus/static/configuration/yarnrc.json @@ -1,7 +1,7 @@ { "title": "JSON Schema for Yarnrc files", "$schema": "https://json-schema.org/draft/2019-09/schema#", - "description": "Yarnrc files (named this way because they must be called `.yarnrc.yml`) are the one place where you'll be able to configure Yarn's internal settings. While Yarn will automatically find them in the parent directories, they should usually be kept at the root of your project (often your repository). **Starting from the v2, they must be written in valid Yaml and have the right extension** (simply calling your file `.yarnrc` won't do).\n\nEnvironment variables can be accessed from setting definitions by using the `${NAME}` syntax when defining the values. By default Yarn will require the variables to be present, but this can be turned off by using either `${NAME-fallback}` (which will return `fallback` if `NAME` isn't set) or `${NAME:-fallback}` (which will return `fallback` if `NAME` isn't set, or is an empty string).\n\nFinally, note that most settings can also be defined through environment variables (at least for the simpler ones; arrays and objects aren't supported yet). To do this, just prefix the names and write them in snake case: `YARN_CACHE_FOLDER` will set the cache folder (such values will overwrite any that might have been defined in the RC files - use them sparingly).", + "description": "Yarnrc files (named this way because they must be called `.yarnrc.yml`) are the one place where you'll be able to configure Yarn's internal settings. While Yarn will automatically find them in the parent directories, they should usually be kept at the root of your project (often your repository). **Starting from the v2, they must be written in valid Yaml and have the right extension** (simply calling your file `.yarnrc` won't do).\n\nEnvironment variables can be accessed from setting definitions by using the `${NAME}` syntax when defining the values. By default Yarn will require the variables to be present, but this can be turned off by using either `${NAME-fallback}` (which will return `fallback` if `NAME` isn't set) or `${NAME:-fallback}` (which will return `fallback` if `NAME` isn't set, or is an empty string). You can also nest this syntax, for example `${PRIMARY:-${SECONDARY:-fallback}}`, which first checks PRIMARY, then SECONDARY, and finally falls back to fallback.\n\nFinally, note that most settings can also be defined through environment variables (at least for the simpler ones; arrays and objects aren't supported yet). To do this, just prefix the names and write them in snake case: `YARN_CACHE_FOLDER` will set the cache folder (such values will overwrite any that might have been defined in the RC files - use them sparingly).", "__info": [ "This file contains the JSON Schema for Yarnrc files and is:", "1) Hosted on the Yarn Website at http://yarnpkg.com/configuration/yarnrc.json", diff --git a/packages/yarnpkg-core/sources/miscUtils.ts b/packages/yarnpkg-core/sources/miscUtils.ts index cd1e35b5feb..b10a146d751 100644 --- a/packages/yarnpkg-core/sources/miscUtils.ts +++ b/packages/yarnpkg-core/sources/miscUtils.ts @@ -470,26 +470,58 @@ export function buildIgnorePattern(ignorePatterns: Array) { }).join(`|`); } +/** + * Replaces environment variable references in a string with their values. + * + * Supported syntax: + * - `${VAR}` - replaced with value, throws if unset + * - `${VAR-fallback}` - uses fallback only if unset + * - `${VAR:-fallback}` - uses fallback if unset or empty + * - `${A:-${B:-fallback}}` - nested variables + * - `\${VAR}` - escaped, not replaced + * + * Algorithm: + * 1. Protect escaped `\${...}` blocks with placeholders (tracking balanced braces) + * 2. Resolve variables from innermost to outermost (regex excludes nested braces) + * 3. Restore placeholders to their original `${...}` form + */ export function replaceEnvVariables(value: string, {env}: {env: {[key: string]: string | undefined}}) { - const regex = /\\?\${(?[\d\w_]+)(?:)?(?:-(?[^}]*))?}/g; - - return value.replace(regex, (match, ...args) => { - if (match.startsWith(`\\`)) - return match.slice(1); + const regex = /\$\{(?[\d\w_]+)(?:)?(?:-(?[^{}]*))?\}/; + + // Protect escaped \${...} blocks (with balanced braces) + const escaped: Array = []; + let result = value; + for (let start; (start = result.indexOf(`\\$\{`)) !== -1;) { + let depth = 1, end = start + 3; + while (end < result.length && depth > 0) { + if (result[end] === `{`) depth++; + else if (result[end] === `}`) depth--; + end++; + } + escaped.push(result.slice(start + 1, end)); + result = `${result.slice(0, start)}\0${escaped.length - 1}\0${result.slice(end)}`; + } - const {variableName, colon, fallback} = args[args.length - 1]; + // Replace innermost variables first (regex excludes nested braces) + for (let match; (match = regex.exec(result));) { + const {variableName, colon, fallback} = match.groups!; const variableExist = Object.hasOwn(env, variableName); const variableValue = env[variableName]; + let replacement: string; if (variableValue) - return variableValue; - if (variableExist && !colon) - return variableValue; - if (fallback != null) - return fallback; + replacement = variableValue; + else if (variableExist && !colon) + replacement = variableValue ?? ``; + else if (fallback != null) + replacement = fallback; + else + throw new UsageError(`Environment variable not found (${variableName})`); + + result = result.slice(0, match.index) + replacement + result.slice(match.index + match[0].length); + } - throw new UsageError(`Environment variable not found (${variableName})`); - }); + return result.replace(/\0(\d+)\0/g, (_, i) => escaped[Number(i)]); } export function parseBoolean(value: unknown): boolean { diff --git a/packages/yarnpkg-core/tests/Configuration.test.ts b/packages/yarnpkg-core/tests/Configuration.test.ts index 108aff2f603..f387b5d3aa1 100644 --- a/packages/yarnpkg-core/tests/Configuration.test.ts +++ b/packages/yarnpkg-core/tests/Configuration.test.ts @@ -114,6 +114,15 @@ describe(`Configuration`, () => { emptyEnvWithEmptyFallback: { npmAuthToken: `\${EMPTY_VARIABLE:-}`, }, + emptyEnvWithNestedEnv: { + npmAuthToken: `prefix-\${EMPTY_VARIABLE:-\${ENV_AUTH_TOKEN-fallback-value}-after-env}-suffix`, + }, + emptyEnvWithNestedEnvWithStrictFallback: { + npmAuthToken: `prefix-\${EMPTY_VARIABLE:-\${EMPTY_VARIABLE-fallback-value}-after-env}-suffix`, + }, + emptyEnvWithNestedEnvWithFallback: { + npmAuthToken: `prefix-\${EMPTY_VARIABLE:-\${EMPTY_VARIABLE:-fallback-value}-after-env}-suffix`, + }, }, }, async dir => { const configuration = await Configuration.find(dir, { @@ -132,6 +141,9 @@ describe(`Configuration`, () => { const emptyEnvWithStrictFallback = getToken(`emptyEnvWithStrictFallback`); const emptyEnvWithFallback = getToken(`emptyEnvWithFallback`); const emptyEnvWithEmptyFallback = getToken(`emptyEnvWithEmptyFallback`); + const emptyEnvWithNestedEnv = getToken(`emptyEnvWithNestedEnv`); + const emptyEnvWithNestedEnvWithStrictFallback = getToken(`emptyEnvWithNestedEnvWithStrictFallback`); + const emptyEnvWithNestedEnvWithFallback = getToken(`emptyEnvWithNestedEnvWithFallback`); expect(onlyEnv).toEqual(`AAA-BBB-CCC`); expect(multipleEnvs).toEqual(`AAA-BBB-CCC-separator-AAA-BBB-CCC`); @@ -142,6 +154,9 @@ describe(`Configuration`, () => { expect(emptyEnvWithStrictFallback).toEqual(``); expect(emptyEnvWithFallback).toEqual(`fallback-for-empty-value`); expect(emptyEnvWithEmptyFallback).toEqual(``); + expect(emptyEnvWithNestedEnv).toEqual(`prefix-AAA-BBB-CCC-after-env-suffix`); + expect(emptyEnvWithNestedEnvWithStrictFallback).toEqual(`prefix--after-env-suffix`); + expect(emptyEnvWithNestedEnvWithFallback).toEqual(`prefix-fallback-value-after-env-suffix`); }); }); diff --git a/packages/yarnpkg-core/tests/miscUtils.test.ts b/packages/yarnpkg-core/tests/miscUtils.test.ts index 713dbb7dd3a..7c76172efd7 100644 --- a/packages/yarnpkg-core/tests/miscUtils.test.ts +++ b/packages/yarnpkg-core/tests/miscUtils.test.ts @@ -18,13 +18,41 @@ describe(`miscUtils`, () => { ).toBe(`VAR_A: ValueA, VAR_B: ValueB`); }); + it(`should use empty strings when environment variables are empty strings`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A-FallbackA}, VAR_B: \${VAR_B-FallbackB}`, + { + env: { + VAR_A: ``, + VAR_B: ``, + }, + }, + ), + ).toBe(`VAR_A: , VAR_B: `); + }); + it(`should use fallback values when environment variables are not set`, () => { expect( miscUtils.replaceEnvVariables( - `VAR_A: \${VAR_A:-ValueA}, VAR_B: \${VAR_B:-ValueB}`, + `VAR_A: \${VAR_A:-FallbackA}, VAR_B: \${VAR_B:-FallbackB}`, {env: {}}, ), - ).toBe(`VAR_A: ValueA, VAR_B: ValueB`); + ).toBe(`VAR_A: FallbackA, VAR_B: FallbackB`); + }); + + it(`should use fallback values when environment variables are empty strings`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-FallbackA}, VAR_B: \${VAR_B:-FallbackB}`, + { + env: { + VAR_A: ``, + VAR_B: ``, + }, + }, + ), + ).toBe(`VAR_A: FallbackA, VAR_B: FallbackB`); }); it(`should not replace escaped environment variables`, () => { @@ -40,6 +68,122 @@ describe(`miscUtils`, () => { ), ).toBe(`VAR_A: \${VAR_A}, VAR_B: \${VAR_B}`); }); + + it(`should replace primary environment variables with their values when there is 1 step of nesting`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A-\${VAR_A2-FallbackA}}, VAR_B: \${VAR_B-\${VAR_B2-FallbackB}}`, + { + env: { + VAR_A: `ValueA`, + VAR_B: `ValueB`, + }, + }, + ), + ).toBe(`VAR_A: ValueA, VAR_B: ValueB`); + }); + + it(`should use empty strings when primary environment variables are empty strings when there is 1 step of nesting`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A-\${VAR_A2-FallbackA}}, VAR_B: \${VAR_B-\${VAR_B2-FallbackB}}`, + { + env: { + VAR_A: ``, + VAR_B: ``, + }, + }, + ), + ).toBe(`VAR_A: , VAR_B: `); + }); + + it(`should replace primary environment variables with their values when there is 1 step of nesting`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-\${VAR_A2:-FallbackA}}, VAR_B: \${VAR_B:-\${VAR_B2:-FallbackB}}`, + { + env: { + VAR_A: `ValueA`, + VAR_B: `ValueB`, + }, + }, + ), + ).toBe(`VAR_A: ValueA, VAR_B: ValueB`); + }); + + it(`should replace secondary environment variables with their values when primary variables are not set`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-\${VAR_A2:-FallbackA}}, VAR_B: \${VAR_B:-\${VAR_B2:-FallbackB}}`, + { + env: { + VAR_A2: `ValueA2`, + VAR_B2: `ValueB2`, + }, + }, + ), + ).toBe(`VAR_A: ValueA2, VAR_B: ValueB2`); + }); + + it(`should use fallback values when primary and secondary variables are not set`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-\${VAR_A2:-FallbackA}}, VAR_B: \${VAR_B:-\${VAR_B2:-FallbackB}}`, + {env: {}}, + ), + ).toBe(`VAR_A: FallbackA, VAR_B: FallbackB`); + }); + + it(`should not replace escaped primary environment variables`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \\\${VAR_A:-\${VAR_A2:-FallbackA}}, VAR_B: \\\${VAR_B:-\${VAR_B2:-FallbackB}}`, + { + env: { + VAR_A: `ValueA`, + VAR_B: `ValueB`, + }, + }, + ), + ).toBe(`VAR_A: \${VAR_A:-\${VAR_A2:-FallbackA}}, VAR_B: \${VAR_B:-\${VAR_B2:-FallbackB}}`); + }); + + it(`should not replace escaped secondary environment variables`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-\\\${VAR_A2:-FallbackA}}, VAR_B: \${VAR_B:-\\\${VAR_B2:-FallbackB}}`, + { + env: { + VAR_A2: `ValueA2`, + VAR_B2: `ValueB2`, + }, + }, + ), + ).toBe(`VAR_A: \${VAR_A2:-FallbackA}, VAR_B: \${VAR_B2:-FallbackB}`); + }); + + it(`should replace tertiary environment variables with their values when primary and secondary variables are not set`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-\${VAR_A2:-\${VAR_A3:-FallbackA}}}, VAR_B: \${VAR_B:-\${VAR_B2:-\${VAR_B3:-FallbackB}}}`, + { + env: { + VAR_A3: `ValueA3`, + VAR_B3: `ValueB3`, + }, + }, + ), + ).toBe(`VAR_A: ValueA3, VAR_B: ValueB3`); + }); + + it(`should use fallback values when primary, secondary and tertiary variables are not set`, () => { + expect( + miscUtils.replaceEnvVariables( + `VAR_A: \${VAR_A:-\${VAR_A2:-\${VAR_A3:-FallbackA}}}, VAR_B: \${VAR_B:-\${VAR_B2:-\${VAR_B3:-FallbackB}}}`, + {env: {}}, + ), + ).toBe(`VAR_A: FallbackA, VAR_B: FallbackB`); + }); }); describe(`mapAndFind`, () => {