Don't ship env-var conflict-throw in generated customer SDK

The generated env.ts getters ship inside @hexclave/stack / @hexclave/react
and are read on a hot, side-effect-free path by SDK consumers. The
conflict-detection helper added with the HEXCLAVE_* rename made those getters
throw when both a HEXCLAVE_* and STACK_* spelling were set to different values
— a breaking change to env-var reading for SDK users.

Revert the generated getters to a plain || dual-read chain (prefer HEXCLAVE_*,
fall back to legacy STACK_*, empty-as-unset), with no throw. Conflict detection
stays in our own non-shipped infra only (packages/shared getEnvVariable/
getProcessEnv, dashboard inline env, CLI auth, docker entrypoint).

Order-preserving dedup of the candidate list is kept so HEXCLAVE_API_URL no
longer emits its STACK_URL aliases twice.
This commit is contained in:
Bilal Godil 2026-06-16 17:56:32 -07:00
parent c9602352df
commit 1ede37281f

View File

@ -63,76 +63,31 @@ const envVarsConfig: Record<string, { allowPublic?: boolean, deprecatedLegacyNam
},
};
function getHexclaveEnvVarName(name: string): string | undefined {
if (!name.includes("STACK_")) {
return undefined;
}
return name.replace("STACK_", "HEXCLAVE_");
}
function getStackEnvVarName(name: string): string | undefined {
if (!name.includes("HEXCLAVE_")) {
return undefined;
}
return name.replace("HEXCLAVE_", "STACK_");
}
function getEnvVarSnippet(variableName: string) {
return deindent`
((typeof process !== "undefined" ? process.env.${variableName} : undefined) || import.meta.env?.${variableName})
`;
}
function getEnvVarCandidateSnippets(allVariables: string[]) {
const allVariablesSet = new Set(allVariables);
const emittedVariables = new Set<string>();
const snippets: string[] = [];
for (const variableName of allVariables) {
if (emittedVariables.has(variableName)) {
continue;
}
const stackName = getStackEnvVarName(variableName);
if (stackName != null && allVariablesSet.has(stackName)) {
emittedVariables.add(variableName);
emittedVariables.add(stackName);
snippets.push(deindent`
resolveHexclaveStackEnvVar("${variableName}", "${stackName}", ${getEnvVarSnippet(variableName)}, ${getEnvVarSnippet(stackName)})
`);
continue;
}
const hexclaveName = getHexclaveEnvVarName(variableName);
if (hexclaveName != null && allVariablesSet.has(hexclaveName)) {
emittedVariables.add(hexclaveName);
emittedVariables.add(variableName);
snippets.push(deindent`
resolveHexclaveStackEnvVar("${hexclaveName}", "${variableName}", ${getEnvVarSnippet(hexclaveName)}, ${getEnvVarSnippet(variableName)})
`);
continue;
}
emittedVariables.add(variableName);
snippets.push(getEnvVarSnippet(variableName));
}
return snippets;
}
function generateEnvVarsConstSnippet() {
const getters: string[] = [];
for (const [key, config] of Object.entries(envVarsConfig)) {
const allVariables = [key, ...(config.deprecatedLegacyNames ?? [])]
.flatMap(k => k.startsWith("HEXCLAVE_") ? [k, k.replace("HEXCLAVE_", "STACK_")] : [k])
.flatMap(k => config.allowPublic ? [k, `NEXT_PUBLIC_${k}`, `VITE_${k}`] : [k]);
const candidateSnippets = getEnvVarCandidateSnippets(allVariables);
// Use || (not ??) between candidates: empty-string env vars (e.g. the empty
// HEXCLAVE_* placeholders in checked-in .env templates) must not shadow a
// real value under a legacy STACK_* name further down the chain.
const allVariables = [...new Set(
[key, ...(config.deprecatedLegacyNames ?? [])]
.flatMap(k => k.startsWith("HEXCLAVE_") ? [k, k.replace("HEXCLAVE_", "STACK_")] : [k])
.flatMap(k => config.allowPublic ? [k, `NEXT_PUBLIC_${k}`, `VITE_${k}`] : [k])
)];
// Prefer the canonical HEXCLAVE_* spelling, falling back to the legacy STACK_*
// name. Use || (not ??) between candidates so an empty-string env var (e.g. an
// empty HEXCLAVE_* placeholder) can't shadow a real value further down the chain.
//
// IMPORTANT: this getter ships in the customer SDK, so it must NOT throw when
// both spellings are set — reading an env var is a hot, side-effect-free path
// for SDK consumers. Conflict detection lives only in our own (non-shipped)
// env helpers (packages/shared, dashboard inline env, CLI), never here.
getters.push(deindent`
get ${key}() {
return ${candidateSnippets.join("\n || ")} || undefined;
return ${allVariables.map(getEnvVarSnippet).join("\n || ")} || undefined;
},
`);
}
@ -141,13 +96,6 @@ function generateEnvVarsConstSnippet() {
// DO NOT EDIT IT BY HAND.
/* eslint-disable no-restricted-properties */
function resolveHexclaveStackEnvVar(hexclaveName: string, stackName: string, hexclaveValue: string | undefined, stackValue: string | undefined): string | undefined {
if (hexclaveValue && stackValue && hexclaveValue !== stackValue) {
throw new Error(\`Environment variables \${hexclaveName} and \${stackName} are both set to different values. Remove one of them or set them to the same value.\`);
}
return hexclaveValue || stackValue || undefined;
}
export const envVars = {
${getters.join("\n")}
};