Skip to content

Commit d71e37f

Browse files
nzakasmdjermanovic
andauthored
feat: Allow flags to be set in ESLINT_FLAGS env variable (#19717)
* feat: Allow flags to be set in ESLINT_FLAGS env variable fixes #19100 * Move environment variable reading into ESLint class * Update docs/src/pages/flags.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Clarify flags are merged * clean up formatting * Trim environment variable before split --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 5687ce7 commit d71e37f

File tree

5 files changed

+190
-1
lines changed

5 files changed

+190
-1
lines changed

docs/src/pages/flags.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ On the command line, you can specify feature flags using the `--flag` option. Yo
8282
args: ["--flag", "flag_one", "--flag", "flag_two", "file.js"]
8383
}) }}
8484

85+
### Enable Feature Flags with Environment Variables
86+
87+
You can also set feature flags using the `ESLINT_FLAGS` environment variable. Multiple flags can be specified as a comma-separated list and are merged with any flags passed on the CLI or in the API. For example, here's how you can add feature flags to your `.bashrc` or `.bash_profile` files:
88+
89+
```bash
90+
export ESLINT_FLAGS="flag_one,flag_two"
91+
```
92+
93+
This approach is especially useful in CI/CD pipelines or when you want to enable the same flags across multiple ESLint commands.
94+
8595
### Enable Feature Flags with the API
8696

8797
When using the API, you can pass a `flags` array to both the `ESLint` and `Linter` classes:
@@ -98,6 +108,10 @@ const linter = new Linter({
98108
});
99109
```
100110

111+
::: tip
112+
The `ESLint` class also reads the `ESLINT_FLAGS` environment variable to set flags.
113+
:::
114+
101115
### Enable Feature Flags in VS Code
102116

103117
To enable flags in the VS Code ESLint Extension for the editor, specify the flags you'd like in the `eslint.options` setting in your `settings.json` file:

lib/eslint/eslint.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,20 @@ function getFixerForFixTypes(fix, fixTypesSet, config) {
390390
originalFix(message);
391391
}
392392

393+
/**
394+
* Retrieves flags from the environment variable ESLINT_FLAGS.
395+
* @param {string[]} flags The flags defined via the API.
396+
* @returns {string[]} The merged flags to use.
397+
*/
398+
function mergeEnvironmentFlags(flags) {
399+
if (!process.env.ESLINT_FLAGS) {
400+
return flags;
401+
}
402+
403+
const envFlags = process.env.ESLINT_FLAGS.trim().split(/\s*,\s*/gu);
404+
return Array.from(new Set([...envFlags, ...flags]));
405+
}
406+
393407
//-----------------------------------------------------------------------------
394408
// Main API
395409
//-----------------------------------------------------------------------------
@@ -420,7 +434,7 @@ class ESLint {
420434
const linter = new Linter({
421435
cwd: processedOptions.cwd,
422436
configType: "flat",
423-
flags: processedOptions.flags,
437+
flags: mergeEnvironmentFlags(processedOptions.flags),
424438
});
425439

426440
const cacheFilePath = getCacheFile(

lib/shared/flags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
*/
2828
const activeFlags = new Map([
2929
["test_only", "Used only for testing."],
30+
["test_only_2", "Used only for testing."],
3031
[
3132
"unstable_config_lookup_from_file",
3233
"Look up `eslint.config.js` from the file being linted.",

tests/lib/cli.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2741,6 +2741,11 @@ describe("cli", () => {
27412741
.returns();
27422742
});
27432743

2744+
afterEach(() => {
2745+
sinon.restore();
2746+
delete process.env.ESLINT_FLAGS;
2747+
});
2748+
27442749
it("should throw an error when an inactive flag whose feature has been abandoned is used", async () => {
27452750
const configPath = getFixturePath("eslint.config.js");
27462751
const filePath = getFixturePath("passing.js");
@@ -2751,6 +2756,18 @@ describe("cli", () => {
27512756
}, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u);
27522757
});
27532758

2759+
it("should throw an error when an inactive flag whose feature has been abandoned is used in an environment variable", async () => {
2760+
const configPath = getFixturePath("eslint.config.js");
2761+
const filePath = getFixturePath("passing.js");
2762+
2763+
process.env.ESLINT_FLAGS = "test_only_abandoned";
2764+
const input = `--config ${configPath} ${filePath}`;
2765+
2766+
await stdAssert.rejects(async () => {
2767+
await cli.execute(input, null, true);
2768+
}, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u);
2769+
});
2770+
27542771
it("should error out when an unknown flag is used", async () => {
27552772
const configPath = getFixturePath("eslint.config.js");
27562773
const filePath = getFixturePath("passing.js");
@@ -2761,6 +2778,18 @@ describe("cli", () => {
27612778
}, /Unknown flag 'test_only_oldx'\./u);
27622779
});
27632780

2781+
it("should error out when an unknown flag is used in an environment variable", async () => {
2782+
const configPath = getFixturePath("eslint.config.js");
2783+
const filePath = getFixturePath("passing.js");
2784+
const input = `--config ${configPath} ${filePath}`;
2785+
2786+
process.env.ESLINT_FLAGS = "test_only_oldx";
2787+
2788+
await stdAssert.rejects(async () => {
2789+
await cli.execute(input, null, true);
2790+
}, /Unknown flag 'test_only_oldx'\./u);
2791+
});
2792+
27642793
it("should emit a warning and not error out when an inactive flag that has been replaced by another flag is used", async () => {
27652794
const configPath = getFixturePath("eslint.config.js");
27662795
const filePath = getFixturePath("passing.js");
@@ -2780,6 +2809,28 @@ describe("cli", () => {
27802809
assert.strictEqual(exitCode, 0);
27812810
});
27822811

2812+
it("should emit a warning and not error out when an inactive flag that has been replaced by another flag is used in an environment variable", async () => {
2813+
const configPath = getFixturePath("eslint.config.js");
2814+
const filePath = getFixturePath("passing.js");
2815+
const input = `--config ${configPath} ${filePath}`;
2816+
2817+
process.env.ESLINT_FLAGS = "test_only_replaced";
2818+
2819+
const exitCode = await cli.execute(input, null, true);
2820+
2821+
assert.strictEqual(
2822+
processStub.callCount,
2823+
1,
2824+
"calls `process.emitWarning()` for flags once",
2825+
);
2826+
assert.deepStrictEqual(processStub.getCall(0).args, [
2827+
"The flag 'test_only_replaced' is inactive: This flag has been renamed 'test_only' to reflect its stabilization. Please use 'test_only' instead.",
2828+
"ESLintInactiveFlag_test_only_replaced",
2829+
]);
2830+
sinon.assert.notCalled(log.error);
2831+
assert.strictEqual(exitCode, 0);
2832+
});
2833+
27832834
it("should emit a warning and not error out when an inactive flag whose feature is enabled by default is used", async () => {
27842835
const configPath = getFixturePath("eslint.config.js");
27852836
const filePath = getFixturePath("passing.js");
@@ -2799,6 +2850,27 @@ describe("cli", () => {
27992850
assert.strictEqual(exitCode, 0);
28002851
});
28012852

2853+
it("should emit a warning and not error out when an inactive flag whose feature is enabled by default is used in an environment variable", async () => {
2854+
const configPath = getFixturePath("eslint.config.js");
2855+
const filePath = getFixturePath("passing.js");
2856+
const input = `--config ${configPath} ${filePath}`;
2857+
2858+
process.env.ESLINT_FLAGS = "test_only_enabled_by_default";
2859+
2860+
const exitCode = await cli.execute(input, null, true);
2861+
assert.strictEqual(
2862+
processStub.callCount,
2863+
1,
2864+
"calls `process.emitWarning()` for flags once",
2865+
);
2866+
assert.deepStrictEqual(processStub.getCall(0).args, [
2867+
"The flag 'test_only_enabled_by_default' is inactive: This feature is now enabled by default.",
2868+
"ESLintInactiveFlag_test_only_enabled_by_default",
2869+
]);
2870+
sinon.assert.notCalled(log.error);
2871+
assert.strictEqual(exitCode, 0);
2872+
});
2873+
28022874
it("should not error when a valid flag is used", async () => {
28032875
const configPath = getFixturePath("eslint.config.js");
28042876
const filePath = getFixturePath("passing.js");
@@ -2808,6 +2880,31 @@ describe("cli", () => {
28082880
sinon.assert.notCalled(log.error);
28092881
assert.strictEqual(exitCode, 0);
28102882
});
2883+
2884+
it("should not error when a valid flag is used in an environment variable", async () => {
2885+
const configPath = getFixturePath("eslint.config.js");
2886+
const filePath = getFixturePath("passing.js");
2887+
const input = `--config ${configPath} ${filePath}`;
2888+
2889+
process.env.ESLINT_FLAGS = "test_only";
2890+
2891+
const exitCode = await cli.execute(input, null, true);
2892+
2893+
sinon.assert.notCalled(log.error);
2894+
assert.strictEqual(exitCode, 0);
2895+
});
2896+
2897+
it("should error when a valid flag is used in an environment variable with an abandoned flag", async () => {
2898+
const configPath = getFixturePath("eslint.config.js");
2899+
const filePath = getFixturePath("passing.js");
2900+
const input = `--config ${configPath} ${filePath}`;
2901+
2902+
process.env.ESLINT_FLAGS = "test_only,test_only_abandoned";
2903+
2904+
await stdAssert.rejects(async () => {
2905+
await cli.execute(input, null, true);
2906+
}, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned\./u);
2907+
});
28112908
});
28122909

28132910
describe("--report-unused-inline-configs option", () => {

tests/lib/eslint/eslint.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,10 @@ describe("ESLint", () => {
428428
.returns();
429429
});
430430

431+
afterEach(() => {
432+
delete process.env.ESLINT_FLAGS;
433+
});
434+
431435
it("should return true if the flag is present and active", () => {
432436
eslint = new ESLint({
433437
cwd: getFixturePath(),
@@ -437,6 +441,47 @@ describe("ESLint", () => {
437441
assert.strictEqual(eslint.hasFlag("test_only"), true);
438442
});
439443

444+
it("should return true if the flag is present and active with ESLINT_FLAGS", () => {
445+
process.env.ESLINT_FLAGS = "test_only";
446+
eslint = new ESLint({
447+
cwd: getFixturePath(),
448+
});
449+
assert.strictEqual(eslint.hasFlag("test_only"), true);
450+
});
451+
452+
it("should merge flags passed through API with flags passed through ESLINT_FLAGS", () => {
453+
process.env.ESLINT_FLAGS = "test_only";
454+
eslint = new ESLint({
455+
cwd: getFixturePath(),
456+
flags: ["test_only_2"],
457+
});
458+
assert.strictEqual(eslint.hasFlag("test_only"), true);
459+
assert.strictEqual(eslint.hasFlag("test_only_2"), true);
460+
});
461+
462+
it("should return true for multiple flags in ESLINT_FLAGS if the flag is present and active and one is duplicated in the API", () => {
463+
process.env.ESLINT_FLAGS = "test_only,test_only_2";
464+
465+
eslint = new ESLint({
466+
cwd: getFixturePath(),
467+
flags: ["test_only"], // intentional duplication
468+
});
469+
470+
assert.strictEqual(eslint.hasFlag("test_only"), true);
471+
assert.strictEqual(eslint.hasFlag("test_only_2"), true);
472+
});
473+
474+
it("should return true for multiple flags in ESLINT_FLAGS if the flag is present and active and there is leading and trailing white space", () => {
475+
process.env.ESLINT_FLAGS = " test_only, test_only_2 ";
476+
477+
eslint = new ESLint({
478+
cwd: getFixturePath(),
479+
});
480+
481+
assert.strictEqual(eslint.hasFlag("test_only"), true);
482+
assert.strictEqual(eslint.hasFlag("test_only_2"), true);
483+
});
484+
440485
it("should return true for the replacement flag if an inactive flag that has been replaced is used", () => {
441486
eslint = new ESLint({
442487
cwd: getFixturePath(),
@@ -485,6 +530,15 @@ describe("ESLint", () => {
485530
}, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned/u);
486531
});
487532

533+
it("should throw an error if an inactive flag whose feature has been abandoned is used in ESLINT_FLAGS", () => {
534+
process.env.ESLINT_FLAGS = "test_only_abandoned";
535+
assert.throws(() => {
536+
eslint = new ESLint({
537+
cwd: getFixturePath(),
538+
});
539+
}, /The flag 'test_only_abandoned' is inactive: This feature has been abandoned/u);
540+
});
541+
488542
it("should throw an error if the flag is unknown", () => {
489543
assert.throws(() => {
490544
eslint = new ESLint({
@@ -494,6 +548,15 @@ describe("ESLint", () => {
494548
}, /Unknown flag 'foo_bar'/u);
495549
});
496550

551+
it("should throw an error if the flag is unknown in ESLINT_FLAGS", () => {
552+
process.env.ESLINT_FLAGS = "foo_bar";
553+
assert.throws(() => {
554+
eslint = new ESLint({
555+
cwd: getFixturePath(),
556+
});
557+
}, /Unknown flag 'foo_bar'/u);
558+
});
559+
497560
it("should return false if the flag is not present", () => {
498561
eslint = new ESLint({ cwd: getFixturePath() });
499562

0 commit comments

Comments
 (0)