Skip to content

Commit 60eed0e

Browse files
committed
feat(server): WEBJS_PUBLIC_* env vars accessible as process.env.* in browser
Adds an SSR-injected inline script that defines window.process.env with all server-side env vars whose name starts with WEBJS_PUBLIC_, plus NODE_ENV based on dev/prod mode. Counterpart of Next.js's NEXT_PUBLIC_ convention, without a build step. Two consequences: * App code can write process.env.WEBJS_PUBLIC_API_URL in components and the value is available at runtime in the browser. No props threading required for things like Stripe publishable keys, analytics IDs, Sentry DSN. * Vendor bundles that probe process.env.NODE_ENV (lit, react, etc.) no longer throw ReferenceError in the browser. Fixes a latent bug. Security: only WEBJS_PUBLIC_* prefixed vars cross the wire. Other server env vars stay on the server. Values are JSON-encoded and '</' sequences are escaped so a value containing '</script>' cannot terminate the inline script tag. The shim emits before importMapTag() so it runs before any vendor bundle or user module executes. CSP nonces, when present on the request, propagate to the shim script tag.
1 parent ac46664 commit 60eed0e

1 file changed

Lines changed: 40 additions & 0 deletions

File tree

packages/server/src/ssr.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,45 @@ function collectHoistedHeadTags(bodyHtml) {
601601
*
602602
* @param {{ metadata: Record<string,any>, moduleUrls: string[], dev: boolean, streaming: boolean, preloads?: string[], lazyComponents?: Record<string, string>, nonce?: string }} opts
603603
*/
604+
/**
605+
* Build an inline `<script>` that exposes server-side environment
606+
* variables to the browser via `window.process.env`. Two purposes:
607+
*
608+
* 1. App code can read `process.env.WEBJS_PUBLIC_X` directly in
609+
* components (counterpart of Next.js's `NEXT_PUBLIC_` prefix,
610+
* but without a build step).
611+
* 2. `process.env.NODE_ENV` is defined for vendor bundles that
612+
* probe it (lit, react, etc.) so they do not throw
613+
* ReferenceError in the browser.
614+
*
615+
* Only variables whose name starts with `WEBJS_PUBLIC_` are exposed.
616+
* Other server env vars stay on the server.
617+
*
618+
* `</...` sequences in stringified values are escaped so an env value
619+
* containing `</script>` cannot terminate the inline script tag.
620+
*
621+
* @param {{ dev: boolean, nonce?: string, env?: Record<string, string|undefined> }} opts
622+
* `env` defaults to `process.env`. Override for tests.
623+
* @returns {string}
624+
*/
625+
export function publicEnvShim(opts) {
626+
const source = opts.env || process.env;
627+
/** @type {Record<string, string>} */
628+
const env = {};
629+
for (const [k, v] of Object.entries(source)) {
630+
if (k.startsWith('WEBJS_PUBLIC_') && v !== undefined) {
631+
env[k] = String(v);
632+
}
633+
}
634+
env.NODE_ENV = opts.dev ? 'development' : 'production';
635+
const json = JSON.stringify(env).replace(/<\//g, '<\\/');
636+
const n = opts.nonce ? ` nonce="${escapeAttr(opts.nonce)}"` : '';
637+
return `<script${n}>`
638+
+ `window.process=window.process||{};`
639+
+ `window.process.env=Object.assign(window.process.env||{},${json});`
640+
+ `</script>`;
641+
}
642+
604643
function wrapHead(opts) {
605644
// CSP nonce: if provided, all inline <script> tags get nonce="…" so they
606645
// pass strict Content-Security-Policy headers. The nonce is extracted from
@@ -973,6 +1012,7 @@ function wrapHead(opts) {
9731012
<meta charset="utf-8">
9741013
${metaTags.join('\n')}
9751014
<title>${escapeHtml(title)}</title>
1015+
${publicEnvShim({ dev: opts.dev, nonce: opts.nonce })}
9761016
${importMapTag()}
9771017
${linkTags.join('\n')}
9781018
${boot}

0 commit comments

Comments
 (0)