diff --git a/.changeset/eleven-taxis-deny.md b/.changeset/eleven-taxis-deny.md new file mode 100644 index 000000000000..676f1efb1154 --- /dev/null +++ b/.changeset/eleven-taxis-deny.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: treeshake load function code if we know it's unused diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index c71a8addf65a..997c8fc957fd 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -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 }; } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0241d226069f..4a441b23d4d5 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -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. @@ -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 @@ -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) { @@ -1038,7 +1054,7 @@ async function kit({ svelte_config }) { remotes }); - remote_exports = metadata.remotes; + build_metadata = metadata; log.info('Building app'); diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index e6e56df7b8e9..3809446f1285 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -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)` @@ -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 | null} @@ -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; } @@ -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; } @@ -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; } @@ -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 | null>} */ diff --git a/packages/kit/src/exports/vite/static_analysis/index.spec.js b/packages/kit/src/exports/vite/static_analysis/index.spec.js index 6c94431b6fb6..662cde28357f 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.spec.js +++ b/packages/kit/src/exports/vite/static_analysis/index.spec.js @@ -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'"] @@ -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); +}); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 4e79a1300d16..768ee347ee4c 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -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) { @@ -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; + } } } @@ -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); + } } } } diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index beaba7ec87d7..3ab871bd5b48 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -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 `