From 796811ff2f76a02c773aecf5ae7607bc3adf2936 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 Jul 2021 09:02:11 -0400 Subject: [PATCH] revert #2008 --- .changeset/sharp-hornets-beam.md | 5 - documentation/docs/11-ssr-and-javascript.md | 10 +- documentation/docs/14-configuration.md | 21 +--- documentation/faq/80-integrations.md | 13 ++- .../test/apps/prerendered/package.json | 6 +- .../adapter-static/test/apps/spa/package.json | 6 +- packages/kit/src/core/build/index.js | 7 +- packages/kit/src/core/config/index.spec.js | 29 +++-- packages/kit/src/core/config/options.js | 34 +----- packages/kit/src/core/config/test/index.js | 23 ++-- packages/kit/src/core/dev/index.js | 1 - packages/kit/src/runtime/server/index.js | 2 +- .../kit/src/runtime/server/page/render.js | 2 +- .../kit/src/runtime/server/page/respond.js | 58 +++++----- .../runtime/server/page/respond_with_error.js | 17 ++- packages/kit/src/runtime/server/utils.js | 12 --- packages/kit/types/config.d.ts | 102 ++++++++++++------ packages/kit/types/internal.d.ts | 14 +-- 18 files changed, 159 insertions(+), 203 deletions(-) delete mode 100644 .changeset/sharp-hornets-beam.md diff --git a/.changeset/sharp-hornets-beam.md b/.changeset/sharp-hornets-beam.md deleted file mode 100644 index 61924bb2eb3c..000000000000 --- a/.changeset/sharp-hornets-beam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@sveltejs/kit': patch ---- - -[feat] More powerful and configurable rendering options diff --git a/documentation/docs/11-ssr-and-javascript.md b/documentation/docs/11-ssr-and-javascript.md index b0675a0d844b..fe9b3e0721a7 100644 --- a/documentation/docs/11-ssr-and-javascript.md +++ b/documentation/docs/11-ssr-and-javascript.md @@ -6,17 +6,15 @@ By default, SvelteKit will render any component first on the server and send it You can control each of these on a per-app or per-page basis. Note that each of the per-page settings use [`context="module"`](https://svelte.dev/docs#script_context_module), and only apply to page components, _not_ [layout](#layouts) components. -The app-wide config options take a function, which lets you set configure the option in an advanced manner on a per-request and per-page basis. E.g. you could disable SSR for `/admin` or enable SSR only for search engine crawlers (aka [dynamic rendering](https://developers.google.com/search/docs/advanced/javascript/dynamic-rendering)). - - Each setting can be controlled independently, but `ssr` and `hydrate` cannot both be `false` since that would result in nothing being rendered at all. +If both are specified, per-page settings override per-app settings in case of conflicts. Each setting can be controlled independently, but `ssr` and `hydrate` cannot both be `false` since that would result in nothing being rendered at all. ### ssr -Disabling [server-side rendering](#appendix-ssr) effectively turns your SvelteKit app into a [**single-page app** or SPA](#appendix-csr-and-spa). The default app-wide config option value is a function which reads the page value. Reading the page value causes the page to be loaded on the server. If you'd like to avoid this because you're building a SPA, you will need to set a value such as a boolean for each of the four rendering options which does not access the page-level settings. +Disabling [server-side rendering](#appendix-ssr) effectively turns your SvelteKit app into a [**single-page app** or SPA](#appendix-csr-and-spa). > In most situations this is not recommended: see [the discussion in the appendix](#appendix-ssr). Consider whether it's truly appropriate to disable and don't simply disable SSR because you've hit an issue with it. -SSR can be configured with app-wide [`ssr` config option](#configuration-ssr), or a page-level `ssr` export: +You can disable SSR app-wide with the [`ssr` config option](#configuration-ssr), or a page-level `ssr` export: ```html ``` diff --git a/packages/adapter-static/test/apps/prerendered/package.json b/packages/adapter-static/test/apps/prerendered/package.json index cdb392f943a2..f92b5976c7bc 100644 --- a/packages/adapter-static/test/apps/prerendered/package.json +++ b/packages/adapter-static/test/apps/prerendered/package.json @@ -2,9 +2,9 @@ "name": "~TODO~", "version": "0.0.1", "scripts": { - "dev": "../../../../kit/svelte-kit.js dev", - "build": "../../../../kit/svelte-kit.js build", - "start": "../../../../kit/svelte-kit.js start" + "dev": "svelte-kit dev", + "build": "svelte-kit build", + "start": "svelte-kit start" }, "devDependencies": { "@sveltejs/kit": "next", diff --git a/packages/adapter-static/test/apps/spa/package.json b/packages/adapter-static/test/apps/spa/package.json index 6cd813fde245..e68cb394eb4b 100644 --- a/packages/adapter-static/test/apps/spa/package.json +++ b/packages/adapter-static/test/apps/spa/package.json @@ -2,9 +2,9 @@ "name": "~TODO~", "version": "0.0.1", "scripts": { - "dev": "../../../../kit/svelte-kit.js dev", - "build": "../../../../kit/svelte-kit.js build", - "start": "../../../../kit/svelte-kit.js start" + "dev": "svelte-kit dev", + "build": "svelte-kit build", + "start": "svelte-kit start" }, "devDependencies": { "@sveltejs/adapter-node": "next", diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index 1eb1037a4023..07d7122ee2e4 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -335,17 +335,16 @@ async function build_server( error.stack = options.get_stack(error); }, hooks: get_hooks(user_hooks), - hydrate: ${config.kit.hydrate}, + hydrate: ${s(config.kit.hydrate)}, initiator: undefined, load_component, manifest, paths: settings.paths, - prerender: ${config.kit.prerender && config.kit.prerender.enabled}, read: settings.read, root, service_worker: ${service_worker_entry_file ? "'/service-worker.js'" : 'null'}, - router: ${config.kit.router}, - ssr: ${config.kit.ssr}, + router: ${s(config.kit.router)}, + ssr: ${s(config.kit.ssr)}, target: ${s(config.kit.target)}, template, trailing_slash: ${s(config.kit.trailingSlash)} diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 45029419bf5e..efddafd97217 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -4,7 +4,9 @@ import { deep_merge, validate_config } from './index.js'; test('fills in defaults', () => { const validated = validate_config({}); - delete_complex_opts(validated); + + // @ts-expect-error + delete validated.kit.vite; assert.equal(validated, { compilerOptions: null, @@ -25,6 +27,7 @@ test('fills in defaults', () => { floc: false, host: null, hostHeader: null, + hydrate: true, package: { dir: 'package', exports: { @@ -46,11 +49,14 @@ test('fills in defaults', () => { }, prerender: { crawl: true, + enabled: true, // TODO: remove this for the 1.0 release force: undefined, onError: 'fail', pages: ['*'] }, + router: true, + ssr: true, target: null, trailingSlash: 'never' }, @@ -101,7 +107,8 @@ test('fills in partial blanks', () => { assert.equal(validated.kit.vite(), {}); - delete_complex_opts(validated); + // @ts-expect-error + delete validated.kit.vite; assert.equal(validated, { compilerOptions: null, @@ -122,6 +129,7 @@ test('fills in partial blanks', () => { floc: false, host: null, hostHeader: null, + hydrate: true, package: { dir: 'package', exports: { @@ -143,11 +151,14 @@ test('fills in partial blanks', () => { }, prerender: { crawl: true, + enabled: true, // TODO: remove this for the 1.0 release force: undefined, onError: 'fail', pages: ['*'] }, + router: true, + ssr: true, target: null, trailingSlash: 'never' }, @@ -506,17 +517,3 @@ deepMergeSuite('merge including toString', () => { }); deepMergeSuite.run(); - -/** @param {import('types/config').ValidatedConfig} validated */ -function delete_complex_opts(validated) { - // @ts-expect-error - delete validated.kit.vite; - // @ts-expect-error - delete validated.kit.hydrate; - // @ts-expect-error - delete validated.kit.prerender.enabled; - // @ts-expect-error - delete validated.kit.router; - // @ts-expect-error - delete validated.kit.ssr; -} diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index ecdb00dda06b..68f688a4c0fb 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -78,10 +78,7 @@ const options = { hostHeader: expect_string(null), - hydrate: expect_page_scriptable(async ({ page }) => { - const leaf = await page; - return 'hydrate' in leaf ? !!leaf.hydrate : true; - }), + hydrate: expect_boolean(true), serviceWorker: { type: 'branch', children: { @@ -123,7 +120,7 @@ const options = { type: 'branch', children: { crawl: expect_boolean(true), - enabled: expect_page_scriptable(async ({ page }) => !!(await page).prerender), + enabled: expect_boolean(true), // TODO: remove this for the 1.0 release force: { type: 'leaf', @@ -173,15 +170,9 @@ const options = { } }, - router: expect_page_scriptable(async ({ page }) => { - const leaf = await page; - return 'router' in leaf ? !!leaf.router : true; - }), + router: expect_boolean(true), - ssr: expect_page_scriptable(async ({ page }) => { - const leaf = await page; - return 'ssr' in leaf ? !!leaf.ssr : true; - }), + ssr: expect_boolean(true), target: expect_string(null), @@ -268,23 +259,6 @@ function expect_boolean(boolean) { }; } -/** - * @param {import('types/config').ScriptablePageOpt} value - * @returns {ConfigDefinition} - */ -function expect_page_scriptable(value) { - return { - type: 'leaf', - default: value, - validate: (option, keypath) => { - if (typeof option !== 'boolean' && typeof option !== 'function') { - throw new Error(`${keypath} should be a boolean or function that returns one`); - } - return option; - } - }; -} - /** * @param {string[]} options * @returns {ConfigDefinition} diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index e26a5f6899ee..c378835273ad 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -14,7 +14,9 @@ async function testLoadDefaultConfig(path) { const cwd = join(__dirname, 'fixtures', path); const config = await load_config({ cwd }); - delete_complex_opts(config); + + // @ts-expect-error + delete config.kit.vite; // can't test equality of a function assert.equal(config, { compilerOptions: null, @@ -35,6 +37,7 @@ async function testLoadDefaultConfig(path) { floc: false, host: null, hostHeader: null, + hydrate: true, package: { dir: 'package', exports: { @@ -51,7 +54,9 @@ async function testLoadDefaultConfig(path) { exclude: [] }, paths: { base: '', assets: '/.' }, - prerender: { crawl: true, force: undefined, onError: 'fail', pages: ['*'] }, + prerender: { crawl: true, enabled: true, force: undefined, onError: 'fail', pages: ['*'] }, + router: true, + ssr: true, target: null, trailingSlash: 'never' }, @@ -98,17 +103,3 @@ test('errors on loading config with incorrect default export', async () => { }); test.run(); - -/** @param {import('types/config').ValidatedConfig} validated */ -function delete_complex_opts(validated) { - // @ts-expect-error - delete validated.kit.vite; - // @ts-expect-error - delete validated.kit.hydrate; - // @ts-expect-error - delete validated.kit.prerender.enabled; - // @ts-expect-error - delete validated.kit.router; - // @ts-expect-error - delete validated.kit.ssr; -} diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index b5ee87ed27f2..551f025cffdd 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -396,7 +396,6 @@ async function create_handler(vite, config, dir, cwd, manifest) { }; }, manifest, - prerender: config.kit.prerender.enabled, read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)), root, router: config.kit.router, diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index f785475ce306..24c8cbb47796 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -48,7 +48,7 @@ export async function respond(incoming, options, state = {}) { return await render_response({ options, $session: await options.hooks.getSession(request), - page_config: { ssr: false, router: true, hydrate: true, prerender: true }, + page_config: { ssr: false, router: true, hydrate: true }, status: 200, branch: [] }); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 7ecf620e155d..492d302398fc 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -10,7 +10,7 @@ const s = JSON.stringify; * @param {{ * options: import('types/internal').SSRRenderOptions; * $session: any; - * page_config: import('types/config').PageOpts; + * page_config: { hydrate: boolean, router: boolean, ssr: boolean }; * status: number; * error?: Error, * branch?: Array; diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index 07d775685684..59a2a361c6a2 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -1,7 +1,7 @@ import { render_response } from './render.js'; import { load_node } from './load_node.js'; import { respond_with_error } from './respond_with_error.js'; -import { coalesce_to_error, resolve_option } from '../utils.js'; +import { coalesce_to_error } from '../utils.js'; /** @typedef {import('./types.js').Loaded} Loaded */ @@ -27,17 +27,36 @@ export async function respond({ request, options, state, $session, route }) { params }; - const leaf_promise = options.load_component(route.a[route.a.length - 1]).then((c) => c.module); + let nodes; + + try { + nodes = await Promise.all(route.a.map((id) => options.load_component(id))); + } catch (/** @type {unknown} */ err) { + const error = coalesce_to_error(err); + + options.handle_error(error); + + return await respond_with_error({ + request, + options, + state, + $session, + status: 500, + error + }); + } + + const leaf = nodes[nodes.length - 1].module; const page_config = { - ssr: await resolve_option(options.ssr, { request, page: leaf_promise }), - router: await resolve_option(options.router, { request, page: leaf_promise }), - hydrate: await resolve_option(options.hydrate, { request, page: leaf_promise }), - prerender: await resolve_option(options.prerender, { request, page: leaf_promise }) + ssr: 'ssr' in leaf ? !!leaf.ssr : options.ssr, + router: 'router' in leaf ? !!leaf.router : options.router, + hydrate: 'hydrate' in leaf ? !!leaf.hydrate : options.hydrate }; - // if prerendering some pages, but not this one - if (state.prerender && !state.prerender.all && !page_config.prerender) { + if (!leaf.prerender && state.prerender && !state.prerender.all) { + // if the page has `export const prerender = true`, continue, + // otherwise bail out at this point return { status: 204, headers: {}, @@ -55,29 +74,6 @@ export async function respond({ request, options, state, $session, route }) { let error; ssr: if (page_config.ssr) { - /** - * The layout components and page components for a page - * @type {import('types/internal').SSRNode[]} - */ - let nodes; - - try { - nodes = await Promise.all(route.a.map((id) => options.load_component(id))); - } catch (/** @type {unknown} */ err) { - const error = coalesce_to_error(err); - - options.handle_error(error); - - return await respond_with_error({ - request, - options, - state, - $session, - status: 500, - error - }); - } - let context = {}; branch = []; 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 990c6931d0a8..1bdaa0e20117 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -1,6 +1,6 @@ import { render_response } from './render.js'; import { load_node } from './load_node.js'; -import { coalesce_to_error, resolve_option } from '../utils.js'; +import { coalesce_to_error } from '../utils.js'; /** * @param {{ @@ -55,20 +55,15 @@ export async function respond_with_error({ request, options, state, $session, st })) ]; - const leaf_promise = async () => branch[branch.length - 1].node.module; - - const page_config = { - ssr: await resolve_option(options.ssr, { request, page: leaf_promise }), - router: await resolve_option(options.router, { request, page: leaf_promise }), - hydrate: await resolve_option(options.hydrate, { request, page: leaf_promise }), - prerender: await resolve_option(options.prerender, { request, page: leaf_promise }) - }; - try { return await render_response({ options, $session, - page_config, + page_config: { + hydrate: options.hydrate, + router: options.router, + ssr: options.ssr + }, status, error, branch, diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index b6208023390e..74fde8c68a70 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -17,15 +17,3 @@ export function lowercase_keys(obj) { export function coalesce_to_error(err) { return err instanceof Error ? err : new Error(JSON.stringify(err)); } - -/** - * @param {any} opt - * @param {object} ctx - * @returns - */ -export async function resolve_option(opt, ctx) { - if (typeof opt === 'function') { - return await opt(ctx); - } - return opt; -} diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 5e4e081b7f30..67838f04f07c 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -1,6 +1,4 @@ import { UserConfig as ViteConfig } from 'vite'; -import { RecursiveRequired } from './helper'; -import { ServerRequest } from './hooks'; import { Logger, TrailingSlash } from './internal'; export interface AdapterUtils { @@ -20,31 +18,6 @@ export interface Adapter { adapt: (context: { utils: AdapterUtils; config: ValidatedConfig }) => Promise; } -export interface PageOpts { - ssr: boolean; - router: boolean; - hydrate: boolean; - prerender: boolean; -} - -export interface PageOptsContext { - request: ServerRequest; - page: Promise; -} - -export type ScriptablePageOpt = T | (({ request, page }: PageOptsContext) => Promise); - -export interface PrerenderErrorHandler { - (details: { - status: number; - path: string; - referrer: string | null; - referenceType: 'linked' | 'fetched'; - }): void; -} - -export type PrerenderOnErrorValue = 'fail' | 'continue' | PrerenderErrorHandler; - export interface Config { compilerOptions?: any; extensions?: string[]; @@ -63,7 +36,7 @@ export interface Config { floc?: boolean; host?: string; hostHeader?: string; - hydrate?: ScriptablePageOpt; + hydrate?: boolean; package?: { dir?: string; emitTypes?: boolean; @@ -82,15 +55,15 @@ export interface Config { }; prerender?: { crawl?: boolean; - enabled?: ScriptablePageOpt; - onError?: PrerenderOnErrorValue; + enabled?: boolean; + force?: boolean; pages?: string[]; }; - router?: ScriptablePageOpt; + router?: boolean; serviceWorker?: { exclude?: string[]; }; - ssr?: ScriptablePageOpt; + ssr?: boolean; target?: string; trailingSlash?: TrailingSlash; vite?: ViteConfig | (() => ViteConfig); @@ -98,6 +71,65 @@ export interface Config { preprocess?: any; } -export type ValidatedConfig = RecursiveRequired & { - kit: { files: { setup: string } }; // only for validated -}; +export type PrerenderErrorHandler = (errorDetails: { + status: number; + path: string; + referrer: string | null; + referenceType: 'linked' | 'fetched'; +}) => void | never; + +export type PrerenderOnErrorValue = 'fail' | 'continue' | PrerenderErrorHandler; + +export interface ValidatedConfig { + compilerOptions: any; + extensions: string[]; + kit: { + adapter: Adapter; + amp: boolean; + appDir: string; + files: { + assets: string; + hooks: string; + lib: string; + routes: string; + serviceWorker: string; + setup: string; + template: string; + }; + floc: boolean; + host: string; + hostHeader: string; + hydrate: boolean; + package: { + dir: string; + emitTypes: boolean; + exports: { + include: string[]; + exclude: string[]; + }; + files: { + include: string[]; + exclude: string[]; + }; + }; + paths: { + assets: string; + base: string; + }; + prerender: { + crawl: boolean; + enabled: boolean; + onError: PrerenderOnErrorValue; + pages: string[]; + }; + router: boolean; + serviceWorker: { + exclude: string[]; + }; + ssr: boolean; + target: string; + trailingSlash: TrailingSlash; + vite: () => ViteConfig; + }; + preprocess: any; +} diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index e0bf948df10a..b76913cf2369 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,4 +1,3 @@ -import { PageOpts, ScriptablePageOpt } from './config'; import { RequestHandler } from './endpoint'; import { Headers, Location, ParameterizedBody } from './helper'; import { GetSession, Handle, ServerResponse, ServerFetch, StrictBody } from './hooks'; @@ -47,7 +46,11 @@ export interface App { ) => Promise; } -export interface SSRComponent extends PageOpts { +export interface SSRComponent { + ssr?: boolean; + router?: boolean; + hydrate?: boolean; + prerender?: boolean; preload?: any; // TODO remove for 1.0 load: Load; default: { @@ -139,19 +142,18 @@ export interface SSRRenderOptions { get_stack: (error: Error) => string | undefined; handle_error: (error: Error) => void; hooks: Hooks; - hydrate: ScriptablePageOpt; + hydrate: boolean; load_component: (id: PageId) => Promise; manifest: SSRManifest; paths: { base: string; assets: string; }; - prerender: ScriptablePageOpt; read: (file: string) => Buffer; root: SSRComponent['default']; - router: ScriptablePageOpt; + router: boolean; service_worker?: string; - ssr: ScriptablePageOpt; + ssr: boolean; target: string; template: ({ head, body }: { head: string; body: string }) => string; trailing_slash: TrailingSlash;