Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .yarn/versions/9c29ee99.yml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion packages/docusaurus/static/configuration/yarnrc.json
Original file line number Diff line number Diff line change
@@ -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).",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

You can also nest this syntax, for example ${PRIMARY:-${SECONDARY:-fallback}}, which first checks PRIMARY, then SECONDARY, and finally falls back to fallback.

"__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",
Expand Down
58 changes: 45 additions & 13 deletions packages/yarnpkg-core/sources/miscUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,26 +470,58 @@ export function buildIgnorePattern(ignorePatterns: Array<string>) {
}).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 = /\\?\${(?<variableName>[\d\w_]+)(?<colon>:)?(?:-(?<fallback>[^}]*))?}/g;

return value.replace(regex, (match, ...args) => {
if (match.startsWith(`\\`))
return match.slice(1);
const regex = /\$\{(?<variableName>[\d\w_]+)(?<colon>:)?(?:-(?<fallback>[^{}]*))?\}/;

// Protect escaped \${...} blocks (with balanced braces)
const escaped: Array<string> = [];
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 {
Expand Down
15 changes: 15 additions & 0 deletions packages/yarnpkg-core/tests/Configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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`);
Expand All @@ -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`);
});
});

Expand Down
148 changes: 146 additions & 2 deletions packages/yarnpkg-core/tests/miscUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`, () => {
Expand All @@ -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`, () => {
Expand Down