diff --git a/.changeset/nervous-bananas-search.md b/.changeset/nervous-bananas-search.md new file mode 100644 index 000000000000..badfdec6e142 --- /dev/null +++ b/.changeset/nervous-bananas-search.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require paths pass to preloadCode to be prefixed with basepath diff --git a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md index 7dabe7ec4666..6cda9848e788 100644 --- a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md +++ b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md @@ -36,6 +36,8 @@ export function load({ cookies }) { } ``` +`svelte-migrate` will add comments highlighting the locations that need to be adjusted. + ## Top-level promises are no longer awaited In SvelteKit version 1, if the top-level properties of the object returned from a `load` function were promises, they were automatically awaited. With the introduction of [streaming](https://svelte.dev/blog/streaming-snapshots-sveltekit) this behavior became a bit awkward as it forces you to nest your streamed data one level deep. @@ -64,20 +66,6 @@ export function load({ fetch }) { } ``` -## `path` is now a required option for cookies - -`cookies.set`, `cookies.delete` and `cookies.serialize` all have an options argument through which certain cookie serialization options are configurable. One of the is the `path` setting, which tells browser under which URLs a cookie is applicable. In SvelteKit 1.x, the `path` is optional and defaults to what the browser does, which is removing everything up to and including the last slash in the pathname of the URL. This means that if you're on `/foo/bar`, then the `path` is `/foo`, but if you're on `/foo/bar/`, the `path` is `/foo/bar`. This behavior is somewhat confusing, and most of the time you probably want to have cookies available more broadly (many people set `path` to `/` for that reason) instead of scratching their heads why a cookie they have set doesn't apply elsewhere. For this reason, `path` is a required option in SvelteKit 2. - -```diff -// file: foo/bar/+page.svelte -export function load ({ cookies }) { -- cookies.set('key', 'value'); -+ cookies.set('key', 'value', { path: '/foo' }); -} -``` - -`svelte-migrate` will add comments highlighting the locations that need to be adjusted. - ## goto(...) no longer accepts external URLs To navigate to an external URL, use `window.location = url`. @@ -92,6 +80,14 @@ This inconsistency is fixed in version 2. Paths are either always relative or al Previously it was possible to track URLs from `fetch`es on the server in order to rerun load functions. This poses a possible security risk (private URLs leaking), and as such it was behind the `dangerZone.trackServerFetches` setting, which is now removed. +## `preloadCode` arguments must be prefixed with `base` + +SvelteKit exposes two functions, [`preloadCode`](/docs/modules#$app-navigation-preloadcode) and [`preloadData`](/docs/modules#$app-navigation-preloaddata), for programmatically loading the code and data associated with a particular path. In version 1, there was a subtle inconsistency — the path passed to `preloadCode` did not need to be prefixed with the `base` path (if set), while the path passed to `preloadData` did. + +This is fixed in SvelteKit 2 — in both cases, the path should be prefixed with `base` if it is set. + +Additionally, `preloadCode` now takes a single argument rather than _n_ arguments. + ## `resolvePath` has been removed SvelteKit 1 included a function called `resolvePath` which allows you to resolve a route ID (like `/blog/[slug]`) and a set of parameters (like `{ slug: 'hello' }`) to a pathname. Unfortunately the return value didn't include the `base` path, limiting its usefulness in cases where `base` was set. diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index d8b942c5e358..cafcd1d63d44 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -81,8 +81,8 @@ export const preloadData = /* @__PURE__ */ client_method('preload_data'); * Unlike `preloadData`, this won't call `load` functions. * Returns a Promise that resolves when the modules have been imported. * - * @type {(...urls: string[]) => Promise} - * @param {...string[]} urls + * @type {(url: string) => Promise} + * @param {string} url * @returns {Promise} */ export const preloadCode = /* @__PURE__ */ client_method('preload_code'); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 313dcd43eff4..3364e04634b4 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -273,19 +273,13 @@ export function create_client(app, target) { return load_cache.promise; } - /** @param {...string} pathnames */ - async function preload_code(...pathnames) { - if (DEV && pathnames.length > 1) { - console.warn('Calling `preloadCode` with multiple arguments is deprecated'); - } - - 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]())); - }); + /** @param {string} pathname */ + async function preload_code(pathname) { + const route = routes.find((route) => route.exec(get_url_path(pathname))); - await Promise.all(promises); + if (route) { + await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]())); + } } /** @param {import('./types.js').NavigationFinished} result */ @@ -951,7 +945,7 @@ export function create_client(app, target) { function get_navigation_intent(url, invalidating) { if (is_external_url(url, base)) return; - const path = get_url_path(url); + const path = get_url_path(url.pathname); for (const route of routes) { const params = route.exec(path); @@ -965,9 +959,9 @@ export function create_client(app, target) { } } - /** @param {URL} url */ - function get_url_path(url) { - return decode_pathname(url.pathname.slice(base.length) || '/'); + /** @param {string} pathname */ + function get_url_path(pathname) { + return decode_pathname(pathname.slice(base.length) || '/'); } /** @@ -1293,9 +1287,7 @@ export function create_client(app, target) { (entries) => { for (const entry of entries) { if (entry.isIntersecting) { - preload_code( - get_url_path(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href)) - ); + preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href); observer.unobserve(entry.target); } } @@ -1336,7 +1328,7 @@ export function create_client(app, target) { } } } else if (priority <= options.preload_code) { - preload_code(get_url_path(/** @type {URL} */ (url))); + preload_code(/** @type {URL} */ (url).pathname); } } } @@ -1356,7 +1348,7 @@ export function create_client(app, target) { } if (options.preload_code === PRELOAD_PRIORITIES.eager) { - preload_code(get_url_path(/** @type {URL} */ (url))); + preload_code(/** @type {URL} */ (url).pathname); } } } @@ -1473,7 +1465,21 @@ export function create_client(app, target) { await preload_data(intent); }, - preload_code, + preload_code: (pathname) => { + if (DEV) { + if (!pathname.startsWith(base)) { + throw new Error( + `pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")` + ); + } + + if (!routes.find((route) => route.exec(get_url_path(pathname)))) { + throw new Error(`'${pathname}' did not match any routes`); + } + } + + return preload_code(pathname); + }, apply_action: async (result) => { if (result.type === 'error') { diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index 6b42dd4eeda1..de4135e992b3 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -239,7 +239,7 @@ test.describe('trailingSlash', () => { // also wait for network processing to complete, see // https://playwright.dev/docs/network#network-events - await app.preloadData('/path-base/preloading/preloaded'); + await app.preloadCode('/path-base/preloading/preloaded'); // svelte request made is environment dependent if (process.env.DEV) { @@ -248,6 +248,9 @@ test.describe('trailingSlash', () => { expect(requests.filter((req) => req.endsWith('.mjs')).length).toBeGreaterThan(0); } + requests = []; + await app.preloadData('/path-base/preloading/preloaded'); + expect(requests.includes('/path-base/preloading/preloaded/__data.json')).toBe(true); requests = []; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3c55fa3db610..f3f47021916e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1982,7 +1982,7 @@ declare module '$app/navigation' { * Returns a Promise that resolves when the modules have been imported. * * */ - export const preloadCode: (...urls: string[]) => Promise; + export const preloadCode: (url: 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. * Calling `cancel()` will prevent the navigation from completing. If the navigation would have directly unloaded the current page, calling `cancel` will trigger the native