stack/packages/template/scripts/generate-env.ts
Bilal Godil 1ede37281f 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.
2026-06-16 17:56:47 -07:00

106 lines
3.2 KiB
TypeScript

import { writeFileSyncIfChanged } from "@hexclave/shared/dist/utils/fs";
import { deindent } from "@hexclave/shared/dist/utils/strings";
const envVarsConfig: Record<string, { allowPublic?: boolean, deprecatedLegacyNames?: string[] }> = {
HEXCLAVE_PORT_PREFIX: {
allowPublic: true,
},
HEXCLAVE_PROJECT_ID: {
allowPublic: true,
},
HEXCLAVE_PUBLISHABLE_CLIENT_KEY: {
allowPublic: true,
},
HEXCLAVE_SECRET_SERVER_KEY: {},
HEXCLAVE_SUPER_SECRET_ADMIN_KEY: {},
HEXCLAVE_EXTRA_REQUEST_HEADERS: {
allowPublic: true,
},
HEXCLAVE_API_URL_BROWSER: {
allowPublic: true,
deprecatedLegacyNames: ["BROWSER_STACK_API_URL", "BROWSER_HEXCLAVE_API_URL"],
},
HEXCLAVE_API_URL_SERVER: {
allowPublic: true,
deprecatedLegacyNames: ["SERVER_STACK_API_URL", "SERVER_HEXCLAVE_API_URL"],
},
HEXCLAVE_API_URL: {
allowPublic: true,
deprecatedLegacyNames: ["HEXCLAVE_URL", "STACK_URL"],
},
HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX: {
allowPublic: true,
},
HEXCLAVE_HOSTED_HANDLER_URL_TEMPLATE: {
allowPublic: true,
},
HEXCLAVE_STRIPE_PUBLISHABLE_KEY: {
allowPublic: true,
},
HEXCLAVE_BOT_CHALLENGE_SITE_KEY: {
allowPublic: true,
},
HEXCLAVE_BOT_CHALLENGE_INVISIBLE_SITE_KEY: {
allowPublic: true,
},
HEXCLAVE_IS_LOCAL_EMULATOR: {
allowPublic: true,
},
HEXCLAVE_POSTHOG_KEY: {
allowPublic: true,
},
HEXCLAVE_SVIX_SERVER_URL: {
allowPublic: true,
},
HEXCLAVE_SENTRY_DSN: {
allowPublic: true,
},
HEXCLAVE_VERSION_ALERTER_SEVERE_ONLY: {
allowPublic: true,
},
NODE_ENV: {
allowPublic: false,
},
};
function getEnvVarSnippet(variableName: string) {
return deindent`
((typeof process !== "undefined" ? process.env.${variableName} : undefined) || import.meta.env?.${variableName})
`;
}
function generateEnvVarsConstSnippet() {
const getters: string[] = [];
for (const [key, config] of Object.entries(envVarsConfig)) {
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 ${allVariables.map(getEnvVarSnippet).join("\n || ")} || undefined;
},
`);
}
return deindent`
// THIS FILE IS AUTO-GENERATED BY THE \`generate-env.ts\` SCRIPT.
// DO NOT EDIT IT BY HAND.
/* eslint-disable no-restricted-properties */
export const envVars = {
${getters.join("\n")}
};
` + "\n";
}
writeFileSyncIfChanged("src/generated/env.ts", generateEnvVarsConstSnippet());