@hairy/lnv
Version:
Loading environment variables in Next.js and other frameworks can be quite cumbersome, and using dotenv or vault at runtime is also inconvenient. That's why my created this tool
303 lines (300 loc) • 10.3 kB
JavaScript
import {createRequire as __createRequire} from 'module';var require=__createRequire(import.meta.url);
import {
context
} from "./chunk-DYEG65P7.js";
import {
entryToFile,
uniq
} from "./chunk-PCQ4K4YG.js";
import {
vault
} from "./chunk-RZXDK7SO.js";
import {
readfile,
readfiles,
replaceLiteralQuantity
} from "./chunk-ANMSLEQF.js";
// src/internal/feature.ts
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import {
confirm,
intro,
isCancel,
multiselect,
outro,
password,
select,
text
} from "@clack/prompts";
import { colors } from "consola/utils";
import { config } from "dotenv";
import { createSpinner } from "nanospinner";
import { loadConfig } from "unconfig";
async function parseUserConfig() {
const { config: config2 = {} } = await loadConfig({
sources: [{ files: "lnv.config" }],
cwd: process.cwd(),
merge: true
});
context.dts = config2.dts ?? false;
context.script = config2.scripts?.[context.entries[0]];
context.entries = context.script ? context.entries.slice(1) : context.entries;
Object.assign(context.before, config2.injects?.before);
Object.assign(context.after, config2.injects?.after);
context.entries.unshift(...config2.injects?.entries || []);
context.depth = context.depth || config2.injects?.depth || false;
}
async function executionScript() {
if (!context.script)
return;
if (typeof context.script === "string") {
context.run = context.script;
return;
}
const {
prompts = [],
entries = [],
command: run,
depth = false,
message,
before,
after,
...options
} = context.script;
Object.assign(context.before, before);
Object.assign(context.after, after);
context.entries.unshift(...entries);
context.depth = context.depth || depth;
if (typeof run === "string" && message)
intro(message);
const parsed = {};
for (const prompt of prompts) {
let value2;
if (prompt.type === "handler") {
value2 = await prompt.handler(parsed);
}
if (prompt.type === "select") {
const choices = typeof prompt.options === "function" ? await prompt.options(parsed) : prompt.options;
value2 = await select({
message: message || `Please select ${prompt.key}`,
options: choices
});
}
if (prompt.type === "multiselect") {
const choices = typeof prompt.options === "function" ? await prompt.options(parsed) : prompt.options;
const selected = await multiselect({
message: prompt.message || `Please select ${prompt.key}`,
options: choices
});
value2 = selected;
}
if (prompt.type === "confirm")
value2 = await confirm({ message: prompt.message || `Please confirm ${prompt.key}` });
if (prompt.type === "text") {
value2 = await text({
message: prompt.message || `Please enter ${prompt.key}`,
...options
});
}
if (prompt.type === "password")
value2 = await password({ message: prompt.message || `Please enter ${prompt.key}` });
if (isCancel(value2)) {
outro("Operation cancelled");
process.exit(0);
}
if (value2)
parsed[prompt.key] = Array.isArray(value2) ? value2.join(",") : value2.toString();
}
Object.assign(context.parsed, parsed);
if (typeof run === "string") {
context.run = run;
return;
}
const value = await select({
message: message || "Please select a command",
options: run
});
if (isCancel(value)) {
outro("Operation cancelled");
process.exit(0);
}
context.run = value;
}
async function authEnvironment() {
const [environment] = context.sources.filter((source) => source.env.startsWith(".env.vault"));
if (!environment || !environment.files.length || process.env.DOTENV_KEY)
return;
const unauthorizedFilepaths = [];
const notSpecifiedFilepaths = [];
for (const file of environment.files) {
const dirpath = path.dirname(file.path);
if (fs.existsSync(path.join(dirpath, ".env.key")) || fs.existsSync(path.join(dirpath, ".env.keys")))
continue;
if (fs.existsSync(path.join(dirpath, ".env.me")))
notSpecifiedFilepaths.push(file.path);
else
unauthorizedFilepaths.push(file.path);
}
if (!unauthorizedFilepaths.length && !notSpecifiedFilepaths.length)
return;
if (unauthorizedFilepaths.length)
intro(`Found ${unauthorizedFilepaths.length} unauthorized directories, Please authorize them to access the vault environment variables.`);
for (const filepath of unauthorizedFilepaths) {
const dirpath = path.dirname(filepath);
console.log(`${colors.dim(`entry: `)}${filepath}`);
await vault("login", {
cwd: dirpath,
stdio: "inherit",
stderr: "inherit",
stdin: "inherit",
stdout: "inherit"
});
process.stdout.write("\x1B[1A");
process.stdout.write("\x1B[2K");
notSpecifiedFilepaths.push(filepath);
}
intro(`Found ${notSpecifiedFilepaths.length} directories not specified environment, Please select environment.`);
for (const filepath of notSpecifiedFilepaths) {
const dirpath = path.dirname(filepath);
const spinner = createSpinner();
spinner.start(" Loading dotenv environment...");
const { stdout } = await vault("keys", { cwd: dirpath });
spinner.stop();
const dotenvKeys = stdout.split("\n").filter((row) => row.includes("dotenv://")).map((row) => {
const [env, key] = row.split("dotenv://").map((part) => part.trim());
return { env, key: `dotenv://${key}` };
});
const value = await select({
message: filepath.replace(/\\\\/g, "/"),
options: [
{
value: "all",
label: "all",
hint: "Ask every time the script runs"
},
...dotenvKeys.map((key) => ({
value: key.env,
label: key.env
}))
]
});
if (isCancel(value)) {
outro("Operation cancelled");
process.exit(0);
}
if (value === "all") {
const content = [
`#/!!!!!!!!!!!!!!!!!! .env.keys !!!!!!!!!!!!!!!!!!!!!/`,
`#/ DOTENV_KEYs. DO NOT commit to source control /`,
`#/ [how it works](https://dotenv.org/env-keys) /`,
`#/--------------------------------------------------/`,
``,
...dotenvKeys.map((key) => `DOTENV_KEY_${key.env.toUpperCase()}="${key.key}"`)
];
fs.writeFileSync(path.join(dirpath, ".env.keys"), content.join("\n"));
} else {
const dotenvKey = dotenvKeys.find(({ env }) => env === value);
const content = [
`#/!!!!!!!!!!!!!!!!!!! .env.key !!!!!!!!!!!!!!!!!!!!!/`,
`#/ DOTENV_KEY. DO NOT commit to source control /`,
`#/ [how it works](https://dotenv.org/env-keys) /`,
`#/--------------------------------------------------/`,
``,
`DOTENV_KEY="${dotenvKey.key}"`
];
fs.writeFileSync(path.join(dirpath, ".env.key"), content.join("\n"));
}
}
}
async function readEnvironment() {
context.files = uniq(context.entries).filter(Boolean).map(entryToFile);
if (context.files.length)
console.log(`Found environment files:`);
for (const file of context.files) {
const [env, defaultScope] = file.split(":");
const files = readfiles(process.cwd(), env, context.depth);
if (!files.length) {
const failedMessage = `Failed to loading ${env} file not found in all scopes`;
![".env", ".env.local"].includes(env) && console.log(failedMessage);
continue;
}
const fileDetails = files.map(async (filepath) => {
const keysPath = path.join(path.dirname(filepath), ".env.keys");
let scope = defaultScope;
if (env === ".env.vault" && fs.existsSync(keysPath)) {
const keys = config({ path: keysPath, processEnv: {} });
const scopes = Object.keys(keys.parsed || {}).map((key) => key.split('="dotenv')[0].replace("DOTENV_KEY_", "").toLowerCase());
const value = await select({
message: filepath.replace(/\\\\/g, "/"),
options: [
...scopes.map((key) => ({
value: key,
label: key
}))
]
});
if (isCancel(value)) {
outro("Operation cancelled");
process.exit(0);
}
scope = value;
}
return { path: filepath, scope };
});
context.sources.push({ env, files: await Promise.all(fileDetails) });
console.log(`- ${colors.gray(env)} (${files.length} ${files.length > 1 ? "files" : "file"} found)`);
}
if (context.files.length)
console.log();
}
async function loadEnvironment() {
for (const { env, files } of context.sources) {
let exist = false;
for (const file of files) {
const output = parse(env, file.path, file.scope);
if (!output?.parsed)
continue;
exist = true;
context.parsedFiles.push(env);
Object.assign(context.parsed, output.parsed);
}
if (!exist)
console.log(`Failed to loading ${env} file not found in all scopes`);
}
}
function mergeParseEnvironment() {
for (const key in context.parsed)
context.parsed[key] = replaceLiteralQuantity(context.parsed[key], context.parsed);
}
function parse(env, filepath, scope) {
if (!env.startsWith(".env.vault"))
return config({ path: filepath });
const DOTENV_KEY = dokey(path.dirname(filepath), scope) || (scope ? process.env[`DOTENV_KEY_${scope.toUpperCase()}`] : process.env.DOTENV_KEY);
if (!DOTENV_KEY)
throw new Error("No DOTENV_KEY found in .env, .env.key or process.env");
delete process.env.DOTENV_KEY;
return config({ DOTENV_KEY, path: filepath });
}
function dokey(root, scope) {
const target = readfile(root, scope ? `.env.keys` : ".env.key");
if (!target)
return void 0;
const dirname = path.dirname(target);
if (root !== dirname && fs.existsSync(path.join(dirname, ".env.vault")))
return;
const parsed = config({ processEnv: {}, DOTENV_KEY: void 0, path: target }).parsed;
const value = scope ? parsed?.[`DOTENV_KEY_${scope.toUpperCase()}`] : parsed?.DOTENV_KEY;
return value;
}
export {
parseUserConfig,
executionScript,
authEnvironment,
readEnvironment,
loadEnvironment,
mergeParseEnvironment,
parse,
dokey
};