vercel-env-push
Version:
The missing Vercel CLI command to push environment variables from .env files.
234 lines (225 loc) • 7.8 kB
JavaScript
import assert from 'node:assert';
import fs from 'node:fs';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import readline from 'node:readline';
import Table from 'cli-table3';
import * as kolorist from 'kolorist';
import { createSpinner } from 'nanospinner';
import wyt from 'wyt';
import { exec as exec$1 } from 'node:child_process';
import { promisify } from 'node:util';
function validateFile(filePath) {
assert(fs.existsSync(filePath), `No file found at '${filePath}'.`);
}
function parseEnvFile(envFilePath) {
const content = fs.readFileSync(envFilePath, "utf8");
const envVars = dotenv.parse(content);
if (Object.keys(envVars).length === 0) {
throw new Error(`No environment variables found in '${envFilePath}'.`);
}
try {
const parsedEnvVars = dotenvExpand.expand({ ignoreProcessEnv: true, parsed: envVars });
if (!parsedEnvVars.parsed || parsedEnvVars.error) {
throw new Error("Unable to expand environment variables.");
}
return parsedEnvVars.parsed;
} catch (error) {
throw new Error(`Unable to parse and expand environment variables in '${envFilePath}'.`, {
cause: error instanceof Error ? error : void 0
});
}
}
const tableColumnWidth = Math.floor(((process.stdout.columns ?? 80) - 10) / 2);
function text(builder) {
console.log(builder(kolorist));
}
function table(builder) {
const [headers, values] = builder(kolorist);
const table2 = new Table({
colWidths: [tableColumnWidth, tableColumnWidth],
head: headers,
style: { head: [] },
wordWrap: true,
wrapOnWordBoundary: false
});
table2.push(...values);
console.log(table2.toString());
}
function redact(value) {
if (value.length < 5) {
return "*".repeat(value.length);
}
return value[0] + "*".repeat(value.length - 2) + value[value.length - 1];
}
function spin(message) {
return createSpinner(message, { color: "cyan" }).start();
}
function confirm(question, defaultYes = true) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve, reject) => {
const answers = getConfirmAnswers(defaultYes);
rl.question(`${question} (${answers[0]}/${answers[1]}) `, (answer) => {
rl.close();
const sanitizedAnswer = answer.trim().toLowerCase();
if (sanitizedAnswer === "" && defaultYes || sanitizedAnswer === "y" || sanitizedAnswer === "yes") {
return resolve();
}
return reject(new Error("User aborted."));
});
});
}
function getConfirmAnswers(defaultYes = true) {
return [defaultYes ? "Y" : "y", !defaultYes ? "N" : "n"];
}
function pluralize(count, wordOrSingular, plural) {
if (!plural) {
return wordOrSingular + (count === 1 ? "" : "s");
}
return count === 1 ? wordOrSingular : plural;
}
const exec = promisify(exec$1);
function isExecError(error) {
return error instanceof Error && typeof error.stderr === "string";
}
function throwIfAnyRejected(results) {
for (const result of results) {
if (isRejected(result)) {
throw result.reason;
}
}
}
function isRejected(input) {
return input.status === "rejected";
}
const vercelEnvs = ["development", "preview", "production"];
let waitForRateLimiter;
function validateVercelEnvs(envs, branch) {
assert(envs.length > 0, "No environments specified.");
for (const env of envs) {
assert(vercelEnvs.includes(env), `Unknown environment '${env}' specified.`);
}
if (branch && branch.length > 0) {
assert(envs.length === 1 && envs[0] === "preview", "Only the preview environment can be specified when specifying a branch.");
}
}
async function replaceEnvVars(envs, envVars, options) {
await removeEnvVars(envs, envVars, options);
await addEnvVars(envs, envVars, options);
}
async function removeEnvVars(envs, envVars, options) {
rateLimit();
const promises = [];
for (const envVarKey of Object.keys(envVars)) {
for (const env of envs) {
promises.push(removeEnvVar(env, envVarKey, options));
}
}
throwIfAnyRejected(await Promise.allSettled(promises));
}
async function addEnvVars(envs, envVars, options) {
rateLimit();
const promises = [];
for (const [envVarKey, envVarValue] of Object.entries(envVars)) {
for (const env of envs) {
promises.push(addEnvVar(env, envVarKey, envVarValue, options));
}
}
throwIfAnyRejected(await Promise.allSettled(promises));
}
async function addEnvVar(env, key, value, options) {
try {
await waitForRateLimiter();
await execCommandWithNpx(`printf "${value}" | npx vercel env add ${key} ${env}${getBranchCommandArgument(options.branch)}${getTokenCommandArgument(options.token)}`);
} catch (error) {
throw new Error(`Unable to add environment variable '${key}' to '${env}'.`, {
cause: error instanceof Error ? error : void 0
});
}
}
async function removeEnvVar(env, key, options) {
try {
await waitForRateLimiter();
await execCommandWithNpx(`npx vercel env rm ${key} ${env}${getBranchCommandArgument(options.branch)} -y${getTokenCommandArgument(options.token)}`);
} catch (error) {
if (!isExecError(error) || !error.stderr.includes("was not found")) {
throw new Error(`Unable to remove environment variable '${key}' from '${env}'.`, {
cause: error instanceof Error ? error : void 0
});
}
}
}
async function execCommandWithNpx(command) {
return exec(command.replace("npx", "npx --yes"));
}
function getTokenCommandArgument(token) {
return token && token.length > 0 ? ` -t ${token}` : "";
}
function getBranchCommandArgument(branch) {
return branch && branch.length > 0 ? ` ${branch}` : "";
}
function rateLimit() {
waitForRateLimiter = wyt(6, 1e4);
}
async function pushEnvVars(envFilePath, envs, options) {
validateVercelEnvs(envs, options?.branch);
validateFile(envFilePath);
if (options?.interactive) {
logParams(envFilePath, envs, options?.branch);
}
let envVars = parseEnvFile(envFilePath);
const envVarsCount = Object.keys(envVars).length;
if (options?.prePush) {
envVars = await options.prePush(envVars);
}
if (options?.interactive) {
logEnvVars(envVars, envVarsCount);
}
if (options?.dryRun) {
return;
}
if (options?.interactive) {
await confirm(`Do you want to push ${pluralize(envVarsCount, "this", "these")} environment ${pluralize(envVarsCount, "variable")}?`);
}
let spinner;
if (options?.interactive) {
spinner = spin(`Pushing environment ${pluralize(envVarsCount, "variable")}`);
}
try {
await replaceEnvVars(envs, envVars, { branch: options?.branch, token: options?.token });
} catch (error) {
if (options?.interactive && spinner) {
spinner.error();
}
throw error;
}
if (options?.interactive && spinner) {
spinner.success({
text: `Pushed ${envVarsCount} environment ${pluralize(envVarsCount, "variable")} to ${envs.length} ${pluralize(envs.length, "environment")}.`
});
}
}
function logParams(envFilePath, envs, branch) {
text(({ cyan, green, red, yellow }) => {
const formatter = new Intl.ListFormat("en", { style: "short", type: "conjunction" });
return `Preparing environment variables push from ${cyan(`'${envFilePath}'`)} to ${formatter.format(envs.map((env) => {
if (env === "development") {
return green(env);
} else if (env === "preview") {
return yellow(`${env}${branch ? ` (branch: ${branch})` : ""}`);
}
return red(env);
}))}.`;
});
}
function logEnvVars(envVars, envVarsCount) {
text(({ dim }) => dim(`The following environment ${pluralize(envVarsCount, "variable")} will be pushed:`));
table(({ bold }) => [
[bold("Variable"), bold("Value")],
Object.entries(envVars).map(([key, value]) => [key, redact(value)])
]);
}
export { pushEnvVars };