diff --git a/.changeset/hip-trains-vanish.md b/.changeset/hip-trains-vanish.md new file mode 100644 index 000000000000..799790015102 --- /dev/null +++ b/.changeset/hip-trains-vanish.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +prevent test suites from getting published diff --git a/.changeset/pre.json b/.changeset/pre.json index f3df5a35e026..3b5163a9888e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -415,6 +415,7 @@ "hip-nails-burn", "hip-nails-taste", "hip-trainers-sort", + "hip-trains-vanish", "hip-walls-flash", "hip-windows-sit", "honest-beers-sing", @@ -778,6 +779,7 @@ "quiet-knives-refuse", "quiet-mangos-shop", "quiet-mugs-matter", + "quiet-poems-tease", "quiet-singers-fly", "quiet-terms-fail", "quiet-waves-compete", @@ -1058,6 +1060,7 @@ "tender-geckos-agree", "tender-pans-explode", "tender-plants-smell", + "tender-spiders-fail", "thick-chicken-applaud", "thick-meals-attend", "thick-oranges-walk", @@ -1251,6 +1254,7 @@ "young-horses-kick", "young-penguins-camp", "young-pens-exist", + "young-pumpkins-approve", "young-scissors-collect", "young-students-chew", "young-swans-burn", diff --git a/.changeset/quiet-poems-tease.md b/.changeset/quiet-poems-tease.md new file mode 100644 index 000000000000..02941511e3f8 --- /dev/null +++ b/.changeset/quiet-poems-tease.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[fix] remove unnecessary JSON serialization of server data diff --git a/.changeset/tender-spiders-fail.md b/.changeset/tender-spiders-fail.md new file mode 100644 index 000000000000..2630783d2465 --- /dev/null +++ b/.changeset/tender-spiders-fail.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +Use devalue to serialize server-only `load` return values diff --git a/.changeset/young-pumpkins-approve.md b/.changeset/young-pumpkins-approve.md new file mode 100644 index 000000000000..02cb8f7d069c --- /dev/null +++ b/.changeset/young-pumpkins-approve.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/package': patch +--- + +[breaking] require Node 16.14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 644b30596cee..51ba2a487b89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,12 +25,12 @@ Entry points to be aware of are: - [`packages/create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte) - code that's run when you create a new project with `npm create svelte@latest` - [`packages/package`](https://github.com/sveltejs/kit/tree/master/packages/package) - for the `svelte-package` command -- [`packages/kit/src/vite/index.js`](https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/vite/index.js) - for the Vite plugin -- [`packages/kit/src/core/sync/index.js`](https://github.com/sveltejs/kit/blob/master/packages/kit/src/core/sync/sync.js) - for `svelte-kit sync`, which regenerates routing info and type definitions +- [`packages/kit/src/core`](https://github.com/sveltejs/kit/tree/master/packages/kit/src/core) - code that's called at dev/build-time +- [`packages/kit/src/core/sync`](https://github.com/sveltejs/kit/tree/master/packages/kit/src/core/sync) - for `svelte-kit sync`, which regenerates routing info and type definitions +- [`packages/kit/src/runtime`](https://github.com/sveltejs/kit/tree/master/packages/kit/src/runtime) - code that's called at runtime +- [`packages/kit/src/exports/vite`](https://github.com/sveltejs/kit/tree/master/packages/kit/src/exports/vite) - for all the Vite plugin related stuff - [`packages/adapter-[platform]`](https://github.com/sveltejs/kit/tree/master/packages) - for the various SvelteKit-provided adapters -Most code called at build-time or from the CLI entry point lives in [packages/kit/src/core](https://github.com/sveltejs/kit/tree/master/packages/kit/src/core). Code that runs for rendering and routing lives in [packages/kit/src/runtime](https://github.com/sveltejs/kit/tree/master/packages/kit/src/runtime). Most changes to SvelteKit itself would involve code in these two directories. - ## Testing Run `pnpm test` to run the tests from all subpackages. Browser tests live in subdirectories of `packages/kit/test` such as `packages/kit/test/apps/basics`. diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md index b085b4f9ac0f..5dff407cc72e 100644 --- a/documentation/docs/03-routing.md +++ b/documentation/docs/03-routing.md @@ -106,7 +106,7 @@ export async function load({ params }) { } ``` -During client-side navigation, SvelteKit will load this data using `fetch`, which means that the returned value must be serializable as JSON. +During client-side navigation, SvelteKit will load this data from the server, which means that the returned value must be serializable using [devalue](https://github.com/rich-harris/devalue). #### Actions diff --git a/documentation/docs/05-load.md b/documentation/docs/05-load.md index 30340f984d8c..e1a1bc5e559e 100644 --- a/documentation/docs/05-load.md +++ b/documentation/docs/05-load.md @@ -4,7 +4,7 @@ title: Loading data A [`+page.svelte`](/docs/routing#page-page-svelte) or [`+layout.svelte`](/docs/routing#layout-layout-svelte) gets its `data` from a `load` function. -If the `load` function is defined in `+page.js` or `+layout.js` it will run both on the server and in the browser. If it's instead defined in `+page.server.js` or `+layout.server.js` it will only run on the server, in which case it can (for example) make database calls and access private [environment variables](/docs/modules#$env-static-private), but can only return data that can be serialized as JSON. In both cases, the return value (if there is one) must be an object. +If the `load` function is defined in `+page.js` or `+layout.js` it will run both on the server and in the browser. If it's instead defined in `+page.server.js` or `+layout.server.js` it will only run on the server, in which case it can (for example) make database calls and access private [environment variables](/docs/modules#$env-static-private), but can only return data that can be serialized with [devalue](https://github.com/rich-harris/devalue). In both cases, the return value (if there is one) must be an object. ```js /// file: src/routes/+page.js @@ -256,7 +256,7 @@ export async function load({ setHeaders }) { ### Output -The returned `data`, if any, must be an object of values. For a server-only `load` function, these values must be JSON-serializable. Top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall: +The returned `data`, if any, must be an object of values. For a server-only `load` function, these values must be serializable with [devalue](https://github.com/rich-harris/devalue). Top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall: ```js // @filename: $types.d.ts diff --git a/documentation/docs/80-migrating.md b/documentation/docs/80-migrating.md index cde26307d723..1ef080470946 100644 --- a/documentation/docs/80-migrating.md +++ b/documentation/docs/80-migrating.md @@ -45,7 +45,7 @@ If you were using plugins for filetypes that are not automatically handled by [V #### src/client.js -This file has no equivalent in SvelteKit. Any custom logic (beyond `sapper.start(...)`) should be expressed in your `__layout.svelte` file, inside an `onMount` callback. +This file has no equivalent in SvelteKit. Any custom logic (beyond `sapper.start(...)`) should be expressed in your `+layout.svelte` file, inside an `onMount` callback. #### src/server.js @@ -81,7 +81,7 @@ Routes now are made up of the folder name exclusively to remove ambiguity, the f | routes/about/index.svelte | routes/about/+page.svelte | | routes/about.svelte | routes/about/+page.svelte | -Your custom error page component should be renamed from `_error.svelte` to `+error.svelte`. Any `_layout.svelte` files should likewise be renamed `+layout.svelte`. The double underscore prefix is reserved for SvelteKit; your own [private modules](/docs/routing#private-modules) are still denoted with a single `_` prefix (configurable via [`routes`](/docs/configuration#routes) config). +Your custom error page component should be renamed from `_error.svelte` to `+error.svelte`. Any `_layout.svelte` files should likewise be renamed `+layout.svelte`. [Any other files are ignored](https://kit.svelte.dev/docs/routing#other-files). #### Imports @@ -97,9 +97,7 @@ As before, pages and layouts can export a function that allows data to be loaded This function has been renamed from `preload` to [`load`](/docs/load), it now lives in a `+page.js` (or `+layout.js`) next to its `+page.svelte` (or `+layout.svelte`), and its API has changed. Instead of two arguments — `page` and `session` — there is a single `event` argument. -There is no more `this` object, and consequently no `this.fetch`, `this.error` or `this.redirect`. Instead of returning props directly, `load` now returns an object that _contains_ `props`, alongside various other things. - -Lastly, if your page has a `load` method, make sure to return something otherwise you will get `Not found`. +There is no more `this` object, and consequently no `this.fetch`, `this.error` or `this.redirect`. Instead, you can get [`fetch`](https://kit.svelte.dev/docs/load#input-methods-fetch) from the input methods, and both [`error`](https://kit.svelte.dev/docs/load#errors) and [`redirect`](https://kit.svelte.dev/docs/load#redirects) are now thrown. #### Stores diff --git a/packages/adapter-auto/CHANGELOG.md b/packages/adapter-auto/CHANGELOG.md index c1f4ee167a04..6c1d6c602beb 100644 --- a/packages/adapter-auto/CHANGELOG.md +++ b/packages/adapter-auto/CHANGELOG.md @@ -1,5 +1,13 @@ # @sveltejs/adapter-auto +## 1.0.0-next.69 + +### Patch Changes + +- Updated dependencies [[`c530d337`](https://github.com/sveltejs/kit/commit/c530d33793a228fac684c71ed7926e6217101a90)]: + - @sveltejs/adapter-netlify@1.0.0-next.75 + - @sveltejs/adapter-vercel@1.0.0-next.71 + ## 1.0.0-next.68 ### Patch Changes diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json index a02fa1a5fc10..c4ebdf44edd4 100644 --- a/packages/adapter-auto/package.json +++ b/packages/adapter-auto/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-auto", - "version": "1.0.0-next.68", + "version": "1.0.0-next.69", "repository": { "type": "git", "url": "https://github.com/sveltejs/kit", diff --git a/packages/adapter-cloudflare/README.md b/packages/adapter-cloudflare/README.md index d209391b3fe1..55d73668f628 100644 --- a/packages/adapter-cloudflare/README.md +++ b/packages/adapter-cloudflare/README.md @@ -49,7 +49,7 @@ When configuring your project settings, you must use the following settings: - **Environment variables** - `NODE_VERSION`: `16` -> **Important:** You need to add a `NODE_VERSION` environment variable to both the "production" and "preview" environments. You can add this during project setup or later in the Pages project settings. SvelteKit requires Node `16.9` or later, so you should use `16` as the `NODE_VERSION` value. +> **Important:** You need to add a `NODE_VERSION` environment variable to both the "production" and "preview" environments. You can add this during project setup or later in the Pages project settings. SvelteKit requires Node `16.14` or later, so you should use `16` as the `NODE_VERSION` value. ## Environment variables diff --git a/packages/adapter-netlify/CHANGELOG.md b/packages/adapter-netlify/CHANGELOG.md index 3645a3d366f6..32ccc7abebe0 100644 --- a/packages/adapter-netlify/CHANGELOG.md +++ b/packages/adapter-netlify/CHANGELOG.md @@ -1,5 +1,11 @@ # @sveltejs/adapter-netlify +## 1.0.0-next.75 + +### Patch Changes + +- Use devalue to serialize server-only `load` return values ([#6318](https://github.com/sveltejs/kit/pull/6318)) + ## 1.0.0-next.74 ### Patch Changes diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 39bc32738954..4528b8a3c6a6 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -211,7 +211,7 @@ async function generate_lambda_functions({ builder, publish, split, esm }) { writeFileSync(`.netlify/functions-internal/${name}.js`, fn); redirects.push(`${pattern} /.netlify/functions/${name} 200`); - redirects.push(`${pattern}/__data.json /.netlify/functions/${name} 200`); + redirects.push(`${pattern}/__data.js /.netlify/functions/${name} 200`); } }; }); diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index 8a5b5914bcb5..350642abb7ed 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-netlify", - "version": "1.0.0-next.74", + "version": "1.0.0-next.75", "repository": { "type": "git", "url": "https://github.com/sveltejs/kit", diff --git a/packages/adapter-vercel/CHANGELOG.md b/packages/adapter-vercel/CHANGELOG.md index 9bd25a1d9af1..721d04784a52 100644 --- a/packages/adapter-vercel/CHANGELOG.md +++ b/packages/adapter-vercel/CHANGELOG.md @@ -1,5 +1,11 @@ # @sveltejs/adapter-vercel +## 1.0.0-next.71 + +### Patch Changes + +- Use devalue to serialize server-only `load` return values ([#6318](https://github.com/sveltejs/kit/pull/6318)) + ## 1.0.0-next.70 ### Patch Changes diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 532d3534e20a..0d0ba443385c 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -224,7 +224,7 @@ export default function ({ external = [], edge, split } = {}) { sliced_pattern = '^/?'; } - const src = `${sliced_pattern}(?:/__data.json)?$`; // TODO adding /__data.json is a temporary workaround — those endpoints should be treated as distinct routes + const src = `${sliced_pattern}(?:/__data.js)?$`; // TODO adding /__data.js is a temporary workaround — those endpoints should be treated as distinct routes await generate_function(route.id || 'index', src, entry.generateManifest); } diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json index 47ea86e039e9..66398471115d 100644 --- a/packages/adapter-vercel/package.json +++ b/packages/adapter-vercel/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-vercel", - "version": "1.0.0-next.70", + "version": "1.0.0-next.71", "repository": { "type": "git", "url": "https://github.com/sveltejs/kit", diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md index 23a74a77263d..b765530e93fc 100644 --- a/packages/kit/CHANGELOG.md +++ b/packages/kit/CHANGELOG.md @@ -1,5 +1,21 @@ # @sveltejs/kit +## 1.0.0-next.448 + +### Patch Changes + +- prevent test suites from getting published ([#6386](https://github.com/sveltejs/kit/pull/6386)) + +* [fix] remove unnecessary JSON serialization of server data ([#6382](https://github.com/sveltejs/kit/pull/6382)) + +- [breaking] require Node 16.14 ([#6388](https://github.com/sveltejs/kit/pull/6388)) + +## 1.0.0-next.447 + +### Patch Changes + +- Use devalue to serialize server-only `load` return values ([#6318](https://github.com/sveltejs/kit/pull/6318)) + ## 1.0.0-next.446 ### Patch Changes diff --git a/packages/kit/package.json b/packages/kit/package.json index 89fb9f00b0ff..b08d609e9166 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/kit", - "version": "1.0.0-next.446", + "version": "1.0.0-next.448", "repository": { "type": "git", "url": "https://github.com/sveltejs/kit", @@ -12,7 +12,7 @@ "dependencies": { "@sveltejs/vite-plugin-svelte": "^1.0.1", "cookie": "^0.5.0", - "devalue": "^2.0.1", + "devalue": "^3.1.2", "kleur": "^4.1.4", "magic-string": "^0.26.2", "mime": "^3.0.0", @@ -36,7 +36,6 @@ "rollup": "^2.75.7", "svelte": "^3.48.0", "svelte-preprocess": "^4.10.6", - "tiny-glob": "^0.2.9", "typescript": "^4.7.4", "uvu": "^0.5.3", "vite": "^3.0.9" @@ -51,9 +50,8 @@ "files": [ "src", "!src/**/*.spec.js", - "!src/packaging/test", "!src/core/**/fixtures", - "!src/core/sync/create_manifest_data/test", + "!src/core/**/test", "types", "svelte-kit.js" ], @@ -66,7 +64,7 @@ "prepublishOnly": "npm run build", "test": "npm run test:unit && npm run test:integration", "test:integration": "pnpm run -r --workspace-concurrency 1 --filter=\"./test/**\" test", - "test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\" -i packaging", + "test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\"", "types": "node scripts/extract-types.js", "postinstall": "node svelte-kit.js sync" }, @@ -91,6 +89,6 @@ }, "types": "types/index.d.ts", "engines": { - "node": ">=16.9" + "node": ">=16.14" } } diff --git a/packages/kit/src/core/constants.js b/packages/kit/src/constants.js similarity index 87% rename from packages/kit/src/core/constants.js rename to packages/kit/src/constants.js index 8dc9aab27904..7d50f152432c 100644 --- a/packages/kit/src/core/constants.js +++ b/packages/kit/src/constants.js @@ -3,3 +3,5 @@ export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets'; export const GENERATED_COMMENT = '// this file is generated — do not edit it\n'; + +export const DATA_SUFFIX = '/__data.js'; diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 2d2006098283..789348a62cd7 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -1,4 +1,4 @@ -import { GENERATED_COMMENT } from './constants.js'; +import { GENERATED_COMMENT } from '../constants.js'; import { runtime_base } from './utils.js'; /** diff --git a/packages/kit/src/core/sync/write_ambient.js b/packages/kit/src/core/sync/write_ambient.js index 9672488bb2f2..9904a1cee043 100644 --- a/packages/kit/src/core/sync/write_ambient.js +++ b/packages/kit/src/core/sync/write_ambient.js @@ -1,6 +1,6 @@ import path from 'path'; import { get_env } from '../../exports/vite/utils.js'; -import { GENERATED_COMMENT } from '../constants.js'; +import { GENERATED_COMMENT } from '../../constants.js'; import { create_types } from '../env.js'; import { write_if_changed } from './utils.js'; diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 77cb0bfb9e86..a74cdddd20ed 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -8,7 +8,7 @@ import { installPolyfills } from '../../../exports/node/polyfills.js'; import { coalesce_to_error } from '../../../utils/error.js'; import { posixify } from '../../../utils/filesystem.js'; import { load_template } from '../../../core/config/index.js'; -import { SVELTE_KIT_ASSETS } from '../../../core/constants.js'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import * as sync from '../../../core/sync/sync.js'; import { get_mime_lookup, runtime_base, runtime_prefix } from '../../../core/utils.js'; import { get_env, prevent_illegal_vite_imports, resolve_entry } from '../utils.js'; diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 54539d3e03c5..9f1a80298e77 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -4,7 +4,7 @@ import sirv from 'sirv'; import { pathToFileURL } from 'url'; import { getRequest, setResponse } from '../../../exports/node/index.js'; import { installPolyfills } from '../../../exports/node/polyfills.js'; -import { SVELTE_KIT_ASSETS } from '../../../core/constants.js'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { loadEnv } from 'vite'; /** @typedef {import('http').IncomingMessage} Req */ diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 4bf1994d710a..64c470be62f6 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -10,6 +10,7 @@ import Root from '__GENERATED__/root.svelte'; import { nodes, server_loads, dictionary, matchers } from '__GENERATED__/client-manifest.js'; import { HttpError, Redirect } from '../control.js'; import { stores } from './singletons.js'; +import { DATA_SUFFIX } from '../../constants.js'; const SCROLL_KEY = 'sveltekit:scroll'; const INDEX_KEY = 'sveltekit:index'; @@ -393,7 +394,7 @@ export function create_client({ target, base, trailing_slash }) { * status: number; * error: HttpError | Error | null; * routeId: string | null; - * validation_errors?: string | undefined; + * validation_errors?: Record | null; * }} opts */ async function get_navigation_result_from_branch({ @@ -715,24 +716,14 @@ export function create_client({ target, base, trailing_slash }) { if (invalid_server_nodes.some(Boolean)) { try { - const res = await native_fetch( - `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`, - { - headers: { - 'x-sveltekit-invalidated': invalid_server_nodes.map((x) => (x ? '1' : '')).join(',') - } - } - ); - - server_data = /** @type {import('types').ServerData} */ (await res.json()); - - if (!res.ok) { - throw server_data; - } - } catch (e) { - // something went catastrophically wrong — bail and defer to the server - native_navigation(url); - return; + server_data = await load_data(url, invalid_server_nodes); + } catch (error) { + return load_root_error_page({ + status: 500, + error: /** @type {Error} */ (error), + url, + routeId: route.id + }); } if (server_data.type === 'redirect') { @@ -882,19 +873,18 @@ export function create_client({ target, base, trailing_slash }) { if (node.server) { // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use // existing root layout data - const res = await native_fetch( - `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`, - { - headers: { - 'x-sveltekit-invalidated': '1' - } - } - ); + try { + const server_data = await load_data(url, [true]); - const server_data_nodes = await res.json(); - server_data_node = server_data_nodes?.[0] ?? null; + if ( + server_data.type !== 'data' || + (server_data.nodes[0] && server_data.nodes[0].type !== 'data') + ) { + throw 0; + } - if (!res.ok || server_data_nodes?.type !== 'data') { + server_data_node = server_data.nodes[0] ?? null; + } catch { // at this point we have no choice but to fall back to the server native_navigation(url); @@ -1298,32 +1288,24 @@ export function create_client({ target, base, trailing_slash }) { }); }, - _hydrate: async ({ status, error, node_ids, params, routeId }) => { + _hydrate: async ({ + status, + error: original_error, // TODO get rid of this + node_ids, + params, + routeId, + data: server_data_nodes, + errors: validation_errors + }) => { const url = new URL(location.href); /** @type {import('./types').NavigationFinished | undefined} */ let result; try { - /** - * @param {string} type - * @param {any} fallback - */ - const parse = (type, fallback) => { - const script = document.querySelector(`script[sveltekit\\:data-type="${type}"]`); - return script?.textContent ? JSON.parse(script.textContent) : fallback; - }; - /** - * @type {Array} - * On initial navigation, this will only consist of data nodes or `null`. - * A possible error is passed through the `error` property, in which case - * the last entry of `node_ids` is an error page and the last entry of - * `server_data_nodes` is `null`. - */ - const server_data_nodes = parse('server_data', []); - const validation_errors = parse('validation_errors', undefined); - const branch_promises = node_ids.map(async (n, i) => { + const server_data_node = server_data_nodes[i]; + return load_node({ loader: nodes[n], url, @@ -1336,7 +1318,7 @@ export function create_client({ target, base, trailing_slash }) { } return data; }, - server_data_node: create_data_node(server_data_nodes[i]) + server_data_node: create_data_node(server_data_node) }); }); @@ -1345,13 +1327,15 @@ export function create_client({ target, base, trailing_slash }) { params, branch: await Promise.all(branch_promises), status, - error: /** @type {import('../server/page/types').SerializedHttpError} */ (error) + error: /** @type {import('../server/page/types').SerializedHttpError} */ (original_error) ?.__is_http_error ? new HttpError( - /** @type {import('../server/page/types').SerializedHttpError} */ (error).status, - error.message + /** @type {import('../server/page/types').SerializedHttpError} */ ( + original_error + ).status, + original_error.message ) - : error, + : original_error, validation_errors, routeId }); @@ -1377,3 +1361,34 @@ export function create_client({ target, base, trailing_slash }) { } }; } + +let data_id = 1; + +/** + * @param {URL} url + * @param {boolean[]} invalid + * @returns {Promise} + */ +async function load_data(url, invalid) { + const data_url = new URL(url); + data_url.pathname = url.pathname.replace(/\/$/, '') + DATA_SUFFIX; + data_url.searchParams.set('__invalid', invalid.map((x) => (x ? 'y' : 'n')).join('')); + data_url.searchParams.set('__id', String(data_id++)); + + // The __data.js file is generated by the server and looks like + // `window.__sveltekit_data = ${devalue(data)}`. We do this instead + // of `export const data` because modules are cached indefinitely, + // and that would cause memory leaks. + // + // The data is read and deleted in the same tick as the promise + // resolves, so it's not vulnerable to race conditions + await import(/* @vite-ignore */ data_url.href); + + // @ts-expect-error + const server_data = window.__sveltekit_data; + + // @ts-expect-error + delete window.__sveltekit_data; + + return server_data; +} diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index 2c679c8a6cc2..aeadbc23d40f 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -20,6 +20,8 @@ export { set_public_env } from '../env-public.js'; * node_ids: number[]; * params: Record; * routeId: string | null; + * data: Array; + * errors: Record | null; * }; * }} opts */ diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 6bcb64787922..e86bc3ae0eb2 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -27,6 +27,8 @@ export interface Client { node_ids: number[]; params: Record; routeId: string | null; + data: Array; + errors: Record | null; }) => Promise; _start_router: () => void; } diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js new file mode 100644 index 000000000000..25296344fca5 --- /dev/null +++ b/packages/kit/src/runtime/server/data/index.js @@ -0,0 +1,146 @@ +import { HttpError, Redirect } from '../../control.js'; +import { normalize_error } from '../../../utils/error.js'; +import { once } from '../../../utils/functions.js'; +import { load_server_data } from '../page/load_data.js'; +import { data_response, error_to_pojo } from '../utils.js'; +import { normalize_path } from '../../../utils/url.js'; +import { DATA_SUFFIX } from '../../../constants.js'; + +/** + * @param {import('types').RequestEvent} event + * @param {import('types').SSRRoute} route + * @param {import('types').SSROptions} options + * @param {import('types').SSRState} state + * @returns {Promise} + */ +export async function render_data(event, route, options, state) { + if (!route.page) { + // requesting /__data.js should fail for a +server.js + return new Response(undefined, { + status: 404 + }); + } + + try { + const node_ids = [...route.page.layouts, route.page.leaf]; + + const invalidated = + event.url.searchParams + .get('__invalid') + ?.split('') + .map((x) => x === 'y') ?? node_ids.map(() => true); + + let aborted = false; + + const url = new URL(event.url); + url.pathname = normalize_path( + url.pathname.slice(0, -DATA_SUFFIX.length), + options.trailing_slash + ); + + url.searchParams.delete('__invalid'); + url.searchParams.delete('__id'); + + const new_event = { ...event, url }; + + const functions = node_ids.map((n, i) => { + return once(async () => { + try { + if (aborted) { + return /** @type {import('types').ServerDataSkippedNode} */ ({ + type: 'skip' + }); + } + + // == because it could be undefined (in dev) or null (in build, because of JSON.stringify) + const node = n == undefined ? n : await options.manifest._.nodes[n](); + return load_server_data({ + event: new_event, + state, + node, + parent: async () => { + /** @type {Record} */ + const data = {}; + for (let j = 0; j < i; j += 1) { + const parent = /** @type {import('types').ServerDataNode | null} */ ( + await functions[j]() + ); + + if (parent) { + Object.assign(data, parent.data); + } + } + return data; + } + }); + } catch (e) { + aborted = true; + throw e; + } + }); + }); + + const promises = functions.map(async (fn, i) => { + if (!invalidated[i]) { + return /** @type {import('types').ServerDataSkippedNode} */ ({ + type: 'skip' + }); + } + + return fn(); + }); + + let length = promises.length; + const nodes = await Promise.all( + promises.map((p, i) => + p.catch((e) => { + const error = normalize_error(e); + + if (error instanceof Redirect) { + throw error; + } + + // Math.min because array isn't guaranteed to resolve in order + length = Math.min(length, i + 1); + + if (error instanceof HttpError) { + return /** @type {import('types').ServerErrorNode} */ ({ + type: 'error', + httperror: { ...error } + }); + } + + options.handle_error(error, event); + + return /** @type {import('types').ServerErrorNode} */ ({ + type: 'error', + error: error_to_pojo(error, options.get_stack) + }); + }) + ) + ); + + /** @type {import('types').ServerData} */ + const server_data = { + type: 'data', + nodes: nodes.slice(0, length) + }; + + return data_response(server_data); + } catch (e) { + const error = normalize_error(e); + + if (error instanceof Redirect) { + /** @type {import('types').ServerData} */ + const server_data = { + type: 'redirect', + location: error.location + }; + + return data_response(server_data); + } else { + // TODO make it clearer that this was an unexpected error + return data_response(error_to_pojo(error, options.get_stack)); + } + } +} diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 804823673a6c..5c03fb045901 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -2,20 +2,16 @@ import { render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; import { respond_with_error } from './page/respond_with_error.js'; -import { coalesce_to_error, normalize_error } from '../../utils/error.js'; -import { serialize_error, GENERIC_ERROR, error_to_pojo } from './utils.js'; +import { coalesce_to_error } from '../../utils/error.js'; +import { serialize_error, GENERIC_ERROR } from './utils.js'; import { decode_params, disable_search, normalize_path } from '../../utils/url.js'; import { exec } from '../../utils/routing.js'; import { negotiate } from '../../utils/http.js'; -import { HttpError, Redirect } from '../control.js'; -import { load_server_data } from './page/load_data.js'; -import { json } from '../../exports/index.js'; -import { once } from '../../utils/functions.js'; +import { render_data } from './data/index.js'; +import { DATA_SUFFIX } from '../../constants.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ -const DATA_SUFFIX = '/__data.json'; - /** @param {{ html: string }} opts */ const default_transform = ({ html }) => html; @@ -69,12 +65,7 @@ export async function respond(request, options, state) { } const is_data_request = decoded.endsWith(DATA_SUFFIX); - - if (is_data_request) { - const data_suffix_length = DATA_SUFFIX.length - (options.trailing_slash === 'always' ? 1 : 0); - decoded = decoded.slice(0, -data_suffix_length) || '/'; - url = new URL(url.origin + url.pathname.slice(0, -data_suffix_length) + url.search); - } + if (is_data_request) decoded = decoded.slice(0, -DATA_SUFFIX.length); if (!state.prerendering?.fallback) { const matchers = await options.manifest._.matchers(); @@ -92,26 +83,19 @@ export async function respond(request, options, state) { } } - if (route) { - if (route.page) { - const normalized = normalize_path(url.pathname, options.trailing_slash); - - if (normalized !== url.pathname && !state.prerendering?.fallback) { - return new Response(undefined, { - status: 301, - headers: { - 'x-sveltekit-normalize': '1', - location: - // ensure paths starting with '//' are not treated as protocol-relative - (normalized.startsWith('//') ? url.origin + normalized : normalized) + - (url.search === '?' ? '' : url.search) - } - }); - } - } else if (is_data_request) { - // requesting /__data.json should fail for a standalone endpoint + if (route?.page && !is_data_request) { + const normalized = normalize_path(url.pathname, options.trailing_slash); + + if (normalized !== url.pathname && !state.prerendering?.fallback) { return new Response(undefined, { - status: 404 + status: 301, + headers: { + 'x-sveltekit-normalize': '1', + location: + // ensure paths starting with '//' are not treated as protocol-relative + (normalized.startsWith('//') ? url.origin + normalized : normalized) + + (url.search === '?' ? '' : url.search) + } }); } } @@ -250,116 +234,9 @@ export async function respond(request, options, state) { if (route) { /** @type {Response} */ let response; - if (is_data_request && route.page) { - try { - const node_ids = [...route.page.layouts, route.page.leaf]; - - const invalidated = - request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ?? - node_ids.map(() => true); - - let aborted = false; - - const functions = node_ids.map((n, i) => { - return once(async () => { - try { - if (aborted) { - return /** @type {import('types').ServerDataSkippedNode} */ ({ - type: 'skip' - }); - } - - // == because it could be undefined (in dev) or null (in build, because of JSON.stringify) - const node = n == undefined ? n : await options.manifest._.nodes[n](); - return load_server_data({ - dev: options.dev, - event, - state, - node, - parent: async () => { - /** @type {Record} */ - const data = {}; - for (let j = 0; j < i; j += 1) { - const parent = /** @type {import('types').ServerDataNode | null} */ ( - await functions[j]() - ); - - if (parent) { - Object.assign(data, parent.data); - } - } - return data; - } - }); - } catch (e) { - aborted = true; - throw e; - } - }); - }); - const promises = functions.map(async (fn, i) => { - if (!invalidated[i]) { - return /** @type {import('types').ServerDataSkippedNode} */ ({ - type: 'skip' - }); - } - - return fn(); - }); - - let length = promises.length; - const nodes = await Promise.all( - promises.map((p, i) => - p.catch((e) => { - const error = normalize_error(e); - - if (error instanceof Redirect) { - throw error; - } - - // Math.min because array isn't guaranteed to resolve in order - length = Math.min(length, i + 1); - - if (error instanceof HttpError) { - return /** @type {import('types').ServerErrorNode} */ ({ - type: 'error', - httperror: { ...error } - }); - } - - options.handle_error(error, event); - - return /** @type {import('types').ServerErrorNode} */ ({ - type: 'error', - error: error_to_pojo(error, options.get_stack) - }); - }) - ) - ); - - /** @type {import('types').ServerData} */ - const server_data = { - type: 'data', - nodes: nodes.slice(0, length) - }; - - response = json(server_data); - } catch (e) { - const error = normalize_error(e); - - if (error instanceof Redirect) { - /** @type {import('types').ServerData} */ - const server_data = { - type: 'redirect', - location: error.location - }; - - response = json(server_data); - } else { - response = json(error_to_pojo(error, options.get_stack), { status: 500 }); - } - } + if (is_data_request) { + response = await render_data(event, route, options, state); } else if (route.page) { response = await render_page(event, route, route.page, options, state, resolve_opts); } else if (route.endpoint) { @@ -371,7 +248,7 @@ export async function respond(request, options, state) { } if (!is_data_request) { - // we only want to set cookies on __data.json requests, we don't + // we only want to set cookies on __data.js requests, we don't // want to cache stuff erroneously etc for (const key in headers) { const value = headers[key]; diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 39f5cbdf74fd..d350441a7158 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,3 +1,4 @@ +import { devalue } from 'devalue'; import { negotiate } from '../../../utils/http.js'; import { render_response } from './render.js'; import { respond_with_error } from './respond_with_error.js'; @@ -8,6 +9,7 @@ import { error, json } from '../../../exports/index.js'; import { compact } from '../../../utils/array.js'; import { normalize_error } from '../../../utils/error.js'; import { load_data, load_server_data } from './load_data.js'; +import { DATA_SUFFIX } from '../../../constants.js'; /** * @typedef {import('./types.js').Loaded} Loaded @@ -102,7 +104,7 @@ export async function render_page(event, route, page, options, state, resolve_op } const should_prerender_data = nodes.some((node) => node?.server); - const data_pathname = `${event.url.pathname.replace(/\/$/, '')}/__data.json`; + const data_pathname = event.url.pathname.replace(/\/$/, '') + DATA_SUFFIX; // it's crucial that we do this before returning the non-SSR response, otherwise // SvelteKit will erroneously believe that the path has been prerendered, @@ -166,7 +168,6 @@ export async function render_page(event, route, page, options, state, resolve_op } return await load_server_data({ - dev: options.dev, event, state, node, @@ -231,12 +232,14 @@ export async function render_page(event, route, page, options, state, resolve_op if (error instanceof Redirect) { if (state.prerendering && should_prerender_data) { + const body = `window.__sveltekit_data = ${JSON.stringify({ + type: 'redirect', + location: error.location + })}`; + state.prerendering.dependencies.set(data_pathname, { - response: new Response(undefined), - body: JSON.stringify({ - type: 'redirect', - location: error.location - }) + response: new Response(body), + body }); } @@ -294,12 +297,14 @@ export async function render_page(event, route, page, options, state, resolve_op } if (state.prerendering && should_prerender_data) { + const body = `window.__sveltekit_data = ${devalue({ + type: 'data', + nodes: branch.map((branch_node) => branch_node?.server_data) + })}`; + state.prerendering.dependencies.set(data_pathname, { - response: new Response(undefined), - body: JSON.stringify({ - type: 'data', - nodes: branch.map((branch_node) => branch_node?.server_data) - }) + response: new Response(body), + body }); } diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 237808de5126..21a753e50b0d 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -3,7 +3,6 @@ import { disable_search, make_trackable } from '../../../utils/url.js'; /** * Calls the user's `load` function. * @param {{ - * dev: boolean; * event: import('types').RequestEvent; * state: import('types').SSRState; * node: import('types').SSRNode | undefined; @@ -11,7 +10,7 @@ import { disable_search, make_trackable } from '../../../utils/url.js'; * }} opts * @returns {Promise} */ -export async function load_server_data({ dev, event, state, node, parent }) { +export async function load_server_data({ event, state, node, parent }) { if (!node?.server) return null; const uses = { @@ -53,10 +52,6 @@ export async function load_server_data({ dev, event, state, node, parent }) { const data = result ? await unwrap_promises(result) : null; - if (dev) { - check_serializability(data, /** @type {string} */ (node.server_id), 'data'); - } - return { type: 'data', data, @@ -127,42 +122,3 @@ async function unwrap_promises(object) { return unwrapped; } - -/** - * Check that the data can safely be serialized to JSON - * @param {any} value - * @param {string} id - * @param {string} path - */ -function check_serializability(value, id, path) { - const type = typeof value; - - if (type === 'string' || type === 'boolean' || type === 'number' || type === 'undefined') { - // primitives are fine - return; - } - - if (type === 'object') { - // nulls are fine... - if (!value) return; - - // ...so are plain arrays... - if (Array.isArray(value)) { - value.forEach((child, i) => { - check_serializability(child, id, `${path}[${i}]`); - }); - return; - } - - // ...and objects - const tag = Object.prototype.toString.call(value); - if (tag === '[object Object]') { - for (const key in value) { - check_serializability(value[key], id, `${path}.${key}`); - } - return; - } - } - - throw new Error(`${path} returned from 'load' in ${id} cannot be serialized as JSON`); -} diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index df6ed2f28cef..19c7e4eb5c26 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -1,4 +1,4 @@ -import devalue from 'devalue'; +import { devalue } from 'devalue'; import { readable, writable } from 'svelte/store'; import * as cookie from 'cookie'; import { hash } from '../../hash.js'; @@ -174,6 +174,31 @@ export async function render_response({ /** @param {string} path */ const prefixed = (path) => (path.startsWith('/') ? path : `${assets}/${path}`); + const serialized = { data: '', errors: 'null' }; + + try { + serialized.data = devalue(branch.map(({ server_data }) => server_data)); + } catch (e) { + // If we're here, the data could not be serialized with devalue + // TODO if we wanted to get super fancy we could track down the origin of the `load` + // function, but it would mean passing more stuff around than we currently do + const error = /** @type {any} */ (e); + const match = /\[(\d+)\]\.data\.(.+)/.exec(error.path); + if (match) throw new Error(`${error.message} (data.${match[2]})`); + throw error; + } + + if (validation_errors) { + try { + serialized.errors = devalue(validation_errors); + } catch (e) { + // If we're here, the data could not be serialized with devalue + const error = /** @type {any} */ (e); + if (error.path) throw new Error(`${error.message} (errors.${error.path})`); + throw error; + } + } + // prettier-ignore const init_app = ` import { set_public_env, start } from ${s(prefixed(entry.file))}; @@ -191,7 +216,9 @@ export async function render_response({ error: ${error && serialize_error(error, e => e.stack)}, node_ids: [${branch.map(({ node }) => node.index).join(', ')}], params: ${devalue(event.params)}, - routeId: ${s(event.routeId)} + routeId: ${s(event.routeId)}, + data: ${serialized.data}, + errors: ${serialized.errors} }` : 'null'} }); `; @@ -272,15 +299,6 @@ export async function render_response({ ); } - if (branch.some((node) => node.server_data)) { - serialized_data.push( - render_json_payload_script( - { type: 'server_data' }, - branch.map(({ server_data }) => server_data) - ) - ); - } - if (validation_errors) { serialized_data.push( render_json_payload_script({ type: 'validation_errors' }, validation_errors) diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index 001fdd5a8cea..b0f87a5f5859 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -35,7 +35,6 @@ export async function respond_with_error({ event, options, state, status, error, const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout const server_data_promise = load_server_data({ - dev: options.dev, event, state, node: default_layout, diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 6efd4f93e5b7..09b36614ebc9 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -1,3 +1,4 @@ +import { devalue } from 'devalue'; import { HttpError } from '../control.js'; /** @param {any} body */ @@ -114,3 +115,23 @@ export function allowed_methods(mod) { return allowed; } + +/** @param {any} data */ +export function data_response(data) { + try { + return new Response(`window.__sveltekit_data = ${devalue(data)}`, { + headers: { + 'content-type': 'application/javascript' + } + }); + } catch (e) { + const error = /** @type {any} */ (e); + const match = /\[(\d+)\]\.data\.(.+)/.exec(error.path); + const message = match ? `${error.message} (data.${match[2]})` : error.message; + return new Response(`throw new Error(${JSON.stringify(message)})`, { + headers: { + 'content-type': 'application/javascript' + } + }); + } +} diff --git a/packages/kit/src/utils/escape.spec.js b/packages/kit/src/utils/escape.spec.js index 1474ddadfa36..57eef069c288 100644 --- a/packages/kit/src/utils/escape.spec.js +++ b/packages/kit/src/utils/escape.spec.js @@ -5,11 +5,13 @@ import { render_json_payload_script, escape_html_attr } from './escape.js'; const json = suite('render_json_payload_script'); json('escapes slashes', () => { + // The type here doesn't really matter for the purposes of escaping, + // but we want to avoid upsetting TypeScript. assert.equal( - render_json_payload_script({ type: 'server_data' }, [ + render_json_payload_script({ type: 'validation_errors' }, [ { unsafe: '' ); @@ -17,10 +19,10 @@ json('escapes slashes', () => { json('escapes exclamation marks', () => { assert.equal( - render_json_payload_script({ type: 'server_data' }, [ + render_json_payload_script({ type: 'validation_errors' }, [ { 'alert("xss")': 'unsafe' } ]), - '' ); diff --git a/packages/kit/test/apps/basics/src/routes/load/devalue/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/devalue/+page.svelte new file mode 100644 index 000000000000..d339ec6ff7d2 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/devalue/+page.svelte @@ -0,0 +1 @@ +/load/devalue/regex diff --git a/packages/kit/test/apps/basics/src/routes/load/devalue/regex/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/devalue/regex/+page.server.js new file mode 100644 index 000000000000..a31da9c7128e --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/devalue/regex/+page.server.js @@ -0,0 +1,6 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load() { + return { + regex: /hello/ + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/devalue/regex/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/devalue/regex/+page.svelte new file mode 100644 index 000000000000..04f11dbe10a3 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/devalue/regex/+page.svelte @@ -0,0 +1,6 @@ + + +

{data.regex.test('hello')}

diff --git a/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.server.js b/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.server.js index ceb16a88757e..ee20c2cc975f 100644 --- a/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.server.js +++ b/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.server.js @@ -1,5 +1,11 @@ +class Nope { + toString() { + return 'should not see me'; + } +} + export function load() { return { - regex: /nope/ + nope: new Nope() }; } diff --git a/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.svelte b/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.svelte index a5a2d66e7841..f8a4718b5545 100644 --- a/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/shadowed/serialization/+page.svelte @@ -3,4 +3,4 @@ export let data; -

{data.regex.test('nope')}

+

{data.nope}

diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 1cc532aaefbf..c659740d2f98 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -291,7 +291,7 @@ test.describe('Shadowed pages', () => { expect(await page.textContent('h1')).toBe('500'); expect(await page.textContent('#message')).toBe( - 'This is your custom error page saying: "data.regex returned from \'load\' in src/routes/shadowed/serialization/+page.server.js cannot be serialized as JSON"' + 'This is your custom error page saying: "Cannot stringify arbitrary non-POJOs (data.nope)"' ); }); } @@ -579,7 +579,7 @@ test.describe('Errors', () => { expect(lines[1]).toContain('+page.server.js:4:8'); } - const error = read_errors('/errors/page-endpoint/get-implicit'); + const error = read_errors('/errors/page-endpoint/get-implicit/__data.js'); expect(error).toContain('oops'); }); @@ -892,6 +892,13 @@ test.describe('Load', () => { expect(await page.textContent('h1')).toBe('foo.bar: Custom layout'); expect(await page.textContent('h2')).toBe('pagedata: pagedata'); }); + + test('Serializes non-JSON data', async ({ page, clicknav }) => { + await page.goto('/load/devalue'); + await clicknav('[href="/load/devalue/regex"]'); + + expect(await page.textContent('h1')).toBe('true'); + }); }); test.describe('Method overrides', () => { diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index cfd4e364652f..f3e17419a3ac 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -163,10 +163,14 @@ test.describe('trailingSlash', () => { expect(await r2.text()).toBe('hi'); }); - test('can fetch data from page-endpoint', async ({ request, baseURL }) => { - const r = await request.get('/path-base/page-endpoint/__data.json'); - expect(r.url()).toBe(`${baseURL}/path-base/page-endpoint/__data.json`); - expect(await r.json()).toEqual({ + test('can fetch data from page-endpoint', async ({ request }) => { + const r = await request.get('/path-base/page-endpoint/__data.js'); + const code = await r.text(); + + const window = {}; + new Function('window', code)(window); + + expect(window.__sveltekit_data).toEqual({ type: 'data', nodes: [null, { type: 'data', data: { message: 'hi' }, uses: {} }] }); @@ -184,7 +188,7 @@ test.describe('trailingSlash', () => { /** @type {string[]} */ let requests = []; - page.on('request', (r) => requests.push(r.url())); + page.on('request', (r) => requests.push(new URL(r.url()).pathname)); // also wait for network processing to complete, see // https://playwright.dev/docs/network#network-events @@ -197,7 +201,7 @@ test.describe('trailingSlash', () => { expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0); } - expect(requests.includes(`${baseURL}/path-base/prefetching/prefetched/__data.json`)).toBe(true); + expect(requests.includes(`/path-base/prefetching/prefetched/__data.js`)).toBe(true); requests = []; await app.goto('/path-base/prefetching/prefetched'); diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js index 2a450e9650b4..29a62b3cebe0 100644 --- a/packages/kit/test/prerendering/basics/test/test.js +++ b/packages/kit/test/prerendering/basics/test/test.js @@ -25,14 +25,14 @@ test('renders a server-side redirect', () => { const html = read('redirect-server.html'); assert.equal(html, ''); - const json = read('redirect-server/__data.json'); - assert.equal( - json, - JSON.stringify({ - type: 'redirect', - location: 'https://example.com/redirected' - }) - ); + const code = read('redirect-server/__data.js'); + const window = {}; + new Function('window', code)(window); + + assert.equal(window.__sveltekit_data, { + type: 'redirect', + location: 'https://example.com/redirected' + }); }); test('does not double-encode redirect locations', () => { @@ -77,31 +77,44 @@ test('loads a file with spaces in the filename', () => { assert.ok(content.includes('

answer: 42

'), content); }); -test('generates __data.json file for shadow endpoints', () => { - assert.equal( - read('__data.json'), - JSON.stringify({ - type: 'data', - nodes: [null, { type: 'data', data: { message: 'hello' }, uses: {} }] - }) - ); - assert.equal( - read('shadowed-get/__data.json'), - JSON.stringify({ - type: 'data', - nodes: [null, { type: 'data', data: { answer: 42 }, uses: {} }] - }) - ); +test('generates __data.js file for shadow endpoints', () => { + const window = {}; + + new Function('window', read('__data.js'))(window); + assert.equal(window.__sveltekit_data, { + type: 'data', + nodes: [ + null, + { + type: 'data', + data: { message: 'hello' }, + uses: { dependencies: undefined, params: undefined, parent: undefined, url: undefined } + } + ] + }); + + new Function('window', read('shadowed-get/__data.js'))(window); + assert.equal(window.__sveltekit_data, { + type: 'data', + nodes: [ + null, + { + type: 'data', + data: { answer: 42 }, + uses: { dependencies: undefined, params: undefined, parent: undefined, url: undefined } + } + ] + }); }); test('does not prerender page with shadow endpoint with non-load handler', () => { assert.ok(!fs.existsSync(`${build}/shadowed-post.html`)); - assert.ok(!fs.existsSync(`${build}/shadowed-post/__data.json`)); + assert.ok(!fs.existsSync(`${build}/shadowed-post/__data.js`)); }); test('does not prerender page with prerender = false in +page.server.js', () => { assert.ok(!fs.existsSync(`${build}/page-server-options.html`)); - assert.ok(!fs.existsSync(`${build}/page-server-options/__data.json`)); + assert.ok(!fs.existsSync(`${build}/page-server-options/__data.js`)); }); test('decodes paths when writing files', () => { @@ -175,13 +188,20 @@ test('prerenders binary data', async () => { }); test('fetches data from local endpoint', () => { - assert.equal( - read('origin/__data.json'), - JSON.stringify({ - type: 'data', - nodes: [null, { type: 'data', data: { message: 'hello' }, uses: {} }] - }) - ); + const window = {}; + new Function('window', read('origin/__data.js'))(window); + + assert.equal(window.__sveltekit_data, { + type: 'data', + nodes: [ + null, + { + type: 'data', + data: { message: 'hello' }, + uses: { dependencies: undefined, params: undefined, parent: undefined, url: undefined } + } + ] + }); assert.equal(read('origin/message.json'), JSON.stringify({ message: 'hello' })); }); diff --git a/packages/kit/test/prerendering/trailing-slash/test/test.js b/packages/kit/test/prerendering/trailing-slash/test/test.js index 8a7c6550a3f9..0344b61298ef 100644 --- a/packages/kit/test/prerendering/trailing-slash/test/test.js +++ b/packages/kit/test/prerendering/trailing-slash/test/test.js @@ -11,7 +11,7 @@ const read = (file) => fs.readFileSync(`${build}/${file}`, 'utf-8'); test('prerendered.paths omits trailing slashes for endpoints', () => { const content = read('service-worker.js'); - for (const path of ['/page/', '/page/__data.json', '/standalone-endpoint.json']) { + for (const path of ['/page/', '/page/__data.js', '/standalone-endpoint.json']) { assert.ok(content.includes(`"${path}"`), `Missing ${path}`); } }); diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index fe27a5a5ed41..aaaab6a3a7fe 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -133,7 +133,6 @@ export interface PageNode { export type PayloadScriptAttributes = | { type: 'data'; url: string; body?: string } - | { type: 'server_data' } | { type: 'validation_errors' }; export interface PrerenderDependency { diff --git a/packages/package/CHANGELOG.md b/packages/package/CHANGELOG.md new file mode 100644 index 000000000000..9f6b2eefcf50 --- /dev/null +++ b/packages/package/CHANGELOG.md @@ -0,0 +1,7 @@ +# @sveltejs/package + +## 1.0.0-next.2 + +### Patch Changes + +- [breaking] require Node 16.14 ([#6388](https://github.com/sveltejs/kit/pull/6388)) diff --git a/packages/package/package.json b/packages/package/package.json index 052859c0a892..90cc5dce2d1c 100644 --- a/packages/package/package.json +++ b/packages/package/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/package", - "version": "1.0.0-next.1", + "version": "1.0.0-next.2", "repository": { "type": "git", "url": "https://github.com/sveltejs/kit", @@ -47,6 +47,6 @@ }, "types": "types/index.d.ts", "engines": { - "node": ">=16.9" + "node": ">=16.14" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0967ad4444da..45616a3e91a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,7 +278,7 @@ importers: '@types/sade': ^1.7.4 '@types/set-cookie-parser': ^2.4.2 cookie: ^0.5.0 - devalue: ^2.0.1 + devalue: ^3.1.2 kleur: ^4.1.4 magic-string: ^0.26.2 marked: ^4.0.16 @@ -298,7 +298,7 @@ importers: dependencies: '@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.48.0+vite@3.0.9 cookie: 0.5.0 - devalue: 2.0.1 + devalue: 3.1.2 kleur: 4.1.5 magic-string: 0.26.2 mime: 3.0.0 @@ -1748,8 +1748,8 @@ packages: resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} engines: {node: '>=8'} - /devalue/2.0.1: - resolution: {integrity: sha512-I2TiqT5iWBEyB8GRfTDP0hiLZ0YeDJZ+upDxjBfOC2lebO5LezQMv7QvIUTzdb64jQyAKLf1AHADtGN+jw6v8Q==} + /devalue/3.1.2: + resolution: {integrity: sha512-wUXbMGPAsBx79UF14nsWSsJlC7RcwPlf2w3bGheODWxKx57e9n68ceoijbqCJCEbjyo0S79nqfPwQgyijwLaqw==} dev: false /diff/5.1.0: