Skip to content

Commit 12988d1

Browse files
authored
feat: hot-reloading for no-undeclared-env-vars ESLint rule (#10468)
### Description Fixes #10140 so that updating the `env` key in `turbo.json` is reflected in ESLint in your editor. ### Testing Instructions TODO
1 parent ac3de48 commit 12988d1

File tree

3 files changed

+161
-5
lines changed

3 files changed

+161
-5
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import path from "node:path";
2+
import fs from "node:fs";
3+
import { RuleTester } from "eslint";
4+
import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
5+
import type { SchemaV1 } from "@turbo/types";
6+
import { RULES } from "../../../../lib/constants";
7+
import rule from "../../../../lib/rules/no-undeclared-env-vars";
8+
import { Project } from "../../../../lib/utils/calculate-inputs";
9+
10+
const ruleTester = new RuleTester({
11+
parserOptions: { ecmaVersion: 2020, sourceType: "module" },
12+
});
13+
14+
const cwd = path.join(__dirname, "../../../../__fixtures__/workspace-configs");
15+
const webFilename = path.join(cwd, "/apps/web/index.js");
16+
17+
describe("Project reload functionality", () => {
18+
let project: Project;
19+
let originalTurboJson: string;
20+
21+
beforeEach(() => {
22+
project = new Project(cwd);
23+
// Store original turbo.json content for restoration
24+
const turboJsonPath = path.join(cwd, "turbo.json");
25+
originalTurboJson = fs.readFileSync(turboJsonPath, "utf8");
26+
});
27+
28+
afterEach(() => {
29+
// Restore original turbo.json content
30+
const turboJsonPath = path.join(cwd, "turbo.json");
31+
fs.writeFileSync(turboJsonPath, originalTurboJson);
32+
});
33+
34+
it("should reload workspace configurations when called", () => {
35+
const initialConfigs = [...project.allConfigs];
36+
37+
// Call reload
38+
project.reload();
39+
40+
// Verify that configurations were reloaded
41+
expect(project.allConfigs).not.toBe(initialConfigs);
42+
expect(project.allConfigs.length).toBe(initialConfigs.length);
43+
44+
// Verify that project root and workspaces were updated
45+
expect(project.projectRoot).toBeDefined();
46+
expect(project.projectWorkspaces.length).toBeGreaterThan(0);
47+
});
48+
49+
it("should regenerate key and test configurations after reload", () => {
50+
const initialKey = project._key;
51+
const initialTest = project._test;
52+
53+
// Call reload
54+
project.reload();
55+
56+
// Verify that key and test configurations were regenerated
57+
expect(project._key).not.toBe(initialKey);
58+
expect(project._test).not.toBe(initialTest);
59+
});
60+
61+
it("should detect changes in turbo.json after reload", () => {
62+
const turboJsonPath = path.join(cwd, "turbo.json");
63+
const initialConfig = project.projectRoot?.turboConfig;
64+
65+
// Modify turbo.json
66+
const modifiedConfig: SchemaV1 = {
67+
...(JSON.parse(originalTurboJson) as SchemaV1),
68+
pipeline: {
69+
...(JSON.parse(originalTurboJson) as SchemaV1).pipeline,
70+
newTask: {
71+
outputs: [],
72+
},
73+
},
74+
};
75+
fs.writeFileSync(turboJsonPath, JSON.stringify(modifiedConfig, null, 2));
76+
77+
// Call reload
78+
project.reload();
79+
80+
// Verify that the new configuration is loaded
81+
expect(project.projectRoot?.turboConfig).not.toEqual(initialConfig);
82+
expect(project.projectRoot?.turboConfig).toEqual(modifiedConfig);
83+
});
84+
85+
it("should handle invalid turbo.json gracefully", () => {
86+
const turboJsonPath = path.join(cwd, "turbo.json");
87+
88+
// Write invalid JSON
89+
fs.writeFileSync(turboJsonPath, "invalid json");
90+
91+
// Call reload - should not throw
92+
expect(() => {
93+
project.reload();
94+
}).not.toThrow();
95+
96+
// Verify that the project still has a valid state
97+
expect(project.projectRoot).toBeDefined();
98+
expect(project.projectWorkspaces.length).toBeGreaterThan(0);
99+
});
100+
101+
it("should maintain consistent state after multiple reloads", () => {
102+
const initialConfigs = [...project.allConfigs];
103+
104+
// Perform multiple reloads
105+
project.reload();
106+
project.reload();
107+
project.reload();
108+
109+
// Verify that the final state is consistent
110+
expect(project.allConfigs.length).toBe(initialConfigs.length);
111+
expect(project.projectRoot).toBeDefined();
112+
expect(project.projectWorkspaces.length).toBeGreaterThan(0);
113+
});
114+
});
115+
116+
// Test that the reload functionality works with the ESLint rule
117+
ruleTester.run(RULES.noUndeclaredEnvVars, rule, {
118+
valid: [
119+
{
120+
code: `
121+
const { ENV_2 } = import.meta.env;
122+
`,
123+
options: [{ cwd }],
124+
filename: webFilename,
125+
},
126+
],
127+
invalid: [
128+
{
129+
code: `
130+
const { ENV_3 } = import.meta.env;
131+
`,
132+
options: [{ cwd }],
133+
filename: webFilename,
134+
errors: [
135+
{
136+
message:
137+
"ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json",
138+
},
139+
],
140+
},
141+
],
142+
});

packages/eslint-plugin-turbo/lib/rules/no-undeclared-env-vars.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ function create(context: RuleContextWithOptions): Rule.RuleListener {
263263
};
264264

265265
return {
266+
Program() {
267+
// Reload project configuration so that changes show in the user's editor
268+
project.reload();
269+
},
266270
MemberExpression(node) {
267271
// we only care about complete process env declarations and non-computed keys
268272
if (isProcessEnv(node) || isImportMetaEnv(node)) {

packages/eslint-plugin-turbo/lib/utils/calculate-inputs.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,10 @@ export class Project {
284284
constructor(cwd: string | undefined) {
285285
this.cwd = cwd;
286286
this.allConfigs = getWorkspaceConfigs(cwd);
287-
this.projectRoot = this.allConfigs.find(
288-
(workspaceConfig) => workspaceConfig.isWorkspaceRoot
289-
);
287+
this.projectRoot = this.allConfigs.find((config) => config.isWorkspaceRoot);
290288
this.projectWorkspaces = this.allConfigs.filter(
291-
(workspaceConfig) => !workspaceConfig.isWorkspaceRoot
289+
(config) => !config.isWorkspaceRoot
292290
);
293-
294291
this._key = this.generateKey();
295292
this._test = this.generateTestConfig();
296293
}
@@ -442,4 +439,17 @@ export class Project {
442439

443440
return tests.flat().some((test) => test(envVar));
444441
}
442+
443+
reload() {
444+
// Reload workspace configurations with caching disabled
445+
this.allConfigs = getWorkspaceConfigs(this.cwd, { cache: false });
446+
this.projectRoot = this.allConfigs.find((config) => config.isWorkspaceRoot);
447+
this.projectWorkspaces = this.allConfigs.filter(
448+
(config) => !config.isWorkspaceRoot
449+
);
450+
451+
// Regenerate key and test configurations
452+
this._key = this.generateKey();
453+
this._test = this.generateTestConfig();
454+
}
445455
}

0 commit comments

Comments
 (0)