Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: support monorepo #190

Merged
merged 24 commits into from
Oct 31, 2023
Merged
4 changes: 2 additions & 2 deletions packages/shared-lib-blitz-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@
"@willbooster/eslint-config-ts": "10.5.1",
"@willbooster/prettier-config": "9.1.2",
"blitz": "2.0.0-beta.34",
"build-ts": "11.0.6",
"build-ts": "11.0.9",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-sort-class-members": "1.19.0",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-unicorn": "48.0.1",
"eslint-plugin-unicorn": "49.0.0",
"lint-staged": "15.0.2",
"micromatch": "4.0.5",
"prettier": "3.0.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/shared-lib-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
"@typescript-eslint/parser": "6.9.0",
"@willbooster/eslint-config-ts": "10.5.1",
"@willbooster/prettier-config": "9.1.2",
"build-ts": "11.0.6",
"build-ts": "11.0.9",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-sort-class-members": "1.19.0",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-unicorn": "48.0.1",
"eslint-plugin-unicorn": "49.0.0",
"lint-staged": "15.0.2",
"micromatch": "4.0.5",
"prettier": "3.0.3",
Expand Down
77 changes: 60 additions & 17 deletions packages/shared-lib-node/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import fs from 'node:fs';
import path from 'node:path';

import { config } from 'dotenv';

interface Options {
env?: (string | number)[];
cascadeEnv?: string;
cascadeNodeEnv?: boolean;
autoCascadeEnv?: boolean;
checkEnv?: string;
verbose?: boolean;
}
import type { ArgumentsCamelCase, InferredOptionTypes } from 'yargs';

export const yargsOptionsBuilderForEnv = {
env: {
Expand All @@ -30,18 +23,34 @@ export const yargsOptionsBuilderForEnv = {
type: 'boolean',
default: true,
},
'include-root-env': {
description: 'Include .env files in root directory if the project is in a monorepo and --env option is not used.',
type: 'boolean',
default: true,
},
'check-env': {
description: 'Check whether the keys of the loaded .env files are same with the given .env file.',
type: 'string',
default: '.env.example',
},
verbose: {
description: 'Whether to show verbose information',
type: 'boolean',
alias: 'v',
},
} as const;

export type EnvReaderOptions = Partial<ArgumentsCamelCase<InferredOptionTypes<typeof yargsOptionsBuilderForEnv>>>;

/**
* This function loads environment variables from `.env` files.
* This function reads environment variables from `.env` files. Note it does not assign them in `process.env`.
* */
export function loadEnvironmentVariables(argv: Options, cwd: string, orgCwd?: string): Record<string, string> {
let envPaths = (argv.env ?? []).map((envPath) => path.resolve(orgCwd ?? cwd, envPath.toString()));
export function readEnvironmentVariables(
argv: EnvReaderOptions,
cwd: string,
cacheEnabled = true
): Record<string, string> {
let envPaths = (argv.env ?? []).map((envPath) => path.resolve(cwd, envPath.toString()));
const cascade =
argv.cascadeEnv ??
(argv.cascadeNodeEnv
Expand All @@ -50,7 +59,15 @@ export function loadEnvironmentVariables(argv: Options, cwd: string, orgCwd?: st
? process.env.WB_ENV || process.env.NODE_ENV || 'development'
: undefined);
if (typeof cascade === 'string') {
if (envPaths.length === 0) envPaths.push(path.join(cwd, '.env'));
if (envPaths.length === 0) {
envPaths.push(path.join(cwd, '.env'));
if (argv.includeRootEnv) {
const rootPath = path.resolve(cwd, '..', '..');
if (fs.existsSync(path.join(rootPath, 'package.json'))) {
envPaths.push(path.join(rootPath, '.env'));
}
}
}
envPaths = envPaths.flatMap((envPath) =>
cascade
? [`${envPath}.${cascade}.local`, `${envPath}.local`, `${envPath}.${cascade}`, envPath]
Expand All @@ -60,13 +77,13 @@ export function loadEnvironmentVariables(argv: Options, cwd: string, orgCwd?: st
envPaths = envPaths.map((envPath) => path.relative(cwd, envPath));
if (argv.verbose) {
console.info(`WB_ENV: ${process.env.WB_ENV}, NODE_ENV: ${process.env.NODE_ENV}`);
console.info('Loading env files:', envPaths);
console.info('Reading env files:', envPaths);
}

let envVars: Record<string, string> = {};
const orgEnvVars = { ...process.env };
for (const envPath of envPaths) {
envVars = { ...config({ path: path.join(cwd, envPath) }).parsed, ...envVars };
envVars = { ...readEnvFile(path.join(cwd, envPath), cacheEnabled), ...envVars };
let count = 0;
for (const [key, value] of Object.entries(envVars)) {
if (orgEnvVars[key] !== value) {
Expand All @@ -75,12 +92,12 @@ export function loadEnvironmentVariables(argv: Options, cwd: string, orgCwd?: st
}
}
if (count > 0) {
console.info(`Loaded ${count} environment variables:`, envPath);
console.info(`Read ${count} environment variables:`, envPath);
}
}

if (argv.checkEnv) {
const exampleKeys = Object.keys(config({ path: path.join(cwd, argv.checkEnv) }).parsed || {});
const exampleKeys = Object.keys(readEnvFile(path.join(cwd, argv.checkEnv), cacheEnabled) || {});
const missingKeys = exampleKeys.filter((key) => !(key in envVars));
if (missingKeys.length > 0) {
throw new Error(`Missing environment variables in [${envPaths.join(', ')}]: [${missingKeys.join(', ')}]`);
Expand All @@ -89,6 +106,32 @@ export function loadEnvironmentVariables(argv: Options, cwd: string, orgCwd?: st
return envVars;
}

/**
* This function read environment variables from `.env` files and assign them in `process.env`.
* */
export function readAndApplyEnvironmentVariables(
argv: EnvReaderOptions,
cwd: string,
cacheEnabled = true
): Record<string, string | undefined> {
const envVars = readEnvironmentVariables(argv, cwd, cacheEnabled);
Object.assign(process.env, envVars);
return envVars;
}

const cachedEnvVars = new Map<string, Record<string, string>>();

function readEnvFile(filePath: string, cacheEnabled = true): Record<string, string> {
const cached = cacheEnabled && cachedEnvVars.get(filePath);
if (cached) return cached;

const parsed = config({ path: path.resolve(filePath), processEnv: {} }).parsed ?? {};
if (cacheEnabled) {
cachedEnvVars.set(filePath, parsed);
}
return parsed;
}

/**
* This function removes environment variables related to npm and yarn from the given environment variables.
* */
Expand Down
8 changes: 7 additions & 1 deletion packages/shared-lib-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { loadEnvironmentVariables, removeNpmAndYarnEnvironmentVariables, yargsOptionsBuilderForEnv } from './env.js';
export {
readEnvironmentVariables,
readAndApplyEnvironmentVariables,
removeNpmAndYarnEnvironmentVariables,
yargsOptionsBuilderForEnv,
} from './env.js';
export type { EnvReaderOptions } from './env.js';
export { existsAsync } from './exists.js';
export { calculateHashFromFiles, canSkipSeed, updateHashFromFiles } from './hash.js';
export { spawnAsync } from './spawn.js';
21 changes: 10 additions & 11 deletions packages/shared-lib-node/tests/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,46 @@
import { beforeEach, describe, expect, it } from 'vitest';

import { loadEnvironmentVariables } from '../src/env.js';
import { readAndApplyEnvironmentVariables } from '../src/env.js';

describe('loadEnvironmentVariables()', () => {
describe('readAndApplyEnvironmentVariables()', () => {
beforeEach(() => {
process.env.WB_ENV = '';
process.env.NODE_ENV = '';
});

it('should load no env vars with empty options', () => {
const envVars = loadEnvironmentVariables({}, 'test-fixtures/app1');
const envVars = readAndApplyEnvironmentVariables({}, 'test-fixtures/app1');
expect(envVars).toEqual({});
});

it('should load env vars with --auto-cascade-env', () => {
const envVars = loadEnvironmentVariables({ autoCascadeEnv: true }, 'test-fixtures/app1');
const envVars = readAndApplyEnvironmentVariables({ autoCascadeEnv: true }, 'test-fixtures/app1');
expect(envVars).toEqual({ NAME: 'app1', ENV: 'development1' });
});

it('should load env vars with --cascade-env=production', () => {
const envVars = loadEnvironmentVariables({ cascadeEnv: 'production', env: ['.env'] }, 'test-fixtures/app1');
const envVars = readAndApplyEnvironmentVariables({ cascadeEnv: 'production', env: ['.env'] }, 'test-fixtures/app1');
expect(envVars).toEqual({ NAME: 'app1', ENV: 'production1' });
});

it('should load env vars with --cascade-node-env and NODE_ENV=""', () => {
process.env.NODE_ENV = '';
const envVars = loadEnvironmentVariables({ cascadeNodeEnv: true, env: ['.env'] }, 'test-fixtures/app1');
const envVars = readAndApplyEnvironmentVariables({ cascadeNodeEnv: true, env: ['.env'] }, 'test-fixtures/app1');
expect(envVars).toEqual({ NAME: 'app1', ENV: 'development1' });
});

it('should load env vars with --cascade-node-env and NODE_ENV=test', () => {
process.env.NODE_ENV = 'test';
const envVars = loadEnvironmentVariables({ cascadeNodeEnv: true, env: ['.env'] }, 'test-fixtures/app1');
const envVars = readAndApplyEnvironmentVariables({ cascadeNodeEnv: true, env: ['.env'] }, 'test-fixtures/app1');
expect(envVars).toEqual({ NAME: 'app1', ENV: 'test1' });
});

it('should load env vars with --env=test-fixtures/app2/.env --auto-cascade-env, WB_ENV=test and NODE_ENV=production', () => {
process.env.WB_ENV = 'test';
process.env.NODE_ENV = 'production';
const envVars = loadEnvironmentVariables(
{ autoCascadeEnv: true, env: ['.env'] },
'test-fixtures/app1',
'test-fixtures/app2'
const envVars = readAndApplyEnvironmentVariables(
{ autoCascadeEnv: true, env: ['../app2/.env'] },
'test-fixtures/app1'
);
expect(envVars).toEqual({ NAME: 'app2', ENV: 'test2' });
});
Expand Down
16 changes: 8 additions & 8 deletions packages/shared-lib-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
"devDependencies": {
"@babel/core": "7.23.2",
"@mdx-js/react": "3.0.0",
"@storybook/addon-actions": "7.5.1",
"@storybook/addon-docs": "7.5.1",
"@storybook/addon-essentials": "7.5.1",
"@storybook/addon-interactions": "7.5.1",
"@storybook/addon-links": "7.5.1",
"@storybook/addon-actions": "7.5.2",
"@storybook/addon-docs": "7.5.2",
"@storybook/addon-essentials": "7.5.2",
"@storybook/addon-interactions": "7.5.2",
"@storybook/addon-links": "7.5.2",
"@storybook/builder-webpack4": "6.5.16",
"@storybook/manager-webpack4": "6.5.16",
"@storybook/react": "7.5.1",
"@storybook/react": "7.5.2",
"@storybook/testing-library": "0.2.2",
"@types/eslint": "8.44.6",
"@types/micromatch": "4.0.4",
Expand All @@ -53,7 +53,7 @@
"@willbooster/eslint-config-ts-react": "10.1.9",
"@willbooster/prettier-config": "9.1.2",
"babel-loader": "9.1.3",
"build-ts": "11.0.6",
"build-ts": "11.0.9",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "3.6.1",
Expand All @@ -63,7 +63,7 @@
"eslint-plugin-sort-class-members": "1.19.0",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-storybook": "0.6.15",
"eslint-plugin-unicorn": "48.0.1",
"eslint-plugin-unicorn": "49.0.0",
"lint-staged": "15.0.2",
"micromatch": "4.0.5",
"prettier": "3.0.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/shared-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@
"@typescript-eslint/parser": "6.9.0",
"@willbooster/eslint-config-ts": "10.5.1",
"@willbooster/prettier-config": "9.1.2",
"build-ts": "11.0.6",
"build-ts": "11.0.9",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-sort-class-members": "1.19.0",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-unicorn": "48.0.1",
"eslint-plugin-unicorn": "49.0.0",
"lint-staged": "15.0.2",
"micromatch": "4.0.5",
"prettier": "3.0.3",
Expand Down
5 changes: 3 additions & 2 deletions packages/wb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@
"@typescript-eslint/parser": "6.9.0",
"@willbooster/eslint-config-ts": "10.5.1",
"@willbooster/prettier-config": "9.1.2",
"build-ts": "11.0.6",
"at-decorators": "1.2.2",
"build-ts": "11.0.9",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-sort-class-members": "1.19.0",
"eslint-plugin-sort-destructure-keys": "1.5.0",
"eslint-plugin-unicorn": "48.0.1",
"eslint-plugin-unicorn": "49.0.0",
"lint-staged": "15.0.2",
"micromatch": "4.0.5",
"prettier": "3.0.3",
Expand Down
Loading