UNPKG

vercel-push-env

Version:

The missing Vercel CLI command to push environment variables from .env files.

128 lines (123 loc) 5.22 kB
import assert from 'node:assert'; import fs from 'node:fs'; import { program, Option } from 'commander'; import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; import axios from 'axios'; program.addOption(new Option("--projectId <projectId>", "the name of your Vercel project").makeOptionMandatory(true)).addOption(new Option("--teamId <teamId>", "the unique id of your Vercel team")).addOption(new Option("--token <token>", "the access token for your Vercel account").makeOptionMandatory(true)).addOption(new Option("--targets <targets>", "the environments to target the variables, separated by comma").makeOptionMandatory(true).choices(["production", "development", "preview"]).argParser((value) => value ? value.split(",") : [])).addOption(new Option("--varType <varType>", "the type of the variables").makeOptionMandatory(true).choices(["system", "encrypted", "plain", "secret"]).default("encrypted")).addOption(new Option("--envFile <envFile>", "the path to your env file").makeOptionMandatory(true)).addOption(new Option("--apiRoot <apiRoot>", "vercel api root").makeOptionMandatory(true).default("https://api.vercel.com")); program.parse(); const supportedEnvTargets = /* @__PURE__ */ new Set(["development", "preview", "production"]); function getConfig() { const config = program.opts(); for (const target of config.targets) { assert(supportedEnvTargets.has(target), `Unknown environment '${target}' specified.`); } assert(fs.existsSync(config.envFile), `No file found at '${config.envFile}'.`); return config; } function parseEnvFile(config) { const content = fs.readFileSync(config.envFile, "utf8"); const envVars = dotenv.parse(content); if (Object.keys(envVars).length === 0) { throw new Error(`No environment variables found in '${config.envFile}'.`); } try { const parsedEnvVars = dotenvExpand.expand({ ignoreProcessEnv: true, parsed: envVars }); if (!parsedEnvVars.parsed || parsedEnvVars.error) { throw new Error("Unable to expand environment variables."); } const variables = Object.entries(parsedEnvVars.parsed).map(([key, value]) => ({ key, value, targets: config.targets })); return variables; } catch (error) { throw new Error(`Unable to parse and expand environment variables in '${config.envFile}'.`, { cause: error instanceof Error ? error : void 0 }); } } function filterTarget(envs, targets) { const targetsKey = targets.sort().join(","); const filtered = envs.filter((env) => env.targets.sort().join(",").includes(targetsKey)); return filtered; } function diffEnvVars(origin, target) { const create = []; const update = []; for (const targetEnv of target) { const originEnv = origin.find((env) => env.key === targetEnv.key); if (!originEnv) { create.push(targetEnv); } else if (targetEnv.value !== originEnv.value) { update.push({ origin: originEnv, target: targetEnv }); } } return { create, update }; } async function fetchVercelEnv(config) { const { apiRoot, projectId, teamId, token } = config; const url = `${apiRoot}/v8/projects/${projectId}/env`; const response = await axios.get(url, { params: { teamId, decrypt: true }, headers: { Authorization: `Bearer ${token}` } }); return response.data.envs.map((env) => ({ id: env.id, key: env.key, value: env.value, targets: env.target })); } async function createVercelEnvVars(envVars, config) { const { apiRoot, projectId, teamId, token, varType } = config; if (envVars.length === 0) { return; } let url = `${apiRoot}/v10/projects/${projectId}/env`; if (teamId) { url = `${url}?teamId=${teamId}`; } const payload = envVars.map((envVar) => ({ key: envVar.key, target: envVar.targets, type: varType, value: envVar.value })); await axios.post(url, payload, { headers: { Authorization: `Bearer ${token}` } }); } function updateVercelEnvVars(update, config) { const { apiRoot, projectId, teamId, token, varType } = config; if (update.length === 0) { return; } const envVarsToUpdate = update.map(({ origin, target }) => ({ id: origin.id, ...target })); return envVarsToUpdate.reduce((lastPromise, envVar) => { const { id, key, value, targets } = envVar; return lastPromise.then(async () => { let url = `${apiRoot}/v9/projects/${projectId}/env/${id}`; if (teamId) { url = `${url}?teamId=${teamId}`; } await axios.patch(url, { key, value, type: varType, target: targets }, { headers: { Authorization: `Bearer ${token}` } }); }); }, Promise.resolve()); } async function start() { const config = getConfig(); const parsedEnvs = parseEnvFile(config); const vercelEnvs = await fetchVercelEnv(config); const vercelEnvsToValidate = filterTarget(vercelEnvs, config.targets); const { create, update } = diffEnvVars(vercelEnvsToValidate, parsedEnvs); console.log("Variables to create:"); console.table(create); console.log("Variables to update:"); console.table(update.map(({ origin, target }) => ({ id: origin.id, ...target }))); await createVercelEnvVars(create, config); await updateVercelEnvVars(update, config); } start();