diff --git a/.changeset/spotty-points-battle.md b/.changeset/spotty-points-battle.md new file mode 100644 index 000000000000..530627587b96 --- /dev/null +++ b/.changeset/spotty-points-battle.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: enable access to public env within app.html diff --git a/documentation/docs/10-getting-started/30-project-structure.md b/documentation/docs/10-getting-started/30-project-structure.md index 1a3d9b6724a8..c75a46eb46d0 100644 --- a/documentation/docs/10-getting-started/30-project-structure.md +++ b/documentation/docs/10-getting-started/30-project-structure.md @@ -45,6 +45,7 @@ The `src` directory contains the meat of your project. - `%sveltekit.body%` — the markup for a rendered page. This should live inside a `
` or other element, rather than directly inside ``, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process. SvelteKit will warn you in development if this is not the case - `%sveltekit.assets%` — either [`paths.assets`](/docs/configuration#paths), if specified, or a relative path to [`paths.base`](/docs/configuration#paths) - `%sveltekit.nonce%` — a [CSP](/docs/configuration#csp) nonce for manually included links and scripts, if used + - `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (usually `PUBLIC_`). It will fallback to `''` if not matched. - `error.html` (optional) is the page that is rendered when everything else fails. It can contain the following placeholders: - `%sveltekit.status%` — the HTTP status - `%sveltekit.error.message%` — the error message diff --git a/packages/kit/src/core/config/index.js b/packages/kit/src/core/config/index.js index 047c67532b47..04a4eaffc0f3 100644 --- a/packages/kit/src/core/config/index.js +++ b/packages/kit/src/core/config/index.js @@ -9,24 +9,33 @@ import options from './options.js'; * @param {string} cwd * @param {import('types').ValidatedConfig} config */ -export function load_template(cwd, config) { - const { appTemplate } = config.kit.files; - const relative = path.relative(cwd, appTemplate); - - if (fs.existsSync(appTemplate)) { - const contents = fs.readFileSync(appTemplate, 'utf8'); - - const expected_tags = ['%sveltekit.head%', '%sveltekit.body%']; - expected_tags.forEach((tag) => { - if (contents.indexOf(tag) === -1) { - throw new Error(`${relative} is missing ${tag}`); - } - }); - } else { +export function load_template(cwd, { kit }) { + const { env, files } = kit; + + const relative = path.relative(cwd, files.appTemplate); + + if (!fs.existsSync(files.appTemplate)) { throw new Error(`${relative} does not exist`); } - return fs.readFileSync(appTemplate, 'utf-8'); + const contents = fs.readFileSync(files.appTemplate, 'utf8'); + + const expected_tags = ['%sveltekit.head%', '%sveltekit.body%']; + expected_tags.forEach((tag) => { + if (contents.indexOf(tag) === -1) { + throw new Error(`${relative} is missing ${tag}`); + } + }); + + for (const match of contents.matchAll(/%sveltekit\.env\.([^%]+)%/g)) { + if (!match[1].startsWith(env.publicPrefix)) { + throw new Error( + `Environment variables in ${relative} must start with ${env.publicPrefix} (saw %sveltekit.env.${match[1]}%)` + ); + } + } + + return contents; } /** diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 162a6efc1944..6cb05a8ee626 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -38,11 +38,15 @@ export const options = { root, service_worker: ${has_service_worker}, templates: { - app: ({ head, body, assets, nonce }) => ${s(template) + app: ({ head, body, assets, nonce, env }) => ${s(template) .replace('%sveltekit.head%', '" + head + "') .replace('%sveltekit.body%', '" + body + "') .replace(/%sveltekit\.assets%/g, '" + assets + "') - .replace(/%sveltekit\.nonce%/g, '" + nonce + "')}, + .replace(/%sveltekit\.nonce%/g, '" + nonce + "') + .replace( + /%sveltekit\.env\.([^%]+)%/g, + (_match, capture) => `" + (env[${s(capture)}] ?? "") + "` + )}, error: ({ status, message }) => ${s(error_page) .replace(/%sveltekit\.status%/g, '" + status + "') .replace(/%sveltekit\.error\.message%/g, '" + message + "')} diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 21b7f7bc7d03..a2ba0d881a2c 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -73,8 +73,7 @@ export async function dev(vite, vite_config, svelte_config) { } catch (error) { manifest_error = /** @type {Error} */ (error); - console.error(colors.bold().red('Invalid routes')); - console.error(error); + console.error(colors.bold().red(manifest_error.message)); vite.ws.send({ type: 'error', err: { @@ -444,8 +443,7 @@ export async function dev(vite, vite_config, svelte_config) { } if (manifest_error) { - console.error(colors.bold().red('Invalid routes')); - console.error(manifest_error); + console.error(colors.bold().red(manifest_error.message)); const error_page = load_error_page(svelte_config); diff --git a/packages/kit/src/runtime/env-public.js b/packages/kit/src/runtime/env-public.js index 481a87046093..800f6262aea6 100644 --- a/packages/kit/src/runtime/env-public.js +++ b/packages/kit/src/runtime/env-public.js @@ -1,3 +1,4 @@ +/** @type {Record} */ export let env = {}; /** @type {(environment: Record) => void} */ diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 85f06742b2d9..e23e8ee006b0 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -366,7 +366,8 @@ export async function render_response({ head, body, assets: resolved_assets, - nonce: /** @type {string} */ (csp.nonce) + nonce: /** @type {string} */ (csp.nonce), + env }); // TODO flush chunks as early as we can diff --git a/packages/kit/test/apps/basics/.env b/packages/kit/test/apps/basics/.env index 1597af1243ec..a406fc2451a1 100644 --- a/packages/kit/test/apps/basics/.env +++ b/packages/kit/test/apps/basics/.env @@ -3,3 +3,5 @@ PRIVATE_DYNAMIC="accessible to server-side code/evaluated at run time" PUBLIC_STATIC="accessible anywhere/replaced at build time" PUBLIC_DYNAMIC="accessible anywhere/evaluated at run time" + +PUBLIC_THEME="groovy" diff --git a/packages/kit/test/apps/basics/src/app.html b/packages/kit/test/apps/basics/src/app.html index 4f4316bd9ff4..aa17593fe727 100644 --- a/packages/kit/test/apps/basics/src/app.html +++ b/packages/kit/test/apps/basics/src/app.html @@ -7,7 +7,7 @@ %sveltekit.head% - +
%sveltekit.body%
outside app target diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 06262df373d1..91b4beec802a 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -600,3 +600,10 @@ test.describe('Content negotiation', () => { await expect(page.locator('[data-testid="form-result"]')).toHaveText('form.submitted: true'); }); }); + +test.describe('env in app.html', () => { + test('can access public env', async ({ page }) => { + await page.goto('/'); + expect(await page.locator('body').getAttribute('class')).toContain('groovy'); + }); +}); diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 54679a608683..85734650ff7f 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -306,7 +306,13 @@ export interface SSROptions { root: SSRComponent['default']; service_worker: boolean; templates: { - app(values: { head: string; body: string; assets: string; nonce: string }): string; + app(values: { + head: string; + body: string; + assets: string; + nonce: string; + env: Record; + }): string; error(values: { message: string; status: number }): string; }; }