Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow access to public env in app.html #8449

Merged
merged 12 commits into from
Jan 19, 2023
5 changes: 5 additions & 0 deletions .changeset/spotty-points-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: enable access to public env within app.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<div>` or other element, rather than directly inside `<body>`, 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
Expand Down
39 changes: 24 additions & 15 deletions packages/kit/src/core/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "')}
Expand Down
6 changes: 2 additions & 4 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/env-public.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @type {Record<string, string>} */
export let env = {};

/** @type {(environment: Record<string, string>) => void} */
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/test/apps/basics/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion packages/kit/test/apps/basics/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<meta name="transform-page" content="__REPLACEME__" />
%sveltekit.head%
</head>
<body>
<body class="%sveltekit.env.PUBLIC_THEME%">
<div>%sveltekit.body%</div>
<a href="/routing/link-outside-app-target/target">outside app target</a>
</body>
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
8 changes: 7 additions & 1 deletion packages/kit/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, string>;
}): string;
error(values: { message: string; status: number }): string;
};
}
Expand Down