diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/reload.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/reload.test.ts new file mode 100644 index 0000000000000..f9dfc550758e6 --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/reload.test.ts @@ -0,0 +1,142 @@ +import path from "node:path"; +import fs from "node:fs"; +import { RuleTester } from "eslint"; +import { describe, expect, it, beforeEach, afterEach } from "@jest/globals"; +import type { SchemaV1 } from "@turbo/types"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; +import { Project } from "../../../../lib/utils/calculate-inputs"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, +}); + +const cwd = path.join(__dirname, "../../../../__fixtures__/workspace-configs"); +const webFilename = path.join(cwd, "/apps/web/index.js"); + +describe("Project reload functionality", () => { + let project: Project; + let originalTurboJson: string; + + beforeEach(() => { + project = new Project(cwd); + // Store original turbo.json content for restoration + const turboJsonPath = path.join(cwd, "turbo.json"); + originalTurboJson = fs.readFileSync(turboJsonPath, "utf8"); + }); + + afterEach(() => { + // Restore original turbo.json content + const turboJsonPath = path.join(cwd, "turbo.json"); + fs.writeFileSync(turboJsonPath, originalTurboJson); + }); + + it("should reload workspace configurations when called", () => { + const initialConfigs = [...project.allConfigs]; + + // Call reload + project.reload(); + + // Verify that configurations were reloaded + expect(project.allConfigs).not.toBe(initialConfigs); + expect(project.allConfigs.length).toBe(initialConfigs.length); + + // Verify that project root and workspaces were updated + expect(project.projectRoot).toBeDefined(); + expect(project.projectWorkspaces.length).toBeGreaterThan(0); + }); + + it("should regenerate key and test configurations after reload", () => { + const initialKey = project._key; + const initialTest = project._test; + + // Call reload + project.reload(); + + // Verify that key and test configurations were regenerated + expect(project._key).not.toBe(initialKey); + expect(project._test).not.toBe(initialTest); + }); + + it("should detect changes in turbo.json after reload", () => { + const turboJsonPath = path.join(cwd, "turbo.json"); + const initialConfig = project.projectRoot?.turboConfig; + + // Modify turbo.json + const modifiedConfig: SchemaV1 = { + ...(JSON.parse(originalTurboJson) as SchemaV1), + pipeline: { + ...(JSON.parse(originalTurboJson) as SchemaV1).pipeline, + newTask: { + outputs: [], + }, + }, + }; + fs.writeFileSync(turboJsonPath, JSON.stringify(modifiedConfig, null, 2)); + + // Call reload + project.reload(); + + // Verify that the new configuration is loaded + expect(project.projectRoot?.turboConfig).not.toEqual(initialConfig); + expect(project.projectRoot?.turboConfig).toEqual(modifiedConfig); + }); + + it("should handle invalid turbo.json gracefully", () => { + const turboJsonPath = path.join(cwd, "turbo.json"); + + // Write invalid JSON + fs.writeFileSync(turboJsonPath, "invalid json"); + + // Call reload - should not throw + expect(() => { + project.reload(); + }).not.toThrow(); + + // Verify that the project still has a valid state + expect(project.projectRoot).toBeDefined(); + expect(project.projectWorkspaces.length).toBeGreaterThan(0); + }); + + it("should maintain consistent state after multiple reloads", () => { + const initialConfigs = [...project.allConfigs]; + + // Perform multiple reloads + project.reload(); + project.reload(); + project.reload(); + + // Verify that the final state is consistent + expect(project.allConfigs.length).toBe(initialConfigs.length); + expect(project.projectRoot).toBeDefined(); + expect(project.projectWorkspaces.length).toBeGreaterThan(0); + }); +}); + +// Test that the reload functionality works with the ESLint rule +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const { ENV_2 } = import.meta.env; + `, + options: [{ cwd }], + filename: webFilename, + }, + ], + invalid: [ + { + code: ` + const { ENV_3 } = import.meta.env; + `, + options: [{ cwd }], + filename: webFilename, + errors: [ + { + message: + "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-turbo/lib/rules/no-undeclared-env-vars.ts b/packages/eslint-plugin-turbo/lib/rules/no-undeclared-env-vars.ts index 7b49d59afd8ee..8e780234025da 100644 --- a/packages/eslint-plugin-turbo/lib/rules/no-undeclared-env-vars.ts +++ b/packages/eslint-plugin-turbo/lib/rules/no-undeclared-env-vars.ts @@ -263,6 +263,10 @@ function create(context: RuleContextWithOptions): Rule.RuleListener { }; return { + Program() { + // Reload project configuration so that changes show in the user's editor + project.reload(); + }, MemberExpression(node) { // we only care about complete process env declarations and non-computed keys if (isProcessEnv(node) || isImportMetaEnv(node)) { diff --git a/packages/eslint-plugin-turbo/lib/utils/calculate-inputs.ts b/packages/eslint-plugin-turbo/lib/utils/calculate-inputs.ts index 66974d3fc37f1..f32e3f925817d 100644 --- a/packages/eslint-plugin-turbo/lib/utils/calculate-inputs.ts +++ b/packages/eslint-plugin-turbo/lib/utils/calculate-inputs.ts @@ -284,13 +284,10 @@ export class Project { constructor(cwd: string | undefined) { this.cwd = cwd; this.allConfigs = getWorkspaceConfigs(cwd); - this.projectRoot = this.allConfigs.find( - (workspaceConfig) => workspaceConfig.isWorkspaceRoot - ); + this.projectRoot = this.allConfigs.find((config) => config.isWorkspaceRoot); this.projectWorkspaces = this.allConfigs.filter( - (workspaceConfig) => !workspaceConfig.isWorkspaceRoot + (config) => !config.isWorkspaceRoot ); - this._key = this.generateKey(); this._test = this.generateTestConfig(); } @@ -442,4 +439,17 @@ export class Project { return tests.flat().some((test) => test(envVar)); } + + reload() { + // Reload workspace configurations with caching disabled + this.allConfigs = getWorkspaceConfigs(this.cwd, { cache: false }); + this.projectRoot = this.allConfigs.find((config) => config.isWorkspaceRoot); + this.projectWorkspaces = this.allConfigs.filter( + (config) => !config.isWorkspaceRoot + ); + + // Regenerate key and test configurations + this._key = this.generateKey(); + this._test = this.generateTestConfig(); + } }