Skip to content

Commit

Permalink
lazy-load shared to allow browser-only globals in +page.js
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm committed Aug 23, 2022
1 parent 0bb5cd3 commit 6a7b8c5
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 46 deletions.
4 changes: 3 additions & 1 deletion documentation/docs/12-page-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,6 @@ Normally, SvelteKit renders your page on the server first and sends that HTML to
export const ssr = false;
```

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).
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`.

> 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.
59 changes: 36 additions & 23 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { negotiate } from '../../../utils/http.js';
import { render_response } from './render.js';
import { respond_with_error } from './respond_with_error.js';
import { method_not_allowed, error_to_pojo, allowed_methods } from '../utils.js';
import { method_not_allowed, error_to_pojo, allowed_methods, load_ssr_node } from '../utils.js';
import { create_fetch } from './fetch.js';
import { HttpError, Redirect } from '../../../index/private.js';
import { error, json } from '../../../index/index.js';
Expand Down Expand Up @@ -48,12 +48,18 @@ export async function render_page(event, route, options, state, resolve_opts) {
}

const { fetcher, fetched, cookies } = create_fetch({ event, options, state, route });
const is_get_request = event.request.method === 'GET' || event.request.method === 'HEAD';

if (is_get_request && !resolve_opts.ssr) {
return await spa_response(200);
}

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

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

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

let status = 200;

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

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

if (!resolve_opts.ssr) {
return await render_response({
branch: [],
validation_errors: undefined,
fetched,
cookies,
page_config: {
hydrate: true,
router: true
},
status,
error: null,
event,
options,
state,
resolve_opts
});
return await spa_response(status);
}

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

let j = i;
while (!branch[j]) j -= 1;
Expand Down Expand Up @@ -338,12 +329,34 @@ export async function render_page(event, route, options, state, resolve_opts) {
resolve_opts
});
}

/**
* @param {number} status
*/
async function spa_response(status) {
return await render_response({
branch: [],
validation_errors: undefined,
fetched,
cookies,
page_config: {
hydrate: true,
router: true
},
status,
error: null,
event,
options,
state,
resolve_opts
});
}
}

/**
* @param {import('types').RequestEvent} event
* @param {import('types').SSROptions} options
* @param {import('types').SSRNode['server']} mod
* @param {Required<import('types').SSRNode>['server']} mod
*/
export async function handle_json_request(event, options, mod) {
const method = /** @type {'POST' | 'PUT' | 'PATCH' | 'DELETE'} */ (event.request.method);
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LoadURL, PrerenderingURL } from '../../../utils/url.js';
* @param {{
* dev: boolean;
* event: import('types').RequestEvent;
* node: import('types').SSRNode | undefined;
* node: import('types').SSRNode | import('types').LoadedSSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* }} opts
* @returns {Promise<import('types').ServerDataNode | null>}
Expand Down Expand Up @@ -78,7 +78,7 @@ export async function load_server_data({ dev, event, node, parent }) {
* @param {{
* event: import('types').RequestEvent;
* fetcher: typeof fetch;
* node: import('types').SSRNode | undefined;
* node: import('types').LoadedSSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* server_data_promise: Promise<import('types').ServerDataNode | null>;
* state: import('types').SSRState;
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { render_response } from './render.js';
import { load_data, load_server_data } from './load_data.js';
import { coalesce_to_error } from '../../../utils/error.js';
import { GENERIC_ERROR } from '../utils.js';
import { GENERIC_ERROR, load_ssr_node } from '../utils.js';
import { create_fetch } from './fetch.js';

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

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

const server_data_promise = load_server_data({
dev: options.dev,
Expand All @@ -59,7 +59,7 @@ export async function respond_with_error({ event, options, state, status, error,
data
},
{
node: await options.manifest._.nodes[1](), // 1 is always the root error
node: await load_ssr_node(options.manifest, 1), // 1 is always the root error
data: null,
server_data: null
}
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/server/page/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResponseHeaders, SSRNode, CspDirectives } from 'types';
import { ResponseHeaders, CspDirectives, LoadedSSRNode } from 'types';
import { HttpError } from '../../../index/private';

export interface Fetched {
Expand All @@ -19,7 +19,7 @@ export interface FetchState {
}

export type Loaded = {
node: SSRNode;
node: LoadedSSRNode;
data: Record<string, any> | null;
server_data: any;
};
Expand Down
13 changes: 13 additions & 0 deletions packages/kit/src/runtime/server/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,16 @@ export function allowed_methods(mod) {

return allowed;
}

/**
* @param {import('types').SSRManifest} manifest
* @param {number} idx
* @returns {Promise<import('types').LoadedSSRNode>}
*/
export async function load_ssr_node(manifest, idx) {
const node = await manifest._.nodes[idx]();
return {
...node,
shared: node.shared && (await node.shared())
};
}
5 changes: 3 additions & 2 deletions packages/kit/src/vite/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ export async function build_server(options, client) {
imported.push(...entry.imports);
stylesheets.push(...entry.stylesheets);

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

if (node.server) {
Expand Down
11 changes: 6 additions & 5 deletions packages/kit/src/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
}

if (node.shared) {
const { module, module_node } = await resolve(node.shared);

module_nodes.push(module_node);
result.shared = async () => {
const { module, module_node } = await resolve(/** @type {string} */ (node.shared));
module_nodes.push(module_node);

result.shared = module;
prevent_illegal_vite_imports(module_node, illegal_imports, extensions);

prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
return module;
};
}

if (node.server) {
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/test/apps/basics/src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const handle = sequence(
}

const response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/no-ssr'),
ssr:
!event.url.pathname.startsWith('/no-ssr') ||
event.url.pathname.startsWith('/no-ssr/ssr-page-config'),
transformPageChunk: event.url.pathname.startsWith('/transform-page-chunk')
? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
: undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
document;
export function load() {}
8 changes: 4 additions & 4 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ test.describe('Shadow DOM', () => {
});

test.describe('SPA mode / no SSR', () => {
test('Can use browser-only global on client-only page through ssr config in handle', async ({
test('Can use browser-only global in +page.svelte and +page.js through ssr config in handle', async ({
page,
read_errors
}) => {
Expand All @@ -590,7 +590,7 @@ test.describe('SPA mode / no SSR', () => {
expect(read_errors('/no-ssr/browser-only-global')).toBe(undefined);
});

test('Can use browser-only global on client-only page through ssr config in layout.js', async ({
test('Can use browser-only global in client-only page through ssr config in layout.js', async ({
page,
read_errors
}) => {
Expand All @@ -599,7 +599,7 @@ test.describe('SPA mode / no SSR', () => {
expect(read_errors('/no-ssr/ssr-page-config')).toBe(undefined);
});

test('Can use browser-only global on client-only page through ssr config in page.js', async ({
test('Can use browser-only global in client-only page through ssr config in page.js', async ({
page,
read_errors
}) => {
Expand All @@ -608,7 +608,7 @@ test.describe('SPA mode / no SSR', () => {
expect(read_errors('/no-ssr/ssr-page-config/layout/inherit')).toBe(undefined);
});

test('Cannot use browser-only global on page because of ssr config in page.js', async ({
test('Cannot use browser-only global in page because of ssr config in page.js', async ({
page
}) => {
await page.goto('/no-ssr/ssr-page-config/layout/overwrite');
Expand Down
12 changes: 9 additions & 3 deletions packages/kit/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export interface SSREndpoint {
}

export interface SSRNode {
/** The component to render. Lazy-loaded due to ssr option */
component: SSRComponentLoader;
/** index into the `components` array in client-manifest.js */
index: number;
Expand All @@ -250,15 +251,16 @@ export interface SSRNode {
/** inlined styles */
inline_styles?: () => MaybePromise<Record<string, string>>;

shared: {
/** The `+page/layout.js` file. Lazy-loaded due to ssr option */
shared?: () => Promise<{
load?: Load;
hydrate?: boolean;
prerender?: boolean;
router?: boolean;
ssr?: boolean;
};
}>;

server: {
server?: {
load?: ServerLoad;
prerender?: boolean;
POST?: Action;
Expand All @@ -271,6 +273,10 @@ export interface SSRNode {
server_id?: string;
}

export interface LoadedSSRNode extends Omit<SSRNode, 'shared'> {
shared?: Awaited<ReturnType<Required<SSRNode>['shared']>>;
}

export type SSRNodeLoader = () => Promise<SSRNode>;

export interface SSROptions {
Expand Down

0 comments on commit 6a7b8c5

Please sign in to comment.