diff --git a/.changeset/cold-shoes-cheat.md b/.changeset/cold-shoes-cheat.md new file mode 100644 index 000000000000..df1b1a100891 --- /dev/null +++ b/.changeset/cold-shoes-cheat.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'create-svelte': patch +--- + +[breaking] Replace `data-sveltekit-prefetch` with `-preload-code` and `-preload-data` diff --git a/.changeset/many-schools-live.md b/.changeset/many-schools-live.md new file mode 100644 index 000000000000..ce84292015c6 --- /dev/null +++ b/.changeset/many-schools-live.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[breaking] Rename `prefetch` to `preloadData` and `prefetchRoutes` to `preloadCode` diff --git a/documentation/docs/10-getting-started/10-introduction.md b/documentation/docs/10-getting-started/10-introduction.md index 8506b355d92f..9507b1944099 100644 --- a/documentation/docs/10-getting-started/10-introduction.md +++ b/documentation/docs/10-getting-started/10-introduction.md @@ -12,7 +12,7 @@ title: Introduction SvelteKit is a framework for building extremely high-performance web apps. -Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/link-options#data-sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/glossary#ssr) or [in the browser](/docs/glossary#csr-and-spa) at runtime or [at build-time](/docs/glossary#prerendering). SvelteKit does all the boring stuff for you so that you can get on with the creative part. +Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [preloading](/docs/link-options#data-sveltekit-preload-data) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/glossary#ssr) or [in the browser](/docs/glossary#csr-and-spa) at runtime or [at build-time](/docs/glossary#prerendering). SvelteKit does all the boring stuff for you so that you can get on with the creative part. It uses [Vite](https://vitejs.dev/) with a [Svelte plugin](https://github.com/sveltejs/vite-plugin-svelte) to provide a lightning-fast and feature-rich development experience with [Hot Module Replacement (HMR)](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#hot), where changes to your code are reflected in the browser instantly. diff --git a/documentation/docs/30-advanced/30-link-options.md b/documentation/docs/30-advanced/30-link-options.md index 59ba993b080c..4745b313835a 100644 --- a/documentation/docs/30-advanced/30-link-options.md +++ b/documentation/docs/30-advanced/30-link-options.md @@ -6,20 +6,53 @@ In SvelteKit, `` elements (rather than framework-specific `` components You can customise the behaviour of links with `data-sveltekit-*` attributes. These can be applied to the `` itself, or to a parent element. -### data-sveltekit-prefetch +### data-sveltekit-preload-data -To get a head start on importing the code and fetching the page's data, use the `data-sveltekit-prefetch` attribute, which will start loading everything as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the `click` event to trigger navigation. Typically, this buys us an extra couple of hundred milliseconds, which is the difference between a user interface that feels laggy, and one that feels snappy. +Before the browser registers that the user has clicked on a link, we can detect that they've hovered the mouse over it (on desktop) or that a `touchstart` or `mousedown` event was triggered. In both cases, we can make an educated guess that a `click` event is coming. -To apply this behaviour across the board, add the attribute to a parent element (or even the `` in your `src/app.html`): +SvelteKit can use this information to get a head start on importing the code and fetching the page's data, which can give us an extra couple of hundred milliseconds — the difference between a user interface that feels laggy and one that feels snappy. + +We can control this behaviour with the `data-sveltekit-preload-data` attribute, which can have one of two values: + +- `"hover"` means that preloading will start if the mouse comes to a rest over a link. On mobile, preloading begins on `touchstart` +- `"tap"` means that preloading will start as soon as a `touchstart` or `mousedown` event is registered + +The default project template has a `data-sveltekit-preload-data="hover"` attribute applied to the `` element in `src/app.html`, meaning that every link is preloaded on hover by default: + +```html + +
%sveltekit.body%
+ +``` + +Sometimes, calling `load` when the user hovers over a link might be undesirable, either because it's likely to result in false positives (a click needn't follow a hover) or because data is updating very quickly and a delay could mean staleness. + +In these cases, you can specify the `"tap"` value, which causes SvelteKit to call `load` only when the user taps or clicks on a link: ```html -/// file: src/routes/+layout.svelte -
- -
+
+ Get current stonk values + ``` -> You can also programmatically invoke `prefetch` from `$app/navigation`. +> You can also programmatically invoke `preloadData` from `$app/navigation`. + +Data will never be preloaded if the user has chosen reduced data usage, meaning [`navigator.connection.saveData`](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData) is `true`. + +### data-sveltekit-preload-code + +Even in cases where you don't want to preload _data_ for a link, it can be beneficial to preload the _code_. The `data-sveltekit-preload-code` attribute works similarly to `data-sveltekit-preload-data`, except that it can take one of four values, in decreasing 'eagerness': + +- `"eager"` means that links will be preloaded straight away +- `"viewport"` means that links will be preloaded once they enter the viewport +- `"hover"` - as above, except that only code is preloaded +- `"tap"` - as above, except that only code is preloaded + +Note that `viewport` and `eager` only apply to links that are present in the DOM immediately following navigation — if a link is added later (in an `{#if ...}` block, for example) it will not be preloaded until triggered by `hover` or `tap`. This is to avoid performance pitfalls resulting from aggressively observing the DOM for changes. + +> Since preloading code is a prerequisite for preloading data, this attribute will only have an effect if it specifies a more eager value than any `data-sveltekit-preload-data` attribute that is present. + +As with `data-sveltekit-preload-data`, this attribute will be ignored if the user has chosen reduced data usage. ### data-sveltekit-reload @@ -50,14 +83,14 @@ In certain cases, you may wish to disable this behaviour. Adding a `data-sveltek To disable any of these options inside an element where they have been enabled, use the `"off"` value: ```html -
- +
+ a b c -
- +
+ d e f diff --git a/documentation/docs/60-appendix/10-migrating.md b/documentation/docs/60-appendix/10-migrating.md index 4827b530cea2..1c46a7f7c694 100644 --- a/documentation/docs/60-appendix/10-migrating.md +++ b/documentation/docs/60-appendix/10-migrating.md @@ -85,7 +85,7 @@ Your custom error page component should be renamed from `_error.svelte` to `+err #### Imports -The `goto`, `prefetch` and `prefetchRoutes` imports from `@sapper/app` should be replaced with identical imports from [`$app/navigation`](/docs/modules#$app-navigation). +The `goto`, `prefetch` and `prefetchRoutes` imports from `@sapper/app` should be replaced with `goto`, `preloadData` and `preloadCode` imports respectively from [`$app/navigation`](/docs/modules#$app-navigation). The `stores` import from `@sapper/app` should be replaced — see the [Stores](/docs/migrating#pages-and-layouts-stores) section below. @@ -133,7 +133,7 @@ This caused problems and is no longer the case in SvelteKit. Instead, relative U #### <a> attributes -- `sapper:prefetch` is now `data-sveltekit-prefetch` +- `sapper:prefetch` is now `data-sveltekit-preload-data` - `sapper:noscroll` is now `data-sveltekit-noscroll` ### Endpoints diff --git a/packages/create-svelte/templates/default/src/app.html b/packages/create-svelte/templates/default/src/app.html index c361a2248b68..effe0d0d266c 100644 --- a/packages/create-svelte/templates/default/src/app.html +++ b/packages/create-svelte/templates/default/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/packages/create-svelte/templates/skeleton/src/app.html b/packages/create-svelte/templates/skeleton/src/app.html index 30c14fdbf9cf..effe0d0d266c 100644 --- a/packages/create-svelte/templates/skeleton/src/app.html +++ b/packages/create-svelte/templates/skeleton/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 974e83a13e90..80f8df1c8908 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -17,7 +17,7 @@ export const disableScrollHandling = ssr export const goto = ssr ? guard('goto') : client.goto; export const invalidate = ssr ? guard('invalidate') : client.invalidate; export const invalidateAll = ssr ? guard('invalidateAll') : client.invalidateAll; -export const prefetch = ssr ? guard('prefetch') : client.prefetch; -export const prefetchRoutes = ssr ? guard('prefetchRoutes') : client.prefetch_routes; +export const preloadData = ssr ? guard('preloadData') : client.preload_data; +export const preloadCode = ssr ? guard('preloadCode') : client.preload_code; export const beforeNavigate = ssr ? () => {} : client.before_navigate; export const afterNavigate = ssr ? () => {} : client.after_navigate; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 7d8c8ea940e6..13233e9a7294 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -6,7 +6,7 @@ import { normalize_path, add_data_suffix } from '../../utils/url.js'; -import { find_anchor, get_base_uri, scroll_state } from './utils.js'; +import { find_anchor, get_base_uri, is_external_url, scroll_state } from './utils.js'; import { lock_fetch, unlock_fetch, @@ -22,9 +22,7 @@ import { HttpError, Redirect } from '../control.js'; import { stores } from './singletons.js'; import { unwrap_promises } from '../../utils/promises.js'; import * as devalue from 'devalue'; - -const SCROLL_KEY = 'sveltekit:scroll'; -const INDEX_KEY = 'sveltekit:index'; +import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY } from './constants.js'; const routes = parse(nodes, server_loads, dictionary, matchers); @@ -66,7 +64,9 @@ function check_for_removed_attributes() { if (!warned_about_attributes[attr]) { warned_about_attributes[attr] = true; console.error( - `The sveltekit:${attr} attribute has been replaced with data-sveltekit-${attr}` + `The sveltekit:${attr} attribute has been replaced with data-sveltekit-${ + attr === 'prefetch' ? 'preload-data' : attr + }` ); } } @@ -159,8 +159,8 @@ export function create_client({ target, base }) { const url = new URL(location.href); const intent = get_navigation_intent(url, true); - // Clear prefetch, it might be affected by the invalidation. - // Also solves an edge case where a prefetch is triggered, the navigation for it + // Clear preload, it might be affected by the invalidation. + // Also solves an edge case where a preload is triggered, the navigation for it // was then triggered and is still running while the invalidation kicks in, // at which point the invalidation should take over and "win". load_cache = null; @@ -210,11 +210,11 @@ export function create_client({ target, base }) { } /** @param {URL} url */ - async function prefetch(url) { + async function preload_data(url) { const intent = get_navigation_intent(url, false); if (!intent) { - throw new Error(`Attempted to prefetch a URL that does not belong to this app: ${url}`); + throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); } load_cache = { @@ -231,6 +231,17 @@ export function create_client({ target, base }) { return load_cache.promise; } + /** @param {...string} pathnames */ + async function preload_code(...pathnames) { + const matching = routes.filter((route) => pathnames.some((pathname) => route.exec(pathname))); + + const promises = matching.map((r) => { + return Promise.all([...r.layouts, r.leaf].map((load) => load?.[1]())); + }); + + await Promise.all(promises); + } + /** * Returns `true` if update completes, `false` if it is aborted * @param {import('./types').NavigationIntent | undefined} intent @@ -302,7 +313,7 @@ export function create_client({ target, base }) { history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); } - // reset prefetch synchronously after the history state has been set to avoid race conditions + // reset preload synchronously after the history state has been set to avoid race conditions load_cache = null; if (started) { @@ -1000,7 +1011,7 @@ export function create_client({ target, base }) { * @param {boolean} invalidating */ function get_navigation_intent(url, invalidating) { - if (is_external_url(url)) return; + if (is_external_url(url, base)) return; const path = decode_pathname(url.pathname.slice(base.length) || '/'); @@ -1016,11 +1027,6 @@ export function create_client({ target, base }) { } } - /** @param {URL} url */ - function is_external_url(url) { - return url.origin !== location.origin || !url.pathname.startsWith(base); - } - /** * @param {{ * url: URL; @@ -1173,6 +1179,77 @@ export function create_client({ target, base }) { }); } + function setup_preload() { + /** @type {NodeJS.Timeout} */ + let mousemove_timeout; + + target.addEventListener('mousemove', (event) => { + const target = /** @type {Element} */ (event.target); + + clearTimeout(mousemove_timeout); + mousemove_timeout = setTimeout(() => { + preload(target, 2); + }, 20); + }); + + /** @param {Event} event */ + function tap(event) { + preload(/** @type {Element} */ (event.composedPath()[0]), 1); + } + + target.addEventListener('mousedown', tap); + target.addEventListener('touchstart', tap, { passive: true }); + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + preload_code(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href).pathname); + observer.unobserve(entry.target); + } + } + }, + { threshold: 0 } + ); + + /** + * @param {Element} element + * @param {number} priority + */ + function preload(element, priority) { + const { url, options, external } = find_anchor(element, base); + + if (!external) { + if (priority <= options.preload_data) { + preload_data(/** @type {URL} */ (url)); + } else if (priority <= options.preload_code) { + preload_code(/** @type {URL} */ (url).pathname); + } + } + } + + function after_navigate() { + observer.disconnect(); + + for (const a of target.querySelectorAll('a')) { + const { url, external, options } = find_anchor(a, base); + + if (external) continue; + + if (options.preload_code === PRELOAD_PRIORITIES.viewport) { + observer.observe(a); + } + + if (options.preload_code === PRELOAD_PRIORITIES.eager) { + preload_code(/** @type {URL} */ (url).pathname); + } + } + } + + callbacks.after_navigate.push(after_navigate); + after_navigate(); + } + return { after_navigate: (fn) => { onMount(() => { @@ -1246,23 +1323,12 @@ export function create_client({ target, base }) { return invalidate(); }, - prefetch: async (href) => { + preload_data: async (href) => { const url = new URL(href, get_base_uri(document)); - await prefetch(url); + await preload_data(url); }, - // TODO rethink this API - prefetch_routes: async (pathnames) => { - const matching = pathnames - ? routes.filter((route) => pathnames.some((pathname) => route.exec(pathname))) - : routes; - - const promises = matching.map((r) => { - return Promise.all([...r.layouts, r.leaf].map((load) => load?.[1]())); - }); - - await Promise.all(promises); - }, + preload_code, apply_action: async (result) => { if (result.type === 'error') { @@ -1361,33 +1427,10 @@ export function create_client({ target, base }) { } }); - /** @param {Event} event */ - const trigger_prefetch = (event) => { - const { url, options, has } = find_anchor(event); - if (url && options.prefetch && !is_external_url(url)) { - if (options.reload || has.rel_external || has.target || has.download) return; - prefetch(url); - } - }; - - /** @type {NodeJS.Timeout} */ - let mousemove_timeout; - - /** @param {MouseEvent|TouchEvent} event */ - const handle_mousemove = (event) => { - clearTimeout(mousemove_timeout); - mousemove_timeout = setTimeout(() => { - // event.composedPath(), which is used in find_anchor, will be empty if the event is read in a timeout - // add a layer of indirection to address that - event.target?.dispatchEvent( - new CustomEvent('sveltekit:trigger_prefetch', { bubbles: true }) - ); - }, 20); - }; - - target.addEventListener('touchstart', trigger_prefetch, { passive: true }); - target.addEventListener('mousemove', handle_mousemove); - target.addEventListener('sveltekit:trigger_prefetch', trigger_prefetch); + // @ts-expect-error this isn't supported everywhere yet + if (!navigator.connection?.saveData) { + setup_preload(); + } /** @param {MouseEvent} event */ target.addEventListener('click', (event) => { @@ -1397,7 +1440,10 @@ export function create_client({ target, base }) { if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; if (event.defaultPrevented) return; - const { a, url, options, has } = find_anchor(event); + const { a, url, options, has } = find_anchor( + /** @type {Element} */ (event.composedPath()[0]), + base + ); if (!a || !url) return; const is_svg_a_element = a instanceof SVGAElement; @@ -1432,8 +1478,8 @@ export function create_client({ target, base }) { // Check if new url only differs by hash and use the browser default behavior in that case // This will ensure the `hashchange` event is fired // Removing the hash does a full page navigation in the browser, so make sure a hash is present - const [base, hash] = url.href.split('#'); - if (hash !== undefined && base === location.href.split('#')[0]) { + const [nonhash, hash] = url.href.split('#'); + if (hash !== undefined && nonhash === location.href.split('#')[0]) { // set this flag to distinguish between navigations triggered by // clicking a hash link and those triggered by popstate // TODO why not update history here directly? diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js new file mode 100644 index 000000000000..b7a8d9d61700 --- /dev/null +++ b/packages/kit/src/runtime/client/constants.js @@ -0,0 +1,10 @@ +export const SCROLL_KEY = 'sveltekit:scroll'; +export const INDEX_KEY = 'sveltekit:index'; + +export const PRELOAD_PRIORITIES = /** @type {const} */ ({ + tap: 1, + hover: 2, + viewport: 3, + eager: 4, + off: -1 +}); diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index d6e39578f548..3622abcf4544 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -5,8 +5,8 @@ import { goto, invalidate, invalidateAll, - prefetch, - prefetchRoutes + preloadCode, + preloadData } from '$app/navigation'; import { CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types'; @@ -18,8 +18,8 @@ export interface Client { goto: typeof goto; invalidate: typeof invalidate; invalidateAll: typeof invalidateAll; - prefetch: typeof prefetch; - prefetch_routes: typeof prefetchRoutes; + preload_code: typeof preloadCode; + preload_data: typeof preloadData; apply_action: typeof applyAction; // private API diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 7c0706210846..c8f7ca529138 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -1,6 +1,7 @@ import { writable } from 'svelte/store'; import { assets } from '../paths.js'; import { version } from '../env.js'; +import { PRELOAD_PRIORITIES } from './constants.js'; /* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */ @@ -23,72 +24,137 @@ export function scroll_state() { }; } -/** @param {Event} event */ -export function find_anchor(event) { +const warned = new WeakSet(); + +/** @typedef {keyof typeof valid_link_options} LinkOptionName */ + +const valid_link_options = /** @type {const} */ ({ + 'preload-code': ['', 'off', 'tap', 'hover', 'viewport', 'eager'], + 'preload-data': ['', 'off', 'tap', 'hover'], + noscroll: ['', 'off'], + reload: ['', 'off'] +}); + +/** + * @template {LinkOptionName} T + * @param {Element} element + * @param {T} name + */ +function link_option(element, name) { + const value = /** @type {typeof valid_link_options[T][number] | null} */ ( + element.getAttribute(`data-sveltekit-${name}`) + ); + + if (__SVELTEKIT_DEV__) validate_link_option(element, name, value); + + return value; +} + +/** + * @template {LinkOptionName} T + * @template {typeof valid_link_options[T][number] | null} U + * @param {Element} element + * @param {T} name + * @param {U} value + */ +function validate_link_option(element, name, value) { + if (value === null) return; + + // @ts-expect-error - includes is dumb + if (!warned.has(element) && !valid_link_options[name].includes(value)) { + console.error( + `Unexpected value for ${name} — should be one of ${valid_link_options[name] + .map((option) => JSON.stringify(option)) + .join(', ')}`, + element + ); + + warned.add(element); + } +} + +const levels = { + ...PRELOAD_PRIORITIES, + '': PRELOAD_PRIORITIES.hover +}; + +/** + * @param {Element} element + * @param {string} base + */ +export function find_anchor(element, base) { /** @type {HTMLAnchorElement | SVGAElement | undefined} */ let a; - /** @type {boolean | null} */ + /** @type {typeof valid_link_options['noscroll'][number] | null} */ let noscroll = null; - /** @type {boolean | null} */ - let prefetch = null; + /** @type {typeof valid_link_options['preload-code'][number] | null} */ + let preload_code = null; - /** @type {boolean | null} */ - let reload = null; + /** @type {typeof valid_link_options['preload-data'][number] | null} */ + let preload_data = null; - for (const element of event.composedPath()) { - if (!(element instanceof Element)) continue; + /** @type {typeof valid_link_options['reload'][number] | null} */ + let reload = null; + while (element !== document.documentElement) { if (!a && element.nodeName.toUpperCase() === 'A') { // SVG elements have a lowercase name a = /** @type {HTMLAnchorElement | SVGAElement} */ (element); } - if (noscroll === null) noscroll = get_link_option(element, 'data-sveltekit-noscroll'); - if (prefetch === null) prefetch = get_link_option(element, 'data-sveltekit-prefetch'); - if (reload === null) reload = get_link_option(element, 'data-sveltekit-reload'); - } + if (a) { + if (preload_code === null) preload_code = link_option(element, 'preload-code'); + if (preload_data === null) preload_data = link_option(element, 'preload-data'); + if (noscroll === null) noscroll = link_option(element, 'noscroll'); + if (reload === null) reload = link_option(element, 'reload'); + } - const url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI); + // @ts-expect-error handle shadow roots + element = element.assignedSlot ?? element.parentNode; - return { - a, - url, - options: { - noscroll, - prefetch, - reload - }, - has: a - ? { - rel_external: (a.getAttribute('rel') || '').split(/\s+/).includes('external'), - download: a.hasAttribute('download'), - target: !!(a instanceof SVGAElement ? a.target.baseVal : a.target) - } - : {} - }; -} + // @ts-expect-error handle shadow roots + if (element.nodeType === 11) element = element.host; + } -const warned = new WeakSet(); + /** @type {URL | undefined} */ + let url; -/** - * @param {Element} element - * @param {string} attribute - */ -function get_link_option(element, attribute) { - const value = element.getAttribute(attribute); - if (value === null) return value; + try { + url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI); + } catch {} - if (value === '') return true; - if (value === 'off') return false; + const options = { + preload_code: levels[preload_code ?? 'off'], + preload_data: levels[preload_data ?? 'off'], + noscroll: noscroll === 'off' ? false : noscroll === '' ? true : null, + reload: reload === 'off' ? false : reload === '' ? true : null + }; - if (__SVELTEKIT_DEV__ && !warned.has(element)) { - console.error(`Unexpected value for ${attribute} — should be "" or "off"`, element); - warned.add(element); - } + const has = a + ? { + rel_external: (a.getAttribute('rel') || '').split(/\s+/).includes('external'), + download: a.hasAttribute('download'), + target: !!(a instanceof SVGAElement ? a.target.baseVal : a.target) + } + : {}; + + const external = + !url || + is_external_url(url, base) || + options.reload || + has.rel_external || + has.target || + has.download; - return false; + return { + a, + url, + options, + external, + has + }; } /** @param {any} value */ @@ -165,3 +231,11 @@ export function create_updated_store() { check }; } + +/** + * @param {URL} url + * @param {string} base + */ +export function is_external_url(url, base) { + return url.origin !== location.origin || !url.pathname.startsWith(base); +} diff --git a/packages/kit/test/ambient.d.ts b/packages/kit/test/ambient.d.ts index 44e3d0c8c8a5..998e04686eaa 100644 --- a/packages/kit/test/ambient.d.ts +++ b/packages/kit/test/ambient.d.ts @@ -13,10 +13,10 @@ declare global { ) => Promise; const invalidate: (url: string) => Promise; - const prefetch: (url: string) => Promise; + const preloadData: (url: string) => Promise; const beforeNavigate: (fn: (url: URL) => void | boolean) => void; const afterNavigate: (fn: () => void) => void; - const prefetchRoutes: (urls?: string[]) => Promise; + const preloadCode: (...urls: string[]) => Promise; } export {}; diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/prefetch/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/prefetch/+page.svelte deleted file mode 100644 index 04ebcd22ce76..000000000000 --- a/packages/kit/test/apps/basics/src/routes/data-sveltekit/prefetch/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ -one - -
- two - three -
diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte new file mode 100644 index 000000000000..e8d2726b3964 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/+page.svelte @@ -0,0 +1,8 @@ +one + +
+ two + three +
diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/prefetch/target/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/target/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/data-sveltekit/prefetch/target/+page.svelte rename to packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/target/+page.svelte diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/hash-route/+page.js b/packages/kit/test/apps/basics/src/routes/routing/preloading/hash-route/+page.js similarity index 100% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/hash-route/+page.js rename to packages/kit/test/apps/basics/src/routes/routing/preloading/hash-route/+page.js diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/hash-route/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/preloading/hash-route/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/hash-route/+page.svelte rename to packages/kit/test/apps/basics/src/routes/routing/preloading/hash-route/+page.svelte diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.js b/packages/kit/test/apps/basics/src/routes/routing/preloading/preload-error/+page.js similarity index 100% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.js rename to packages/kit/test/apps/basics/src/routes/routing/preloading/preload-error/+page.js diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/preloading/preload-error/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.svelte rename to packages/kit/test/apps/basics/src/routes/routing/preloading/preload-error/+page.svelte diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched.json/+server.js b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded.json/+server.js similarity index 69% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched.json/+server.js rename to packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded.json/+server.js index 04004e8da206..0c42a739e0aa 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched.json/+server.js +++ b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded.json/+server.js @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; export function GET() { - return json('prefetched'); + return json('preloaded'); } diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js similarity index 53% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js rename to packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js index 2c658c282bea..5d7154c4dc7e 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js +++ b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.js @@ -1,5 +1,5 @@ /** @type {import('@sveltejs/kit').Load} */ export async function load({ fetch }) { - const message = await fetch('/routing/prefetching/prefetched.json').then((r) => r.json()); + const message = await fetch('/routing/preloading/preloaded.json').then((r) => r.json()); return { message }; } diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.svelte rename to packages/kit/test/apps/basics/src/routes/routing/preloading/preloaded/+page.svelte diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 103855f3437f..96190d55edf9 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -664,8 +664,8 @@ test.describe('Prefetching', () => { // also wait for network processing to complete, see // https://playwright.dev/docs/network#network-events await Promise.all([ - page.waitForResponse(`${baseURL}/routing/prefetching/prefetched.json`), - app.prefetch('/routing/prefetching/prefetched') + page.waitForResponse(`${baseURL}/routing/preloading/preloaded.json`), + app.preloadData('/routing/preloading/preloaded') ]); // svelte request made is environment dependent @@ -677,53 +677,53 @@ test.describe('Prefetching', () => { expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0); } - expect(requests.includes(`${baseURL}/routing/prefetching/prefetched.json`)).toBe(true); + expect(requests.includes(`${baseURL}/routing/preloading/preloaded.json`)).toBe(true); requests = []; - await app.goto('/routing/prefetching/prefetched'); + await app.goto('/routing/preloading/preloaded'); expect(requests).toEqual([]); try { - await app.prefetch('https://example.com'); + await app.preloadData('https://example.com'); throw new Error('Error was not thrown'); } catch (/** @type {any} */ e) { - expect(e.message).toMatch('Attempted to prefetch a URL that does not belong to this app'); + expect(e.message).toMatch('Attempted to preload a URL that does not belong to this app'); } }); - test('chooses correct route when hash route is prefetched but regular route is clicked', async ({ + test('chooses correct route when hash route is preloaded but regular route is clicked', async ({ app, page }) => { await page.goto('/routing/a'); - await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me'); - await app.goto('/routing/prefetching/hash-route'); + await app.preloadData('/routing/preloading/hash-route#please-dont-show-me'); + await app.goto('/routing/preloading/hash-route'); await expect(page.locator('h1')).not.toHaveText('Oopsie'); }); test('does not rerun load on calls to duplicate preload hash route', async ({ app, page }) => { await page.goto('/routing/a'); - await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me'); - await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me'); - await app.goto('/routing/prefetching/hash-route#please-dont-show-me'); + await app.preloadData('/routing/preloading/hash-route#please-dont-show-me'); + await app.preloadData('/routing/preloading/hash-route#please-dont-show-me'); + await app.goto('/routing/preloading/hash-route#please-dont-show-me'); await expect(page.locator('p')).toHaveText('Loaded 1 times.'); }); test('does not rerun load on calls to different preload hash route', async ({ app, page }) => { await page.goto('/routing/a'); - await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me'); - await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me-jr'); - await app.goto('/routing/prefetching/hash-route#please-dont-show-me'); + await app.preloadData('/routing/preloading/hash-route#please-dont-show-me'); + await app.preloadData('/routing/preloading/hash-route#please-dont-show-me-jr'); + await app.goto('/routing/preloading/hash-route#please-dont-show-me'); await expect(page.locator('p')).toHaveText('Loaded 1 times.'); }); - test('does rerun load when prefetch errored', async ({ app, page }) => { + test('does rerun load when preload errored', async ({ app, page }) => { await page.goto('/routing/a'); - await app.prefetch('/routing/prefetching/prefetch-error'); - await app.goto('/routing/prefetching/prefetch-error'); + await app.preloadData('/routing/preloading/preload-error'); + await app.goto('/routing/preloading/preload-error'); await expect(page.locator('p')).toHaveText('hello'); }); }); @@ -732,7 +732,7 @@ test.describe('Routing', () => { test('navigates to a new page without reloading', async ({ app, page, clicknav }) => { await page.goto('/routing'); - await app.prefetch('/routing/a').catch((e) => { + await app.preloadData('/routing/a').catch((e) => { // from error handler tests; ignore if (!e.message.includes('Crashing now')) throw e; }); @@ -816,7 +816,7 @@ test.describe('Shadow DOM', () => { test('client router captures anchors in shadow dom', async ({ app, page, clicknav }) => { await page.goto('/routing/shadow-dom'); - await app.prefetch('/routing/a').catch((e) => { + await app.preloadData('/routing/a').catch((e) => { // from error handler tests; ignore if (!e.message.includes('Crashing now')) throw e; }); @@ -1049,38 +1049,38 @@ test.describe.serial('Invalidation', () => { }); test.describe('data-sveltekit attributes', () => { - test('data-sveltekit-prefetch', async ({ baseURL, page }) => { + test('data-sveltekit-preload-data', async ({ baseURL, page }) => { /** @type {string[]} */ const requests = []; page.on('request', (r) => requests.push(r.url())); const module = process.env.DEV - ? `${baseURL}/src/routes/data-sveltekit/prefetch/target/+page.svelte` - : `${baseURL}/_app/immutable/components/pages/data-sveltekit/prefetch/target/_page`; + ? `${baseURL}/src/routes/data-sveltekit/preload-data/target/+page.svelte` + : `${baseURL}/_app/immutable/components/pages/data-sveltekit/preload-data/target/_page`; - await page.goto('/data-sveltekit/prefetch'); + await page.goto('/data-sveltekit/preload-data'); await page.locator('#one').dispatchEvent('mousemove'); await Promise.all([ - page.waitForTimeout(100), // wait for prefetching to start - page.waitForLoadState('networkidle') // wait for prefetching to finish + page.waitForTimeout(100), // wait for preloading to start + page.waitForLoadState('networkidle') // wait for preloading to finish ]); expect(requests.find((r) => r.startsWith(module))).toBeDefined(); requests.length = 0; - await page.goto('/data-sveltekit/prefetch'); + await page.goto('/data-sveltekit/preload-data'); await page.locator('#two').dispatchEvent('mousemove'); await Promise.all([ - page.waitForTimeout(100), // wait for prefetching to start - page.waitForLoadState('networkidle') // wait for prefetching to finish + page.waitForTimeout(100), // wait for preloading to start + page.waitForLoadState('networkidle') // wait for preloading to finish ]); expect(requests.find((r) => r.startsWith(module))).toBeDefined(); requests.length = 0; - await page.goto('/data-sveltekit/prefetch'); + await page.goto('/data-sveltekit/preload-data'); await page.locator('#three').dispatchEvent('mousemove'); await Promise.all([ - page.waitForTimeout(100), // wait for prefetching to start - page.waitForLoadState('networkidle') // wait for prefetching to finish + page.waitForTimeout(100), // wait for preloading to start + page.waitForLoadState('networkidle') // wait for preloading to finish ]); expect(requests.find((r) => r.startsWith(module))).toBeUndefined(); }); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 633ea3b7a880..82ef086752cf 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1164,7 +1164,7 @@ test.describe('$app/stores', () => { expect(await page.textContent('#nav-status')).toBe('not currently navigating'); if (javaScriptEnabled) { - await app.prefetchRoutes(['/store/navigating/b']); + await app.preloadCode('/store/navigating/b'); const res = await Promise.all([ page.click('a[href="/store/navigating/b"]'), diff --git a/packages/kit/test/apps/options/source/pages/custom-extensions/+layout.svelte b/packages/kit/test/apps/options/source/pages/custom-extensions/+layout.svelte index aa61d9f00f23..839e4e610c88 100644 --- a/packages/kit/test/apps/options/source/pages/custom-extensions/+layout.svelte +++ b/packages/kit/test/apps/options/source/pages/custom-extensions/+layout.svelte @@ -1,8 +1,8 @@ diff --git a/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte b/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte deleted file mode 100644 index 4d55ffcdf64e..000000000000 --- a/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte +++ /dev/null @@ -1 +0,0 @@ -click me diff --git a/packages/kit/test/apps/options/source/pages/preloading/+page.svelte b/packages/kit/test/apps/options/source/pages/preloading/+page.svelte new file mode 100644 index 000000000000..10d9cdce50d8 --- /dev/null +++ b/packages/kit/test/apps/options/source/pages/preloading/+page.svelte @@ -0,0 +1 @@ +click me diff --git a/packages/kit/test/apps/options/source/pages/prefetching/prefetched/+page.server.js b/packages/kit/test/apps/options/source/pages/preloading/preloaded/+page.server.js similarity index 63% rename from packages/kit/test/apps/options/source/pages/prefetching/prefetched/+page.server.js rename to packages/kit/test/apps/options/source/pages/preloading/preloaded/+page.server.js index 085b3690a2c2..eab0a6b365ab 100644 --- a/packages/kit/test/apps/options/source/pages/prefetching/prefetched/+page.server.js +++ b/packages/kit/test/apps/options/source/pages/preloading/preloaded/+page.server.js @@ -1,5 +1,5 @@ export function load() { return { - message: 'prefetched' + message: 'preloaded' }; } diff --git a/packages/kit/test/apps/options/source/pages/prefetching/prefetched/+page.svelte b/packages/kit/test/apps/options/source/pages/preloading/preloaded/+page.svelte similarity index 100% rename from packages/kit/test/apps/options/source/pages/prefetching/prefetched/+page.svelte rename to packages/kit/test/apps/options/source/pages/preloading/preloaded/+page.svelte diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index 149111ca34c8..a60fdcb5520d 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -184,10 +184,10 @@ test.describe('trailingSlash', () => { }); }); - test('accounts for trailingSlash when prefetching', async ({ app, page, javaScriptEnabled }) => { + test('accounts for trailingSlash when preloading', async ({ app, page, javaScriptEnabled }) => { if (!javaScriptEnabled) return; - await page.goto('/path-base/prefetching'); + await page.goto('/path-base/preloading'); /** @type {string[]} */ let requests = []; @@ -195,7 +195,7 @@ test.describe('trailingSlash', () => { // also wait for network processing to complete, see // https://playwright.dev/docs/network#network-events - await app.prefetch('/path-base/prefetching/prefetched'); + await app.preloadData('/path-base/preloading/preloaded'); // svelte request made is environment dependent if (process.env.DEV) { @@ -204,10 +204,10 @@ test.describe('trailingSlash', () => { expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0); } - expect(requests.includes(`/path-base/prefetching/prefetched/__data.json`)).toBe(true); + expect(requests.includes(`/path-base/preloading/preloaded/__data.json`)).toBe(true); requests = []; - await app.goto('/path-base/prefetching/prefetched'); + await app.goto('/path-base/preloading/preloaded'); expect(requests).toEqual([]); }); }); diff --git a/packages/kit/test/setup.js b/packages/kit/test/setup.js index 6bc6ca76fa8b..60931035115d 100644 --- a/packages/kit/test/setup.js +++ b/packages/kit/test/setup.js @@ -1,8 +1,8 @@ import { goto, invalidate, - prefetch, - prefetchRoutes, + preloadCode, + preloadData, beforeNavigate, afterNavigate } from '$app/navigation'; @@ -14,8 +14,8 @@ export function setup() { Object.assign(window, { goto, invalidate, - prefetch, - prefetchRoutes, + preloadCode, + preloadData, beforeNavigate, afterNavigate }); diff --git a/packages/kit/test/utils.d.ts b/packages/kit/test/utils.d.ts index 31d83058f9e4..2fda168d741c 100644 --- a/packages/kit/test/utils.d.ts +++ b/packages/kit/test/utils.d.ts @@ -17,8 +17,8 @@ export const test: TestType< invalidate(url: string): Promise; beforeNavigate(url: URL): void | boolean; afterNavigate(url: URL): void; - prefetch(url: string): Promise; - prefetchRoutes(urls: string[]): Promise; + preloadCode(...urls: string[]): Promise; + preloadData(url: string): Promise; }; clicknav(selector: string, options?: { timeout?: number }): Promise; in_view(selector: string): Promise; diff --git a/packages/kit/test/utils.js b/packages/kit/test/utils.js index 4bfc9ebdc94e..a4591d546bae 100644 --- a/packages/kit/test/utils.js +++ b/packages/kit/test/utils.js @@ -37,16 +37,16 @@ export const test = base.extend({ afterNavigate: () => page.evaluate(() => afterNavigate(() => {})), /** - * @param {string} url + * @param {string[]} urls * @returns {Promise} */ - prefetch: (url) => page.evaluate((/** @type {string} */ url) => prefetch(url), url), + preloadCode: (...urls) => page.evaluate((urls) => preloadCode(...urls), urls), /** - * @param {string[]} [urls] + * @param {string} url * @returns {Promise} */ - prefetchRoutes: (urls) => page.evaluate((urls) => prefetchRoutes(urls), urls) + preloadData: (url) => page.evaluate((/** @type {string} */ url) => preloadData(url), url) }); }, diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index 6f8c3c77b348..ee95180b434d 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -186,8 +186,8 @@ declare module '$app/forms' { * goto, * invalidate, * invalidateAll, - * prefetch, - * prefetchRoutes + * preloadCode, + * preloadData * } from '$app/navigation'; * ``` */ @@ -251,28 +251,27 @@ declare module '$app/navigation' { */ export function invalidateAll(): Promise; /** - * Programmatically prefetches the given page, which means + * Programmatically preloads the given page, which means * 1. ensuring that the code for the page is loaded, and * 2. calling the page's load function with the appropriate options. * - * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `` element with `data-sveltekit-prefetch`. + * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `` element with `data-sveltekit-preload-data`. * If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous. - * Returns a Promise that resolves when the prefetch is complete. + * Returns a Promise that resolves when the preload is complete. * - * @param href Page to prefetch + * @param href Page to preload */ - export function prefetch(href: string): Promise; + export function preloadData(href: string): Promise; /** - * Programmatically prefetches the code for routes that haven't yet been fetched. + * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. * - * If no argument is given, all routes will be fetched, otherwise you can specify routes by any matching pathname - * such as `/about` (to match `src/routes/about.svelte`) or `/blog/*` (to match `src/routes/blog/[slug].svelte`). + * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about.svelte`) or `/blog/*` (to match `src/routes/blog/[slug].svelte`). * - * Unlike prefetch, this won't call load for individual pages. - * Returns a Promise that resolves when the routes have been prefetched. + * Unlike `preloadData`, this won't call `load` functions. + * Returns a Promise that resolves when the modules have been imported. */ - export function prefetchRoutes(routes?: string[]): Promise; + export function preloadCode(...urls: string[]): Promise; /** * A navigation interceptor that triggers before we navigate to a new URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 056557421782..99db5ee627f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -592,22 +592,6 @@ importers: uvu: 0.5.6 vite: 3.2.3 - packages/kit/test/prerendering/fallback: - specifiers: - '@sveltejs/kit': workspace:* - svelte: ^3.52.0 - svelte-check: ^2.9.2 - typescript: ^4.8.4 - uvu: ^0.5.6 - vite: ^3.2.1 - devDependencies: - '@sveltejs/kit': link:../../.. - svelte: 3.53.1 - svelte-check: 2.9.2_svelte@3.53.1 - typescript: 4.8.4 - uvu: 0.5.6 - vite: 3.2.3 - packages/kit/test/prerendering/options: specifiers: '@sveltejs/kit': workspace:* diff --git a/sites/kit.svelte.dev/src/lib/docs/Contents.svelte b/sites/kit.svelte.dev/src/lib/docs/Contents.svelte index 3a4e398fc00c..28f5ab516bcb 100644 --- a/sites/kit.svelte.dev/src/lib/docs/Contents.svelte +++ b/sites/kit.svelte.dev/src/lib/docs/Contents.svelte @@ -17,7 +17,7 @@ {#each section.pages as { title, path }}
  • dispatch('select', { href: result.href })} data-has-node={result.node ? true : undefined} diff --git a/sites/kit.svelte.dev/src/routes/+page.svelte b/sites/kit.svelte.dev/src/routes/+page.svelte index bcd2a02bd792..77409b3682a6 100644 --- a/sites/kit.svelte.dev/src/routes/+page.svelte +++ b/sites/kit.svelte.dev/src/routes/+page.svelte @@ -41,7 +41,7 @@ of an SPA

    -
    read the docs + read the docs
  • @@ -51,7 +51,7 @@ support and more

    - read the docs + read the docs
    @@ -78,7 +78,7 @@ cd my-app npm install npm run dev -- --open - get started + get started