diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c3bd68..27406d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ jobs: strategy: fail-fast: false matrix: - # Min is 22.13 because the pinned pnpm (packageManager) requires it. - # The published builder itself runs on Node >=18.13 (see package.json engines). + # The published builder requires Node >=22 (package.json engines), which + # also satisfies the pinned pnpm's own >=22.13 requirement. node: [22, 24] steps: - uses: actions/checkout@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41e35ce..81230bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ focused package, so the workflow is lightweight. ## Prerequisites - **Node.js ≥ 22.13** — required by the pinned pnpm version. (The *published* - builder runs on Node ≥ 18.13; the higher floor only applies to development.) + builder requires Node ≥ 22; development just needs the slightly higher pnpm floor.) - **pnpm** — managed via Corepack, which ships with Node: ```sh diff --git a/README.md b/README.md index c3e855c..dd81945 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ pnpm add -D angular-deploy-bunny Requires **Angular 17+** using the esbuild-based **application builder** — the default since v17, which emits the browser bundle into a `browser/` folder — and -**Node 18.13+**. The test suite runs in CI against Angular 17 through 21. +**Node 22+**. The test suite runs in CI against Angular 17 through 21. ## Quick start diff --git a/package.json b/package.json index 246ff9b..ef6e598 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "CHANGELOG.md" ], "engines": { - "node": ">=18.13.0" + "node": ">=22.0.0" }, "packageManager": "pnpm@11.0.8", "scripts": { diff --git a/src/env.spec.ts b/src/env.spec.ts index 27035ed..564cca5 100644 --- a/src/env.spec.ts +++ b/src/env.spec.ts @@ -1,3 +1,6 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { loadSecrets, _resetDotenvLoaded } from './env.js'; @@ -47,4 +50,25 @@ describe('loadSecrets', () => { /BUNNY_ACCOUNT_API_KEY/, ); }); + + it('loads .env.local for each distinct workspaceRoot, not only the first', () => { + const dirA = mkdtempSync(join(tmpdir(), 'bunny-env-a-')); // no .env.local + const dirB = mkdtempSync(join(tmpdir(), 'bunny-env-b-')); + writeFileSync(join(dirB, '.env.local'), 'BUNNY_STORAGE_PASSWORD=fromB\n'); + try { + // First call against a root with no .env.local must not latch globally + // and skip loading later roots. + expect(() => loadSecrets({ requireAccountApiKey: false, workspaceRoot: dirA })).toThrow( + /BUNNY_STORAGE_PASSWORD/, + ); + // A different root that DOES have .env.local must still be loaded. + expect(loadSecrets({ requireAccountApiKey: false, workspaceRoot: dirB })).toEqual({ + storagePassword: 'fromB', + accountApiKey: null, + }); + } finally { + rmSync(dirA, { recursive: true, force: true }); + rmSync(dirB, { recursive: true, force: true }); + } + }); }); diff --git a/src/env.ts b/src/env.ts index 77580df..891c2bf 100644 --- a/src/env.ts +++ b/src/env.ts @@ -13,15 +13,18 @@ export interface LoadSecretsOptions { workspaceRoot?: string; } -let dotenvLoaded = false; +// Keyed by the resolved .env.local path so each distinct workspace root loads +// its own file once. A single boolean would latch on the first root and skip +// every later one (breaks multi-project workspaces deploying in one process). +const loadedDotenvPaths = new Set(); function ensureDotenv(workspaceRoot: string): void { - if (dotenvLoaded) return; const path = resolve(workspaceRoot, '.env.local'); + if (loadedDotenvPaths.has(path)) return; if (existsSync(path)) { loadDotenv({ path }); } - dotenvLoaded = true; + loadedDotenvPaths.add(path); } export function loadSecrets(options: LoadSecretsOptions): Secrets { @@ -46,5 +49,5 @@ export function loadSecrets(options: LoadSecretsOptions): Secrets { // Test-only escape hatch. export function _resetDotenvLoaded(): void { - dotenvLoaded = false; + loadedDotenvPaths.clear(); }