diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 08bf3bcb890b1..77c0a921b1352 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "type": "shell", "label": "prepare turbo", - "command": "cargo build -p turbo" + "command": "cargo build --package turbo" } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf5d662a49aa3..2cebca83df1cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ Dependencies Building -- Building `turbo` CLI: `cargo build -p turbo` +- Building `turbo` CLI: `cargo build --package turbo` - Using `turbo` to build `turbo` CLI: `./turbow.js` ### TLS Implementation diff --git a/cli/package.json b/cli/package.json index 430365fa92d32..deff9108f9185 100644 --- a/cli/package.json +++ b/cli/package.json @@ -3,8 +3,8 @@ "private": true, "version": "0.0.0", "scripts": { - "clean": "cargo clean -p turbo", - "build": "cargo build -p turbo", - "build:release": "cargo build -p turbo --profile release-turborepo" + "clean": "cargo clean --package turbo", + "build": "cargo build --package turbo", + "build:release": "cargo build --package turbo --profile release-turborepo" } } diff --git a/crates/turborepo-lib/src/framework.rs b/crates/turborepo-lib/src/framework.rs index 85d6aefd4d0b2..385d651e54d40 100644 --- a/crates/turborepo-lib/src/framework.rs +++ b/crates/turborepo-lib/src/framework.rs @@ -1,146 +1,48 @@ use std::sync::OnceLock; +use serde::Deserialize; use turborepo_repository::package_graph::PackageInfo; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] enum Strategy { All, Some, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] struct Matcher { strategy: Strategy, - dependencies: Vec<&'static str>, + dependencies: Vec, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Framework { - slug: &'static str, - env_wildcards: Vec<&'static str>, + slug: String, + env_wildcards: Vec, dependency_match: Matcher, } impl Framework { - pub fn slug(&self) -> &'static str { - self.slug + pub fn slug(&self) -> String { + self.slug.clone() } - pub fn env_wildcards(&self) -> &[&'static str] { + pub fn env_wildcards(&self) -> &[String] { &self.env_wildcards } } -static FRAMEWORKS: OnceLock<[Framework; 13]> = OnceLock::new(); +static FRAMEWORKS: OnceLock> = OnceLock::new(); -fn get_frameworks() -> &'static [Framework] { +const FRAMEWORKS_JSON: &str = + include_str!("../../../packages/turbo-types/src/json/frameworks.json"); + +fn get_frameworks() -> &'static Vec { FRAMEWORKS.get_or_init(|| { - [ - Framework { - slug: "blitzjs", - env_wildcards: vec!["NEXT_PUBLIC_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["blitz"], - }, - }, - Framework { - slug: "nextjs", - env_wildcards: vec!["NEXT_PUBLIC_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["next"], - }, - }, - Framework { - slug: "gatsby", - env_wildcards: vec!["GATSBY_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["gatsby"], - }, - }, - Framework { - slug: "astro", - env_wildcards: vec!["PUBLIC_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["astro"], - }, - }, - Framework { - slug: "solidstart", - env_wildcards: vec!["VITE_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["solid-js", "solid-start"], - }, - }, - Framework { - slug: "vue", - env_wildcards: vec!["VUE_APP_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["@vue/cli-service"], - }, - }, - Framework { - slug: "sveltekit", - env_wildcards: vec!["VITE_*", "PUBLIC_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["@sveltejs/kit"], - }, - }, - Framework { - slug: "create-react-app", - env_wildcards: vec!["REACT_APP_*"], - dependency_match: Matcher { - strategy: Strategy::Some, - dependencies: vec!["react-scripts", "react-dev-utils"], - }, - }, - Framework { - slug: "nitro", - env_wildcards: vec!["NITRO_*"], - dependency_match: Matcher { - strategy: Strategy::Some, - dependencies: vec!["nitropack", "nitropack-nightly"], - }, - }, - Framework { - slug: "nuxtjs", - env_wildcards: vec!["NUXT_*", "NITRO_*"], - dependency_match: Matcher { - strategy: Strategy::Some, - dependencies: vec!["nuxt", "nuxt-edge", "nuxt3", "nuxt3-edge"], - }, - }, - Framework { - slug: "redwoodjs", - env_wildcards: vec!["REDWOOD_ENV_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["@redwoodjs/core"], - }, - }, - Framework { - slug: "vite", - env_wildcards: vec!["VITE_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["vite"], - }, - }, - Framework { - slug: "sanity", - env_wildcards: vec!["SANITY_STUDIO_*"], - dependency_match: Matcher { - strategy: Strategy::All, - dependencies: vec!["@sanity/cli"], - }, - }, - ] + serde_json::from_str(FRAMEWORKS_JSON).expect("Unable to parse embedded JSON") }) } @@ -159,16 +61,16 @@ impl Matcher { Strategy::All => self .dependencies .iter() - .all(|dep| deps.map_or(false, |deps| deps.contains_key(*dep))), + .all(|dep| deps.map_or(false, |deps| deps.contains_key(dep))), Strategy::Some => self .dependencies .iter() - .any(|dep| deps.map_or(false, |deps| deps.contains_key(*dep))), + .any(|dep| deps.map_or(false, |deps| deps.contains_key(dep))), } } } -pub fn infer_framework(workspace: &PackageInfo, is_monorepo: bool) -> Option<&'static Framework> { +pub fn infer_framework(workspace: &PackageInfo, is_monorepo: bool) -> Option<&Framework> { let frameworks = get_frameworks(); frameworks @@ -183,7 +85,7 @@ mod tests { use crate::framework::{get_frameworks, infer_framework, Framework}; - fn get_framework_by_slug(slug: &str) -> &'static Framework { + fn get_framework_by_slug(slug: &str) -> &Framework { get_frameworks() .iter() .find(|framework| framework.slug == slug) @@ -291,7 +193,7 @@ mod tests { )] fn test_infer_framework( workspace_info: PackageInfo, - expected: Option<&'static Framework>, + expected: Option<&Framework>, is_monorepo: bool, ) { let framework = infer_framework(&workspace_info, is_monorepo); diff --git a/crates/turborepo-telemetry/src/events/task.rs b/crates/turborepo-telemetry/src/events/task.rs index 08a97c49852e4..17ab9302cc354 100644 --- a/crates/turborepo-telemetry/src/events/task.rs +++ b/crates/turborepo-telemetry/src/events/task.rs @@ -94,10 +94,10 @@ impl PackageTaskEventBuilder { } // event methods - pub fn track_framework(&self, framework: &str) -> &Self { + pub fn track_framework(&self, framework: String) -> &Self { self.track(Event { key: "framework".to_string(), - value: framework.to_string(), + value: framework, is_sensitive: EventType::NonSensitive, send_in_ci: false, }); diff --git a/docs/repo-docs/crafting-your-repository/using-environment-variables.mdx b/docs/repo-docs/crafting-your-repository/using-environment-variables.mdx index dd244a4baa668..aa3015494b854 100644 --- a/docs/repo-docs/crafting-your-repository/using-environment-variables.mdx +++ b/docs/repo-docs/crafting-your-repository/using-environment-variables.mdx @@ -6,6 +6,7 @@ description: Learn how to handle environments for your applications. import { Callout } from '#/components/callout'; import { Tabs, Tab } from '#/components/tabs'; import { Accordion, Accordions } from '#/components/accordion'; +import { frameworks } from '@turbo/types'; Environment variable inputs are a vital part of your applications that you'll need to account for in your Turborepo configuration. @@ -56,21 +57,24 @@ Turborepo needs to be aware of your environment variables to account for changes Turborepo automatically adds prefix wildcards to your [`env`](/repo/docs/reference/configuration#env) key for common frameworks. If you're using one of the frameworks below in a package, you don't need to specify environment variables with these prefixes: -| Framework | `env` wildcard | -| ---------------- | ------------------- | -| Astro | `PUBLIC_*` | -| Blitz | `NEXT_PUBLIC_*` | -| Create React App | `REACT_APP_*` | -| Gatsby | `GATSBY_*` | -| Next.js | `NEXT_PUBLIC_*` | -| Nitro | `NITRO_*` | -| Nuxt.js | `NUXT_*`, `NITRO_*` | -| RedwoodJS | `REDWOOD_ENV_*` | -| Sanity Studio | `SANITY_STUDIO_*` | -| Solid | `VITE_*` | -| SvelteKit | `VITE_*` | -| Vite | `VITE_*` | -| Vue | `VUE_APP_*` | + + + + + + + + + {frameworks.map(({ name, envWildcards }) => ( + + + + + ))} + +
Framework + env wildcards +
{name}{envWildcards.map((w) => {w}).join(', ')}
Framework inference is per-package. diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/.eslintrc.js b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/.eslintrc.js new file mode 100644 index 0000000000000..8dc66dca7067c --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ["plugin:turbo/recommended"], +}; diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/kitchen-sink/index.js b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/kitchen-sink/index.js new file mode 100644 index 0000000000000..006cf9b8c8179 --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/kitchen-sink/index.js @@ -0,0 +1,3 @@ +process.env.NEXT_PUBLIC_ZILTOID; +process.env.GATSBY_THE; +process.env.NITRO_OMNISCIENT; diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/kitchen-sink/package.json b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/kitchen-sink/package.json new file mode 100644 index 0000000000000..86930291d732c --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/kitchen-sink/package.json @@ -0,0 +1,19 @@ +{ + "name": "nextjs", + "dependencies": { + "next": "*", + "blitz": "*", + "react": "*", + "left-pad": "*", + "event-stream": "*", + "gatsby": "*", + "is-promise": "*", + "@faker-js/faker": "*", + "ua-parser-js": "*", + "nitropack": "*" + }, + "devDependencies": { + "eslint": "8.57.0", + "eslint-plugin-turbo": "../../../../" + } +} diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/nextjs/index.js b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/nextjs/index.js new file mode 100644 index 0000000000000..19582df5a92c1 --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/nextjs/index.js @@ -0,0 +1 @@ +process.env.NEXT_PUBLIC_ZILTOID; diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/nextjs/package.json b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/nextjs/package.json new file mode 100644 index 0000000000000..513d44b4c8d2e --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/nextjs/package.json @@ -0,0 +1,10 @@ +{ + "name": "nextjs", + "dependencies": { + "next": "*" + }, + "devDependencies": { + "eslint": "8.57.0", + "eslint-plugin-turbo": "../../../../" + } +} diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/vite/index.js b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/vite/index.js new file mode 100644 index 0000000000000..97dde691a3c90 --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/vite/index.js @@ -0,0 +1 @@ +process.env.VITE_THING; diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/vite/package.json b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/vite/package.json new file mode 100644 index 0000000000000..fba50b3f190e6 --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/apps/vite/package.json @@ -0,0 +1,10 @@ +{ + "name": "vite", + "dependencies": { + "vite": "*" + }, + "devDependencies": { + "eslint": "8.57.0", + "eslint-plugin-turbo": "../../../../" + } +} diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/package.json b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/package.json new file mode 100644 index 0000000000000..ecd2d11d6222a --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/package.json @@ -0,0 +1,7 @@ +{ + "name": "framework-inference", + "devDependencies": { + "eslint": "8.57.0", + "eslint-plugin-turbo": "../../" + } +} diff --git a/packages/eslint-plugin-turbo/__fixtures__/framework-inference/turbo.json b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/turbo.json new file mode 100644 index 0000000000000..9116ce5b21177 --- /dev/null +++ b/packages/eslint-plugin-turbo/__fixtures__/framework-inference/turbo.json @@ -0,0 +1,7 @@ +{ + "tasks": { + "build": { + "dependsOn": ["^build"] + } + } +} diff --git a/packages/eslint-plugin-turbo/__tests__/cwd.test.ts b/packages/eslint-plugin-turbo/__tests__/cwd.test.ts index c7c5079d6c3d6..3901a1cbcd512 100644 --- a/packages/eslint-plugin-turbo/__tests__/cwd.test.ts +++ b/packages/eslint-plugin-turbo/__tests__/cwd.test.ts @@ -1,7 +1,7 @@ -import path from "path"; -import JSON5 from "json5"; -import { execSync } from "child_process"; -import { Schema } from "@turbo/types"; +import path from "node:path"; +import { execSync } from "node:child_process"; +import { type Schema } from "@turbo/types"; +import { parse, stringify } from "json5"; import { setupTestFixtures } from "@turbo/test-utils"; describe("eslint settings check", () => { @@ -17,7 +17,7 @@ describe("eslint settings check", () => { cwd, encoding: "utf8", }); - const configJson = JSON5.parse(configString); + const configJson: Record = parse(configString); expect(configJson.settings).toEqual({ turbo: { @@ -77,7 +77,7 @@ describe("eslint settings check", () => { encoding: "utf8", } ); - const configJson = JSON5.parse(configString); + const configJson: Record = parse(configString); expect(configJson.settings).toEqual({ turbo: { @@ -144,8 +144,10 @@ describe("eslint cache is busted", () => { cwd, encoding: "utf8", }); - } catch (error: any) { - const outputJson = JSON5.parse(error.stdout); + } catch (error: unknown) { + const outputJson: Record = parse( + (error as { stdout: string }).stdout + ); expect(outputJson).toMatchObject([ { messages: [ @@ -162,7 +164,7 @@ describe("eslint cache is busted", () => { const turboJson = readJson("turbo.json"); if (turboJson && "globalEnv" in turboJson) { turboJson.globalEnv = ["CI", "NONEXISTENT"]; - write("turbo.json", JSON5.stringify(turboJson, null, 2)); + write("turbo.json", stringify(turboJson, null, 2)); } // test that we invalidated the eslint cache @@ -170,7 +172,7 @@ describe("eslint cache is busted", () => { cwd, encoding: "utf8", }); - const outputJson = JSON5.parse(output); + const outputJson: Record = parse(output); expect(outputJson).toMatchObject([{ errorCount: 0 }]); }); }); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars.test.ts deleted file mode 100644 index 5e912fdae4876..0000000000000 --- a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars.test.ts +++ /dev/null @@ -1,1074 +0,0 @@ -import path from "node:path"; -import { RuleTester } from "eslint"; -import { RULES } from "../../lib/constants"; -import rule from "../../lib/rules/no-undeclared-env-vars"; - -const ruleTester = new RuleTester({ - parserOptions: { ecmaVersion: 2020 }, -}); - -const moduleRuleTester = new RuleTester({ - parserOptions: { ecmaVersion: 2020, sourceType: "module" }, -}); - -ruleTester.run(RULES.noUndeclaredEnvVars, rule, { - valid: [ - { - code: ` - const env2 = process.env['ENV_2']; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const env2 = process.env["ENV_2"]; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ENV_2 } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ROOT_DOT_ENV, WEB_DOT_ENV } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { NEXT_PUBLIC_HAHAHAHA } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ENV_1 } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ENV_1 } = process.env; - `, - options: [{ cwd: "/some/random/path" }], - }, - { - code: ` - const { CI } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const { NEW_STYLE_ENV_KEY, TASK_ENV_KEY } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const { NEW_STYLE_GLOBAL_ENV_KEY, TASK_ENV_KEY } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const val = process.env["NEW_STYLE_GLOBAL_ENV_KEY"]; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const x = process.env.GLOBAL_ENV_KEY; - const { TASK_ENV_KEY, GLOBAL_ENV_KEY: renamedX } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "var x = process.env.GLOBAL_ENV_KEY;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "let x = process.env.TASK_ENV_KEY;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "const x = process.env.ANOTHER_KEY_VALUE;", - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ANOTHER_KEY_[A-Z]+$"], - }, - ], - }, - { - code: ` - var x = process.env.ENV_VAR_ONE; - var y = process.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - var x = process.env.ENV_VAR_ONE; - var y = process.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_O[A-Z]+$", "ENV_VAR_TWO"], - }, - ], - }, - { - code: ` - var globalOrTask = process.env.TASK_ENV_KEY || process.env.GLOBAL_ENV_KEY; - var oneOrTwo = process.env.ENV_VAR_ONE || process.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - () => { return process.env.GLOBAL_ENV_KEY } - () => { return process.env.TASK_ENV_KEY } - () => { return process.env.ENV_VAR_ALLOWED } - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - var foo = process?.env.GLOBAL_ENV_KEY - var foo = process?.env.TASK_ENV_KEY - var foo = process?.env.ENV_VAR_ALLOWED - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - function test(arg1 = process.env.GLOBAL_ENV_KEY) {}; - function test(arg1 = process.env.TASK_ENV_KEY) {}; - function test(arg1 = process.env.ENV_VAR_ALLOWED) {}; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - (arg1 = process.env.GLOBAL_ENV_KEY) => {} - (arg1 = process.env.TASK_ENV_KEY) => {} - (arg1 = process.env.ENV_VAR_ALLOWED) => {} - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: "const getEnv = (key) => process.env[key];", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "function getEnv(key) { return process.env[key]; }", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "for (let x of ['ONE', 'TWO', 'THREE']) { console.log(process.env[x]); }", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - ], - - invalid: [ - { - code: ` - const env2 = process.env['ENV_3']; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - errors: [ - { - message: - "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - ], - }, - { - code: ` - const env2 = process.env["ENV_3"]; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - errors: [ - { - message: - "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - ], - }, - { - code: ` - const { ENV_2 } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/docs/index.js" - ), - errors: [ - { - message: - "ENV_2 is not listed as a dependency in the root turbo.json or workspace (apps/docs) turbo.json", - }, - ], - }, - { - code: ` - const { NEXT_PUBLIC_HAHAHAHA, NEXT_PUBLIC_EXCLUDE, NEXT_PUBLIC_EXCLUDED } = process.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - errors: [ - { - message: - "NEXT_PUBLIC_EXCLUDE is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - { - message: - "NEXT_PUBLIC_EXCLUDED is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - ], - }, - { - code: "let { X } = process.env;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [{ message: "X is not listed as a dependency in turbo.json" }], - }, - { - code: "const { X, Y, Z } = process.env;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [ - { message: "X is not listed as a dependency in turbo.json" }, - { message: "Y is not listed as a dependency in turbo.json" }, - { message: "Z is not listed as a dependency in turbo.json" }, - ], - }, - { - code: "const { X, Y: NewName, Z } = process.env;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [ - { message: "X is not listed as a dependency in turbo.json" }, - { message: "Y is not listed as a dependency in turbo.json" }, - { message: "Z is not listed as a dependency in turbo.json" }, - ], - }, - { - code: "var x = process.env.NOT_THERE;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [ - { - message: "NOT_THERE is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: "var x = process.env.KEY;", - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ANOTHER_KEY_[A-Z]+$"], - }, - ], - errors: [{ message: "KEY is not listed as a dependency in turbo.json" }], - }, - { - code: ` - var globalOrTask = process.env.TASK_ENV_KEY_NEW || process.env.GLOBAL_ENV_KEY_NEW; - var oneOrTwo = process.env.ENV_VAR_ONE || process.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: "ENV_VAR_ONE is not listed as a dependency in turbo.json", - }, - { - message: "ENV_VAR_TWO is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - () => { return process.env.GLOBAL_ENV_KEY_NEW } - () => { return process.env.TASK_ENV_KEY_NEW } - () => { return process.env.ENV_VAR_NOT_ALLOWED } - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - var foo = process?.env.GLOBAL_ENV_KEY_NEW - var foo = process?.env.TASK_ENV_KEY_NEW - var foo = process?.env.ENV_VAR_NOT_ALLOWED - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - function test(arg1 = process.env.GLOBAL_ENV_KEY_NEW) {}; - function test(arg1 = process.env.TASK_ENV_KEY_NEW) {}; - function test(arg1 = process.env.ENV_VAR_NOT_ALLOWED) {}; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - (arg1 = process.env.GLOBAL_ENV_KEY_NEW) => {} - (arg1 = process.env.TASK_ENV_KEY_NEW) => {} - (arg1 = process.env.ENV_VAR_NOT_ALLOWED) => {} - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - ], -}); - -moduleRuleTester.run(RULES.noUndeclaredEnvVars, rule, { - valid: [ - { - code: ` - const env2 = import.meta.env['ENV_2']; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const env2 = import.meta.env["ENV_2"]; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ENV_2 } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ROOT_DOT_ENV, WEB_DOT_ENV } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { NEXT_PUBLIC_HAHAHAHA } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ENV_1 } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { ENV_1 } = import.meta.env; - `, - options: [{ cwd: "/some/random/path" }], - }, - { - code: ` - const { CI } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - }, - { - code: ` - const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const { NEW_STYLE_ENV_KEY, TASK_ENV_KEY } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const { NEW_STYLE_GLOBAL_ENV_KEY, TASK_ENV_KEY } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const val = import.meta.env["NEW_STYLE_GLOBAL_ENV_KEY"]; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: ` - const x = import.meta.env.GLOBAL_ENV_KEY; - const { TASK_ENV_KEY, GLOBAL_ENV_KEY: renamedX } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "var x = import.meta.env.GLOBAL_ENV_KEY;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "let x = import.meta.env.TASK_ENV_KEY;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "const x = import.meta.env.ANOTHER_KEY_VALUE;", - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ANOTHER_KEY_[A-Z]+$"], - }, - ], - }, - { - code: ` - var x = import.meta.env.ENV_VAR_ONE; - var y = import.meta.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - var x = import.meta.env.ENV_VAR_ONE; - var y = import.meta.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_O[A-Z]+$", "ENV_VAR_TWO"], - }, - ], - }, - { - code: ` - var globalOrTask = import.meta.env.TASK_ENV_KEY || import.meta.env.GLOBAL_ENV_KEY; - var oneOrTwo = import.meta.env.ENV_VAR_ONE || import.meta.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - () => { return import.meta.env.GLOBAL_ENV_KEY } - () => { return import.meta.env.TASK_ENV_KEY } - () => { return import.meta.env.ENV_VAR_ALLOWED } - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - var foo = process?.env.GLOBAL_ENV_KEY - var foo = process?.env.TASK_ENV_KEY - var foo = process?.env.ENV_VAR_ALLOWED - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - function test1(arg1 = import.meta.env.GLOBAL_ENV_KEY) {}; - function test2(arg1 = import.meta.env.TASK_ENV_KEY) {}; - function test3(arg1 = import.meta.env.ENV_VAR_ALLOWED) {}; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: ` - (arg1 = import.meta.env.GLOBAL_ENV_KEY) => {} - (arg1 = import.meta.env.TASK_ENV_KEY) => {} - (arg1 = import.meta.env.ENV_VAR_ALLOWED) => {} - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ENV_VAR_[A-Z]+$"], - }, - ], - }, - { - code: "const getEnv = (key) => import.meta.env[key];", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "function getEnv(key) { return import.meta.env[key]; }", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - { - code: "for (let x of ['ONE', 'TWO', 'THREE']) { console.log(import.meta.env[x]); }", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - }, - ], - - invalid: [ - { - code: ` - const env2 = import.meta.env['ENV_3']; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - errors: [ - { - message: - "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - ], - }, - { - code: ` - const env2 = import.meta.env["ENV_3"]; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - errors: [ - { - message: - "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - ], - }, - { - code: ` - const { ENV_2 } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/docs/index.js" - ), - errors: [ - { - message: - "ENV_2 is not listed as a dependency in the root turbo.json or workspace (apps/docs) turbo.json", - }, - ], - }, - { - code: ` - const { NEXT_PUBLIC_HAHAHAHA, NEXT_PUBLIC_EXCLUDE, NEXT_PUBLIC_EXCLUDED } = import.meta.env; - `, - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/workspace-configs") }, - ], - filename: path.join( - __dirname, - "../../__fixtures__/workspace-configs/apps/web/index.js" - ), - errors: [ - { - message: - "NEXT_PUBLIC_EXCLUDE is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - { - message: - "NEXT_PUBLIC_EXCLUDED is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", - }, - ], - }, - { - code: "let { X } = import.meta.env;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [{ message: "X is not listed as a dependency in turbo.json" }], - }, - { - code: "const { X, Y, Z } = import.meta.env;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [ - { message: "X is not listed as a dependency in turbo.json" }, - { message: "Y is not listed as a dependency in turbo.json" }, - { message: "Z is not listed as a dependency in turbo.json" }, - ], - }, - { - code: "const { X, Y: NewName, Z } = import.meta.env;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [ - { message: "X is not listed as a dependency in turbo.json" }, - { message: "Y is not listed as a dependency in turbo.json" }, - { message: "Z is not listed as a dependency in turbo.json" }, - ], - }, - { - code: "var x = import.meta.env.NOT_THERE;", - options: [ - { cwd: path.join(__dirname, "../../__fixtures__/configs/single") }, - ], - errors: [ - { - message: "NOT_THERE is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: "var x = import.meta.env.KEY;", - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - allowList: ["^ANOTHER_KEY_[A-Z]+$"], - }, - ], - errors: [{ message: "KEY is not listed as a dependency in turbo.json" }], - }, - { - code: ` - var globalOrTask = import.meta.env.TASK_ENV_KEY_NEW || import.meta.env.GLOBAL_ENV_KEY_NEW; - var oneOrTwo = import.meta.env.ENV_VAR_ONE || import.meta.env.ENV_VAR_TWO; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: "ENV_VAR_ONE is not listed as a dependency in turbo.json", - }, - { - message: "ENV_VAR_TWO is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - () => { return import.meta.env.GLOBAL_ENV_KEY_NEW } - () => { return import.meta.env.TASK_ENV_KEY_NEW } - () => { return import.meta.env.ENV_VAR_NOT_ALLOWED } - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - var foo = process?.env.GLOBAL_ENV_KEY_NEW - var foo = process?.env.TASK_ENV_KEY_NEW - var foo = process?.env.ENV_VAR_NOT_ALLOWED - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - function test1(arg1 = import.meta.env.GLOBAL_ENV_KEY_NEW) {}; - function test2(arg1 = import.meta.env.TASK_ENV_KEY_NEW) {}; - function test3(arg1 = import.meta.env.ENV_VAR_NOT_ALLOWED) {}; - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - { - code: ` - (arg1 = import.meta.env.GLOBAL_ENV_KEY_NEW) => {} - (arg1 = import.meta.env.TASK_ENV_KEY_NEW) => {} - (arg1 = import.meta.env.ENV_VAR_NOT_ALLOWED) => {} - `, - options: [ - { - cwd: path.join(__dirname, "../../__fixtures__/configs/single"), - }, - ], - errors: [ - { - message: - "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", - }, - { - message: - "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", - }, - ], - }, - ], -}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/configs/no-undeclared-env-vars.commonjs.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/configs/no-undeclared-env-vars.commonjs.test.ts new file mode 100644 index 0000000000000..b778eb2688895 --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/configs/no-undeclared-env-vars.commonjs.test.ts @@ -0,0 +1,306 @@ +import path from "node:path"; +import { RuleTester } from "eslint"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020 }, +}); + +const cwd = path.join(__dirname, "../../../../__fixtures__/configs/single"); +const options = (extra: Record = {}) => ({ + options: [ + { + cwd, + ...extra, + }, + ], +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = process.env; + `, + ...options(), + }, + { + code: ` + const { NEW_STYLE_ENV_KEY, TASK_ENV_KEY } = process.env; + `, + ...options(), + }, + { + code: ` + const { NEW_STYLE_GLOBAL_ENV_KEY, TASK_ENV_KEY } = process.env; + `, + ...options(), + }, + { + code: ` + const val = process.env["NEW_STYLE_GLOBAL_ENV_KEY"]; + `, + ...options(), + }, + { + code: ` + const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = process.env; + `, + ...options(), + }, + { + code: ` + const x = process.env.GLOBAL_ENV_KEY; + const { TASK_ENV_KEY, GLOBAL_ENV_KEY: renamedX } = process.env; + `, + ...options(), + }, + { + code: "var x = process.env.GLOBAL_ENV_KEY;", + ...options(), + }, + { + code: "let x = process.env.TASK_ENV_KEY;", + ...options(), + }, + { + code: "const x = process.env.ANOTHER_KEY_VALUE;", + ...options({ + allowList: ["^ANOTHER_KEY_[A-Z]+$"], + }), + }, + { + code: ` + var x = process.env.ENV_VAR_ONE; + var y = process.env.ENV_VAR_TWO; + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + var x = process.env.ENV_VAR_ONE; + var y = process.env.ENV_VAR_TWO; + `, + ...options({ + allowList: ["^ENV_VAR_O[A-Z]+$", "ENV_VAR_TWO"], + }), + }, + { + code: ` + var globalOrTask = process.env.TASK_ENV_KEY || process.env.GLOBAL_ENV_KEY; + var oneOrTwo = process.env.ENV_VAR_ONE || process.env.ENV_VAR_TWO; + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + () => { return process.env.GLOBAL_ENV_KEY } + () => { return process.env.TASK_ENV_KEY } + () => { return process.env.ENV_VAR_ALLOWED } + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + var foo = process?.env.GLOBAL_ENV_KEY + var foo = process?.env.TASK_ENV_KEY + var foo = process?.env.ENV_VAR_ALLOWED + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + function test(arg1 = process.env.GLOBAL_ENV_KEY) {}; + function test(arg1 = process.env.TASK_ENV_KEY) {}; + function test(arg1 = process.env.ENV_VAR_ALLOWED) {}; + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + (arg1 = process.env.GLOBAL_ENV_KEY) => {} + (arg1 = process.env.TASK_ENV_KEY) => {} + (arg1 = process.env.ENV_VAR_ALLOWED) => {} + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: "const getEnv = (key) => process.env[key];", + ...options(), + }, + { + code: "function getEnv(key) { return process.env[key]; }", + ...options(), + }, + { + code: "for (let x of ['ONE', 'TWO', 'THREE']) { console.log(process.env[x]); }", + ...options(), + }, + ], + invalid: [ + { + code: "let { X } = process.env;", + ...options(), + errors: [{ message: "X is not listed as a dependency in turbo.json" }], + }, + { + code: "const { X, Y, Z } = process.env;", + ...options(), + errors: [ + { message: "X is not listed as a dependency in turbo.json" }, + { message: "Y is not listed as a dependency in turbo.json" }, + { message: "Z is not listed as a dependency in turbo.json" }, + ], + }, + { + code: "const { X, Y: NewName, Z } = process.env;", + ...options(), + errors: [ + { message: "X is not listed as a dependency in turbo.json" }, + { message: "Y is not listed as a dependency in turbo.json" }, + { message: "Z is not listed as a dependency in turbo.json" }, + ], + }, + { + code: "var x = process.env.NOT_THERE;", + ...options(), + errors: [ + { + message: "NOT_THERE is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: "var x = process.env.KEY;", + ...options({ + allowList: ["^ANOTHER_KEY_[A-Z]+$"], + }), + + errors: [{ message: "KEY is not listed as a dependency in turbo.json" }], + }, + { + code: ` + var globalOrTask = process.env.TASK_ENV_KEY_NEW || process.env.GLOBAL_ENV_KEY_NEW; + var oneOrTwo = process.env.ENV_VAR_ONE || process.env.ENV_VAR_TWO; + `, + ...options(), + errors: [ + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: "ENV_VAR_ONE is not listed as a dependency in turbo.json", + }, + { + message: "ENV_VAR_TWO is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + () => { return process.env.GLOBAL_ENV_KEY_NEW } + () => { return process.env.TASK_ENV_KEY_NEW } + () => { return process.env.ENV_VAR_NOT_ALLOWED } + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + var foo = process?.env.GLOBAL_ENV_KEY_NEW + var foo = process?.env.TASK_ENV_KEY_NEW + var foo = process?.env.ENV_VAR_NOT_ALLOWED + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + function test(arg1 = process.env.GLOBAL_ENV_KEY_NEW) {}; + function test(arg1 = process.env.TASK_ENV_KEY_NEW) {}; + function test(arg1 = process.env.ENV_VAR_NOT_ALLOWED) {}; + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + (arg1 = process.env.GLOBAL_ENV_KEY_NEW) => {} + (arg1 = process.env.TASK_ENV_KEY_NEW) => {} + (arg1 = process.env.ENV_VAR_NOT_ALLOWED) => {} + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/configs/no-undeclared-env-vars.module.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/configs/no-undeclared-env-vars.module.test.ts new file mode 100644 index 0000000000000..f6d58306b4b55 --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/configs/no-undeclared-env-vars.module.test.ts @@ -0,0 +1,306 @@ +import path from "node:path"; +import { RuleTester } from "eslint"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, +}); + +const cwd = path.join(__dirname, "../../../../__fixtures__/configs/single"); +const options = (extra: Record = {}) => ({ + options: [ + { + cwd, + ...extra, + }, + ], +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = import.meta.env; + `, + ...options(), + }, + { + code: ` + const { NEW_STYLE_ENV_KEY, TASK_ENV_KEY } = import.meta.env; + `, + ...options(), + }, + { + code: ` + const { NEW_STYLE_GLOBAL_ENV_KEY, TASK_ENV_KEY } = import.meta.env; + `, + ...options(), + }, + { + code: ` + const val = import.meta.env["NEW_STYLE_GLOBAL_ENV_KEY"]; + `, + ...options(), + }, + { + code: ` + const { TASK_ENV_KEY, ANOTHER_ENV_KEY } = import.meta.env; + `, + ...options(), + }, + { + code: ` + const x = import.meta.env.GLOBAL_ENV_KEY; + const { TASK_ENV_KEY, GLOBAL_ENV_KEY: renamedX } = import.meta.env; + `, + ...options(), + }, + { + code: "var x = import.meta.env.GLOBAL_ENV_KEY;", + ...options(), + }, + { + code: "let x = import.meta.env.TASK_ENV_KEY;", + ...options(), + }, + { + code: "const x = import.meta.env.ANOTHER_KEY_VALUE;", + ...options({ + allowList: ["^ANOTHER_KEY_[A-Z]+$"], + }), + }, + { + code: ` + var x = import.meta.env.ENV_VAR_ONE; + var y = import.meta.env.ENV_VAR_TWO; + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + var x = import.meta.env.ENV_VAR_ONE; + var y = import.meta.env.ENV_VAR_TWO; + `, + ...options({ + allowList: ["^ENV_VAR_O[A-Z]+$", "ENV_VAR_TWO"], + }), + }, + { + code: ` + var globalOrTask = import.meta.env.TASK_ENV_KEY || import.meta.env.GLOBAL_ENV_KEY; + var oneOrTwo = import.meta.env.ENV_VAR_ONE || import.meta.env.ENV_VAR_TWO; + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + () => { return import.meta.env.GLOBAL_ENV_KEY } + () => { return import.meta.env.TASK_ENV_KEY } + () => { return import.meta.env.ENV_VAR_ALLOWED } + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + var foo = process?.env.GLOBAL_ENV_KEY + var foo = process?.env.TASK_ENV_KEY + var foo = process?.env.ENV_VAR_ALLOWED + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + function test1(arg1 = import.meta.env.GLOBAL_ENV_KEY) {}; + function test2(arg1 = import.meta.env.TASK_ENV_KEY) {}; + function test3(arg1 = import.meta.env.ENV_VAR_ALLOWED) {}; + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: ` + (arg1 = import.meta.env.GLOBAL_ENV_KEY) => {} + (arg1 = import.meta.env.TASK_ENV_KEY) => {} + (arg1 = import.meta.env.ENV_VAR_ALLOWED) => {} + `, + ...options({ + allowList: ["^ENV_VAR_[A-Z]+$"], + }), + }, + { + code: "const getEnv = (key) => import.meta.env[key];", + ...options(), + }, + { + code: "function getEnv(key) { return import.meta.env[key]; }", + ...options(), + }, + { + code: "for (let x of ['ONE', 'TWO', 'THREE']) { console.log(import.meta.env[x]); }", + ...options(), + }, + ], + + invalid: [ + { + code: "let { X } = import.meta.env;", + ...options(), + errors: [{ message: "X is not listed as a dependency in turbo.json" }], + }, + { + code: "const { X, Y, Z } = import.meta.env;", + ...options(), + errors: [ + { message: "X is not listed as a dependency in turbo.json" }, + { message: "Y is not listed as a dependency in turbo.json" }, + { message: "Z is not listed as a dependency in turbo.json" }, + ], + }, + { + code: "const { X, Y: NewName, Z } = import.meta.env;", + ...options(), + errors: [ + { message: "X is not listed as a dependency in turbo.json" }, + { message: "Y is not listed as a dependency in turbo.json" }, + { message: "Z is not listed as a dependency in turbo.json" }, + ], + }, + { + code: "var x = import.meta.env.NOT_THERE;", + ...options(), + errors: [ + { + message: "NOT_THERE is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: "var x = import.meta.env.KEY;", + ...options({ + allowList: ["^ANOTHER_KEY_[A-Z]+$"], + }), + errors: [{ message: "KEY is not listed as a dependency in turbo.json" }], + }, + { + code: ` + var globalOrTask = import.meta.env.TASK_ENV_KEY_NEW || import.meta.env.GLOBAL_ENV_KEY_NEW; + var oneOrTwo = import.meta.env.ENV_VAR_ONE || import.meta.env.ENV_VAR_TWO; + `, + ...options(), + errors: [ + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: "ENV_VAR_ONE is not listed as a dependency in turbo.json", + }, + { + message: "ENV_VAR_TWO is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + () => { return import.meta.env.GLOBAL_ENV_KEY_NEW } + () => { return import.meta.env.TASK_ENV_KEY_NEW } + () => { return import.meta.env.ENV_VAR_NOT_ALLOWED } + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + var foo = process?.env.GLOBAL_ENV_KEY_NEW + var foo = process?.env.TASK_ENV_KEY_NEW + var foo = process?.env.ENV_VAR_NOT_ALLOWED + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + function test1(arg1 = import.meta.env.GLOBAL_ENV_KEY_NEW) {}; + function test2(arg1 = import.meta.env.TASK_ENV_KEY_NEW) {}; + function test3(arg1 = import.meta.env.ENV_VAR_NOT_ALLOWED) {}; + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: ` + (arg1 = import.meta.env.GLOBAL_ENV_KEY_NEW) => {} + (arg1 = import.meta.env.TASK_ENV_KEY_NEW) => {} + (arg1 = import.meta.env.ENV_VAR_NOT_ALLOWED) => {} + `, + ...options(), + errors: [ + { + message: + "GLOBAL_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "TASK_ENV_KEY_NEW is not listed as a dependency in turbo.json", + }, + { + message: + "ENV_VAR_NOT_ALLOWED is not listed as a dependency in turbo.json", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/framework-inference/no-undeclared-env-vars.commonjs.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/framework-inference/no-undeclared-env-vars.commonjs.test.ts new file mode 100644 index 0000000000000..2e48a404e8e6b --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/framework-inference/no-undeclared-env-vars.commonjs.test.ts @@ -0,0 +1,77 @@ +import path from "node:path"; +import { RuleTester } from "eslint"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020 }, +}); + +const cwd = path.join( + __dirname, + "../../../../__fixtures__/framework-inference" +); +const nextJsFilename = path.join(cwd, "/apps/nextjs/index.js"); +const viteFilename = path.join(cwd, "/apps/vite/index.js"); +const kitchenSinkFilename = path.join(cwd, "/apps/kitchen-sink/index.js"); +const options = (extra: Record = {}) => ({ + options: [ + { + cwd, + ...extra, + }, + ], +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: `const { NEXT_PUBLIC_ZILTOID } = process.env;`, + ...options(), + filename: nextJsFilename, + }, + { + code: `const { VITE_THINGS } = process.env;`, + ...options(), + filename: viteFilename, + }, + { + code: `const { NEXT_PUBLIC_ZILTOID, GATSBY_THE, NITRO_OMNISCIENT } = process.env;`, + ...options(), + filename: kitchenSinkFilename, + }, + ], + invalid: [ + { + code: `const { NEXT_PUBLIC_ZILTOID } = process.env;`, + ...options(), + filename: viteFilename, + errors: [ + { + message: + "NEXT_PUBLIC_ZILTOID is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: `const { VITE_THINGS } = process.env;`, + ...options(), + filename: nextJsFilename, + errors: [ + { + message: "VITE_THINGS is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: `const { VITE_THINGS } = process.env;`, + ...options(), + filename: kitchenSinkFilename, + errors: [ + { + message: "VITE_THINGS is not listed as a dependency in turbo.json", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/framework-inference/no-undeclared-env-vars.module.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/framework-inference/no-undeclared-env-vars.module.test.ts new file mode 100644 index 0000000000000..bd8ab7737b122 --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/framework-inference/no-undeclared-env-vars.module.test.ts @@ -0,0 +1,77 @@ +import path from "node:path"; +import { RuleTester } from "eslint"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, +}); + +const cwd = path.join( + __dirname, + "../../../../__fixtures__/framework-inference" +); +const nextJsFilename = path.join(cwd, "/apps/nextjs/index.js"); +const viteFilename = path.join(cwd, "/apps/vite/index.js"); +const kitchenSinkFilename = path.join(cwd, "/apps/kitchen-sink/index.js"); +const options = (extra: Record = {}) => ({ + options: [ + { + cwd, + ...extra, + }, + ], +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: `const { NEXT_PUBLIC_ZILTOID } = import.meta.env;`, + ...options(), + filename: nextJsFilename, + }, + { + code: `const { VITE_THINGS } = import.meta.env;`, + ...options(), + filename: viteFilename, + }, + { + code: `const { NEXT_PUBLIC_ZILTOID, GATSBY_THE, NITRO_OMNISCIENT } = import.meta.env;`, + ...options(), + filename: kitchenSinkFilename, + }, + ], + invalid: [ + { + code: `const { NEXT_PUBLIC_ZILTOID } = import.meta.env;`, + ...options(), + filename: viteFilename, + errors: [ + { + message: + "NEXT_PUBLIC_ZILTOID is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: `const { VITE_THINGS } = import.meta.env;`, + ...options(), + filename: nextJsFilename, + errors: [ + { + message: "VITE_THINGS is not listed as a dependency in turbo.json", + }, + ], + }, + { + code: `const { VITE_THINGS } = import.meta.env;`, + ...options(), + filename: kitchenSinkFilename, + errors: [ + { + message: "VITE_THINGS is not listed as a dependency in turbo.json", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/no-undeclared-env-vars.commonjs.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/no-undeclared-env-vars.commonjs.test.ts new file mode 100644 index 0000000000000..81e40b3b79a00 --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/no-undeclared-env-vars.commonjs.test.ts @@ -0,0 +1,25 @@ +import { RuleTester } from "eslint"; +import { RULES } from "../../../lib/constants"; +import rule from "../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020 }, +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const { TZ } = process.env; + `, + options: [{ cwd: "/some/random/path" }], + }, + { + code: ` + const { ENV_1 } = process.env; + `, + options: [{ cwd: "/some/random/path" }], + }, + ], + invalid: [], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/no-undeclared-env-vars.module.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/no-undeclared-env-vars.module.test.ts new file mode 100644 index 0000000000000..ee75c05f50d24 --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/no-undeclared-env-vars.module.test.ts @@ -0,0 +1,25 @@ +import { RuleTester } from "eslint"; +import { RULES } from "../../../lib/constants"; +import rule from "../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const { TZ } = import.meta.env; + `, + options: [{ cwd: "/some/random/path" }], + }, + { + code: ` + const { ENV_1 } = import.meta.env; + `, + options: [{ cwd: "/some/random/path" }], + }, + ], + invalid: [], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/no-undeclared-env-vars.commonjs.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/no-undeclared-env-vars.commonjs.test.ts new file mode 100644 index 0000000000000..de6129ae2b92a --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/no-undeclared-env-vars.commonjs.test.ts @@ -0,0 +1,132 @@ +import path from "node:path"; +import { RuleTester } from "eslint"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020 }, +}); + +const cwd = path.join(__dirname, "../../../../__fixtures__/workspace-configs"); +const webFilename = path.join(cwd, "/apps/web/index.js"); +const docsFilename = path.join(cwd, "/apps/docs/index.js"); +const options = (extra: Record = {}) => ({ + options: [ + { + cwd, + ...extra, + }, + ], +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const env2 = process.env['ENV_2']; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const env2 = process.env["ENV_2"]; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { ENV_2 } = process.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { ROOT_DOT_ENV, WEB_DOT_ENV } = process.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { NEXT_PUBLIC_HAHAHAHA } = process.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { ENV_1 } = process.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { CI } = process.env; + `, + ...options(), + filename: webFilename, + }, + ], + invalid: [ + { + code: ` + const env2 = process.env['ENV_3']; + `, + ...options(), + filename: webFilename, + errors: [ + { + message: + "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + ], + }, + { + code: ` + const env2 = process.env["ENV_3"]; + `, + ...options(), + filename: webFilename, + errors: [ + { + message: + "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + ], + }, + { + code: ` + const { ENV_2 } = process.env; + `, + ...options(), + filename: docsFilename, + errors: [ + { + message: + "ENV_2 is not listed as a dependency in the root turbo.json or workspace (apps/docs) turbo.json", + }, + ], + }, + { + code: ` + const { NEXT_PUBLIC_HAHAHAHA, NEXT_PUBLIC_EXCLUDE, NEXT_PUBLIC_EXCLUDED } = process.env; + `, + ...options(), + filename: webFilename, + errors: [ + { + message: + "NEXT_PUBLIC_EXCLUDE is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + { + message: + "NEXT_PUBLIC_EXCLUDED is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/no-undeclared-env-vars.module.test.ts b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/no-undeclared-env-vars.module.test.ts new file mode 100644 index 0000000000000..5ca66e6b4761f --- /dev/null +++ b/packages/eslint-plugin-turbo/__tests__/lib/no-undeclared-env-vars/workspace-configs/no-undeclared-env-vars.module.test.ts @@ -0,0 +1,132 @@ +import path from "node:path"; +import { RuleTester } from "eslint"; +import { RULES } from "../../../../lib/constants"; +import rule from "../../../../lib/rules/no-undeclared-env-vars"; + +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"); +const docsFilename = path.join(cwd, "/apps/docs/index.js"); +const options = (extra: Record = {}) => ({ + options: [ + { + cwd, + ...extra, + }, + ], +}); + +ruleTester.run(RULES.noUndeclaredEnvVars, rule, { + valid: [ + { + code: ` + const env2 = import.meta.env['ENV_2']; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const env2 = import.meta.env["ENV_2"]; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { ENV_2 } = import.meta.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { ROOT_DOT_ENV, WEB_DOT_ENV } = import.meta.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { NEXT_PUBLIC_HAHAHAHA } = import.meta.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { ENV_1 } = import.meta.env; + `, + ...options(), + filename: webFilename, + }, + { + code: ` + const { CI } = import.meta.env; + `, + ...options(), + filename: webFilename, + }, + ], + invalid: [ + { + code: ` + const env2 = import.meta.env['ENV_3']; + `, + ...options(), + filename: webFilename, + errors: [ + { + message: + "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + ], + }, + { + code: ` + const env2 = import.meta.env["ENV_3"]; + `, + ...options(), + filename: webFilename, + errors: [ + { + message: + "ENV_3 is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + ], + }, + { + code: ` + const { ENV_2 } = import.meta.env; + `, + ...options(), + filename: docsFilename, + errors: [ + { + message: + "ENV_2 is not listed as a dependency in the root turbo.json or workspace (apps/docs) turbo.json", + }, + ], + }, + { + code: ` + const { NEXT_PUBLIC_HAHAHAHA, NEXT_PUBLIC_EXCLUDE, NEXT_PUBLIC_EXCLUDED } = import.meta.env; + `, + ...options(), + filename: webFilename, + errors: [ + { + message: + "NEXT_PUBLIC_EXCLUDE is not listed as a dependency in the root turbo.json or workspace (apps/web) turbo.json", + }, + { + message: + "NEXT_PUBLIC_EXCLUDED 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/index.ts b/packages/eslint-plugin-turbo/lib/index.ts index 02cf4ae77ec12..44bd9013f327b 100644 --- a/packages/eslint-plugin-turbo/lib/index.ts +++ b/packages/eslint-plugin-turbo/lib/index.ts @@ -1,7 +1,5 @@ import { RULES } from "./constants"; -// rules import noUndeclaredEnvVars from "./rules/no-undeclared-env-vars"; -// configs import recommended from "./configs/recommended"; const rules = { 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 3bd6945a8da98..7b49d59afd8ee 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 @@ -1,10 +1,18 @@ import path from "node:path"; +import { readFileSync } from "node:fs"; import type { Rule } from "eslint"; import type { Node, MemberExpression } from "estree"; -import { logger } from "@turbo/utils"; +import { type PackageJson, logger, searchUp } from "@turbo/utils"; +import { frameworks } from "@turbo/types"; import { RULES } from "../constants"; import { Project, getWorkspaceFromFilePath } from "../utils/calculate-inputs"; +const debug = process.env.RUNNER_DEBUG + ? logger.info + : (_: string) => { + /* noop */ + }; + export interface RuleContextWithOptions extends Rule.RuleContext { options: Array<{ cwd?: string; @@ -67,11 +75,88 @@ function normalizeCwd( return undefined; } +/** for a given `package.json` file path, this will compile a Set of that package's listed dependencies */ +const packageJsonDependencies = (filePath: string): Set => { + // get the contents of the package.json + let packageJsonString; + + try { + packageJsonString = readFileSync(filePath, "utf-8"); + } catch (e) { + logger.error(`Could not read package.json at ${filePath}`); + return new Set(); + } + + let packageJson: PackageJson; + try { + packageJson = JSON.parse(packageJsonString) as PackageJson; + } catch (e) { + logger.error(`Could not parse package.json at ${filePath}`); + return new Set(); + } + + return ( + [ + "dependencies", + "devDependencies", + "peerDependencies", + // intentionally not including `optionalDependencies` or `bundleDependencies` because at the time of writing they are not used for any of the frameworks we support + ] as const + ) + .flatMap((key) => Object.keys(packageJson[key] ?? {})) + .reduce((acc, dependency) => acc.add(dependency), new Set()); +}; + +/** + * Turborepo does some nice framework detection based on the dependencies in the package.json. This function ports that logic to this ESLint rule. + * + * Imagine you have a Vue app. That means you have Vue in your `package.json` dependencies. This function will return a list of regular expressions that match the environment variables that Vue depends on, which is information encoded into the `frameworks.json` file. In Vue's case, it would return the regex `VUE_APP_*` since you have `@vue/cli-service` in your dependencies. + */ +const frameworkEnvMatches = (filePath: string): Set => { + const directory = path.dirname(filePath); + const packageJsonDir = searchUp({ cwd: directory, target: "package.json" }); + if (!packageJsonDir) { + logger.error(`Could not determine package for ${filePath}`); + return new Set(); + } + debug(`found package.json in: ${packageJsonDir}`); + + const dependencies = packageJsonDependencies( + `${packageJsonDir}/package.json` + ); + const hasDependency = (dep: string) => dependencies.has(dep); + debug(`dependencies for ${filePath}: ${Array.from(dependencies).join(",")}`); + + return frameworks.reduce( + ( + acc, + { + dependencyMatch: { dependencies: searchDependencies, strategy }, + envWildcards, + } + ) => { + const hasMatch = + strategy === "all" + ? searchDependencies.every(hasDependency) + : searchDependencies.some(hasDependency); + + if (hasMatch) { + return new Set([ + ...acc, + ...envWildcards.map((envWildcard) => RegExp(envWildcard)), + ]); + } + return acc; + }, + new Set() + ); +}; + function create(context: RuleContextWithOptions): Rule.RuleListener { const { options } = context; const allowList: Array = options[0]?.allowList || []; - const regexAllowList: Array = []; + let regexAllowList: Array = []; allowList.forEach((allowed) => { try { regexAllowList.push(new RegExp(allowed)); @@ -81,6 +166,17 @@ function create(context: RuleContextWithOptions): Rule.RuleListener { } }); + const filename = context.getFilename(); + debug(`Checking file: ${filename}`); + + const matches = frameworkEnvMatches(filename); + regexAllowList = [...regexAllowList, ...matches]; + debug( + `Allow list: ${regexAllowList.map((r) => r.source).join(",")}, ${ + regexAllowList.length + }` + ); + const cwd = normalizeCwd( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- needed to support older eslint versions context.getCwd ? context.getCwd() : undefined, diff --git a/packages/eslint-plugin-turbo/package.json b/packages/eslint-plugin-turbo/package.json index a56dfd4d22375..1165f6c0525ea 100644 --- a/packages/eslint-plugin-turbo/package.json +++ b/packages/eslint-plugin-turbo/package.json @@ -19,6 +19,7 @@ }, "author": "Vercel", "main": "./dist/index.js", + "types": "./dist/index.d.ts", "files": [ "dist/**" ], diff --git a/packages/eslint-plugin-turbo/tsup.config.ts b/packages/eslint-plugin-turbo/tsup.config.ts index bbda8cb604ddc..12dc08af087d4 100644 --- a/packages/eslint-plugin-turbo/tsup.config.ts +++ b/packages/eslint-plugin-turbo/tsup.config.ts @@ -1,8 +1,9 @@ -import { defineConfig, Options } from "tsup"; +import { defineConfig, type Options } from "tsup"; export default defineConfig((options: Options) => ({ entry: ["lib/index.ts"], clean: true, minify: true, + dts: true, ...options, })); diff --git a/packages/turbo-benchmark/README.md b/packages/turbo-benchmark/README.md index 68946de6a3b46..2fe53013c516d 100644 --- a/packages/turbo-benchmark/README.md +++ b/packages/turbo-benchmark/README.md @@ -3,5 +3,5 @@ To run benchmarks for turborepo 1. Follow the [Building Turborepo](../CONTRIBUTING.md#building-turborepo) instructions to install dependencies -2. `cargo build -p turbo --profile release-turborepo` to build turbo +2. `cargo build --package turbo --profile release-turborepo` to build turbo 3. From this directory `pnpm run benchmark` diff --git a/packages/turbo-types/src/index.ts b/packages/turbo-types/src/index.ts index b6f907d11490b..99cef354039ec 100644 --- a/packages/turbo-types/src/index.ts +++ b/packages/turbo-types/src/index.ts @@ -1,3 +1,10 @@ +import type { Framework as FW } from "./types/frameworks"; +import frameworksJson from "./json/frameworks.json"; + +export const frameworks = frameworksJson as Array; +export type Framework = FW; +export type { FrameworkStrategy } from "./types/frameworks"; + export { type BaseSchema, type BaseSchema as BaseSchemaV2, diff --git a/packages/turbo-types/src/json/frameworks.json b/packages/turbo-types/src/json/frameworks.json new file mode 100644 index 0000000000000..f89c82e79824b --- /dev/null +++ b/packages/turbo-types/src/json/frameworks.json @@ -0,0 +1,119 @@ +[ + { + "slug": "astro", + "name": "Astro", + "envWildcards": ["PUBLIC_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["astro"] + } + }, + { + "slug": "blitzjs", + "name": "Blitz", + "envWildcards": ["NEXT_PUBLIC_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["blitz"] + } + }, + { + "slug": "create-react-app", + "name": "Create React App", + "envWildcards": ["REACT_APP_*"], + "dependencyMatch": { + "strategy": "some", + "dependencies": ["react-scripts", "react-dev-utils"] + } + }, + { + "slug": "gatsby", + "name": "Gatsby", + "envWildcards": ["GATSBY_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["gatsby"] + } + }, + { + "slug": "nextjs", + "name": "Next.js", + "envWildcards": ["NEXT_PUBLIC_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["next"] + } + }, + { + "slug": "nitro", + "name": "Nitro", + "envWildcards": ["NITRO_*"], + "dependencyMatch": { + "strategy": "some", + "dependencies": ["nitropack", "nitropack-nightly"] + } + }, + { + "slug": "nuxtjs", + "name": "Nuxt.js", + "envWildcards": ["NUXT_*", "NITRO_*"], + "dependencyMatch": { + "strategy": "some", + "dependencies": ["nuxt", "nuxt-edge", "nuxt3", "nuxt3-edge"] + } + }, + { + "slug": "redwoodjs", + "name": "RedwoodJS", + "envWildcards": ["REDWOOD_ENV_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["@redwoodjs/core"] + } + }, + { + "slug": "sanity", + "name": "Sanity Studio", + "envWildcards": ["SANITY_STUDIO_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["@sanity/cli"] + } + }, + { + "slug": "solidstart", + "name": "Solid", + "envWildcards": ["VITE_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["solid-js", "solid-start"] + } + }, + { + "slug": "sveltekit", + "name": "SvelteKit", + "envWildcards": ["VITE_*", "PUBLIC_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["@sveltejs/kit"] + } + }, + { + "slug": "vite", + "name": "Vite", + "envWildcards": ["VITE_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["vite"] + } + }, + { + "slug": "vue", + "name": "Vue", + "envWildcards": ["VUE_APP_*"], + "dependencyMatch": { + "strategy": "all", + "dependencies": ["@vue/cli-service"] + } + } +] diff --git a/packages/turbo-types/src/types/frameworks.ts b/packages/turbo-types/src/types/frameworks.ts new file mode 100644 index 0000000000000..1f91e4f958474 --- /dev/null +++ b/packages/turbo-types/src/types/frameworks.ts @@ -0,0 +1,11 @@ +export type FrameworkStrategy = "all" | "some"; + +export interface Framework { + slug: string; + name: string; + envWildcards: Array; + dependencyMatch: { + strategy: FrameworkStrategy; + dependencies: Array; + }; +} diff --git a/packages/turbo-utils/src/searchUp.ts b/packages/turbo-utils/src/searchUp.ts index 0c5c4b7eb77dd..8b9de5bc5afcc 100644 --- a/packages/turbo-utils/src/searchUp.ts +++ b/packages/turbo-utils/src/searchUp.ts @@ -1,13 +1,23 @@ import fs from "node:fs"; import path from "node:path"; +/** + * recursively search up the file tree looking for a `target` file, starting with the provided `cwd` + * + * If found, return the directory containing the file. If not found, return null. + */ export function searchUp({ target, cwd, contentCheck, }: { + /** The name of the file we're looking for */ target: string; + + /** The directory to start the search */ cwd: string; + + /** a predicate for examining the content of any found file */ contentCheck?: (content: string) => boolean; }): string | null { const root = path.parse(cwd).root;