Skip to content

Commit 6a7b8c5

Browse files
committed
lazy-load shared to allow browser-only globals in +page.js
1 parent 0bb5cd3 commit 6a7b8c5

File tree

12 files changed

+86
-46
lines changed

12 files changed

+86
-46
lines changed

documentation/docs/12-page-options.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,6 @@ Normally, SvelteKit renders your page on the server first and sends that HTML to
8383
export const ssr = false;
8484
```
8585

86-
In contrast to the other options, you can set this option in both `+page.js` and `+layout.js`. `ssr` options in subsequent layouts or the page overwrite earlier options. You cannot set this option in `+page.server.js` or `+layout.server.js`. This option overwrites the `ssr` option [in the handle hook](/docs/hooks#handle).
86+
In contrast to the other options, you can set this option in both `+page.js` and `+layout.js`. `ssr` options in subsequent layouts or the page overwrite earlier options. You cannot set this option in `+page.server.js` or `+layout.server.js`. This option does not take effect if the `ssr` option [in the handle hook](/docs/hooks#handle) is evaluated to `false`.
87+
88+
> Why two `ssr` options? The `ssr` option in the handle hook is useful if your `+page.js` is already eagerly accessing browser-only objects, which means the server would crash while trying to evaluate the `ssr` option.

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

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { negotiate } from '../../../utils/http.js';
22
import { render_response } from './render.js';
33
import { respond_with_error } from './respond_with_error.js';
4-
import { method_not_allowed, error_to_pojo, allowed_methods } from '../utils.js';
4+
import { method_not_allowed, error_to_pojo, allowed_methods, load_ssr_node } from '../utils.js';
55
import { create_fetch } from './fetch.js';
66
import { HttpError, Redirect } from '../../../index/private.js';
77
import { error, json } from '../../../index/index.js';
@@ -48,12 +48,18 @@ export async function render_page(event, route, options, state, resolve_opts) {
4848
}
4949

5050
const { fetcher, fetched, cookies } = create_fetch({ event, options, state, route });
51+
const is_get_request = event.request.method === 'GET' || event.request.method === 'HEAD';
52+
53+
if (is_get_request && !resolve_opts.ssr) {
54+
return await spa_response(200);
55+
}
5156

5257
try {
58+
/** @type {Array<import('types').LoadedSSRNode | undefined>} */
5359
const nodes = await Promise.all([
5460
// we use == here rather than === because [undefined] serializes as "[null]"
55-
...route.layouts.map((n) => (n == undefined ? n : options.manifest._.nodes[n]())),
56-
options.manifest._.nodes[route.leaf]()
61+
...route.layouts.map(async (n) => (n == undefined ? n : load_ssr_node(options.manifest, n))),
62+
load_ssr_node(options.manifest, route.leaf)
5763
]);
5864

5965
resolve_opts = {
@@ -65,7 +71,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
6571
) ?? resolve_opts.ssr
6672
};
6773

68-
const leaf_node = /** @type {import('types').SSRNode} */ (nodes.at(-1));
74+
const leaf_node = /** @type {import('types').LoadedSSRNode} */ (nodes.at(-1));
6975

7076
let status = 200;
7177

@@ -75,7 +81,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
7581
/** @type {Record<string, string> | undefined} */
7682
let validation_errors;
7783

78-
if (leaf_node.server && event.request.method !== 'GET' && event.request.method !== 'HEAD') {
84+
if (leaf_node.server && !is_get_request) {
7985
// for non-GET requests, first call handler in +page.server.js
8086
// (this also determines status code)
8187
try {
@@ -112,22 +118,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
112118
const data_pathname = `${event.url.pathname.replace(/\/$/, '')}/__data.json`;
113119

114120
if (!resolve_opts.ssr) {
115-
return await render_response({
116-
branch: [],
117-
validation_errors: undefined,
118-
fetched,
119-
cookies,
120-
page_config: {
121-
hydrate: true,
122-
router: true
123-
},
124-
status,
125-
error: null,
126-
event,
127-
options,
128-
state,
129-
resolve_opts
130-
});
121+
return await spa_response(status);
131122
}
132123

133124
const should_prerender =
@@ -255,7 +246,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
255246
while (i--) {
256247
if (route.errors[i]) {
257248
const index = /** @type {number} */ (route.errors[i]);
258-
const node = await options.manifest._.nodes[index]();
249+
const node = await load_ssr_node(options.manifest, index);
259250

260251
let j = i;
261252
while (!branch[j]) j -= 1;
@@ -338,12 +329,34 @@ export async function render_page(event, route, options, state, resolve_opts) {
338329
resolve_opts
339330
});
340331
}
332+
333+
/**
334+
* @param {number} status
335+
*/
336+
async function spa_response(status) {
337+
return await render_response({
338+
branch: [],
339+
validation_errors: undefined,
340+
fetched,
341+
cookies,
342+
page_config: {
343+
hydrate: true,
344+
router: true
345+
},
346+
status,
347+
error: null,
348+
event,
349+
options,
350+
state,
351+
resolve_opts
352+
});
353+
}
341354
}
342355

343356
/**
344357
* @param {import('types').RequestEvent} event
345358
* @param {import('types').SSROptions} options
346-
* @param {import('types').SSRNode['server']} mod
359+
* @param {Required<import('types').SSRNode>['server']} mod
347360
*/
348361
export async function handle_json_request(event, options, mod) {
349362
const method = /** @type {'POST' | 'PUT' | 'PATCH' | 'DELETE'} */ (event.request.method);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { LoadURL, PrerenderingURL } from '../../../utils/url.js';
55
* @param {{
66
* dev: boolean;
77
* event: import('types').RequestEvent;
8-
* node: import('types').SSRNode | undefined;
8+
* node: import('types').SSRNode | import('types').LoadedSSRNode | undefined;
99
* parent: () => Promise<Record<string, any>>;
1010
* }} opts
1111
* @returns {Promise<import('types').ServerDataNode | null>}
@@ -78,7 +78,7 @@ export async function load_server_data({ dev, event, node, parent }) {
7878
* @param {{
7979
* event: import('types').RequestEvent;
8080
* fetcher: typeof fetch;
81-
* node: import('types').SSRNode | undefined;
81+
* node: import('types').LoadedSSRNode | undefined;
8282
* parent: () => Promise<Record<string, any>>;
8383
* server_data_promise: Promise<import('types').ServerDataNode | null>;
8484
* state: import('types').SSRState;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render_response } from './render.js';
22
import { load_data, load_server_data } from './load_data.js';
33
import { coalesce_to_error } from '../../../utils/error.js';
4-
import { GENERIC_ERROR } from '../utils.js';
4+
import { GENERIC_ERROR, load_ssr_node } from '../utils.js';
55
import { create_fetch } from './fetch.js';
66

77
/**
@@ -32,7 +32,7 @@ export async function respond_with_error({ event, options, state, status, error,
3232
const branch = [];
3333

3434
if (resolve_opts.ssr) {
35-
const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
35+
const default_layout = await load_ssr_node(options.manifest, 0); // 0 is always the root layout
3636

3737
const server_data_promise = load_server_data({
3838
dev: options.dev,
@@ -59,7 +59,7 @@ export async function respond_with_error({ event, options, state, status, error,
5959
data
6060
},
6161
{
62-
node: await options.manifest._.nodes[1](), // 1 is always the root error
62+
node: await load_ssr_node(options.manifest, 1), // 1 is always the root error
6363
data: null,
6464
server_data: null
6565
}

packages/kit/src/runtime/server/page/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ResponseHeaders, SSRNode, CspDirectives } from 'types';
1+
import { ResponseHeaders, CspDirectives, LoadedSSRNode } from 'types';
22
import { HttpError } from '../../../index/private';
33

44
export interface Fetched {
@@ -19,7 +19,7 @@ export interface FetchState {
1919
}
2020

2121
export type Loaded = {
22-
node: SSRNode;
22+
node: LoadedSSRNode;
2323
data: Record<string, any> | null;
2424
server_data: any;
2525
};

packages/kit/src/runtime/server/utils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,16 @@ export function allowed_methods(mod) {
114114

115115
return allowed;
116116
}
117+
118+
/**
119+
* @param {import('types').SSRManifest} manifest
120+
* @param {number} idx
121+
* @returns {Promise<import('types').LoadedSSRNode>}
122+
*/
123+
export async function load_ssr_node(manifest, idx) {
124+
const node = await manifest._.nodes[idx]();
125+
return {
126+
...node,
127+
shared: node.shared && (await node.shared())
128+
};
129+
}

packages/kit/src/vite/build/build_server.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,9 @@ export async function build_server(options, client) {
268268
imported.push(...entry.imports);
269269
stylesheets.push(...entry.stylesheets);
270270

271-
imports.push(`import * as shared from '../${vite_manifest[node.shared].file}';`);
272-
exports.push(`export { shared };`);
271+
exports.push(
272+
`export const shared = async () => (await import('../${vite_manifest[node.shared].file}'));`
273+
);
273274
}
274275

275276
if (node.server) {

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,14 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
9797
}
9898

9999
if (node.shared) {
100-
const { module, module_node } = await resolve(node.shared);
101-
102-
module_nodes.push(module_node);
100+
result.shared = async () => {
101+
const { module, module_node } = await resolve(/** @type {string} */ (node.shared));
102+
module_nodes.push(module_node);
103103

104-
result.shared = module;
104+
prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
105105

106-
prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
106+
return module;
107+
};
107108
}
108109

109110
if (node.server) {

packages/kit/test/apps/basics/src/hooks.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export const handle = sequence(
3333
}
3434

3535
const response = await resolve(event, {
36-
ssr: !event.url.pathname.startsWith('/no-ssr'),
36+
ssr:
37+
!event.url.pathname.startsWith('/no-ssr') ||
38+
event.url.pathname.startsWith('/no-ssr/ssr-page-config'),
3739
transformPageChunk: event.url.pathname.startsWith('/transform-page-chunk')
3840
? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
3941
: undefined
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
document;
2+
export function load() {}

0 commit comments

Comments
 (0)