Skip to content

Commit cfb8eff

Browse files
Rich-Harriswighawagbenmccanndummdidumm
authored
feat: set dynamic base when rendering page (#9220)
* dynamic base * fix failing test * reuse base expression if possible * minor tweaks * shorthand * fix * hardcode fallbacks * changeset * use dynamic base path when server rendering * use relative assets * tidy up * fix * reduce indirection * add paths.reset() function * add paths.relative * fix tests * simplify * small tweak * update changesets * add tests, and fix the bug revealed by the tests * fix * Update packages/kit/src/core/config/options.js Co-authored-by: Simon H <[email protected]> * Update packages/kit/src/runtime/server/page/render.js Co-authored-by: Rich Harris <[email protected]> --------- Co-authored-by: wighawag <[email protected]> Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Simon H <[email protected]>
1 parent 53e168f commit cfb8eff

File tree

19 files changed

+174
-68
lines changed

19 files changed

+174
-68
lines changed

.changeset/few-lions-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add `paths.relative` option to control interpretation of `paths.assets` and `paths.base`

packages/kit/src/core/config/index.spec.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ const get_defaults = (prefix = '') => ({
9898
typescript: {},
9999
paths: {
100100
base: '',
101-
assets: ''
101+
assets: '',
102+
relative: undefined
102103
},
103104
prerender: {
104105
concurrency: 1,
@@ -236,6 +237,7 @@ test('fails if paths.base is not root-relative', () => {
236237
validate_config({
237238
kit: {
238239
paths: {
240+
// @ts-expect-error
239241
base: 'https://example.com/somewhere/else'
240242
}
241243
}
@@ -260,6 +262,7 @@ test('fails if paths.assets is relative', () => {
260262
validate_config({
261263
kit: {
262264
paths: {
265+
// @ts-expect-error
263266
assets: 'foo'
264267
}
265268
}
@@ -294,8 +297,8 @@ test('fails if prerender.entries are invalid', () => {
294297

295298
/**
296299
* @param {string} name
297-
* @param {{ base?: string, assets?: string }} input
298-
* @param {{ base?: string, assets?: string }} output
300+
* @param {import('types').KitConfig['paths']} input
301+
* @param {import('types').KitConfig['paths']} output
299302
*/
300303
function validate_paths(name, input, output) {
301304
test(name, () => {
@@ -317,7 +320,8 @@ validate_paths(
317320
},
318321
{
319322
base: '/path/to/base',
320-
assets: ''
323+
assets: '',
324+
relative: undefined
321325
}
322326
);
323327

@@ -328,7 +332,8 @@ validate_paths(
328332
},
329333
{
330334
base: '',
331-
assets: 'https://cdn.example.com'
335+
assets: 'https://cdn.example.com',
336+
relative: undefined
332337
}
333338
);
334339

@@ -340,7 +345,8 @@ validate_paths(
340345
},
341346
{
342347
base: '/path/to/base',
343-
assets: 'https://cdn.example.com'
348+
assets: 'https://cdn.example.com',
349+
relative: undefined
344350
}
345351
);
346352

packages/kit/src/core/config/options.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ const options = object(
171171
}
172172
}
173173

174+
return input;
175+
}),
176+
relative: validate(undefined, (input, keypath) => {
177+
if (typeof input !== 'boolean') {
178+
throw new Error(`${keypath} option must be a boolean or undefined`);
179+
}
180+
174181
return input;
175182
})
176183
}),

packages/kit/src/core/sync/write_server.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const server_template = ({
2727
}) => `
2828
import root from '../root.svelte';
2929
import { set_building } from '__sveltekit/environment';
30-
import { set_assets, set_private_env, set_public_env } from '${runtime_directory}/shared-server.js';
30+
import { set_assets } from '__sveltekit/paths';
31+
import { set_private_env, set_public_env } from '${runtime_directory}/shared-server.js';
3132
3233
export const options = {
3334
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},

packages/kit/src/exports/vite/dev/index.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,15 +451,14 @@ export async function dev(vite, vite_config, svelte_config) {
451451
await vite.ssrLoadModule(`${runtime_base}/server/index.js`)
452452
);
453453

454-
const { set_assets, set_fix_stack_trace } =
455-
/** @type {import('types').ServerInternalModule} */ (
456-
await vite.ssrLoadModule(`${runtime_base}/shared-server.js`)
457-
);
454+
const { set_fix_stack_trace } = await vite.ssrLoadModule(
455+
`${runtime_base}/shared-server.js`
456+
);
457+
set_fix_stack_trace(fix_stack_trace);
458458

459+
const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths');
459460
set_assets(assets);
460461

461-
set_fix_stack_trace(fix_stack_trace);
462-
463462
const server = new Server(manifest);
464463

465464
await server.init({ env });

packages/kit/src/exports/vite/index.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,17 +379,34 @@ function kit({ svelte_config }) {
379379
case '\0__sveltekit/paths':
380380
const { assets, base } = svelte_config.kit.paths;
381381

382+
// use the values defined in `global`, but fall back to hard-coded values
383+
// for the sake of things like Vitest which may import this module
384+
// outside the context of a page
382385
if (browser) {
383-
return `export const base = ${s(base)};
384-
export const assets = ${global}.assets;`;
386+
return `export const base = ${global}?.base ?? ${s(base)};
387+
export const assets = ${global}?.assets ?? ${assets ? s(assets) : 'base'};`;
385388
}
386389

387-
return `export const base = ${s(base)};
390+
return `export let base = ${s(base)};
388391
export let assets = ${assets ? s(assets) : 'base'};
389392
393+
export const relative = ${svelte_config.kit.paths.relative};
394+
395+
const initial = { base, assets };
396+
397+
export function override(paths) {
398+
base = paths.base;
399+
assets = paths.assets;
400+
}
401+
402+
export function reset() {
403+
base = initial.base;
404+
assets = initial.assets;
405+
}
406+
390407
/** @param {string} path */
391408
export function set_assets(path) {
392-
assets = path;
409+
assets = initial.assets = path;
393410
}`;
394411

395412
case '\0__sveltekit/environment':

packages/kit/src/internal.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ declare module '__sveltekit/environment' {
77

88
/** Internal version of $app/paths */
99
declare module '__sveltekit/paths' {
10-
export const base: `/${string}`;
11-
export let assets: `https://${string}` | `http://${string}`;
10+
export let base: '' | `/${string}`;
11+
export let assets: '' | `https://${string}` | `http://${string}` | '/_svelte_kit_assets';
12+
export let relative: boolean | undefined; // TODO in 2.0, make this a `boolean` that defaults to `true`
13+
export function reset(): void;
14+
export function override(paths: { base: string; assets: string }): void;
1215
export function set_assets(path: string): void;
1316
}

packages/kit/src/runtime/server/page/render.js

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as devalue from 'devalue';
22
import { readable, writable } from 'svelte/store';
33
import { DEV } from 'esm-env';
4-
import { assets, base } from '__sveltekit/paths';
4+
import * as paths from '__sveltekit/paths';
55
import { hash } from '../../hash.js';
66
import { serialize_data } from './serialize_data.js';
77
import { s } from '../../../utils/misc.js';
@@ -11,6 +11,7 @@ import { clarify_devalue_error, stringify_uses, handle_error_and_jsonify } from
1111
import { public_env } from '../../shared-server.js';
1212
import { text } from '../../../exports/index.js';
1313
import { create_async_iterator } from '../../../utils/streaming.js';
14+
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
1415

1516
// TODO rename this function/module
1617

@@ -80,6 +81,43 @@ export async function render_response({
8081
? action_result.data ?? null
8182
: null;
8283

84+
/** @type {string} */
85+
let base = paths.base;
86+
87+
/** @type {string} */
88+
let assets = paths.assets;
89+
90+
/**
91+
* An expression that will evaluate in the client to determine the resolved base path.
92+
* We use a relative path when possible to support IPFS, the internet archive, etc.
93+
*/
94+
let base_expression = s(paths.base);
95+
96+
// if appropriate, use relative paths for greater portability
97+
if (paths.relative !== false && !state.prerendering?.fallback) {
98+
const segments = event.url.pathname.slice(paths.base.length).split('/');
99+
100+
if (segments.length === 1 && paths.base !== '') {
101+
// if we're on `/my-base-path`, relative links need to start `./my-base-path` rather than `.`
102+
base = `./${paths.base.split('/').at(-1)}`;
103+
104+
base_expression = `new URL(${s(base)}, location).pathname`;
105+
} else {
106+
base =
107+
segments
108+
.slice(2)
109+
.map(() => '..')
110+
.join('/') || '.';
111+
112+
// resolve e.g. '../..' against current location, then remove trailing slash
113+
base_expression = `new URL(${s(base)}, location).pathname.slice(0, -1)`;
114+
}
115+
116+
if (!paths.assets || (paths.assets[0] === '/' && paths.assets !== SVELTE_KIT_ASSETS)) {
117+
assets = base;
118+
}
119+
}
120+
83121
if (page_config.ssr) {
84122
if (__SVELTEKIT_DEV__ && !branch.at(-1)?.node.component) {
85123
// Can only be the leaf, layouts have a fallback component generated
@@ -116,6 +154,10 @@ export async function render_response({
116154
form: form_value
117155
};
118156

157+
// use relative paths during rendering, so that the resulting HTML is as
158+
// portable as possible, but reset afterwards
159+
if (paths.relative) paths.override({ base, assets });
160+
119161
if (__SVELTEKIT_DEV__) {
120162
const fetch = globalThis.fetch;
121163
let warned = false;
@@ -138,9 +180,14 @@ export async function render_response({
138180
rendered = options.root.render(props);
139181
} finally {
140182
globalThis.fetch = fetch;
183+
paths.reset();
141184
}
142185
} else {
143-
rendered = options.root.render(props);
186+
try {
187+
rendered = options.root.render(props);
188+
} finally {
189+
paths.reset();
190+
}
144191
}
145192

146193
for (const { node } of branch) {
@@ -156,35 +203,6 @@ export async function render_response({
156203
rendered = { head: '', html: '', css: { code: '', map: null } };
157204
}
158205

159-
/**
160-
* The prefix to use for static assets. Replaces `%sveltekit.assets%` in the template
161-
* @type {string}
162-
*/
163-
let resolved_assets;
164-
165-
/**
166-
* An expression that will evaluate in the client to determine the resolved asset path
167-
*/
168-
let asset_expression;
169-
170-
if (assets) {
171-
// if an asset path is specified, use it
172-
resolved_assets = assets;
173-
asset_expression = s(assets);
174-
} else if (state.prerendering?.fallback) {
175-
// if we're creating a fallback page, asset paths need to be root-relative
176-
resolved_assets = base;
177-
asset_expression = s(base);
178-
} else {
179-
// otherwise we want asset paths to be relative to the page, so that they
180-
// will work in odd contexts like IPFS, the internet archive, and so on
181-
const segments = event.url.pathname.slice(base.length).split('/').slice(2);
182-
resolved_assets = segments.length > 0 ? segments.map(() => '..').join('/') : '.';
183-
asset_expression = `new URL(${s(
184-
resolved_assets
185-
)}, location.href).pathname.replace(/^\\\/$/, '')`;
186-
}
187-
188206
let head = '';
189207
let body = rendered.html;
190208

@@ -198,9 +216,9 @@ export async function render_response({
198216
// Vite makes the start script available through the base path and without it.
199217
// We load it via the base path in order to support remote IDE environments which proxy
200218
// all URLs under the base path during development.
201-
return base + path;
219+
return paths.base + path;
202220
}
203-
return `${resolved_assets}/${path}`;
221+
return `${assets}/${path}`;
204222
};
205223

206224
if (inline_styles.size > 0) {
@@ -286,9 +304,10 @@ export async function render_response({
286304

287305
const properties = [
288306
`env: ${s(public_env)}`,
289-
`assets: ${asset_expression}`,
307+
paths.assets && `assets: ${s(paths.assets)}`,
308+
`base: ${base_expression}`,
290309
`element: document.currentScript.parentElement`
291-
];
310+
].filter(Boolean);
292311

293312
if (chunks) {
294313
blocks.push(`const deferred = new Map();`);
@@ -419,7 +438,7 @@ export async function render_response({
419438
const html = options.templates.app({
420439
head,
421440
body,
422-
assets: resolved_assets,
441+
assets,
423442
nonce: /** @type {string} */ (csp.nonce),
424443
env: public_env
425444
});

packages/kit/src/runtime/shared-server.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
export { set_assets } from '__sveltekit/paths';
2-
31
/** @type {Record<string, string>} */
42
export let private_env = {};
53

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import { base, assets } from '$app/paths';
3+
</script>
4+
5+
<pre>{JSON.stringify({ base, assets })}</pre>

0 commit comments

Comments
 (0)