vercel-env-push
Version:
The missing Vercel CLI command to push environment variables from .env files.
261 lines (247 loc) • 8.95 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const assert = require('node:assert');
const fs = require('node:fs');
const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
const readline = require('node:readline');
const Table = require('cli-table3');
const kolorist = require('kolorist');
const nanospinner = require('nanospinner');
const wyt = require('wyt');
const node_child_process = require('node:child_process');
const node_util = require('node:util');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
const n = Object.create(null);
if (e) {
for (const k in e) {
n[k] = e[k];
}
}
n["default"] = e;
return n;
}
const assert__default = /*#__PURE__*/_interopDefaultLegacy(assert);
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
const dotenv__default = /*#__PURE__*/_interopDefaultLegacy(dotenv);
const dotenvExpand__default = /*#__PURE__*/_interopDefaultLegacy(dotenvExpand);
const readline__default = /*#__PURE__*/_interopDefaultLegacy(readline);
const Table__default = /*#__PURE__*/_interopDefaultLegacy(Table);
const kolorist__namespace = /*#__PURE__*/_interopNamespace(kolorist);
const wyt__default = /*#__PURE__*/_interopDefaultLegacy(wyt);
function validateFile(filePath) {
assert__default(fs__default.existsSync(filePath), `No file found at '${filePath}'.`);
}
function parseEnvFile(envFilePath) {
const content = fs__default.readFileSync(envFilePath, "utf8");
const envVars = dotenv__default.parse(content);
if (Object.keys(envVars).length === 0) {
throw new Error(`No environment variables found in '${envFilePath}'.`);
}
try {
const parsedEnvVars = dotenvExpand__default.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__namespace));
}
function table(builder) {
const [headers, values] = builder(kolorist__namespace);
const table2 = new Table__default({
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 nanospinner.createSpinner(message, { color: "cyan" }).start();
}
function confirm(question, defaultYes = true) {
const rl = readline__default.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 = node_util.promisify(node_child_process.exec);
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__default(envs.length > 0, "No environments specified.");
for (const env of envs) {
assert__default(vercelEnvs.includes(env), `Unknown environment '${env}' specified.`);
}
if (branch && branch.length > 0) {
assert__default(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__default(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)])
]);
}
exports.pushEnvVars = pushEnvVars;