Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-taxis-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

chore: treeshake load function code if we know it's unused
3 changes: 2 additions & 1 deletion packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ async function analyse({
}

metadata.nodes[node.index] = {
has_server_load: has_server_load(node)
has_server_load: has_server_load(node),
has_universal_load: node.universal?.load !== undefined
};
}

Expand Down
28 changes: 22 additions & 6 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ let secondary_build_started = false;
/** @type {import('types').ManifestData} */
let manifest_data;

/** @type {import('types').ServerMetadata['remotes'] | undefined} only set at build time */
let remote_exports = undefined;
/** @type {import('types').ServerMetadata | undefined} only set at build time once analysis is finished */
let build_metadata = undefined;

/**
* Returns the SvelteKit Vite plugin. Vite executes Rollup hooks as well as some of its own.
Expand Down Expand Up @@ -369,12 +369,28 @@ async function kit({ svelte_config }) {

if (!secondary_build_started) {
manifest_data = sync.all(svelte_config, config_env.mode).manifest_data;
// During the initial server build we don't know yet
new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = 'true';
new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = 'true';
} else {
const nodes = Object.values(
/** @type {import('types').ServerMetadata} */ (build_metadata).nodes
);

// Through the finished analysis we can now check if any node has server or universal load functions
const has_server_load = nodes.some((node) => node.has_server_load);
const has_universal_load = nodes.some((node) => node.has_universal_load);

new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = s(has_server_load);
new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = s(has_universal_load);
}
} else {
new_config.define = {
...define,
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0',
__SVELTEKIT_PAYLOAD__: 'globalThis.__sveltekit_dev'
__SVELTEKIT_PAYLOAD__: 'globalThis.__sveltekit_dev',
__SVELTEKIT_HAS_SERVER_LOAD__: 'true',
__SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true'
};

// @ts-ignore this prevents a reference error if `client.js` is imported on the server
Expand Down Expand Up @@ -733,8 +749,8 @@ async function kit({ svelte_config }) {

// in prod, we already built and analysed the server code before
// building the client code, so `remote_exports` is populated
else if (remote_exports) {
const exports = remote_exports.get(remote.hash);
else if (build_metadata?.remotes) {
const exports = build_metadata?.remotes.get(remote.hash);
if (!exports) throw new Error('Expected to find metadata for remote file ' + id);

for (const [name, value] of exports) {
Expand Down Expand Up @@ -1038,7 +1054,7 @@ async function kit({ svelte_config }) {
remotes
});

remote_exports = metadata.remotes;
build_metadata = metadata;

log.info('Building app');

Expand Down
27 changes: 23 additions & 4 deletions packages/kit/src/exports/vite/static_analysis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { read } from '../../../utils/filesystem.js';

const inheritable_page_options = new Set(['ssr', 'prerender', 'csr', 'trailingSlash', 'config']);

const valid_page_options = new Set([...inheritable_page_options, 'entries']);
const valid_page_options = new Set([...inheritable_page_options, 'entries', 'load']);

const skip_parsing_regex = new RegExp(
`${Array.from(valid_page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)`
Expand All @@ -14,7 +14,8 @@ const parser = Parser.extend(tsPlugin());

/**
* Collects page options from a +page.js/+layout.js file, ignoring reassignments
* and using the declared value. Returns `null` if any export is too difficult to analyse.
* and using the declared value (except for load functions, for which the value is `true`).
* Returns `null` if any export is too difficult to analyse.
* @param {string} filename The name of the file to report when an error occurs
* @param {string} input
* @returns {Record<string, any> | null}
Expand Down Expand Up @@ -116,6 +117,13 @@ export function statically_analyse_page_options(filename, input) {
continue;
}

// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
if (variable_declarator.id.name === 'load') {
page_options.set('load', null);
export_specifiers.delete('load');
continue;
}

// references a declaration we can't easily evaluate statically
return null;
}
Expand All @@ -138,7 +146,12 @@ export function statically_analyse_page_options(filename, input) {
// class and function declarations
if (statement.declaration.type !== 'VariableDeclaration') {
if (valid_page_options.has(statement.declaration.id.name)) {
return null;
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
if (statement.declaration.id.name === 'load') {
page_options.set('load', null);
} else {
return null;
}
}
continue;
}
Expand All @@ -157,6 +170,12 @@ export function statically_analyse_page_options(filename, input) {
continue;
}

// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
if (declaration.id.name === 'load') {
page_options.set('load', null);
continue;
}

// references a declaration we can't easily evaluate statically
return null;
}
Expand Down Expand Up @@ -187,7 +206,7 @@ export function get_name(node) {
*/
export function create_node_analyser({ resolve, static_exports = new Map() }) {
/**
* Computes the final page options for a node (if possible). Otherwise, returns `null`.
* Computes the final page options (may include load function as `load: null`; special case) for a node (if possible). Otherwise, returns `null`.
* @param {import('types').PageNode} node
* @returns {Promise<Record<string, any> | null>}
*/
Expand Down
18 changes: 17 additions & 1 deletion packages/kit/src/exports/vite/static_analysis/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ test.each([
});

test.each([
['load function', 'export async function load () { return {} }'],
['private export', "export let _foo = 'bar'"],
['export all declaration alias', 'export * as bar from "./foo"'],
['non-page option export', "export const foo = 'bar'"]
Expand Down Expand Up @@ -188,3 +187,20 @@ test.each([
const exports = statically_analyse_page_options('', input);
expect(exports).toEqual(null);
});

test.each([
['(function)', 'export async function load () { return {} }'],
['(variable)', 'export const load = () => { return {} }']
])('special-cases load function %s', (_, input) => {
const exports = statically_analyse_page_options('', input);
expect(exports).toEqual({ load: null });
});

test('special-cases load function (static analysis fails)', () => {
const input = `
export const load = () => { return {} };
export const ssr = process.env.SSR;
`;
const exports = statically_analyse_page_options('', input);
expect(exports).toEqual(null);
});
117 changes: 61 additions & 56 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
}
}

if (node.universal?.load) {
if (__SVELTEKIT_HAS_UNIVERSAL_LOAD__ && node.universal?.load) {
/** @param {string[]} deps */
function depends(...deps) {
for (const dep of deps) {
Expand Down Expand Up @@ -1004,49 +1004,52 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
const search_params_changed = diff_search_params(current.url, url);

let parent_invalid = false;
const invalid_server_nodes = loaders.map((loader, i) => {
const previous = current.branch[i];

const invalid =
!!loader?.[0] &&
(previous?.loader !== loader[1] ||
has_changed(
parent_invalid,
route_changed,
url_changed,
search_params_changed,
previous.server?.uses,
params
));

if (invalid) {
// For the next one
parent_invalid = true;
}
if (__SVELTEKIT_HAS_SERVER_LOAD__) {
const invalid_server_nodes = loaders.map((loader, i) => {
const previous = current.branch[i];

const invalid =
!!loader?.[0] &&
(previous?.loader !== loader[1] ||
has_changed(
parent_invalid,
route_changed,
url_changed,
search_params_changed,
previous.server?.uses,
params
));

if (invalid) {
// For the next one
parent_invalid = true;
}

return invalid;
});
return invalid;
});

if (invalid_server_nodes.some(Boolean)) {
try {
server_data = await load_data(url, invalid_server_nodes);
} catch (error) {
const handled_error = await handle_error(error, { url, params, route: { id } });
if (invalid_server_nodes.some(Boolean)) {
try {
server_data = await load_data(url, invalid_server_nodes);
} catch (error) {
const handled_error = await handle_error(error, { url, params, route: { id } });

if (preload_tokens.has(preload)) {
return preload_error({ error: handled_error, url, params, route });
}
if (preload_tokens.has(preload)) {
return preload_error({ error: handled_error, url, params, route });
}

return load_root_error_page({
status: get_status(error),
error: handled_error,
url,
route
});
}
return load_root_error_page({
status: get_status(error),
error: handled_error,
url,
route
});
}

if (server_data.type === 'redirect') {
return server_data;
if (server_data.type === 'redirect') {
return server_data;
}
}
}

Expand Down Expand Up @@ -1232,27 +1235,29 @@ async function load_root_error_page({ status, error, url, route }) {
/** @type {import('types').ServerDataNode | null} */
let server_data_node = null;

const default_layout_has_server_load = app.server_loads[0] === 0;
if (__SVELTEKIT_HAS_SERVER_LOAD__) {
const default_layout_has_server_load = app.server_loads[0] === 0;

if (default_layout_has_server_load) {
// TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
// existing root layout data
try {
const server_data = await load_data(url, [true]);
if (default_layout_has_server_load) {
// TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
// existing root layout data
try {
const server_data = await load_data(url, [true]);

if (
server_data.type !== 'data' ||
(server_data.nodes[0] && server_data.nodes[0].type !== 'data')
) {
throw 0;
}
if (
server_data.type !== 'data' ||
(server_data.nodes[0] && server_data.nodes[0].type !== 'data')
) {
throw 0;
}

server_data_node = server_data.nodes[0] ?? null;
} catch {
// at this point we have no choice but to fall back to the server, if it wouldn't
// bring us right back here, turning this into an endless loop
if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
await native_navigation(url);
server_data_node = server_data.nodes[0] ?? null;
} catch {
// at this point we have no choice but to fall back to the server, if it wouldn't
// bring us right back here, turning this into an endless loop
if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
await native_navigation(url);
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/src/types/global-private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ declare global {
const __SVELTEKIT_EXPERIMENTAL__REMOTE_FUNCTIONS__: boolean;
/** True if `config.kit.router.resolution === 'client'` */
const __SVELTEKIT_CLIENT_ROUTING__: boolean;
/**
* True if any node in the manifest has a server load function.
* Used for treeshaking server load code from client bundles when no server loads exist.
*/
const __SVELTEKIT_HAS_SERVER_LOAD__: boolean;
/**
* True if any node in the manifest has a universal load function.
* Used for treeshaking universal load code from client bundles when no universal loads exist.
*/
const __SVELTEKIT_HAS_UNIVERSAL_LOAD__: boolean;
/** The `__sveltekit_abc123` object in the init `<script>` */
const __SVELTEKIT_PAYLOAD__: {
/** The basepath, usually relative to the current page */
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export interface ServerMetadata {
nodes: Array<{
/** Also `true` when using `trailingSlash`, because we need to do a server request in that case to get its value. */
has_server_load: boolean;
has_universal_load: boolean;
}>;
routes: Map<string, ServerMetadataRoute>;
/** For each hashed remote file, a map of export name -> { type, dynamic }, where `dynamic` is `false` for non-dynamic prerender functions */
Expand All @@ -395,6 +396,7 @@ export interface SSRComponent {
export type SSRComponentLoader = () => Promise<SSRComponent>;

export interface UniversalNode {
/** Is `null` in case static analysis succeeds but the node is ssr=false */
load?: Load;
prerender?: PrerenderOption;
ssr?: boolean;
Expand Down
1 change: 1 addition & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2515,6 +2515,7 @@ declare module '@sveltejs/kit' {
type SSRComponentLoader = () => Promise<SSRComponent>;

interface UniversalNode {
/** Is `null` in case static analysis succeeds but the node is ssr=false */
load?: Load;
prerender?: PrerenderOption;
ssr?: boolean;
Expand Down
Loading