From 0ecffa354c66cf85e24062e6483142e520a0f26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Tue, 28 Jul 2026 10:44:32 +0100 Subject: [PATCH] Replace dotenv with Node's built-in --env-file-if-exists (#929) * Replace dotenv with Node's built-in --env-file-if-exists Node 24 (this repo's runtime) has a built-in flag that loads a .env file when present and silently no-ops when absent - exactly the behavior dotenv/config provided. Remove the dotenv package from server and db, and switch all invocations to . Key details: - (not ) preserves the silent no-op when .env is absent, matching dotenv/config. CI/prod get env from the docker-compose directive, so .env is not present there. - is NOT allowed in NODE_OPTIONS, so the test scripts were restructured to invoke directly while keeping the ts-node loader in NODE_OPTIONS. - Removed from 4 test/e2e fixture files; env is now loaded by the node flag before any module reads process.env. - Removed dotenv from db/knip.json ignoreDependencies (no longer needed). - Updated db/src/index.ts shebang and db/README.md. uuid was NOT removed: the server uses v1 (timestamp) UUIDs in 23 files, and crypto.randomUUID() only generates v4 - there is no Node built-in replacement for v1. Co-Authored-By: pi * Remove redundant env-loading comments The comments restated the env-loading mechanism; removing them keeps the files clean. Behavior unchanged. Co-Authored-By: pi * Load env in test:e2e via --env-file-if-exists The e2e suite boots the IoC container (via the coop.ts fixture), which reads env vars like UI_URL at module load. Removing the `import 'dotenv/config'` from the fixture left nothing to load server/.env when playwright runs, so CI failed with "Missing env var UI_URL". Invoke playwright through `node --env-file-if-exists=.env` (the same pattern used for jest) so env loads before any test module imports the container. CI gets server/.env from the workflow's `cp server/.env.example server/.env` step; the -if-exists variant no-ops if it's absent. Co-Authored-By: pi --- db/README.md | 2 +- db/knip.json | 2 +- db/package-lock.json | 14 ----------- db/package.json | 1 - db/src/index.ts | 2 +- package.json | 10 ++++---- server/e2e/fixtures/coop.ts | 3 --- server/package-lock.json | 10 -------- server/package.json | 25 +++++++++---------- .../harness/transactionalPgPool.integ.test.ts | 2 -- .../test/integ/ncmec-submission.integ.test.ts | 2 -- server/test/integ/setupIntegrationServer.ts | 4 --- 12 files changed, 20 insertions(+), 57 deletions(-) diff --git a/db/README.md b/db/README.md index 48b0ca1..665fc46 100644 --- a/db/README.md +++ b/db/README.md @@ -6,7 +6,7 @@ A Node.js CLI for running database migrations and seeds. ``` npm install -node --loader ts-node/esm --require dotenv/config index.ts +node --env-file-if-exists=.env --loader ts-node/esm index.ts ``` This displays available commands and arguments. diff --git a/db/knip.json b/db/knip.json index 55d171c..134f5d3 100644 --- a/db/knip.json +++ b/db/knip.json @@ -2,5 +2,5 @@ "$schema": "./node_modules/knip/schema.json", "entry": ["src/index.ts"], "project": ["src/**/*.ts"], - "ignoreDependencies": ["ts-node", "dotenv"] + "ignoreDependencies": ["ts-node"] } diff --git a/db/package-lock.json b/db/package-lock.json index 56aa171..1ce98f7 100644 --- a/db/package-lock.json +++ b/db/package-lock.json @@ -20,7 +20,6 @@ }, "devDependencies": { "@types/pg": "^8.10.2", - "dotenv": "^17.4.2", "knip": "^6.26.0", "typescript": "^6.0.3" } @@ -1144,19 +1143,6 @@ "node": ">=0.3.1" } }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dottie": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", diff --git a/db/package.json b/db/package.json index 39bcea0..1528dcf 100644 --- a/db/package.json +++ b/db/package.json @@ -34,7 +34,6 @@ }, "devDependencies": { "@types/pg": "^8.10.2", - "dotenv": "^17.4.2", "knip": "^6.26.0", "typescript": "^6.0.3" } diff --git a/db/src/index.ts b/db/src/index.ts index b240b12..40c254b 100755 --- a/db/src/index.ts +++ b/db/src/index.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S node --loader ts-node/esm --require dotenv/config +#!/usr/bin/env -S node --env-file-if-exists=.env --loader ts-node/esm import { makeCli } from '@roostorg/db-migrator'; import apiServerPostgresConfig from './configs/api-server-pg.js'; diff --git a/package.json b/package.json index 2faf519..37d43fb 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,11 @@ "client:start": "cd client && npm start", "server:start": "cd server && npm start", "create-org": "cd server && npm run create-org --", - "db:add": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm --require dotenv/config\" node src/index.ts add", - "db:clean": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm --require dotenv/config\" node src/index.ts clean", - "db:update": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm --require dotenv/config\" node src/index.ts apply", - "db:create": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm --require dotenv/config\" node src/index.ts create", - "db:drop": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm --require dotenv/config\" node src/index.ts drop", + "db:add": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env src/index.ts add", + "db:clean": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env src/index.ts clean", + "db:update": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env src/index.ts apply", + "db:create": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env src/index.ts create", + "db:drop": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env src/index.ts drop", "check:prepush": "cd server && npm run check:prepush && cd ../client && npm run check:prepush", "typecheck": "cd server && npm run typecheck && cd ../client && npx tsc --noEmit", "prettier": "prettier --check \"./**/*.{ts,tsx,js,jsx,mjs,cjs,json,md,yaml,yml}\"", diff --git a/server/e2e/fixtures/coop.ts b/server/e2e/fixtures/coop.ts index 50a476a..0450013 100644 --- a/server/e2e/fixtures/coop.ts +++ b/server/e2e/fixtures/coop.ts @@ -1,6 +1,3 @@ -// Load .env before anything reads process.env (the DI container does, heavily). -import 'dotenv/config'; - import { test as base, type APIRequestContext } from '@playwright/test'; import { uid } from 'uid'; diff --git a/server/package-lock.json b/server/package-lock.json index e92403f..687ac8b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -103,7 +103,6 @@ "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", "copyfiles": "^2.4.1", - "dotenv": "^10.0.0", "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^3.6.0", "eslint-plugin-functional": "^9.0.4", @@ -14117,15 +14116,6 @@ "node": ">=8" } }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/server/package.json b/server/package.json index 6b8d2bc..90f1b97 100644 --- a/server/package.json +++ b/server/package.json @@ -6,23 +6,23 @@ "scripts": { "build": "tsc && npm run copy-assets", "copy-assets": "copyfiles \"lib/**/*.lua\" transpiled/", - "start": "npm run copy-assets && tsc-watch --onSuccess \"node --trace-warnings --require dotenv/config ./transpiled/bin/www.js\"", - "start:trace": "npm run copy-assets && tsc-watch --onSuccess \"node --trace-warnings --require dotenv/config --require ../nodejs-instrumentation/transpiled/autoinstrumentation.js ./transpiled/bin/www.js\"", + "start": "npm run copy-assets && tsc-watch --onSuccess \"node --trace-warnings --env-file-if-exists=.env ./transpiled/bin/www.js\"", + "start:trace": "npm run copy-assets && tsc-watch --onSuccess \"node --trace-warnings --env-file-if-exists=.env --require ../nodejs-instrumentation/transpiled/autoinstrumentation.js ./transpiled/bin/www.js\"", "test": "npm run test:local", - "test:local": "NODE_OPTIONS=\"--no-warnings --loader ts-node/esm --require dotenv/config\" jest --watch --detectOpenHandles", - "test:prepush": "NODE_OPTIONS=\"--no-warnings --loader ts-node/esm --require dotenv/config\" jest --detectOpenHandles --no-cache --forceExit", - "test:ci": "NODE_OPTIONS=\"--loader ts-node/esm\" jest --ci --reporters=default --silent=false --reporters=jest-junit --no-cache --forceExit --runInBand", - "test:integ": "NODE_OPTIONS=\"--loader ts-node/esm\" jest --ci --reporters=default --silent=false --reporters=jest-junit --detectOpenHandles --no-cache --forceExit --runInBand --config jest.integ.config.cjs", + "test:local": "NODE_OPTIONS=\"--no-warnings --loader ts-node/esm\" node --env-file-if-exists=.env node_modules/.bin/jest --watch --detectOpenHandles", + "test:prepush": "NODE_OPTIONS=\"--no-warnings --loader ts-node/esm\" node --env-file-if-exists=.env node_modules/.bin/jest --detectOpenHandles --no-cache --forceExit", + "test:ci": "NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env node_modules/.bin/jest --ci --reporters=default --silent=false --reporters=jest-junit --no-cache --forceExit --runInBand", + "test:integ": "NODE_OPTIONS=\"--loader ts-node/esm\" node --env-file-if-exists=.env node_modules/.bin/jest --ci --reporters=default --silent=false --reporters=jest-junit --detectOpenHandles --no-cache --forceExit --runInBand --config jest.integ.config.cjs", "typecheck": "tsc --noEmit", - "test:e2e": "playwright test --config e2e/playwright.config.ts", - "test:e2e:ui": "playwright test --config e2e/playwright.config.ts --ui", + "test:e2e": "node --env-file-if-exists=.env node_modules/.bin/playwright test --config e2e/playwright.config.ts", + "test:e2e:ui": "node --env-file-if-exists=.env node_modules/.bin/playwright test --config e2e/playwright.config.ts --ui", "e2e:install-browsers": "playwright install --with-deps chromium", "check:prepush": "npm run typecheck && npm run test:prepush", "lint": "eslint \"./**/*.{ts,tsx,js}\"", - "runWorkerOrJob": "node --loader ts-node/esm --require dotenv/config bin/run-worker-or-job.ts", - "create-org": "node --loader ts-node/esm --require dotenv/config bin/create-org-and-user.ts", - "get-invite": "node --loader ts-node/esm --require dotenv/config bin/get-invite-token.ts", - "recover-mrt-queue": "node --loader ts-node/esm --require dotenv/config bin/recover-mrt-queue.ts", + "runWorkerOrJob": "node --env-file-if-exists=.env --loader ts-node/esm bin/run-worker-or-job.ts", + "create-org": "node --env-file-if-exists=.env --loader ts-node/esm bin/create-org-and-user.ts", + "get-invite": "node --env-file-if-exists=.env --loader ts-node/esm bin/get-invite-token.ts", + "recover-mrt-queue": "node --env-file-if-exists=.env --loader ts-node/esm bin/recover-mrt-queue.ts", "knip": "knip" }, "author": "Roostorg", @@ -122,7 +122,6 @@ "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", "copyfiles": "^2.4.1", - "dotenv": "^10.0.0", "eslint": "^9.39.4", "eslint-import-resolver-typescript": "^3.6.0", "eslint-plugin-functional": "^9.0.4", diff --git a/server/test/harness/transactionalPgPool.integ.test.ts b/server/test/harness/transactionalPgPool.integ.test.ts index 9009b6b..d8b299b 100644 --- a/server/test/harness/transactionalPgPool.integ.test.ts +++ b/server/test/harness/transactionalPgPool.integ.test.ts @@ -4,8 +4,6 @@ * Proves that `createTransactionalTestDb` lets us wrap a whole test in a single * Postgres transaction that is rolled back at the end. */ -import 'dotenv/config'; - import { Kysely, PostgresDialect, sql } from 'kysely'; import pg from 'pg'; diff --git a/server/test/integ/ncmec-submission.integ.test.ts b/server/test/integ/ncmec-submission.integ.test.ts index 33af41a..fc79794 100644 --- a/server/test/integ/ncmec-submission.integ.test.ts +++ b/server/test/integ/ncmec-submission.integ.test.ts @@ -1,5 +1,3 @@ -import 'dotenv/config'; - import { uid } from 'uid'; import { Headers } from 'undici'; diff --git a/server/test/integ/setupIntegrationServer.ts b/server/test/integ/setupIntegrationServer.ts index 79d0bca..fcf8875 100644 --- a/server/test/integ/setupIntegrationServer.ts +++ b/server/test/integ/setupIntegrationServer.ts @@ -6,10 +6,6 @@ * Requires the docker-compose stack from `npm run up` and migrations applied * via `npm run db:update`. */ -// Load .env before any module that reads process.env (notably the IoC -// container). The unit-test `npm test` path goes through dotenv via its -// NODE_OPTIONS; `test:integ` does not, so we do it here. -import 'dotenv/config'; import * as superTest from 'supertest'; -- 2.51.2