From ae50644091495991e410c203f76476fcd0d37741 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Fri, 6 Feb 2026 13:03:05 +0100 Subject: [PATCH] refactor(tooling): add turbo eslint plugin (#65734) --- api/src/utils/env.ts | 7 +-- api/turbo.json | 45 +++++++++++++++++++ client/gatsby-config.js | 5 +++ curriculum/turbo.json | 8 ++-- e2e/certification.spec.ts | 2 +- e2e/eslint.config.mjs | 3 +- e2e/report-user.spec.ts | 2 +- e2e/update-email.spec.ts | 2 +- e2e/utils/{mailhog.ts => email.ts} | 7 +-- packages/eslint-config/base.js | 2 + packages/eslint-config/package.json | 1 + pnpm-lock.yaml | 31 ++++++++++--- .../helpers/get-file-name.test.ts | 1 + .../helpers/get-project-info.test.ts | 1 + .../helpers/get-project-info.ts | 1 + .../helpers/project-metadata.test.ts | 1 + tools/challenge-helper-scripts/utils.test.ts | 1 + turbo.json | 1 + 18 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 api/turbo.json rename e2e/utils/{mailhog.ts => email.ts} (69%) diff --git a/api/src/utils/env.ts b/api/src/utils/env.ts index 98c1fd5f2fb..06d51611839 100644 --- a/api/src/utils/env.ts +++ b/api/src/utils/env.ts @@ -159,12 +159,7 @@ if (process.env.FREECODECAMP_NODE_ENV !== 'development') { export const HOME_LOCATION = process.env.HOME_LOCATION; // Mailpit is used in development and test environments, hence the localhost // default. -// TODO: Remove MAILHOG_HOST in a few months -// We renamed MailHog to MailPit, but kept the same port and API -// This is to keep backward compatibility with existing setups -// that might still use MAILHOG_HOST environment variable -export const MAILPIT_HOST = - process.env.MAILPIT_HOST ?? process.env.MAILHOG_HOST ?? 'localhost'; +export const MAILPIT_HOST = process.env.MAILPIT_HOST ?? 'localhost'; export const MONGOHQ_URL = process.env.NODE_ENV === 'test' ? createTestConnectionURL( diff --git a/api/turbo.json b/api/turbo.json new file mode 100644 index 00000000000..8107abf4d92 --- /dev/null +++ b/api/turbo.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://v2-8-1.turborepo.dev/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "env": [ + "API_LOCATION", + "AUTH0_CLIENT_ID", + "AUTH0_CLIENT_SECRET", + "AUTH0_DOMAIN", + "COOKIE_DOMAIN", + "COOKIE_SECRET", + "DEPLOYMENT_ENV", + "DEPLOYMENT_VERSION", + "EMAIL_PROVIDER", + "FCC_API_LOG_LEVEL", + "FCC_API_LOG_TRANSPORT", + "FCC_ENABLE_DEV_LOGIN_MODE", + "FCC_ENABLE_SENTRY_ROUTES", + "FCC_ENABLE_SHADOW_CAPTURE", + "FCC_ENABLE_SWAGGER_UI", + "FCC_ENABLE_TEST_LOGGING", + "FREECODECAMP_NODE_ENV", + "GROWTHBOOK_FASTIFY_API_HOST", + "GROWTHBOOK_FASTIFY_CLIENT_KEY", + "HOME_LOCATION", + "HOST", + "JWT_SECRET", + "MAILPIT_HOST", + "NODE_ENV", + "PORT", + "SENTRY_DSN", + "SENTRY_ENVIRONMENT", + "SES_ID", + "SES_REGION", + "SES_SECRET", + "SHOW_UPCOMING_CHANGES", + "STRIPE_SECRET_KEY" + ] + }, + "test": { + "passThroughEnv": ["VITEST_WORKER_ID"] + } + } +} diff --git a/client/gatsby-config.js b/client/gatsby-config.js index cadc83e27dc..d693406d68d 100644 --- a/client/gatsby-config.js +++ b/client/gatsby-config.js @@ -26,6 +26,11 @@ module.exports = { resolve: 'gatsby-plugin-webpack-bundle-analyser-v2', options: { analyzerMode: 'disabled', + // It doesn't matter if the file is generated or not as far as caching + // is concerned. It doesn't affect any tasks in any way, so we can + // ignore it. + + // eslint-disable-next-line turbo/no-undeclared-env-vars generateStatsFile: process.env.CI } }, diff --git a/curriculum/turbo.json b/curriculum/turbo.json index ede8b6407bb..30755fb01ca 100644 --- a/curriculum/turbo.json +++ b/curriculum/turbo.json @@ -4,13 +4,15 @@ "tasks": { "build": { "outputs": ["dist/**", "generated/**"], - "env": ["FCC_*"] + "env": ["FCC_*", "SHOW_UPCOMING_CHANGES"] }, "test": { - "env": ["FCC_*"] + "passThroughEnv": ["VITEST_POOL_ID", "PUPPETEER_WS_ENDPOINT"], + "env": ["FCC_*", "CURRICULUM_LOCALE", "SHOW_UPCOMING_CHANGES"] }, "test-content": { - "env": ["FCC_*"] + "passThroughEnv": ["VITEST_POOL_ID", "PUPPETEER_WS_ENDPOINT"], + "env": ["FCC_*", "CURRICULUM_LOCALE", "SHOW_UPCOMING_CHANGES"] } } } diff --git a/e2e/certification.spec.ts b/e2e/certification.spec.ts index 60916039a7a..01ad4652349 100644 --- a/e2e/certification.spec.ts +++ b/e2e/certification.spec.ts @@ -7,7 +7,7 @@ import { getAllEmails, getFirstEmail, getSubject -} from './utils/mailhog'; +} from './utils/email'; test.describe('Claim a certification - almost certified user', () => { test.beforeEach(async () => { diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index 2318beabaea..89a59c8bffa 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -6,6 +6,7 @@ export default defineConfig(globalIgnores(['./playwright']), { rules: { '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off' + '@typescript-eslint/no-unsafe-assignment': 'off', + 'turbo/no-undeclared-env-vars': 'off' // If/when we make e2e tests into a turbo task, we can enable this rule again. } }); diff --git a/e2e/report-user.spec.ts b/e2e/report-user.spec.ts index 63fb341e1a0..6524173356e 100644 --- a/e2e/report-user.spec.ts +++ b/e2e/report-user.spec.ts @@ -4,7 +4,7 @@ import { getAllEmails, getFirstEmail, getSubject -} from './utils/mailhog'; +} from './utils/email'; test.beforeEach(async () => { await deleteAllEmails(); diff --git a/e2e/update-email.spec.ts b/e2e/update-email.spec.ts index 1dc1dbcfb5d..db878cf9145 100644 --- a/e2e/update-email.spec.ts +++ b/e2e/update-email.spec.ts @@ -6,7 +6,7 @@ import { getAllEmails, getFirstEmail, getSubject -} from './utils/mailhog'; +} from './utils/email'; test.beforeEach(async () => { await deleteAllEmails(); diff --git a/e2e/utils/mailhog.ts b/e2e/utils/email.ts similarity index 69% rename from e2e/utils/mailhog.ts rename to e2e/utils/email.ts index 513b21970bc..c63bd0ad9bc 100644 --- a/e2e/utils/mailhog.ts +++ b/e2e/utils/email.ts @@ -11,12 +11,7 @@ type AllEmails = { count: number; }; -// TODO: Remove MAILHOG_HOST in a few months -// We renamed MailHog to MailPit, but kept the same port and API -// This is to keep backward compatibility with existing setups -// that might still use MAILHOG_HOST environment variable -const host = - process.env.MAILPIT_HOST || process.env.MAILHOG_HOST || 'localhost'; +const host = process.env.MAILPIT_HOST || 'localhost'; export const getAllEmails = async (): Promise => { const res = await fetch(`http://${host}:8025/api/v1/messages`); diff --git a/packages/eslint-config/base.js b/packages/eslint-config/base.js index 3aeaf29bb78..fee3abccce9 100644 --- a/packages/eslint-config/base.js +++ b/packages/eslint-config/base.js @@ -11,6 +11,7 @@ import jsxAllyPlugin from 'eslint-plugin-jsx-a11y'; import importPlugin from 'eslint-plugin-import'; import testingLibraryPlugin from 'eslint-plugin-testing-library'; import babelParser from '@babel/eslint-parser'; // TODO: can we get away from using babel? +import turbo from 'eslint-plugin-turbo'; import { FlatCompat } from '@eslint/eslintrc'; @@ -32,6 +33,7 @@ const testFiles = [ const base = defineConfig( globalIgnores(['dist', '.turbo']), + turbo.configs['flat/recommended'], js.configs.recommended, eslintConfigPrettier, { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 75df8f0c11d..bf3e4dbb00a 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -27,6 +27,7 @@ "eslint-plugin-react": "7.37.4", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-testing-library": "7.1.1", + "eslint-plugin-turbo": "^2.8.3", "typescript": "5.9.3", "typescript-eslint": "^8.47.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3f1205818..739a5503b3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -936,6 +936,9 @@ importers: eslint-plugin-testing-library: specifier: 7.1.1 version: 7.1.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-turbo: + specifier: ^2.8.3 + version: 2.8.3(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.1) typescript: specifier: 5.9.3 version: 5.9.3 @@ -7617,6 +7620,10 @@ packages: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} + dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -7998,6 +8005,12 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 + eslint-plugin-turbo@2.8.3: + resolution: {integrity: sha512-9ACQrrjzOfrbBGG1CqzyC67NtOSRcA+vyc9cjbyLyIoVtcK27czO7/WM+R5K3Opz0fb4Uezo6X+csMfL//RfJQ==} + peerDependencies: + eslint: '>6.6.0' + turbo: '>2.0.0' + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -23399,6 +23412,8 @@ snapshots: dotenv-expand@10.0.0: {} + dotenv@16.0.3: {} + dotenv@16.4.5: {} dotenv@16.6.1: {} @@ -23793,7 +23808,7 @@ snapshots: confusing-browser-globals: 1.0.11 eslint: 7.32.0 eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.4(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 4.6.0(eslint@9.39.2(jiti@2.6.1)) @@ -23823,7 +23838,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -23877,7 +23892,7 @@ snapshots: - typescript - utf-8-validate - eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -23888,7 +23903,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -24023,6 +24038,12 @@ snapshots: - supports-color - typescript + eslint-plugin-turbo@2.8.3(eslint@9.39.2(jiti@2.6.1))(turbo@2.8.1): + dependencies: + dotenv: 16.0.3 + eslint: 9.39.2(jiti@2.6.1) + turbo: 2.8.1 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -25118,7 +25139,7 @@ snapshots: eslint-config-react-app: 6.0.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.0(eslint@7.32.0))(eslint-plugin-react@7.37.4(eslint@7.32.0))(eslint@7.32.0)(typescript@5.9.3) eslint-plugin-flowtype: 5.10.0(eslint@7.32.0) eslint-plugin-graphql: 4.0.0(@types/node@24.10.9)(graphql@15.8.0)(typescript@5.9.3) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.4(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 4.6.0(eslint@9.39.2(jiti@2.6.1)) diff --git a/tools/challenge-helper-scripts/helpers/get-file-name.test.ts b/tools/challenge-helper-scripts/helpers/get-file-name.test.ts index 5203f105b46..0338c04f755 100644 --- a/tools/challenge-helper-scripts/helpers/get-file-name.test.ts +++ b/tools/challenge-helper-scripts/helpers/get-file-name.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ import fs from 'fs'; import { join } from 'path'; diff --git a/tools/challenge-helper-scripts/helpers/get-project-info.test.ts b/tools/challenge-helper-scripts/helpers/get-project-info.test.ts index fba6d102068..995a924f63a 100644 --- a/tools/challenge-helper-scripts/helpers/get-project-info.test.ts +++ b/tools/challenge-helper-scripts/helpers/get-project-info.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ import { describe, it, expect } from 'vitest'; import { getProjectName, getProjectPath } from './get-project-info.js'; diff --git a/tools/challenge-helper-scripts/helpers/get-project-info.ts b/tools/challenge-helper-scripts/helpers/get-project-info.ts index 97414c118b2..08fdbd413af 100644 --- a/tools/challenge-helper-scripts/helpers/get-project-info.ts +++ b/tools/challenge-helper-scripts/helpers/get-project-info.ts @@ -1,4 +1,5 @@ export function getProjectPath(): string { + // eslint-disable-next-line turbo/no-undeclared-env-vars return (process.env.INIT_CWD || process.cwd()) + '/'; } diff --git a/tools/challenge-helper-scripts/helpers/project-metadata.test.ts b/tools/challenge-helper-scripts/helpers/project-metadata.test.ts index 19fd52f0ceb..83736382ed1 100644 --- a/tools/challenge-helper-scripts/helpers/project-metadata.test.ts +++ b/tools/challenge-helper-scripts/helpers/project-metadata.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ import { join } from 'path'; import { describe, it, expect, vi } from 'vitest'; import { getBlockStructure } from '@freecodecamp/curriculum/file-handler'; diff --git a/tools/challenge-helper-scripts/utils.test.ts b/tools/challenge-helper-scripts/utils.test.ts index 801066feb1c..b5b12f982bb 100644 --- a/tools/challenge-helper-scripts/utils.test.ts +++ b/tools/challenge-helper-scripts/utils.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable turbo/no-undeclared-env-vars */ import fs from 'fs'; import path, { join } from 'path'; import matter from 'gray-matter'; diff --git a/turbo.json b/turbo.json index 6a5c8ad034a..36961f84684 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,6 @@ { "$schema": "https://v2-8-1.turborepo.dev/schema.json", + "globalPassThroughEnv": ["MONGOHQ_URL"], "tasks": { "build": { "dependsOn": ["setup"], "outputs": ["dist/**"] }, "develop": { "dependsOn": ["setup"], "cache": false, "persistent": true },