initial commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
/* ============================================================================
|
||||
SERVER ENVIRONMENT
|
||||
----------------------------------------------------------------------------
|
||||
The `.server` suffix is enforced by the React Router Vite plugin: this
|
||||
module is stripped from the browser bundle, and importing it from component
|
||||
code fails the build rather than leaking STRAPI_TOKEN to the client.
|
||||
|
||||
Validation is lazy and memoised. Reading it at module load would crash the
|
||||
whole server on a typo in one variable; reading it on first use lets the
|
||||
error surface as an ordinary 500 with every problem listed at once.
|
||||
========================================================================= */
|
||||
|
||||
export type NodeEnv = "development" | "production" | "test";
|
||||
|
||||
export interface ServerEnv {
|
||||
readonly NODE_ENV: NodeEnv;
|
||||
/** Strapi REST origin, no trailing slash. Default http://localhost:1337 */
|
||||
readonly STRAPI_URL: string;
|
||||
/** Optional API token. Sent as a bearer token when present. */
|
||||
readonly STRAPI_TOKEN?: string;
|
||||
/** Public origin of this site, no trailing slash. Used for canonical URLs. */
|
||||
readonly SITE_URL: string;
|
||||
readonly isProduction: boolean;
|
||||
readonly isDevelopment: boolean;
|
||||
}
|
||||
|
||||
/** Thrown when one or more variables are missing or malformed. */
|
||||
export class EnvError extends Error {
|
||||
override readonly name = "EnvError";
|
||||
readonly issues: readonly string[];
|
||||
|
||||
constructor(issues: readonly string[]) {
|
||||
super(`Invalid server environment:\n - ${issues.join("\n - ")}`);
|
||||
this.issues = issues;
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULTS = {
|
||||
STRAPI_URL: "http://localhost:1337",
|
||||
SITE_URL: "https://sharper.ly",
|
||||
} as const;
|
||||
|
||||
function trimTrailingSlash(value: string): string {
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function readOrigin(
|
||||
name: string,
|
||||
raw: string | undefined,
|
||||
fallback: string,
|
||||
issues: string[],
|
||||
): string {
|
||||
const value = raw?.trim() || fallback;
|
||||
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
issues.push(`${name} must be an http(s) URL, received "${value}"`);
|
||||
return trimTrailingSlash(fallback);
|
||||
}
|
||||
return trimTrailingSlash(value);
|
||||
} catch {
|
||||
issues.push(`${name} must be an absolute URL, received "${value}"`);
|
||||
return trimTrailingSlash(fallback);
|
||||
}
|
||||
}
|
||||
|
||||
function readNodeEnv(raw: string | undefined): NodeEnv {
|
||||
return raw === "production" || raw === "test" ? raw : "development";
|
||||
}
|
||||
|
||||
let cached: ServerEnv | undefined;
|
||||
|
||||
/** The validated environment. Memoised after the first successful read. */
|
||||
export function getServerEnv(): ServerEnv {
|
||||
if (cached) return cached;
|
||||
|
||||
const source = process.env;
|
||||
const issues: string[] = [];
|
||||
|
||||
const NODE_ENV = readNodeEnv(source.NODE_ENV);
|
||||
const STRAPI_URL = readOrigin(
|
||||
"STRAPI_URL",
|
||||
source.STRAPI_URL,
|
||||
DEFAULTS.STRAPI_URL,
|
||||
issues,
|
||||
);
|
||||
const SITE_URL = readOrigin(
|
||||
"SITE_URL",
|
||||
source.SITE_URL,
|
||||
DEFAULTS.SITE_URL,
|
||||
issues,
|
||||
);
|
||||
const token = source.STRAPI_TOKEN?.trim();
|
||||
|
||||
if (issues.length > 0) throw new EnvError(issues);
|
||||
|
||||
cached = {
|
||||
NODE_ENV,
|
||||
STRAPI_URL,
|
||||
SITE_URL,
|
||||
...(token ? { STRAPI_TOKEN: token } : {}),
|
||||
isProduction: NODE_ENV === "production",
|
||||
isDevelopment: NODE_ENV === "development",
|
||||
};
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** Drop the memoised value. Only useful in tests. */
|
||||
export function resetServerEnv(): void {
|
||||
cached = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The origin canonical URLs and hreflang alternates are built from.
|
||||
*
|
||||
* SITE_URL wins when it is set to something other than the default, so a
|
||||
* preview deployment behind a proxy still emits its own canonical. Otherwise
|
||||
* the incoming request is trusted, honouring the standard forwarding headers.
|
||||
*/
|
||||
export function getSiteUrl(request: Request): string {
|
||||
const { SITE_URL } = getServerEnv();
|
||||
if (process.env.SITE_URL?.trim()) return SITE_URL;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const forwardedHost =
|
||||
request.headers.get("x-forwarded-host") ?? request.headers.get("host");
|
||||
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||||
|
||||
const protocol = forwardedProto?.split(",")[0]?.trim() ?? url.protocol.replace(":", "");
|
||||
const host = forwardedHost?.split(",")[0]?.trim() ?? url.host;
|
||||
|
||||
return trimTrailingSlash(`${protocol}://${host}`);
|
||||
}
|
||||
Reference in New Issue
Block a user