UNPKG

envy

Version:

Load .env files and environment variables

122 lines (100 loc) 4.12 kB
import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import isWsl from 'is-wsl'; import { isDirectorySync } from 'path-type'; import { includeKeys } from 'filter-obj'; import camelcase from 'camelcase'; import camelcaseKeys from 'camelcase-keys'; import loadEnvFile from './lib/load-env-file.js'; const permissionMask = 0o777; const windowsPermission = 0o555; const ownerReadWrite = fs.constants.S_IRUSR | fs.constants.S_IWUSR; const checkMode = (filepath, mask) => { const status = fs.statSync(filepath); if (!status.isFile()) { throw new Error(`Filepath must be a file: ${filepath}`); } return status.mode & mask; }; const assertHidden = (filepath) => { const filename = path.basename(filepath); if (!filename.startsWith('.')) { throw new Error(`Filepath must be hidden. Fix: mv '${filename}' '.${filename}'`); } }; const assertIgnored = (filepath) => { const failMessage = `File must be ignored by git. Fix: echo '${path.basename(filepath)}' >> .gitignore`; let ignores; try { ignores = fs.readFileSync(path.join(filepath, '..', '.gitignore'), 'utf8'); } catch (error) { if (error.code === 'ENOENT') { if (!isDirectorySync(path.join(filepath, '..', '.git'))) { return; } throw new Error(failMessage); } throw error; } if (!ignores.split(/\r?\n/u).includes(path.basename(filepath))) { throw new Error(failMessage); } }; const isWindows = () => { return isWsl || process.platform === 'win32'; }; const envy = (input) => { const envPath = input || '.env'; const examplePath = envPath + '.example'; assertHidden(envPath); if (checkMode(examplePath, fs.constants.S_IWOTH) === fs.constants.S_IWOTH) { throw new Error(`File must not be writable by others. Fix: chmod o-w '${examplePath}'`); } const exampleEnv = loadEnvFile(examplePath); const exampleEnvKeys = Object.keys(exampleEnv); const camelizedExampleEnvKeys = Object.keys(camelcaseKeys(exampleEnv)); const exampleHasValues = Object.values(exampleEnv).some((value) => { return value !== ''; }); if (exampleHasValues) { throw new Error(`No values are allowed in ${examplePath}, put them in ${envPath} instead`); } const camelizedGlobalEnv = camelcaseKeys(process.env); const camelizedGlobalEnvKeys = Object.keys(camelizedGlobalEnv); // We treat env vars as case insensitive, like Windows does. const needsEnvFile = camelizedExampleEnvKeys.some((key) => { return !camelizedGlobalEnvKeys.includes(key); }); if (!needsEnvFile) { return includeKeys(camelizedGlobalEnv, camelizedExampleEnvKeys); } if (isWindows() && checkMode(envPath, permissionMask) !== windowsPermission) { throw new Error(`File permissions are unsafe. Make them 555 '${envPath}'`); } else if (!isWindows() && checkMode(envPath, permissionMask) !== ownerReadWrite) { throw new Error(`File permissions are unsafe. Fix: chmod 600 '${envPath}'`); } assertIgnored(envPath); const camelizedLocalEnv = camelcaseKeys(loadEnvFile(envPath)); const camelizedMergedEnv = { ...camelizedLocalEnv, ...camelizedGlobalEnv }; const camelizedMergedEnvKeys = Object.keys(camelizedMergedEnv); const camelizedMissingKeys = camelizedExampleEnvKeys.filter((key) => { return !camelizedMergedEnv[key] || !camelizedMergedEnvKeys.includes(key); }); if (camelizedMissingKeys.length > 0) { const missingKeys = camelizedMissingKeys.map((camelizedMissingKey) => { return exampleEnvKeys.find((exampleKey) => { return camelcase(exampleKey) === camelizedMissingKey; }); }); throw new Error(`Environment variables are missing: ${missingKeys.join(', ')}`); } const keepKeys = new Set([...Object.keys(camelizedLocalEnv), ...camelizedExampleEnvKeys]); return includeKeys(camelizedMergedEnv, keepKeys); }; export default envy;