From 37ad3e2070c1b3a3b0060a71995940a471bec6ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 08:49:26 -0500 Subject: [PATCH 01/36] this should be an internal type --- packages/kit/src/runtime/server/index.js | 2 +- packages/kit/types/ambient-modules.d.ts | 14 -------------- packages/kit/types/internal.d.ts | 10 +++++++++- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 8b152b6f496c..e71d11ac02f0 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -8,7 +8,7 @@ import { hash } from '../hash.js'; import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; -/** @type {import('@sveltejs/kit/ssr').Respond} */ +/** @type {import('types/internal').Respond} */ export async function respond(incoming, options, state = {}) { if (incoming.url.pathname !== '/' && options.trailing_slash !== 'ignore') { const has_trailing_slash = incoming.url.pathname.endsWith('/'); diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index c4c9671554af..80111143a64d 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -183,20 +183,6 @@ declare module '@sveltejs/kit/node' { export const getRawBody: GetRawBody; } -declare module '@sveltejs/kit/ssr' { - import { IncomingRequest, Response } from '@sveltejs/kit'; - // TODO import from public types, right now its heavily coupled with internal - type Options = import('@sveltejs/kit/types/internal').SSRRenderOptions; - type State = import('@sveltejs/kit/types/internal').SSRRenderState; - - export interface Respond { - (incoming: IncomingRequest & { url: URL }, options: Options, state?: State): Promise< - Response | undefined - >; - } - export const respond: Respond; -} - declare module '@sveltejs/kit/install-fetch' { import fetch, { Headers, Request, Response } from 'node-fetch'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 2d34d85ad43d..94c011e49c18 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,6 +1,6 @@ import { OutputAsset, OutputChunk } from 'rollup'; import { RequestHandler } from './endpoint'; -import { InternalApp, SSRManifest } from './app'; +import { IncomingRequest, InternalApp, SSRManifest } from './app'; import { ExternalFetch, GetSession, @@ -235,3 +235,11 @@ export interface MethodOverride { parameter: string; allowed: string[]; } + +export interface Respond { + ( + incoming: IncomingRequest & { url: URL }, + options: SSRRenderOptions, + state?: SSRRenderState + ): Promise; +} From df834ca38a282fe8dff62203c98112311527fe42 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 08:58:58 -0500 Subject: [PATCH 02/36] change app.render return type to Response --- packages/kit/src/core/adapt/prerender/prerender.js | 11 +++++------ packages/kit/src/core/build/build_server.js | 3 ++- packages/kit/src/core/preview/index.js | 4 ++-- packages/kit/types/app.d.ts | 5 ++--- packages/kit/types/internal.d.ts | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 2aa5ef535c71..857a8b5402d0 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -150,8 +150,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a if (rendered) { const response_type = Math.floor(rendered.status / 100); - const headers = rendered.headers; - const type = headers && headers['content-type']; + const type = rendered.headers.get('content-type'); const is_html = response_type === REDIRECT || type === 'text/html'; const parts = decoded_path.split('/'); @@ -162,7 +161,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a const file = `${out}${parts.join('/')}`; if (response_type === REDIRECT) { - const location = get_single_valued_header(headers, 'location'); + const location = rendered.headers.get('location'); if (location) { mkdirp(dirname(file)); @@ -185,7 +184,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a mkdirp(dirname(file)); log.info(`${rendered.status} ${decoded_path}`); - writeFileSync(file, rendered.body || ''); + writeFileSync(file, await rendered.text()); paths.push(normalize(decoded_path)); } else if (response_type !== OK) { error({ status: rendered.status, path, referrer, referenceType: 'linked' }); @@ -222,7 +221,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a }); if (is_html && config.kit.prerender.crawl) { - for (const href of crawl(/** @type {string} */ (rendered.body))) { + for (const href of crawl(await rendered.text())) { if (href.startsWith('data:')) continue; const resolved = resolve(path, href); @@ -283,7 +282,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a const file = join(out, fallback); mkdirp(dirname(file)); - writeFileSync(file, rendered.body || ''); + writeFileSync(file, await rendered.text()); } return { diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 86a1f3c35dd5..9743b0f76576 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -105,7 +105,8 @@ export class App { : 'default_protocol' }; - return respond({ ...request, url: new URL(request.url, protocol + '://' + host) }, this.options, { prerender }); + const { status, headers, body } = respond({ ...request, url: new URL(request.url, protocol + '://' + host) }, this.options, { prerender }); + return new Response(body, { status, headers }); } } `; diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index b833d4eee0a6..527a1de7c64f 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -100,8 +100,8 @@ export async function preview({ })); if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) res.write(rendered.body); + res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); + res.write(await rendered.arrayBuffer()); res.end(); } else { res.statusCode = 404; diff --git a/packages/kit/types/app.d.ts b/packages/kit/types/app.d.ts index c62a12e8f9c2..36a9a7e6cdbf 100644 --- a/packages/kit/types/app.d.ts +++ b/packages/kit/types/app.d.ts @@ -1,10 +1,9 @@ import { ReadOnlyFormData, RequestHeaders } from './helper'; -import { ServerResponse } from './hooks'; import { PrerenderOptions, SSRNodeLoader, SSRRoute } from './internal'; export class App { constructor(manifest: SSRManifest); - render(incoming: IncomingRequest): Promise; + render(incoming: IncomingRequest): Promise; } export class InternalApp extends App { @@ -13,7 +12,7 @@ export class InternalApp extends App { options?: { prerender: PrerenderOptions; } - ): Promise; + ): Promise; } export type RawBody = null | Uint8Array; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 94c011e49c18..3568e6085143 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -241,5 +241,5 @@ export interface Respond { incoming: IncomingRequest & { url: URL }, options: SSRRenderOptions, state?: SSRRenderState - ): Promise; + ): Promise; } From 2f4ca679f4de44dc639cb2ad229f877b543af3d0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 09:14:57 -0500 Subject: [PATCH 03/36] update adapters --- .../adapter-cloudflare-workers/files/entry.js | 30 +------------------ packages/adapter-cloudflare/files/worker.js | 29 +----------------- packages/adapter-netlify/src/handler.js | 25 +++++++--------- packages/adapter-node/src/handler.js | 10 +++---- packages/adapter-vercel/files/entry.js | 16 ++++------ 5 files changed, 23 insertions(+), 87 deletions(-) diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 4fbe5789eadd..0cde9beb33a5 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -50,46 +50,18 @@ async function handle(event) { // dynamically-generated pages try { - const rendered = await app.render({ + return await app.render({ url: request.url, rawBody: await read(request), headers: Object.fromEntries(request.headers), method: request.method }); - - if (rendered) { - return new Response(rendered.body, { - status: rendered.status, - headers: make_headers(rendered.headers) - }); - } } catch (e) { return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 }); } - - return new Response('Not Found', { - status: 404, - statusText: 'Not Found' - }); } /** @param {Request} request */ async function read(request) { return new Uint8Array(await request.arrayBuffer()); } - -/** @param {Record} headers */ -function make_headers(headers) { - const result = new Headers(); - for (const header in headers) { - const value = headers[header]; - if (typeof value === 'string') { - result.set(header, value); - continue; - } - for (const sub of value) { - result.append(header, sub); - } - } - return result; -} diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index c10c2b217328..c489b6c7d8ca 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -45,41 +45,14 @@ export default { // dynamically-generated pages try { - const rendered = await app.render({ + return await app.render({ url, rawBody: new Uint8Array(await req.arrayBuffer()), headers: Object.fromEntries(req.headers), method: req.method }); - - if (rendered) { - return new Response(rendered.body, { - status: rendered.status, - headers: make_headers(rendered.headers) - }); - } } catch (e) { return new Response('Error rendering route: ' + (e.message || e.toString()), { status: 500 }); } - - return new Response({ - status: 404, - statusText: 'Not Found' - }); } }; - -function make_headers(headers) { - const result = new Headers(); - for (const header in headers) { - const value = headers[header]; - if (typeof value === 'string') { - result.set(header, value); - continue; - } - for (const sub of value) { - result.append(header, sub); - } - } - return result; -} diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js index 1200339eaf71..1b843b579657 100644 --- a/packages/adapter-netlify/src/handler.js +++ b/packages/adapter-netlify/src/handler.js @@ -25,18 +25,12 @@ export function init(manifest) { rawBody }); - if (!rendered) { - return { - statusCode: 404, - body: 'Not found' - }; - } - const partial_response = { statusCode: rendered.status, ...split_headers(rendered.headers) }; + // TODO this is probably wrong now? if (rendered.body instanceof Uint8Array) { // Function responses should be strings (or undefined), and responses with binary // content should be base64 encoded and set isBase64Encoded to true. @@ -50,14 +44,14 @@ export function init(manifest) { return { ...partial_response, - body: rendered.body + body: await rendered.text() }; }; } /** * Splits headers into two categories: single value and multi value - * @param {Record} headers + * @param {Headers} headers * @returns {{ * headers: Record, * multiValueHeaders: Record @@ -70,11 +64,14 @@ function split_headers(headers) { /** @type {Record} */ const m = {}; - for (const key in headers) { - const value = headers[key]; - const target = Array.isArray(value) ? m : h; - target[key] = value; - } + headers.forEach((value, key) => { + if (key === 'set-cookie') { + m[key] = value.split(', '); + } else { + h[key] = value; + } + }); + return { headers: h, multiValueHeaders: m diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 3344430adc4c..f1d87f06ea82 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -11,7 +11,7 @@ import { manifest } from 'MANIFEST'; __fetch_polyfill(); -const app = new App(manifest); +const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -47,15 +47,13 @@ const ssr = async (req, res) => { const rendered = await app.render({ url: req.url, method: req.method, - headers: req.headers, // TODO: what about repeated headers, i.e. string[] + headers: req.headers, rawBody: body }); if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) { - res.write(rendered.body); - } + res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); + res.write(await rendered.arrayBuffer()); res.end(); } else { res.statusCode = 404; diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index a477ecc950bf..4750d1fe3245 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -5,13 +5,13 @@ import { manifest } from 'MANIFEST'; __fetch_polyfill(); -const app = new App(manifest); +const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); export default async (req, res) => { - let body; + let rawBody; try { - body = await getRawBody(req); + rawBody = await getRawBody(req); } catch (err) { res.statusCode = err.status || 400; return res.end(err.reason || 'Invalid request body'); @@ -21,13 +21,9 @@ export default async (req, res) => { url: req.url, method: req.method, headers: req.headers, - rawBody: body + rawBody }); - if (rendered) { - const { status, headers, body } = rendered; - return res.writeHead(status, headers).end(body); - } - - return res.writeHead(404).end(); + res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); + res.end(await rendered.arrayBuffer()); }; From b7eb5b9ddb3e62d83ba3a83ee111192b816c4f47 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 11:46:59 -0500 Subject: [PATCH 04/36] lint --- packages/kit/src/core/adapt/prerender/prerender.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 124136952cbe..39781c071e23 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -4,7 +4,6 @@ import { pathToFileURL, URL } from 'url'; import { mkdirp } from '../../../utils/filesystem.js'; import { __fetch_polyfill } from '../../../install-fetch.js'; import { SVELTE_KIT } from '../../constants.js'; -import { get_single_valued_header } from '../../../utils/http.js'; import { is_root_relative, resolve } from '../../../utils/url.js'; import { queue } from './queue.js'; import { crawl } from './crawl.js'; From 46b36dd9ddee7bc824c791ac7123e29c87f4a663 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 12:06:13 -0500 Subject: [PATCH 05/36] fix tests --- packages/adapter-node/src/handler.js | 2 +- .../fixtures/prerender/.svelte-kit/output/server/app.js | 7 +++---- packages/kit/src/core/build/build_server.js | 4 ++-- packages/kit/src/core/preview/index.js | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index f1d87f06ea82..697ce299ba2f 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -53,7 +53,7 @@ const ssr = async (req, res) => { if (rendered) { res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); - res.write(await rendered.arrayBuffer()); + if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); res.end(); } else { res.statusCode = 404; diff --git a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js index a59d5d30f0a5..554862072253 100644 --- a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js +++ b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js @@ -1,12 +1,11 @@ export class App { render() { - return { + return new Response('', { status: 200, headers: { 'content-type': 'text/html' - }, - body: '' - }; + } + }); } } diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 9743b0f76576..eaa97d7b8a96 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -84,7 +84,7 @@ export class App { }; } - render(request, { + async render(request, { prerender } = {}) { // TODO remove this for 1.0 @@ -105,7 +105,7 @@ export class App { : 'default_protocol' }; - const { status, headers, body } = respond({ ...request, url: new URL(request.url, protocol + '://' + host) }, this.options, { prerender }); + const { status, headers, body } = await respond({ ...request, url: new URL(request.url, protocol + '://' + host) }, this.options, { prerender }); return new Response(body, { status, headers }); } } diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 527a1de7c64f..8d7cbbb27516 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -101,7 +101,7 @@ export async function preview({ if (rendered) { res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); - res.write(await rendered.arrayBuffer()); + if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); res.end(); } else { res.statusCode = 404; From b3b00d8e1ae93e8acefbbc1564037b0127cc38d0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 16:59:13 -0500 Subject: [PATCH 06/36] app.render takes a Request as input --- .../kit/src/core/adapt/prerender/prerender.js | 16 ++----- packages/kit/src/core/build/build_server.js | 20 ++------ packages/kit/src/core/dev/plugin.js | 7 ++- packages/kit/src/core/preview/index.js | 43 +++++++++++++---- packages/kit/src/runtime/server/index.js | 46 ++++++++++--------- .../kit/src/runtime/server/page/load_node.js | 7 +-- packages/kit/types/app.d.ts | 11 +---- packages/kit/types/index.d.ts | 2 +- packages/kit/types/internal.d.ts | 10 ++-- 9 files changed, 75 insertions(+), 87 deletions(-) diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 39781c071e23..3e19883a6ee8 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -133,12 +133,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a const dependencies = new Map(); const rendered = await app.render( - { - url: `${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}${path}`, - method: 'GET', - headers: {}, - rawBody: null - }, + new Request(`${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}${path}`), { prerender: { all, @@ -264,12 +259,9 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a if (fallback) { const rendered = await app.render( - { - url: `${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}/[fallback]`, - method: 'GET', - headers: {}, - rawBody: null - }, + new Request( + `${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}/[fallback]` + ), { prerender: { fallback, diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index eaa97d7b8a96..76a4d1391f09 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -87,25 +87,11 @@ export class App { async render(request, { prerender } = {}) { - // TODO remove this for 1.0 - if (Object.keys(request).sort().join() !== 'headers,method,rawBody,url') { - throw new Error('Adapters should call app.render({ url, method, headers, rawBody })'); + if (!(request instanceof Request)) { + throw new Error('The first argument to app.render must be a Request object'); } - const host = ${ - config.kit.host - ? s(config.kit.host) - : `request.headers[${s(config.kit.headers.host || 'host')}]` - }; - const protocol = ${ - config.kit.protocol - ? s(config.kit.protocol) - : config.kit.headers.protocol - ? `request.headers[${s(config.kit.headers.protocol)}] || default_protocol` - : 'default_protocol' - }; - - const { status, headers, body } = await respond({ ...request, url: new URL(request.url, protocol + '://' + host) }, this.options, { prerender }); + const { status, headers, body } = await respond(request, this.options, { prerender }); return new Response(body, { status, headers }); } } diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index 765ae7abc525..e8154757fa27 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -217,12 +217,11 @@ export async function create_plugin(config, cwd) { } const rendered = await respond( - { - url, + new Request(url.href, { headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), method: req.method, - rawBody: body - }, + body + }), { amp: config.kit.amp, dev: true, diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 8d7cbbb27516..d2b50c52bbb7 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -90,16 +90,22 @@ export async function preview({ return res.end(err.reason || 'Invalid request body'); } - const rendered = - initial_url.startsWith(config.kit.paths.base) && - (await app.render({ - url: initial_url, - method: req.method, - headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), - rawBody: body - })); - - if (rendered) { + if (initial_url.startsWith(config.kit.paths.base)) { + const protocol = + config.kit.protocol || + (config.kit.headers.protocol && req.headers[config.kit.headers.protocol]) || + (use_https ? 'https' : 'http'); + + const host = config.kit.host || req.headers[config.kit.headers.host || 'host']; + + const rendered = await app.render( + new Request(`${protocol}://${host}${initial_url}`, { + method: req.method, + headers: get_headers(req.headers), + body + }) + ); + res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); res.end(); @@ -163,3 +169,20 @@ async function get_server(use_https, user_config, handler) { : http.createServer(handler) ); } + +/** @param {import('http').IncomingHttpHeaders} object */ +function get_headers(object) { + const headers = new Headers(); + + for (const key in object) { + if (key === 'set-cookie') { + object[key]?.forEach((value) => { + headers.append(key, value); + }); + } else { + headers.set(key, /** @type {string} */ (object[key])); + } + } + + return headers; +} diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 6f607999648c..94fcb4e2bcbe 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -3,53 +3,55 @@ 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 { parse_body } from './parse_body/index.js'; -import { lowercase_keys } from './utils.js'; import { hash } from '../hash.js'; import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; /** @type {import('types/internal').Respond} */ -export async function respond(incoming, options, state = {}) { - if (incoming.url.pathname !== '/' && options.trailing_slash !== 'ignore') { - const has_trailing_slash = incoming.url.pathname.endsWith('/'); +export async function respond(request, options, state = {}) { + const url = new URL(request.url); + + if (url.pathname !== '/' && options.trailing_slash !== 'ignore') { + const has_trailing_slash = url.pathname.endsWith('/'); if ( (has_trailing_slash && options.trailing_slash === 'never') || (!has_trailing_slash && options.trailing_slash === 'always' && - !(incoming.url.pathname.split('/').pop() || '').includes('.')) + !(url.pathname.split('/').pop() || '').includes('.')) ) { - incoming.url.pathname = has_trailing_slash - ? incoming.url.pathname.slice(0, -1) - : incoming.url.pathname + '/'; + url.pathname = has_trailing_slash ? url.pathname.slice(0, -1) : url.pathname + '/'; - if (incoming.url.search === '?') incoming.url.search = ''; + if (url.search === '?') url.search = ''; return { status: 301, headers: { - location: incoming.url.pathname + incoming.url.search + location: url.pathname + url.search } }; } } - const headers = lowercase_keys(incoming.headers); - const request = { - ...incoming, + const headers = Object.fromEntries(request.headers); + const rawBody = new Uint8Array(await request.arrayBuffer()); + const request_details = { + url, + method: request.method, headers, - body: parse_body(incoming.rawBody, headers), + rawBody, + body: parse_body(rawBody, headers), params: {}, locals: {} }; const { parameter, allowed } = options.method_override; - const method_override = incoming.url.searchParams.get(parameter)?.toUpperCase(); + const method_override = url.searchParams.get(parameter)?.toUpperCase(); if (method_override) { - if (request.method.toUpperCase() === 'POST') { + if (request_details.method.toUpperCase() === 'POST') { if (allowed.includes(method_override)) { - request.method = method_override; + request_details.method = method_override; } else { const verb = allowed.length === 0 ? 'enabled' : 'allowed'; const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`; @@ -71,7 +73,7 @@ export async function respond(incoming, options, state = {}) { * @param {string} replacement */ const print_error = (property, replacement) => { - Object.defineProperty(request, property, { + Object.defineProperty(request_details, property, { get: () => { throw new Error(`request.${property} has been replaced by request.url.${replacement}`); } @@ -86,7 +88,7 @@ export async function respond(incoming, options, state = {}) { try { return await options.hooks.handle({ - request, + request: request_details, resolve: async (request, opts) => { if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr); @@ -185,12 +187,12 @@ export async function respond(incoming, options, state = {}) { } catch (/** @type {unknown} */ e) { const error = coalesce_to_error(e); - options.handle_error(error, request); + options.handle_error(error, request_details); try { - const $session = await options.hooks.getSession(request); + const $session = await options.hooks.getSession(request_details); return await respond_with_error({ - request, + request: request_details, options, state, $session, diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index 94c80eb0e08a..dfbe99d9186f 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -148,12 +148,7 @@ export async function load_node({ } const rendered = await respond( - { - url: new URL(requested, request.url), - method: opts.method || 'GET', - headers: Object.fromEntries(opts.headers), - rawBody: opts.body == null ? null : new TextEncoder().encode(opts.body) - }, + new Request(new URL(requested, request.url).href, opts), options, { fetched: requested, diff --git a/packages/kit/types/app.d.ts b/packages/kit/types/app.d.ts index 36a9a7e6cdbf..b59e4a214995 100644 --- a/packages/kit/types/app.d.ts +++ b/packages/kit/types/app.d.ts @@ -3,12 +3,12 @@ import { PrerenderOptions, SSRNodeLoader, SSRRoute } from './internal'; export class App { constructor(manifest: SSRManifest); - render(incoming: IncomingRequest): Promise; + render(request: Request): Promise; } export class InternalApp extends App { render( - incoming: IncomingRequest, + request: Request, options?: { prerender: PrerenderOptions; } @@ -20,13 +20,6 @@ export type ParameterizedBody = Body extends FormData ? ReadOnlyFormData : (string | RawBody | ReadOnlyFormData) & Body; -export interface IncomingRequest { - url: string | URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; -} - export interface SSRManifest { appDir: string; assets: Set; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index ac820b00b414..6f4efdd12a57 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3,7 +3,7 @@ import './ambient-modules'; -export { App, IncomingRequest, RawBody, SSRManifest } from './app'; +export { App, RawBody, SSRManifest } from './app'; export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; export { EndpointOutput, RequestHandler } from './endpoint'; export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 3568e6085143..cf965f3d16ec 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,6 +1,6 @@ import { OutputAsset, OutputChunk } from 'rollup'; import { RequestHandler } from './endpoint'; -import { IncomingRequest, InternalApp, SSRManifest } from './app'; +import { InternalApp, SSRManifest } from './app'; import { ExternalFetch, GetSession, @@ -237,9 +237,7 @@ export interface MethodOverride { } export interface Respond { - ( - incoming: IncomingRequest & { url: URL }, - options: SSRRenderOptions, - state?: SSRRenderState - ): Promise; + (request: Request, options: SSRRenderOptions, state?: SSRRenderState): Promise< + ServerResponse | undefined + >; } From d6fcac3f0558be9ac200c1c17a63d8846864295b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Jan 2022 17:02:11 -0500 Subject: [PATCH 07/36] only read body once --- packages/kit/src/core/adapt/prerender/prerender.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 3e19883a6ee8..3ee638d75a91 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -174,11 +174,13 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a return; } + const text = await rendered.text(); + if (rendered.status === 200) { mkdirp(dirname(file)); log.info(`${rendered.status} ${decoded_path}`); - writeFileSync(file, await rendered.text()); + writeFileSync(file, text); paths.push(normalize(decoded_path)); } else if (response_type !== OK) { error({ status: rendered.status, path, referrer, referenceType: 'linked' }); @@ -215,7 +217,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a }); if (is_html && config.kit.prerender.crawl) { - for (const href of crawl(await rendered.text())) { + for (const href of crawl(text)) { if (href.startsWith('data:') || href.startsWith('#')) continue; const resolved = resolve(path, href); From 44c9ae2050a61ba3e3a3f4f0698792293ae0e231 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 09:51:31 -0500 Subject: [PATCH 08/36] update adapters, remove host/protocol options --- .../adapter-cloudflare-workers/files/entry.js | 12 +----- packages/adapter-cloudflare/files/worker.js | 9 +---- packages/adapter-netlify/src/handler.js | 13 ++++--- packages/adapter-node/index.d.ts | 5 +++ packages/adapter-node/index.js | 13 ++++++- packages/adapter-node/src/handler.js | 39 +++++++++++-------- packages/adapter-node/src/types.d.ts | 4 ++ packages/adapter-vercel/files/entry.js | 16 +++----- .../kit/src/core/adapt/prerender/prerender.js | 30 ++++++-------- packages/kit/src/core/config/index.spec.js | 12 ------ packages/kit/src/core/config/options.js | 9 ----- packages/kit/src/core/config/test/index.js | 6 --- packages/kit/src/core/preview/index.js | 8 +--- packages/kit/src/node.js | 20 ++++++++++ .../kit/test/apps/options/svelte.config.js | 4 +- packages/kit/types/ambient-modules.d.ts | 5 +++ packages/kit/types/config.d.ts | 6 --- 17 files changed, 97 insertions(+), 114 deletions(-) diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 0cde9beb33a5..f38caa761f13 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -50,18 +50,8 @@ async function handle(event) { // dynamically-generated pages try { - return await app.render({ - url: request.url, - rawBody: await read(request), - headers: Object.fromEntries(request.headers), - method: request.method - }); + return await app.render(request); } catch (e) { return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 }); } } - -/** @param {Request} request */ -async function read(request) { - return new Uint8Array(await request.arrayBuffer()); -} diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index c489b6c7d8ca..6c9ab94c17e7 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -1,7 +1,7 @@ import { App } from '../output/server/app.js'; import { manifest, prerendered } from './manifest.js'; -const app = new App(manifest); +const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); const prefix = `/${manifest.appDir}/`; @@ -45,12 +45,7 @@ export default { // dynamically-generated pages try { - return await app.render({ - url, - rawBody: new Uint8Array(await req.arrayBuffer()), - headers: Object.fromEntries(req.headers), - method: req.method - }); + return await app.render(req); } catch (e) { return new Response('Error rendering route: ' + (e.message || e.toString()), { status: 500 }); } diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js index 1b843b579657..91e508ebcf47 100644 --- a/packages/adapter-netlify/src/handler.js +++ b/packages/adapter-netlify/src/handler.js @@ -18,12 +18,13 @@ export function init(manifest) { const encoding = isBase64Encoded ? 'base64' : 'utf-8'; const rawBody = typeof body === 'string' ? Buffer.from(body, encoding) : body; - const rendered = await app.render({ - url: rawUrl, - method: httpMethod, - headers, - rawBody - }); + const rendered = await app.render( + new Request(rawUrl, { + method: httpMethod, + headers: new Headers(headers), + body: rawBody + }) + ); const partial_response = { statusCode: rendered.status, diff --git a/packages/adapter-node/index.d.ts b/packages/adapter-node/index.d.ts index f8799d634759..7564c68954d4 100644 --- a/packages/adapter-node/index.d.ts +++ b/packages/adapter-node/index.d.ts @@ -7,6 +7,11 @@ interface AdapterOptions { path?: string; host?: string; port?: string; + base?: string; + headers?: { + protocol?: string; + host?: string; + }; }; } diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 9d7f400090b3..821272abb5a5 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -13,7 +13,13 @@ const files = fileURLToPath(new URL('./files', import.meta.url)); export default function ({ out = 'build', precompress, - env: { path: path_env = 'SOCKET_PATH', host: host_env = 'HOST', port: port_env = 'PORT' } = {} + env: { + path: path_env = 'SOCKET_PATH', + host: host_env = 'HOST', + port: port_env = 'PORT', + base: base_env, + headers: { protocol: protocol_header_env, host: host_header_env = 'host' } + } = {} } = {}) { return { name: '@sveltejs/adapter-node', @@ -44,7 +50,10 @@ export default function ({ MANIFEST: './manifest.js', PATH_ENV: JSON.stringify(path_env), HOST_ENV: JSON.stringify(host_env), - PORT_ENV: JSON.stringify(port_env) + PORT_ENV: JSON.stringify(port_env), + BASE: base_env ? `process.env[${JSON.stringify(base_env)}]` : 'undefined', + PROTOCOL_HEADER: JSON.stringify(protocol_header_env), + HOST_HEADER: JSON.stringify(host_header_env) } }); diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 697ce299ba2f..1efc4924feb6 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -2,16 +2,21 @@ import fs from 'fs'; import path from 'path'; import sirv from 'sirv'; import { fileURLToPath } from 'url'; -import { getRawBody } from '@sveltejs/kit/node'; +import { getRequest } from '@sveltejs/kit/node'; import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; // @ts-ignore import { App } from 'APP'; import { manifest } from 'MANIFEST'; +/* global BASE_ENV, PROTOCOL_HEADER, HOST_HEADER */ + __fetch_polyfill(); const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); +const base = BASE; +const protocol_header = PROTOCOL_HEADER && process.env[PROTOCOL_HEADER]; +const host_header = (HOST_HEADER && process.env[HOST_HEADER]) || 'host'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -35,30 +40,20 @@ function serve(path, max_age, immutable = false) { /** @type {import('polka').Middleware} */ const ssr = async (req, res) => { - let body; + let request; try { - body = await getRawBody(req); + request = await getRequest(base || get_base(req.headers), req); } catch (err) { res.statusCode = err.status || 400; return res.end(err.reason || 'Invalid request body'); } - const rendered = await app.render({ - url: req.url, - method: req.method, - headers: req.headers, - rawBody: body - }); + const rendered = await app.render(request); - if (rendered) { - res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); - if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); - res.end(); - } else { - res.statusCode = 404; - res.end('Not found'); - } + res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); + if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); + res.end(); }; /** @param {import('polka').Middleware[]} handlers */ @@ -77,6 +72,16 @@ function sequence(handlers) { }; } +/** + * @param {import('http').IncomingHttpHeaders} headers + * @returns + */ +function get_base(headers) { + const protocol = (protocol_header && headers[protocol_header]) || 'https'; + const host = headers[host_header]; + return `${protocol}://${host}`; +} + export const handler = sequence( [ serve(path.join(__dirname, '/client'), 31536000, true), diff --git a/packages/adapter-node/src/types.d.ts b/packages/adapter-node/src/types.d.ts index 4fc8a2cd7a87..66ab40e7a8cf 100644 --- a/packages/adapter-node/src/types.d.ts +++ b/packages/adapter-node/src/types.d.ts @@ -2,6 +2,10 @@ declare global { const PATH_ENV: string; const HOST_ENV: string; const PORT_ENV: string; + const BASE_ENV: string; + + const PROTOCOL_HEADER: string; + const HOST_HEADER: string; } export {}; diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index 4750d1fe3245..d2654d1f60ba 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -1,5 +1,5 @@ import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; -import { getRawBody } from '@sveltejs/kit/node'; +import { getRequest } from '@sveltejs/kit/node'; import { App } from 'APP'; import { manifest } from 'MANIFEST'; @@ -8,22 +8,18 @@ __fetch_polyfill(); const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); export default async (req, res) => { - let rawBody; + let request; try { - rawBody = await getRawBody(req); + request = await getRequest(req, manifest); } catch (err) { res.statusCode = err.status || 400; return res.end(err.reason || 'Invalid request body'); } - const rendered = await app.render({ - url: req.url, - method: req.method, - headers: req.headers, - rawBody - }); + const rendered = await app.render(request); res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); - res.end(await rendered.arrayBuffer()); + if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); + res.end(); }; diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 3ee638d75a91..fdc3f2d631ef 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -132,15 +132,12 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a /** @type {Map} */ const dependencies = new Map(); - const rendered = await app.render( - new Request(`${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}${path}`), - { - prerender: { - all, - dependencies - } + const rendered = await app.render(new Request(`http://sveltekit-prerender${path}`), { + prerender: { + all, + dependencies } - ); + }); if (rendered) { const response_type = Math.floor(rendered.status / 100); @@ -260,18 +257,13 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } if (fallback) { - const rendered = await app.render( - new Request( - `${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}/[fallback]` - ), - { - prerender: { - fallback, - all: false, - dependencies: new Map() - } + const rendered = await app.render(new Request(`http://sveltekit-prerender/[fallback]`), { + prerender: { + fallback, + all: false, + dependencies: new Map() } - ); + }); const file = join(out, fallback); mkdirp(dirname(file)); diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 636163b9f41d..022b41f08b1c 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -29,11 +29,6 @@ test('fills in defaults', () => { template: 'src/app.html' }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -60,7 +55,6 @@ test('fills in defaults', () => { onError: 'fail', pages: undefined }, - protocol: null, router: true, ssr: null, target: null, @@ -141,11 +135,6 @@ test('fills in partial blanks', () => { template: 'src/app.html' }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -172,7 +161,6 @@ test('fills in partial blanks', () => { onError: 'fail', pages: undefined }, - protocol: null, router: true, ssr: null, target: null, diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 0eaf5e8567e6..4e8862b997a4 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -66,13 +66,6 @@ const options = object( floc: boolean(false), - headers: object({ - host: string(null), - protocol: string(null) - }), - - host: string(null), - hydrate: boolean(true), inlineStyleThreshold: number(0), @@ -179,8 +172,6 @@ const options = object( }) }), - protocol: string(null), - router: boolean(true), serviceWorker: object({ diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index c85d2e60c2ed..d3048e71296e 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -31,11 +31,6 @@ test('load default config (esm)', async () => { template: join(cwd, 'src/app.html') }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -59,7 +54,6 @@ test('load default config (esm)', async () => { onError: 'fail', pages: undefined }, - protocol: null, router: true, ssr: null, target: null, diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index d2b50c52bbb7..605497b00c6f 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -91,12 +91,8 @@ export async function preview({ } if (initial_url.startsWith(config.kit.paths.base)) { - const protocol = - config.kit.protocol || - (config.kit.headers.protocol && req.headers[config.kit.headers.protocol]) || - (use_https ? 'https' : 'http'); - - const host = config.kit.host || req.headers[config.kit.headers.host || 'host']; + const protocol = use_https ? 'https' : 'http'; + const host = req.headers['host']; const rendered = await app.render( new Request(`${protocol}://${host}${initial_url}`, { diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js index 4d7ca271a53b..2aec9f80648b 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node.js @@ -1,3 +1,14 @@ +class HttpError extends Error { + /** + * @param {string} message + * @param {number} statusCode + */ + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + } +} + /** @type {import('@sveltejs/kit/node').GetRawBody} */ export function getRawBody(req) { return new Promise((fulfil, reject) => { @@ -47,3 +58,12 @@ export function getRawBody(req) { }); }); } + +/** @type {import('@sveltejs/kit/node').GetRequest} */ +export async function getRequest(base, req) { + return new Request(base + req.url, { + method: req.method, + headers: /** @type {Record} */ (req.headers), + body: await getRawBody(req) + }); +} diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js index 5891eff90787..f916490ddc01 100644 --- a/packages/kit/test/apps/options/svelte.config.js +++ b/packages/kit/test/apps/options/svelte.config.js @@ -15,7 +15,6 @@ const config = { appDir: '_wheee', floc: true, target: '#content-goes-here', - host: 'example.com', inlineStyleThreshold: 1024, trailingSlash: 'always', vite: { @@ -32,8 +31,7 @@ const config = { paths: { base: '/path-base', assets: 'https://cdn.example.com/stuff' - }, - protocol: 'https' + } } }; diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index 80111143a64d..8445f4d0421e 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -181,6 +181,11 @@ declare module '@sveltejs/kit/node' { (request: IncomingMessage): Promise; } export const getRawBody: GetRawBody; + + export interface GetRequest { + (base: string, request: IncomingMessage): Promise; + } + export const getUrl: GetRequest; } declare module '@sveltejs/kit/install-fetch' { diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 7bc00b3bdaf1..fefeb68343ae 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -125,11 +125,6 @@ export interface Config { template?: string; }; floc?: boolean; - headers?: { - host?: string; - protocol?: string; - }; - host?: string; hydrate?: boolean; inlineStyleThreshold?: number; methodOverride?: { @@ -153,7 +148,6 @@ export interface Config { entries?: string[]; onError?: PrerenderOnErrorValue; }; - protocol?: string; router?: boolean; serviceWorker?: { register?: boolean; From a1bccb3082a33fc4d43cd396518b699c58adceb6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 09:57:50 -0500 Subject: [PATCH 09/36] lint --- packages/adapter-node/src/handler.js | 2 +- packages/kit/src/core/adapt/prerender/prerender.js | 2 +- packages/kit/src/node.js | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 1efc4924feb6..629947a61818 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -14,7 +14,7 @@ import { manifest } from 'MANIFEST'; __fetch_polyfill(); const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); -const base = BASE; +const base = BASE_ENV && process.env[BASE_ENV]; const protocol_header = PROTOCOL_HEADER && process.env[PROTOCOL_HEADER]; const host_header = (HOST_HEADER && process.env[HOST_HEADER]) || 'host'; diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index fdc3f2d631ef..6de6d26a613b 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -257,7 +257,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } if (fallback) { - const rendered = await app.render(new Request(`http://sveltekit-prerender/[fallback]`), { + const rendered = await app.render(new Request('http://sveltekit-prerender/[fallback]'), { prerender: { fallback, all: false, diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js index 2aec9f80648b..c3e52bc50053 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node.js @@ -1,14 +1,3 @@ -class HttpError extends Error { - /** - * @param {string} message - * @param {number} statusCode - */ - constructor(message, statusCode) { - super(message); - this.statusCode = statusCode; - } -} - /** @type {import('@sveltejs/kit/node').GetRawBody} */ export function getRawBody(req) { return new Promise((fulfil, reject) => { From 3ce5ac3325db7cf6d77dc66ec83bc45b5ba6adc8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 10:33:57 -0500 Subject: [PATCH 10/36] remove obsolete origin test --- .../options/source/pages/origin/index.json.js | 6 ---- .../options/source/pages/origin/index.svelte | 30 ------------------- packages/kit/test/apps/options/test/test.js | 12 -------- 3 files changed, 48 deletions(-) delete mode 100644 packages/kit/test/apps/options/source/pages/origin/index.json.js delete mode 100644 packages/kit/test/apps/options/source/pages/origin/index.svelte diff --git a/packages/kit/test/apps/options/source/pages/origin/index.json.js b/packages/kit/test/apps/options/source/pages/origin/index.json.js deleted file mode 100644 index 7930839485c8..000000000000 --- a/packages/kit/test/apps/options/source/pages/origin/index.json.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ url }) { - return { - body: { origin: url.origin } - }; -} diff --git a/packages/kit/test/apps/options/source/pages/origin/index.svelte b/packages/kit/test/apps/options/source/pages/origin/index.svelte deleted file mode 100644 index ecdf102ef558..000000000000 --- a/packages/kit/test/apps/options/source/pages/origin/index.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - -

{origin}

-

{$page.url.origin}

-

{data.origin}

diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index 6d5ebafbc3cd..d2d7b818fa3f 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -110,18 +110,6 @@ test.describe.parallel('Headers', () => { }); }); -test.describe.parallel('Origin', () => { - test('sets origin', async ({ baseURL, page }) => { - await page.goto('/path-base/origin/'); - - const origin = process.env.DEV ? baseURL : 'https://example.com'; - - expect(await page.textContent('[data-source="load"]')).toBe(origin); - expect(await page.textContent('[data-source="store"]')).toBe(origin); - expect(await page.textContent('[data-source="endpoint"]')).toBe(origin); - }); -}); - test.describe.parallel('trailingSlash', () => { test('adds trailing slash', async ({ baseURL, page, clicknav }) => { await page.goto('/path-base/slash'); From 2fbb868bbb7dc0bea04481e892d5b21e8fa7aa25 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 12:57:08 -0500 Subject: [PATCH 11/36] change endpoint signature --- packages/kit/src/core/build/build_server.js | 15 ++- packages/kit/src/core/dev/plugin.js | 17 ++- packages/kit/src/hooks.js | 14 +-- packages/kit/src/node.js | 2 + packages/kit/src/runtime/server/endpoint.js | 12 +- packages/kit/src/runtime/server/index.js | 96 ++++++++------- packages/kit/src/runtime/server/page/index.js | 10 +- .../kit/src/runtime/server/page/load_node.js | 27 +++-- .../kit/src/runtime/server/page/respond.js | 24 ++-- .../runtime/server/page/respond_with_error.js | 24 ++-- .../src/runtime/server/parse_body/index.js | 109 ------------------ .../server/parse_body/read_only_form_data.js | 78 ------------- .../parse_body/read_only_form_data.spec.js | 65 ----------- packages/kit/test/apps/basics/src/hooks.js | 24 ++-- .../apps/basics/src/routes/etag/custom.js | 4 +- .../apps/basics/src/routes/headers/echo.js | 10 +- .../basics/src/routes/load/raw-body.json.js | 9 +- .../routes/load/serialization-post.json.js | 8 +- .../src/routes/method-override/fetch.json.js | 8 +- .../src/routes/routing/shadow/action.js | 5 +- packages/kit/types/ambient-modules.d.ts | 3 +- packages/kit/types/app.d.ts | 6 - packages/kit/types/endpoint.d.ts | 7 +- packages/kit/types/helper.d.ts | 1 - packages/kit/types/hooks.d.ts | 30 +++-- packages/kit/types/index.d.ts | 3 +- packages/kit/types/internal.d.ts | 4 +- 27 files changed, 197 insertions(+), 418 deletions(-) delete mode 100644 packages/kit/src/runtime/server/parse_body/index.js delete mode 100644 packages/kit/src/runtime/server/parse_body/read_only_form_data.js delete mode 100644 packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 76a4d1391f09..9286173c9766 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -38,7 +38,7 @@ set_paths(${s(config.kit.paths)}); // named imports without triggering Rollup's missing import detection const get_hooks = hooks => ({ getSession: hooks.getSession || (() => ({})), - handle: hooks.handle || (({ request, resolve }) => resolve(request)), + handle: hooks.handle || (({ event, resolve }) => resolve(event)), handleError: hooks.handleError || (({ error }) => console.error(error.stack)), externalFetch: hooks.externalFetch || fetch }); @@ -63,8 +63,17 @@ export class App { dev: false, floc: ${config.kit.floc}, get_stack: error => String(error), // for security - handle_error: (error, request) => { - hooks.handleError({ error, request }); + handle_error: (error, event) => { + hooks.handleError({ + error, + event, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error('request in handleError has been replaced with event'); + } + }); error.stack = this.options.get_stack(error); }, hooks, diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index e8154757fa27..b4bd99e9f573 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -170,7 +170,7 @@ export async function create_plugin(config, cwd) { /** @type {import('types/internal').Hooks} */ const hooks = { getSession: user_hooks.getSession || (() => ({})), - handle: user_hooks.handle || (({ request, resolve }) => resolve(request)), + handle: user_hooks.handle || (({ event, resolve }) => resolve(event)), handleError: user_hooks.handleError || (({ /** @type {Error & { frame?: string }} */ error }) => { @@ -218,7 +218,7 @@ export async function create_plugin(config, cwd) { const rendered = await respond( new Request(url.href, { - headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), + headers: /** @type {Record} */ (req.headers), method: req.method, body }), @@ -230,9 +230,18 @@ export async function create_plugin(config, cwd) { vite.ssrFixStacktrace(error); return error.stack; }, - handle_error: (error, request) => { + handle_error: (error, event) => { vite.ssrFixStacktrace(error); - hooks.handleError({ error, request }); + hooks.handleError({ + error, + event, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error('request in handleError has been replaced with event'); + } + }); }, hooks, hydrate: config.kit.hydrate, diff --git a/packages/kit/src/hooks.js b/packages/kit/src/hooks.js index 822c09b21e02..23b4214c64c1 100644 --- a/packages/kit/src/hooks.js +++ b/packages/kit/src/hooks.js @@ -4,22 +4,22 @@ */ export function sequence(...handlers) { const length = handlers.length; - if (!length) return ({ request, resolve }) => resolve(request); + if (!length) return ({ event, resolve }) => resolve(event); - return ({ request, resolve }) => { - return apply_handle(0, request); + return ({ event, resolve }) => { + return apply_handle(0, event); /** * @param {number} i - * @param {import('types/hooks').ServerRequest} request + * @param {import('types/hooks').RequestEvent} event * @returns {import('types/helper').MaybePromise} */ - function apply_handle(i, request) { + function apply_handle(i, event) { const handle = handlers[i]; return handle({ - request, - resolve: i < length - 1 ? (request) => apply_handle(i + 1, request) : resolve + event, + resolve: i < length - 1 ? (event) => apply_handle(i + 1, event) : resolve }); } }; diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js index c3e52bc50053..822aa05e1389 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node.js @@ -1,3 +1,5 @@ +// TODO this should eventually be replaced with a ReadableStream equivalent + /** @type {import('@sveltejs/kit/node').GetRawBody} */ export function getRawBody(req) { return new Promise((fulfil, reject) => { diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index dede4fb26ff2..3f4de2cf5f95 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -36,16 +36,16 @@ export function is_text(content_type) { } /** - * @param {import('types/hooks').ServerRequest} request + * @param {import('types/hooks').RequestEvent} event * @param {import('types/internal').SSREndpoint} route * @param {RegExpExecArray} match * @returns {Promise} */ -export async function render_endpoint(request, route, match) { +export async function render_endpoint(event, route, match) { const mod = await route.load(); /** @type {import('types/endpoint').RequestHandler} */ - const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word + const handler = mod[event.request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word if (!handler) { return; @@ -54,10 +54,10 @@ export async function render_endpoint(request, route, match) { // we're mutating `request` so that we don't have to do { ...request, params } // on the next line, since that breaks the getters that replace path, query and // origin. We could revert that once we remove the getters - request.params = route.params ? decode_params(route.params(match)) : {}; + event.params = route.params ? decode_params(route.params(match)) : {}; - const response = await handler(request); - const preface = `Invalid response from route ${request.url.pathname}`; + const response = await handler(event); + const preface = `Invalid response from route ${event.url.pathname}`; if (typeof response !== 'object') { return error(`${preface}: expected an object, got ${typeof response}`); diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 94fcb4e2bcbe..ab61fb905ca9 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -2,7 +2,6 @@ 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 { parse_body } from './parse_body/index.js'; import { hash } from '../hash.js'; import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; @@ -33,25 +32,18 @@ export async function respond(request, options, state = {}) { } } - const headers = Object.fromEntries(request.headers); - const rawBody = new Uint8Array(await request.arrayBuffer()); - const request_details = { - url, - method: request.method, - headers, - rawBody, - body: parse_body(rawBody, headers), - params: {}, - locals: {} - }; - const { parameter, allowed } = options.method_override; const method_override = url.searchParams.get(parameter)?.toUpperCase(); if (method_override) { - if (request_details.method.toUpperCase() === 'POST') { + if (request.method === 'POST') { if (allowed.includes(method_override)) { - request_details.method = method_override; + request = new Proxy(request, { + get: (target, property, _receiver) => { + if (property === 'method') return method_override; + return Reflect.get(target, property, target); + } + }); } else { const verb = allowed.length === 0 ? 'enabled' : 'allowed'; const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`; @@ -67,38 +59,58 @@ export async function respond(request, options, state = {}) { } } + /** @type {import('types/hooks').RequestEvent} */ + const event = { + request, + url, + params: {}, + locals: {} + }; + // TODO remove this for 1.0 /** * @param {string} property * @param {string} replacement */ - const print_error = (property, replacement) => { - Object.defineProperty(request_details, property, { - get: () => { - throw new Error(`request.${property} has been replaced by request.url.${replacement}`); - } - }); + const removed = (property, replacement) => ({ + get: () => { + throw new Error(`event.${property} has been replaced by event.${replacement}`); + } + }); + + const body_getter = { + get: () => { + throw new Error( + 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' + ); + } }; - print_error('origin', 'origin'); - print_error('path', 'pathname'); - print_error('query', 'searchParams'); + Object.defineProperties(event, { + method: removed('method', 'request.method'), + headers: removed('headers', 'request.headers'), + origin: removed('origin', 'url.origin'), + path: removed('path', 'url.pathname'), + query: removed('query', 'url.searchParams'), + body: body_getter, + rawBody: body_getter + }); let ssr = true; try { return await options.hooks.handle({ - request: request_details, - resolve: async (request, opts) => { + event, + resolve: async (event, opts) => { if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr); if (state.prerender && state.prerender.fallback) { return await render_response({ - url: request.url, - params: request.params, + url: event.url, + params: event.params, options, state, - $session: await options.hooks.getSession(request), + $session: await options.hooks.getSession(event), page_config: { router: true, hydrate: true }, stuff: {}, status: 200, @@ -107,7 +119,7 @@ export async function respond(request, options, state = {}) { }); } - let decoded = decodeURI(request.url.pathname); + let decoded = decodeURI(event.url.pathname); if (options.paths.base) { if (!decoded.startsWith(options.paths.base)) return; @@ -120,8 +132,8 @@ export async function respond(request, options, state = {}) { const response = route.type === 'endpoint' - ? await render_endpoint(request, route, match) - : await render_page(request, route, match, options, state, ssr); + ? await render_endpoint(event, route, match) + : await render_page(event, route, match, options, state, ssr); if (response) { // inject ETags for 200 responses, if the endpoint @@ -129,7 +141,7 @@ export async function respond(request, options, state = {}) { if (response.status === 200 && !response.headers.etag) { const cache_control = get_single_valued_header(response.headers, 'cache-control'); if (!cache_control || !/(no-store|immutable)/.test(cache_control)) { - let if_none_match_value = request.headers['if-none-match']; + let if_none_match_value = request.headers.get('if-none-match'); // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives if (if_none_match_value?.startsWith('W/"')) { if_none_match_value = if_none_match_value.substring(2); @@ -171,28 +183,34 @@ export async function respond(request, options, state = {}) { // if this request came direct from the user, rather than // via a `fetch` in a `load`, render a 404 page if (!state.initiator) { - const $session = await options.hooks.getSession(request); + const $session = await options.hooks.getSession(event); return await respond_with_error({ - request, + event, options, state, $session, status: 404, - error: new Error(`Not found: ${request.url.pathname}`), + error: new Error(`Not found: ${event.url.pathname}`), ssr }); } + }, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error('request in handle has been replaced with event'); } }); } catch (/** @type {unknown} */ e) { const error = coalesce_to_error(e); - options.handle_error(error, request_details); + options.handle_error(error, event); try { - const $session = await options.hooks.getSession(request_details); + const $session = await options.hooks.getSession(event); return await respond_with_error({ - request: request_details, + event, options, state, $session, diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 1359cdb0eadb..2520c4262a43 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -2,7 +2,7 @@ import { decode_params } from '../utils.js'; import { respond } from './respond.js'; /** - * @param {import('types/hooks').ServerRequest} request + * @param {import('types/hooks').RequestEvent} event * @param {import('types/internal').SSRPage} route * @param {RegExpExecArray} match * @param {import('types/internal').SSRRenderOptions} options @@ -10,22 +10,22 @@ import { respond } from './respond.js'; * @param {boolean} ssr * @returns {Promise} */ -export async function render_page(request, route, match, options, state, ssr) { +export async function render_page(event, route, match, options, state, ssr) { if (state.initiator === route) { // infinite request cycle detected return { status: 404, headers: {}, - body: `Not found: ${request.url.pathname}` + body: `Not found: ${event.url.pathname}` }; } const params = route.params ? decode_params(route.params(match)) : {}; - const $session = await options.hooks.getSession(request); + const $session = await options.hooks.getSession(event); const response = await respond({ - request, + event, options, state, $session, diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index dfbe99d9186f..892ed3b6c059 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -7,7 +7,7 @@ import { create_prerendering_url_proxy } from './utils.js'; /** * @param {{ - * request: import('types/hooks').ServerRequest; + * event: import('types/hooks').RequestEvent; * options: import('types/internal').SSRRenderOptions; * state: import('types/internal').SSRRenderState; * route: import('types/internal').SSRPage | null; @@ -23,7 +23,7 @@ import { create_prerendering_url_proxy } from './utils.js'; * @returns {Promise} undefined for fallthrough */ export async function load_node({ - request, + event, options, state, route, @@ -94,7 +94,7 @@ export async function load_node({ opts.headers = new Headers(opts.headers); - const resolved = resolve(request.url.pathname, requested.split('?')[0]); + const resolved = resolve(event.url.pathname, requested.split('?')[0]); let response; @@ -130,12 +130,15 @@ export async function load_node({ if (opts.credentials !== 'omit') { uses_credentials = true; - if (request.headers.cookie) { - opts.headers.set('cookie', request.headers.cookie); + const cookie = event.request.headers.get('cookie'); + const authorization = event.request.headers.get('authorization'); + + if (cookie) { + opts.headers.set('cookie', cookie); } - if (request.headers.authorization && !opts.headers.has('authorization')) { - opts.headers.set('authorization', request.headers.authorization); + if (authorization && !opts.headers.has('authorization')) { + opts.headers.set('authorization', authorization); } } @@ -148,7 +151,7 @@ export async function load_node({ } const rendered = await respond( - new Request(new URL(requested, request.url).href, opts), + new Request(new URL(requested, event.url).href, opts), options, { fetched: requested, @@ -171,7 +174,7 @@ export async function load_node({ } else { // we can't load the endpoint from our own manifest, // so we need to make an actual HTTP request - return fetch(new URL(requested, request.url).href, { + return fetch(new URL(requested, event.url).href, { method: opts.method || 'GET', headers: opts.headers }); @@ -194,11 +197,13 @@ export async function load_node({ // ports do not affect the resolution // leading dot prevents mydomain.com matching domain.com if ( - `.${new URL(requested).hostname}`.endsWith(`.${request.url.hostname}`) && + `.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) && opts.credentials !== 'omit' ) { uses_credentials = true; - opts.headers.set('cookie', request.headers.cookie); + + const cookie = event.request.headers.get('cookie'); + if (cookie) opts.headers.set('cookie', cookie); } const external_request = new Request(requested, /** @type {RequestInit} */ (opts)); diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index c38fb7264365..f7f0c0c84699 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -13,7 +13,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; /** * @param {{ - * request: import('types/hooks').ServerRequest; + * event: import('types/hooks').RequestEvent; * options: SSRRenderOptions; * state: SSRRenderState; * $session: any; @@ -24,7 +24,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; * @returns {Promise} */ export async function respond(opts) { - const { request, options, state, $session, route, ssr } = opts; + const { event, options, state, $session, route, ssr } = opts; /** @type {Array} */ let nodes; @@ -38,7 +38,7 @@ export async function respond(opts) { router: true }, status: 200, - url: request.url, + url: event.url, stuff: {} }); } @@ -50,10 +50,10 @@ export async function respond(opts) { } catch (err) { const error = coalesce_to_error(err); - options.handle_error(error, request); + options.handle_error(error, event); return await respond_with_error({ - request, + event, options, state, $session, @@ -102,7 +102,7 @@ export async function respond(opts) { try { loaded = await load_node({ ...opts, - url: request.url, + url: event.url, node, stuff, is_error: false @@ -130,7 +130,7 @@ export async function respond(opts) { } catch (err) { const e = coalesce_to_error(err); - options.handle_error(e, request); + options.handle_error(e, event); status = 500; error = e; @@ -156,7 +156,7 @@ export async function respond(opts) { const error_loaded = /** @type {import('./types').Loaded} */ ( await load_node({ ...opts, - url: request.url, + url: event.url, node: error_node, stuff: node_loaded.stuff, is_error: true, @@ -176,7 +176,7 @@ export async function respond(opts) { } catch (err) { const e = coalesce_to_error(err); - options.handle_error(e, request); + options.handle_error(e, event); continue; } @@ -188,7 +188,7 @@ export async function respond(opts) { // for now just return regular error page return with_cookies( await respond_with_error({ - request, + event, options, state, $session, @@ -215,7 +215,7 @@ export async function respond(opts) { await render_response({ ...opts, stuff, - url: request.url, + url: event.url, page_config, status, error, @@ -226,7 +226,7 @@ export async function respond(opts) { } catch (err) { const error = coalesce_to_error(err); - options.handle_error(error, request); + options.handle_error(error, event); return with_cookies( await respond_with_error({ 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 cfd05eec5dc4..383b77d3b998 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -10,7 +10,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; /** * @param {{ - * request: import('types/hooks').ServerRequest; + * event: import('types/hooks').RequestEvent; * options: SSRRenderOptions; * state: SSRRenderState; * $session: any; @@ -19,15 +19,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; * ssr: boolean; * }} opts */ -export async function respond_with_error({ - request, - options, - state, - $session, - status, - error, - ssr -}) { +export async function respond_with_error({ event, options, state, $session, status, error, ssr }) { try { const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout const default_error = await options.manifest._.nodes[1](); // 1 is always the root error @@ -37,11 +29,11 @@ export async function respond_with_error({ const layout_loaded = /** @type {Loaded} */ ( await load_node({ - request, + event, options, state, route: null, - url: request.url, // TODO this is redundant, no? + url: event.url, // TODO this is redundant, no? params, node: default_layout, $session, @@ -52,11 +44,11 @@ export async function respond_with_error({ const error_loaded = /** @type {Loaded} */ ( await load_node({ - request, + event, options, state, route: null, - url: request.url, + url: event.url, params, node: default_error, $session, @@ -79,14 +71,14 @@ export async function respond_with_error({ status, error, branch: [layout_loaded, error_loaded], - url: request.url, + url: event.url, params, ssr }); } catch (err) { const error = coalesce_to_error(err); - options.handle_error(error, request); + options.handle_error(error, event); return { status: 500, diff --git a/packages/kit/src/runtime/server/parse_body/index.js b/packages/kit/src/runtime/server/parse_body/index.js deleted file mode 100644 index 0d8f31582242..000000000000 --- a/packages/kit/src/runtime/server/parse_body/index.js +++ /dev/null @@ -1,109 +0,0 @@ -import { read_only_form_data } from './read_only_form_data.js'; - -/** - * @param {import('types/app').RawBody} raw - * @param {import('types/helper').RequestHeaders} headers - */ -export function parse_body(raw, headers) { - if (!raw) return raw; - - const content_type = headers['content-type']; - const [type, ...directives] = content_type ? content_type.split(/;\s*/) : []; - - const text = () => new TextDecoder(headers['content-encoding'] || 'utf-8').decode(raw); - - switch (type) { - case 'text/plain': - return text(); - - case 'application/json': - return JSON.parse(text()); - - case 'application/x-www-form-urlencoded': - return get_urlencoded(text()); - - case 'multipart/form-data': { - const boundary = directives.find((directive) => directive.startsWith('boundary=')); - if (!boundary) throw new Error('Missing boundary'); - return get_multipart(text(), boundary.slice('boundary='.length)); - } - default: - return raw; - } -} - -/** @param {string} text */ -function get_urlencoded(text) { - const { data, append } = read_only_form_data(); - - text - .replace(/\+/g, ' ') - .split('&') - .forEach((str) => { - const [key, value] = str.split('='); - append(decodeURIComponent(key), decodeURIComponent(value)); - }); - - return data; -} - -/** - * @param {string} text - * @param {string} boundary - */ -function get_multipart(text, boundary) { - const parts = text.split(`--${boundary}`); - - if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') { - throw new Error('Malformed form data'); - } - - const { data, append } = read_only_form_data(); - - parts.slice(1, -1).forEach((part) => { - const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part); - if (!match) { - throw new Error('Malformed form data'); - } - const raw_headers = match[1]; - const body = match[2].trim(); - - let key; - - /** @type {Record} */ - const headers = {}; - raw_headers.split('\r\n').forEach((str) => { - const [raw_header, ...raw_directives] = str.split('; '); - let [name, value] = raw_header.split(': '); - - name = name.toLowerCase(); - headers[name] = value; - - /** @type {Record} */ - const directives = {}; - raw_directives.forEach((raw_directive) => { - const [name, value] = raw_directive.split('='); - directives[name] = JSON.parse(value); // TODO is this right? - }); - - if (name === 'content-disposition') { - if (value !== 'form-data') throw new Error('Malformed form data'); - - if (directives.filename) { - // TODO we probably don't want to do this automatically - throw new Error('File upload is not yet implemented'); - } - - if (directives.name) { - key = directives.name; - } - } - }); - - if (!key) throw new Error('Malformed form data'); - - append(key, body); - }); - - return data; -} diff --git a/packages/kit/src/runtime/server/parse_body/read_only_form_data.js b/packages/kit/src/runtime/server/parse_body/read_only_form_data.js deleted file mode 100644 index 43d47f6dc966..000000000000 --- a/packages/kit/src/runtime/server/parse_body/read_only_form_data.js +++ /dev/null @@ -1,78 +0,0 @@ -export function read_only_form_data() { - /** @type {Map} */ - const map = new Map(); - - return { - /** - * @param {string} key - * @param {string} value - */ - append(key, value) { - const existing_values = map.get(key); - if (existing_values) { - existing_values.push(value); - } else { - map.set(key, [value]); - } - }, - - data: new ReadOnlyFormData(map) - }; -} - -class ReadOnlyFormData { - /** @type {Map} */ - #map; - - /** @param {Map} map */ - constructor(map) { - this.#map = map; - } - - /** @param {string} key */ - get(key) { - const value = this.#map.get(key); - if (!value) { - return null; - } - return value[0]; - } - - /** @param {string} key */ - getAll(key) { - return this.#map.get(key) || []; - } - - /** @param {string} key */ - has(key) { - return this.#map.has(key); - } - - *[Symbol.iterator]() { - for (const [key, value] of this.#map) { - for (let i = 0; i < value.length; i += 1) { - yield [key, value[i]]; - } - } - } - - *entries() { - for (const [key, value] of this.#map) { - for (let i = 0; i < value.length; i += 1) { - yield [key, value[i]]; - } - } - } - - *keys() { - for (const [key] of this.#map) yield key; - } - - *values() { - for (const [, value] of this.#map) { - for (let i = 0; i < value.length; i += 1) { - yield value[i]; - } - } - } -} diff --git a/packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js b/packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js deleted file mode 100644 index 62d88d9f39d4..000000000000 --- a/packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { test } from 'uvu'; -import * as assert from 'uvu/assert'; -import { read_only_form_data } from './read_only_form_data.js'; - -test('ro-fd get returns null and getAll an empty array for no values at given key', () => { - const { data } = read_only_form_data(); - assert.is(data.get('foo'), null); - assert.equal(data.getAll('foo'), []); -}); - -const { data, append } = read_only_form_data(); -append('foo', '1'), append('foo', '2'), append('foo', '3'); -append('bar', '2'), append('bar', '1'); - -test('ro-fd get returns first value', () => { - assert.equal(data.get('foo'), '1'); - assert.equal(data.get('bar'), '2'); -}); - -test('ro-fd getAll returns array', () => { - assert.equal(data.getAll('foo'), ['1', '2', '3']); - assert.equal(data.getAll('bar'), ['2', '1']); -}); - -test('ro-fd has returns boolean flag', () => { - assert.equal(data.has('foo'), true); - assert.equal(data.has('bar'), true); - assert.equal(data.has('baz'), false); -}); - -test('ro-fd iterator yields all key-value pairs', () => { - const values = []; - for (const [key, val] of data) values.push({ key, val }); - - assert.equal(values.length, 5); - assert.equal(values[0], { key: 'foo', val: '1' }); - assert.equal(values[3], { key: 'bar', val: '2' }); -}); - -test('ro-fd entries() yields all key-value pairs', () => { - const values = []; - for (const [key, val] of data.entries()) values.push({ key, val }); - - assert.equal(values.length, 5); - assert.equal(values[0], { key: 'foo', val: '1' }); - assert.equal(values[3], { key: 'bar', val: '2' }); -}); - -test('ro-fd keys() yields all unique keys', () => { - const values = []; - for (const key of data.keys()) values.push(key); - - assert.equal(values.length, 2); - assert.equal(values, ['foo', 'bar']); -}); - -test('ro-fd values() yields all nested values', () => { - const values = []; - for (const val of data.values()) values.push(val); - - assert.equal(values.length, 5); - assert.equal(values, ['1', '2', '3', '2', '1']); -}); - -test.run(); diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index d82a0f8379bc..6723452ff60e 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -8,7 +8,7 @@ export function getSession(request) { } /** @type {import('@sveltejs/kit').HandleError} */ -export const handleError = ({ request, error }) => { +export const handleError = ({ event, error }) => { // TODO we do this because there's no other way (that i'm aware of) // to communicate errors back to the test suite. even if we could // capture stderr, attributing an error to a specific request @@ -16,26 +16,26 @@ export const handleError = ({ request, error }) => { const errors = fs.existsSync('test/errors.json') ? JSON.parse(fs.readFileSync('test/errors.json', 'utf8')) : {}; - errors[request.url.pathname] = error.stack || error.message; + errors[event.url.pathname] = error.stack || error.message; fs.writeFileSync('test/errors.json', JSON.stringify(errors)); }; export const handle = sequence( - ({ request, resolve }) => { - request.locals.answer = 42; - return resolve(request); + ({ event, resolve }) => { + event.locals.answer = 42; + return resolve(event); }, - ({ request, resolve }) => { - const cookies = cookie.parse(request.headers.cookie || ''); - request.locals.name = cookies.name; - return resolve(request); + ({ event, resolve }) => { + const cookies = cookie.parse(event.request.headers.get('cookie') || ''); + event.locals.name = cookies.name; + return resolve(event); }, - async ({ request, resolve }) => { - if (request.url.pathname === '/errors/error-in-handle') { + async ({ event, resolve }) => { + if (event.url.pathname === '/errors/error-in-handle') { throw new Error('Error in handle'); } - const response = await resolve(request, { ssr: !request.url.pathname.startsWith('/no-ssr') }); + const response = await resolve(event, { ssr: !event.url.pathname.startsWith('/no-ssr') }); return { ...response, diff --git a/packages/kit/test/apps/basics/src/routes/etag/custom.js b/packages/kit/test/apps/basics/src/routes/etag/custom.js index 71136e3a2f13..8815e4104b13 100644 --- a/packages/kit/test/apps/basics/src/routes/etag/custom.js +++ b/packages/kit/test/apps/basics/src/routes/etag/custom.js @@ -1,6 +1,6 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ headers }) { - if (headers['if-none-match'] === '@1234@') return { status: 304 }; +export function get({ request }) { + if (request.headers.get('if-none-match') === '@1234@') return { status: 304 }; return { body: `${Math.random()}`, headers: { diff --git a/packages/kit/test/apps/basics/src/routes/headers/echo.js b/packages/kit/test/apps/basics/src/routes/headers/echo.js index 629b0579ef05..e9d31c7c9ca0 100644 --- a/packages/kit/test/apps/basics/src/routes/headers/echo.js +++ b/packages/kit/test/apps/basics/src/routes/headers/echo.js @@ -1,6 +1,12 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ headers }) { - delete headers.cookie; +export function get({ request }) { + /** @type {Record} */ + const headers = {}; + request.headers.forEach((value, key) => { + if (key !== 'cookie') { + headers[key] = value; + } + }); return { body: headers diff --git a/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js b/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js index abd0fb74908e..b3e65bc0b035 100644 --- a/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js +++ b/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js @@ -1,9 +1,12 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function post(request) { +export async function post({ request }) { + const text = await request.text(); + const json = JSON.parse(text); + return { body: { - body: /** @type {string} */ (request.body), - rawBody: new TextDecoder().decode(/** @type {Uint8Array} */ (request.rawBody)) + body: json, + rawBody: text } }; } diff --git a/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js b/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js index 5ea0ae3ff398..c613becb0d1a 100644 --- a/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js +++ b/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js @@ -1,6 +1,8 @@ -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function post(request) { +/** @type {import('@sveltejs/kit').RequestHandler} */ +export async function post({ request }) { + const body = await request.text(); + return { - body: request.body.toUpperCase() + body: body.toUpperCase() }; } diff --git a/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js b/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js index f33471c06b09..39823db30d38 100644 --- a/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js +++ b/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js @@ -6,21 +6,21 @@ const buildResponse = (/** @type {string} */ method) => ({ }); /** @type {import('@sveltejs/kit').RequestHandler} */ -export const get = (request) => { +export const get = ({ request }) => { return buildResponse(request.method); }; /** @type {import('@sveltejs/kit').RequestHandler} */ -export const post = (request) => { +export const post = ({ request }) => { return buildResponse(request.method); }; /** @type {import('@sveltejs/kit').RequestHandler} */ -export const patch = (request) => { +export const patch = ({ request }) => { return buildResponse(request.method); }; /** @type {import('@sveltejs/kit').RequestHandler} */ -export const del = (request) => { +export const del = ({ request }) => { return buildResponse(request.method); }; diff --git a/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js b/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js index bbd535eee3b4..7aea26efcc70 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js +++ b/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js @@ -1,7 +1,8 @@ let random = 0; -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function post({ body }) { +/** @type {import('@sveltejs/kit').RequestHandler} */ +export async function post({ request }) { + const body = await request.formData(); random = +(body.get('random') || '0'); return { fallthrough: true }; } diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index 8445f4d0421e..fabe037384c7 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -175,10 +175,9 @@ declare module '@sveltejs/kit/hooks' { declare module '@sveltejs/kit/node' { import { IncomingMessage } from 'http'; - import { RawBody } from '@sveltejs/kit'; export interface GetRawBody { - (request: IncomingMessage): Promise; + (request: IncomingMessage): Promise; } export const getRawBody: GetRawBody; diff --git a/packages/kit/types/app.d.ts b/packages/kit/types/app.d.ts index b59e4a214995..818b89eee68d 100644 --- a/packages/kit/types/app.d.ts +++ b/packages/kit/types/app.d.ts @@ -1,4 +1,3 @@ -import { ReadOnlyFormData, RequestHeaders } from './helper'; import { PrerenderOptions, SSRNodeLoader, SSRRoute } from './internal'; export class App { @@ -15,11 +14,6 @@ export class InternalApp extends App { ): Promise; } -export type RawBody = null | Uint8Array; -export type ParameterizedBody = Body extends FormData - ? ReadOnlyFormData - : (string | RawBody | ReadOnlyFormData) & Body; - export interface SSRManifest { appDir: string; assets: Set; diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts index 05e6f50530d4..9497d5785ea2 100644 --- a/packages/kit/types/endpoint.d.ts +++ b/packages/kit/types/endpoint.d.ts @@ -1,4 +1,4 @@ -import { ServerRequest } from './hooks'; +import { RequestEvent } from './hooks'; import { JSONString, MaybePromise, ResponseHeaders, Either, Fallthrough } from './helper'; type DefaultBody = JSONString | Uint8Array; @@ -11,10 +11,7 @@ export interface EndpointOutput { export interface RequestHandler< Locals = Record, - Input = unknown, Output extends DefaultBody = DefaultBody > { - (request: ServerRequest): MaybePromise< - Either, Fallthrough> - >; + (request: RequestEvent): MaybePromise, Fallthrough>>; } diff --git a/packages/kit/types/helper.d.ts b/packages/kit/types/helper.d.ts index 1b58d4d51018..02ccfe84d8c8 100644 --- a/packages/kit/types/helper.d.ts +++ b/packages/kit/types/helper.d.ts @@ -21,7 +21,6 @@ export type JSONString = /** `string[]` is only for set-cookie, everything else must be type of `string` */ export type ResponseHeaders = Record; -export type RequestHeaders = Record; // Utility Types export type InferValue = T extends Record diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index 3dc01a6afca5..b298e66dc9d6 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -1,15 +1,11 @@ -import { ParameterizedBody, RawBody } from './app'; -import { MaybePromise, RequestHeaders, ResponseHeaders } from './helper'; +import { MaybePromise, ResponseHeaders } from './helper'; export type StrictBody = string | Uint8Array; -export interface ServerRequest, Body = unknown> { +export interface RequestEvent> { + request: Request; url: URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; params: Record; - body: ParameterizedBody; locals: Locals; } @@ -19,35 +15,35 @@ export interface ServerResponse { body?: StrictBody; } -export interface GetSession, Body = unknown, Session = any> { - (request: ServerRequest): MaybePromise; +export interface GetSession, Session = any> { + (event: RequestEvent): MaybePromise; } export interface ResolveOpts { ssr?: boolean; } -export interface Handle, Body = unknown> { +export interface Handle> { (input: { - request: ServerRequest; - resolve(request: ServerRequest, opts?: ResolveOpts): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; }): MaybePromise; } // internally, `resolve` could return `undefined`, so we differentiate InternalHandle // from the public Handle type -export interface InternalHandle, Body = unknown> { +export interface InternalHandle> { (input: { - request: ServerRequest; + event: RequestEvent; resolve( - request: ServerRequest, + event: RequestEvent, opts?: ResolveOpts ): MaybePromise; }): MaybePromise; } -export interface HandleError, Body = unknown> { - (input: { error: Error & { frame?: string }; request: ServerRequest }): void; +export interface HandleError> { + (input: { error: Error & { frame?: string }; event: RequestEvent }): void; } export interface ExternalFetch { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 6f4efdd12a57..e4ccdcadf49b 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3,7 +3,7 @@ import './ambient-modules'; -export { App, RawBody, SSRManifest } from './app'; +export { App, SSRManifest } from './app'; export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; export { EndpointOutput, RequestHandler } from './endpoint'; export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page'; @@ -12,7 +12,6 @@ export { GetSession, Handle, HandleError, - ServerRequest as Request, ServerResponse as Response, ResolveOpts } from './hooks'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index cf965f3d16ec..9bd9842032a2 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -6,7 +6,7 @@ import { GetSession, HandleError, InternalHandle, - ServerRequest, + RequestEvent, ServerResponse } from './hooks'; import { Load } from './page'; @@ -127,7 +127,7 @@ export interface SSRRenderOptions { dev: boolean; floc: boolean; get_stack: (error: Error) => string | undefined; - handle_error(error: Error & { frame?: string }, request: ServerRequest): void; + handle_error(error: Error & { frame?: string }, event: RequestEvent): void; hooks: Hooks; hydrate: boolean; manifest: SSRManifest; From 365f65e841c29e3751e6509d7b488106e38e089e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 13:15:17 -0500 Subject: [PATCH 12/36] fix vercel adapter --- packages/adapter-vercel/files/entry.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index d2654d1f60ba..90b0f0648b6e 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -7,11 +7,15 @@ __fetch_polyfill(); const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); +/** + * @param {import('http').IncomingMessage} req + * @param {import('http').ServerResponse} res + */ export default async (req, res) => { let request; try { - request = await getRequest(req, manifest); + request = await getRequest(`https://${req.headers.host}`, req); } catch (err) { res.statusCode = err.status || 400; return res.end(err.reason || 'Invalid request body'); From 0f1e6b1962418180e6ab9e3c79e509e3ee208db0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 13:19:47 -0500 Subject: [PATCH 13/36] add setResponse helper --- packages/adapter-node/src/handler.js | 8 ++------ packages/adapter-vercel/files/entry.js | 8 ++------ packages/kit/src/node.js | 7 +++++++ packages/kit/types/ambient-modules.d.ts | 9 +++++++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 629947a61818..a2f54f7f309b 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import sirv from 'sirv'; import { fileURLToPath } from 'url'; -import { getRequest } from '@sveltejs/kit/node'; +import { getRequest, setResponse } from '@sveltejs/kit/node'; import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; // @ts-ignore @@ -49,11 +49,7 @@ const ssr = async (req, res) => { return res.end(err.reason || 'Invalid request body'); } - const rendered = await app.render(request); - - res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); - if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); - res.end(); + setResponse(res, await app.render(request)); }; /** @param {import('polka').Middleware[]} handlers */ diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index 90b0f0648b6e..143b0efd642e 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -1,5 +1,5 @@ import { __fetch_polyfill } from '@sveltejs/kit/install-fetch'; -import { getRequest } from '@sveltejs/kit/node'; +import { getRequest, setResponse } from '@sveltejs/kit/node'; import { App } from 'APP'; import { manifest } from 'MANIFEST'; @@ -21,9 +21,5 @@ export default async (req, res) => { return res.end(err.reason || 'Invalid request body'); } - const rendered = await app.render(request); - - res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); - if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); - res.end(); + setResponse(res, await app.render(request)); }; diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js index 822aa05e1389..95440b730268 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node.js @@ -58,3 +58,10 @@ export async function getRequest(base, req) { body: await getRawBody(req) }); } + +/** @type {import('@sveltejs/kit/node').SetResponse} */ +export async function setResponse(res, response) { + res.writeHead(response.status, Object.fromEntries(response.headers)); + if (response.body) res.write(new Uint8Array(await response.arrayBuffer())); + res.end(); +} diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index fabe037384c7..a5e4aeb39ff9 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -174,7 +174,7 @@ declare module '@sveltejs/kit/hooks' { } declare module '@sveltejs/kit/node' { - import { IncomingMessage } from 'http'; + import { IncomingMessage, ServerResponse } from 'http'; export interface GetRawBody { (request: IncomingMessage): Promise; @@ -184,7 +184,12 @@ declare module '@sveltejs/kit/node' { export interface GetRequest { (base: string, request: IncomingMessage): Promise; } - export const getUrl: GetRequest; + export const getRequest: GetRequest; + + export interface SetResponse { + (res: ServerResponse, response: Response): void; + } + export const setResponse: SetResponse; } declare module '@sveltejs/kit/install-fetch' { From b00698084d536c5b8eb661a720c1e53074dd2ea6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 16:38:10 -0500 Subject: [PATCH 14/36] allow returning Response or Headers from endpoints --- .../kit/src/core/adapt/prerender/prerender.js | 10 +-- packages/kit/src/core/dev/plugin.js | 6 +- packages/kit/src/core/preview/index.js | 20 +---- packages/kit/src/hooks.js | 2 +- packages/kit/src/node.js | 14 +++- packages/kit/src/runtime/server/endpoint.js | 61 +++++++++----- packages/kit/src/runtime/server/index.js | 80 ++++++++----------- packages/kit/src/runtime/server/page/index.js | 18 ++--- .../kit/src/runtime/server/page/load_node.js | 8 +- .../kit/src/runtime/server/page/render.js | 33 ++++---- .../kit/src/runtime/server/page/respond.js | 20 ++--- .../runtime/server/page/respond_with_error.js | 8 +- packages/kit/src/utils/http.js | 23 ++++++ packages/kit/test/apps/basics/src/hooks.js | 9 +-- .../src/routes/endpoint-output/fetched.js | 8 ++ .../routes/endpoint-output/headers-object.js | 8 ++ .../src/routes/endpoint-output/proxy.js | 2 + packages/kit/test/apps/basics/test/test.js | 25 ++++++ packages/kit/types/endpoint.d.ts | 6 +- packages/kit/types/hooks.d.ts | 19 ++--- packages/kit/types/index.d.ts | 9 +-- packages/kit/types/internal.d.ts | 13 +-- 22 files changed, 214 insertions(+), 188 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js create mode 100644 packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js create mode 100644 packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 6de6d26a613b..469c0b79314a 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -129,7 +129,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a * @param {string?} referrer */ async function visit(path, decoded_path, referrer) { - /** @type {Map} */ + /** @type {Map} */ const dependencies = new Map(); const rendered = await app.render(new Request(`http://sveltekit-prerender${path}`), { @@ -183,10 +183,10 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a error({ status: rendered.status, path, referrer, referenceType: 'linked' }); } - dependencies.forEach((result, dependency_path) => { + for (const [dependency_path, result] of dependencies) { const response_type = Math.floor(result.status / 100); - const is_html = result.headers['content-type'] === 'text/html'; + const is_html = result.headers.get('content-type') === 'text/html'; const parts = dependency_path.split('/'); if (is_html && parts[parts.length - 1] !== 'index.html') { @@ -197,7 +197,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a mkdirp(dirname(file)); if (result.body) { - writeFileSync(file, result.body); + writeFileSync(file, await result.text()); paths.push(dependency_path); } @@ -211,7 +211,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a referenceType: 'fetched' }); } - }); + } if (is_html && config.kit.prerender.crawl) { for (const href of crawl(text)) { diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index b4bd99e9f573..576d64f45cd0 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -7,7 +7,7 @@ import { respond } from '../../runtime/server/index.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { create_app } from '../create_app/index.js'; import create_manifest_data from '../create_manifest_data/index.js'; -import { getRawBody } from '../../node.js'; +import { getRawBody, setResponse } from '../../node.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; import { get_mime_lookup, resolve_entry, runtime } from '../utils.js'; import { coalesce_to_error } from '../../utils/error.js'; @@ -314,9 +314,7 @@ export async function create_plugin(config, cwd) { ); if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) res.write(rendered.body); - res.end(); + setResponse(res, rendered); } else { not_found(res); } diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 605497b00c6f..cd8ef1b7178d 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -7,6 +7,7 @@ import { pathToFileURL } from 'url'; import { getRawBody } from '../../node.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; +import { to_headers } from '../../utils/http.js'; /** @param {string} dir */ const mutable = (dir) => @@ -97,7 +98,7 @@ export async function preview({ const rendered = await app.render( new Request(`${protocol}://${host}${initial_url}`, { method: req.method, - headers: get_headers(req.headers), + headers: to_headers(req.headers), body }) ); @@ -165,20 +166,3 @@ async function get_server(use_https, user_config, handler) { : http.createServer(handler) ); } - -/** @param {import('http').IncomingHttpHeaders} object */ -function get_headers(object) { - const headers = new Headers(); - - for (const key in object) { - if (key === 'set-cookie') { - object[key]?.forEach((value) => { - headers.append(key, value); - }); - } else { - headers.set(key, /** @type {string} */ (object[key])); - } - } - - return headers; -} diff --git a/packages/kit/src/hooks.js b/packages/kit/src/hooks.js index 23b4214c64c1..5fb1d7d7be3f 100644 --- a/packages/kit/src/hooks.js +++ b/packages/kit/src/hooks.js @@ -12,7 +12,7 @@ export function sequence(...handlers) { /** * @param {number} i * @param {import('types/hooks').RequestEvent} event - * @returns {import('types/helper').MaybePromise} + * @returns {import('types/helper').MaybePromise} */ function apply_handle(i, event) { const handle = handlers[i]; diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js index 95440b730268..b7cd213d9d59 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node.js @@ -1,4 +1,4 @@ -// TODO this should eventually be replaced with a ReadableStream equivalent +import { Readable } from 'stream'; /** @type {import('@sveltejs/kit/node').GetRawBody} */ export function getRawBody(req) { @@ -62,6 +62,14 @@ export async function getRequest(base, req) { /** @type {import('@sveltejs/kit/node').SetResponse} */ export async function setResponse(res, response) { res.writeHead(response.status, Object.fromEntries(response.headers)); - if (response.body) res.write(new Uint8Array(await response.arrayBuffer())); - res.end(); + + if (response.body instanceof Readable) { + response.body.pipe(res); + } else { + if (response.body) { + res.write(await response.arrayBuffer()); + } + + res.end(); + } } diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 3f4de2cf5f95..c19febc01339 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,13 +1,12 @@ -import { get_single_valued_header } from '../../utils/http.js'; -import { decode_params, lowercase_keys } from './utils.js'; +import { to_headers } from '../../utils/http.js'; +import { hash } from '../hash.js'; +import { decode_params } from './utils.js'; /** @param {string} body */ function error(body) { - return { - status: 500, - body, - headers: {} - }; + return new Response(body, { + status: 500 + }); } /** @param {unknown} s */ @@ -39,7 +38,7 @@ export function is_text(content_type) { * @param {import('types/hooks').RequestEvent} event * @param {import('types/internal').SSREndpoint} route * @param {RegExpExecArray} match - * @returns {Promise} + * @returns {Promise} */ export async function render_endpoint(event, route, match) { const mod = await route.load(); @@ -67,10 +66,11 @@ export async function render_endpoint(event, route, match) { return; } - let { status = 200, body, headers = {} } = response; + const { status = 200, body = {} } = response; + const headers = + response.headers instanceof Headers ? response.headers : to_headers(response.headers); - headers = lowercase_keys(headers); - const type = get_single_valued_header(headers, 'content-type'); + const type = headers.get('content-type'); if (!is_text(type) && !(body instanceof Uint8Array || is_string(body))) { return error( @@ -81,17 +81,38 @@ export async function render_endpoint(event, route, match) { /** @type {import('types/hooks').StrictBody} */ let normalized_body; - // ensure the body is an object - if ( - (typeof body === 'object' || typeof body === 'undefined') && - !(body instanceof Uint8Array) && - (!type || type.startsWith('application/json')) - ) { - headers = { ...headers, 'content-type': 'application/json; charset=utf-8' }; - normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body); + if (is_pojo(body) && (!type || type.startsWith('application/json'))) { + headers.set('content-type', 'application/json; charset=utf-8'); + normalized_body = JSON.stringify(body); } else { normalized_body = /** @type {import('types/hooks').StrictBody} */ (body); } - return { status, body: normalized_body, headers }; + // TODO or Uint8Array? + if (typeof normalized_body === 'string' && !headers.has('etag')) { + const cache_control = headers.get('cache-control'); + if (!cache_control || !/(no-store|immutable)/.test(cache_control)) { + headers.set('etag', `"${hash(normalized_body)}"`); + } + } + + return new Response(normalized_body, { + status, + headers + }); +} + +/** @param {any} body */ +function is_pojo(body) { + if (typeof body !== 'object') return false; + if (body instanceof Uint8Array) return false; + + // body could be a node Readable, but we don't want to import + // node built-ins, so we use duck typing + if (body._readableState && body._writableState && body._events) return false; + + // similarly, it could be a web ReadableStream + if (body[Symbol.toStringTag] === 'ReadableStream') return false; + + return true; } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index ab61fb905ca9..7209048a8fd8 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -23,12 +23,12 @@ export async function respond(request, options, state = {}) { if (url.search === '?') url.search = ''; - return { + return new Response(undefined, { status: 301, headers: { location: url.pathname + url.search } - }; + }); } } @@ -48,11 +48,9 @@ export async function respond(request, options, state = {}) { const verb = allowed.length === 0 ? 'enabled' : 'allowed'; const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`; - return { - status: 400, - headers: {}, - body - }; + return new Response(body, { + status: 400 + }); } } else { throw new Error(`${parameter}=${method_override} is only allowed with POST requests`); @@ -136,43 +134,37 @@ export async function respond(request, options, state = {}) { : await render_page(event, route, match, options, state, ssr); if (response) { - // inject ETags for 200 responses, if the endpoint - // doesn't have its own ETag handling - if (response.status === 200 && !response.headers.etag) { - const cache_control = get_single_valued_header(response.headers, 'cache-control'); - if (!cache_control || !/(no-store|immutable)/.test(cache_control)) { - let if_none_match_value = request.headers.get('if-none-match'); - // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives - if (if_none_match_value?.startsWith('W/"')) { - if_none_match_value = if_none_match_value.substring(2); - } + // respond with 304 if etag matches + if (response.status === 200 && response.headers.has('etag')) { + let if_none_match_value = request.headers.get('if-none-match'); - const etag = `"${hash(response.body || '')}"`; - - if (if_none_match_value === etag) { - /** @type {import('types/helper').ResponseHeaders} */ - const headers = { etag }; - - // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 - for (const key of [ - 'cache-control', - 'content-location', - 'date', - 'expires', - 'vary' - ]) { - if (key in response.headers) { - headers[key] = /** @type {string} */ (response.headers[key]); - } - } + // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives + if (if_none_match_value?.startsWith('W/"')) { + if_none_match_value = if_none_match_value.substring(2); + } - return { - status: 304, - headers - }; + const etag = /** @type {string} */ (response.headers.get('etag')); + + if (if_none_match_value === etag) { + const headers = new Headers({ etag }); + + // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + for (const key of [ + 'cache-control', + 'content-location', + 'date', + 'expires', + 'vary' + ]) { + if (key in response.headers) { + headers.set(key, /** @type {string} */ (response.headers.get(key))); + } } - response.headers['etag'] = etag; + return new Response(undefined, { + status: 304, + headers + }); } } @@ -221,11 +213,9 @@ export async function respond(request, options, state = {}) { } catch (/** @type {unknown} */ e) { const error = coalesce_to_error(e); - return { - status: 500, - headers: {}, - body: options.dev ? error.stack : error.message - }; + return new Response(options.dev ? error.stack : error.message, { + status: 500 + }); } } } diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 2520c4262a43..8ead58326e12 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -8,16 +8,14 @@ import { respond } from './respond.js'; * @param {import('types/internal').SSRRenderOptions} options * @param {import('types/internal').SSRRenderState} state * @param {boolean} ssr - * @returns {Promise} + * @returns {Promise} */ export async function render_page(event, route, match, options, state, ssr) { if (state.initiator === route) { // infinite request cycle detected - return { - status: 404, - headers: {}, - body: `Not found: ${event.url.pathname}` - }; + return new Response(`Not found: ${event.url.pathname}`, { + status: 404 + }); } const params = route.params ? decode_params(route.params(match)) : {}; @@ -43,10 +41,8 @@ export async function render_page(event, route, match, options, state, ssr) { // rather than render the error page — which could lead to an // infinite loop, if the `load` belonged to the root layout, // we respond with a bare-bones 500 - return { - status: 500, - headers: {}, - body: `Bad request in load function: failed to fetch ${state.fetched}` - }; + return new Response(`Bad request in load function: failed to fetch ${state.fetched}`, { + status: 500 + }); } } diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index 892ed3b6c059..cf87f94d3743 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -164,13 +164,7 @@ export async function load_node({ state.prerender.dependencies.set(relative, rendered); } - // Set-Cookie must be filtered out (done below) and that's the only header value that - // can be an array so we know we have only simple values - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - response = new Response(rendered.body, { - status: rendered.status, - headers: /** @type {Record} */ (rendered.headers) - }); + response = rendered; } else { // we can't load the endpoint from our own manifest, // so we need to make an actual HTTP request diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index db66d9949841..0cb59e443144 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -200,32 +200,29 @@ export async function render_response({ } } - /** @type {import('types/helper').ResponseHeaders} */ - const headers = { - 'content-type': 'text/html' - }; + const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2); + const assets = + options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); + + const html = options.template({ head, body, assets }); + + const headers = new Headers({ + 'content-type': 'text/html', + etag: `"${hash(html)}"` + }); if (maxage) { - headers['cache-control'] = `${is_private ? 'private' : 'public'}, max-age=${maxage}`; + headers.set('cache-control', `${is_private ? 'private' : 'public'}, max-age=${maxage}`); } if (!options.floc) { - headers['permissions-policy'] = 'interest-cohort=()'; + headers.set('permissions-policy', 'interest-cohort=()'); } - const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2); - const assets = - options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); - - return { + return new Response(html, { status, - headers, - body: options.template({ - head, - body, - assets - }) - }; + headers + }); } /** diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index f7f0c0c84699..791f19158122 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -5,7 +5,6 @@ import { coalesce_to_error } from '../../../utils/error.js'; /** * @typedef {import('./types.js').Loaded} Loaded - * @typedef {import('types/hooks').ServerResponse} ServerResponse * @typedef {import('types/internal').SSRNode} SSRNode * @typedef {import('types/internal').SSRRenderOptions} SSRRenderOptions * @typedef {import('types/internal').SSRRenderState} SSRRenderState @@ -21,7 +20,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; * params: Record; * ssr: boolean; * }} opts - * @returns {Promise} + * @returns {Promise} */ export async function respond(opts) { const { event, options, state, $session, route, ssr } = opts; @@ -71,10 +70,9 @@ export async function respond(opts) { 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: {} - }; + return new Response(undefined, { + status: 204 + }); } /** @type {Array} */ @@ -114,12 +112,12 @@ export async function respond(opts) { if (loaded.loaded.redirect) { return with_cookies( - { + new Response(undefined, { status: loaded.loaded.status, headers: { location: encodeURI(loaded.loaded.redirect) } - }, + }), set_cookie_headers ); } @@ -258,12 +256,14 @@ function get_page_config(leaf, options) { } /** - * @param {ServerResponse} response + * @param {Response} response * @param {string[]} set_cookie_headers */ function with_cookies(response, set_cookie_headers) { if (set_cookie_headers.length) { - response.headers['set-cookie'] = set_cookie_headers; + set_cookie_headers.forEach((value) => { + response.headers.append('set-cookie', value); + }); } return response; } 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 383b77d3b998..8eb5c1730e29 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -80,10 +80,8 @@ export async function respond_with_error({ event, options, state, $session, stat options.handle_error(error, event); - return { - status: 500, - headers: {}, - body: error.stack - }; + return new Response(error.stack, { + status: 500 + }); } } diff --git a/packages/kit/src/utils/http.js b/packages/kit/src/utils/http.js index b2c0c44bbf10..99219cd41b3e 100644 --- a/packages/kit/src/utils/http.js +++ b/packages/kit/src/utils/http.js @@ -19,3 +19,26 @@ export function get_single_valued_header(headers, key) { } return value; } + +/** @param {Partial | undefined} object */ +export function to_headers(object) { + const headers = new Headers(); + + if (object) { + for (const key in object) { + const value = object[key]; + + if (value) { + if (typeof value === 'string') { + headers.set(key, value); + } else { + value.forEach((value) => { + headers.append(key, value); + }); + } + } + } + } + + return headers; +} diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index 6723452ff60e..373522078de2 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -36,14 +36,9 @@ export const handle = sequence( } const response = await resolve(event, { ssr: !event.url.pathname.startsWith('/no-ssr') }); + response.headers.set('set-cookie', 'name=SvelteKit; path=/; HttpOnly'); - return { - ...response, - headers: { - ...response.headers, - 'set-cookie': 'name=SvelteKit; path=/; HttpOnly' - } - }; + return response; } ); diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js new file mode 100644 index 000000000000..a845c86ad2f6 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js @@ -0,0 +1,8 @@ +export function get() { + return { + headers: { + 'x-foo': 'bar' + }, + body: 'ok' + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js new file mode 100644 index 000000000000..e6be7f259d62 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js @@ -0,0 +1,8 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function get() { + return { + headers: new Headers({ + 'X-Foo': 'bar' + }) + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js new file mode 100644 index 000000000000..2a6715734643 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js @@ -0,0 +1,2 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export const get = ({ url }) => fetch(`http://localhost:${url.searchParams.get('port')}`); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 469ebe3d9eeb..b03ffe539864 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -419,6 +419,31 @@ test.describe.parallel('Endpoints', () => { expect(await page.textContent('h1')).toBe(random); }); + + test('allows headers to be a Headers object', async ({ request }) => { + const response = await request.get('/endpoint-output/headers-object'); + + expect(response.headers()['x-foo']).toBe('bar'); + }); + + test('allows return value to be a Response', async ({ request }) => { + const { server, port } = await start_server((req, res) => { + res.writeHead(200, { + 'X-Foo': 'bar' + }); + + res.end('ok'); + }); + + try { + const response = await request.get(`/endpoint-output/proxy?port=${port}`); + + expect(await response.text()).toBe('ok'); + expect(response.headers()['x-foo']).toBe('bar'); + } finally { + server.close(); + } + }); }); test.describe.parallel('Encoded paths', () => { diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts index 9497d5785ea2..8f2b979202d6 100644 --- a/packages/kit/types/endpoint.d.ts +++ b/packages/kit/types/endpoint.d.ts @@ -5,7 +5,7 @@ type DefaultBody = JSONString | Uint8Array; export interface EndpointOutput { status?: number; - headers?: Partial; + headers?: Headers | Partial; body?: Body; } @@ -13,5 +13,7 @@ export interface RequestHandler< Locals = Record, Output extends DefaultBody = DefaultBody > { - (request: RequestEvent): MaybePromise, Fallthrough>>; + (request: RequestEvent): MaybePromise< + Either, Fallthrough> + >; } diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index b298e66dc9d6..cb3e7d4e0558 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -1,4 +1,4 @@ -import { MaybePromise, ResponseHeaders } from './helper'; +import { MaybePromise } from './helper'; export type StrictBody = string | Uint8Array; @@ -9,12 +9,6 @@ export interface RequestEvent> { locals: Locals; } -export interface ServerResponse { - status: number; - headers: Partial; - body?: StrictBody; -} - export interface GetSession, Session = any> { (event: RequestEvent): MaybePromise; } @@ -26,8 +20,8 @@ export interface ResolveOpts { export interface Handle> { (input: { event: RequestEvent; - resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; - }): MaybePromise; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + }): MaybePromise; } // internally, `resolve` could return `undefined`, so we differentiate InternalHandle @@ -35,11 +29,8 @@ export interface Handle> { export interface InternalHandle> { (input: { event: RequestEvent; - resolve( - event: RequestEvent, - opts?: ResolveOpts - ): MaybePromise; - }): MaybePromise; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + }): MaybePromise; } export interface HandleError> { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index e4ccdcadf49b..3ea28f28c529 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -7,11 +7,4 @@ export { App, SSRManifest } from './app'; export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; export { EndpointOutput, RequestHandler } from './endpoint'; export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page'; -export { - ExternalFetch, - GetSession, - Handle, - HandleError, - ServerResponse as Response, - ResolveOpts -} from './hooks'; +export { ExternalFetch, GetSession, Handle, HandleError, ResolveOpts } from './hooks'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 9bd9842032a2..b82ff4b26720 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,14 +1,7 @@ import { OutputAsset, OutputChunk } from 'rollup'; import { RequestHandler } from './endpoint'; import { InternalApp, SSRManifest } from './app'; -import { - ExternalFetch, - GetSession, - HandleError, - InternalHandle, - RequestEvent, - ServerResponse -} from './hooks'; +import { ExternalFetch, GetSession, HandleError, InternalHandle, RequestEvent } from './hooks'; import { Load } from './page'; import { Either, Fallthrough } from './helper'; @@ -17,7 +10,7 @@ type PageId = string; export interface PrerenderOptions { fallback?: string; all: boolean; - dependencies: Map; + dependencies: Map; } export interface AppModule { @@ -238,6 +231,6 @@ export interface MethodOverride { export interface Respond { (request: Request, options: SSRRenderOptions, state?: SSRRenderState): Promise< - ServerResponse | undefined + Response | undefined >; } From d4d7ac9e679ba60217d473f3b4efc21cedce512e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 16:52:20 -0500 Subject: [PATCH 15/36] fixes --- packages/kit/src/runtime/server/endpoint.js | 21 +++++++++++++-------- packages/kit/src/runtime/server/index.js | 5 ++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index c19febc01339..794217e2cb55 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -88,8 +88,10 @@ export async function render_endpoint(event, route, match) { normalized_body = /** @type {import('types/hooks').StrictBody} */ (body); } - // TODO or Uint8Array? - if (typeof normalized_body === 'string' && !headers.has('etag')) { + if ( + (typeof normalized_body === 'string' || normalized_body instanceof Uint8Array) && + !headers.has('etag') + ) { const cache_control = headers.get('cache-control'); if (!cache_control || !/(no-store|immutable)/.test(cache_control)) { headers.set('etag', `"${hash(normalized_body)}"`); @@ -105,14 +107,17 @@ export async function render_endpoint(event, route, match) { /** @param {any} body */ function is_pojo(body) { if (typeof body !== 'object') return false; - if (body instanceof Uint8Array) return false; - // body could be a node Readable, but we don't want to import - // node built-ins, so we use duck typing - if (body._readableState && body._writableState && body._events) return false; + if (body) { + if (body instanceof Uint8Array) return false; - // similarly, it could be a web ReadableStream - if (body[Symbol.toStringTag] === 'ReadableStream') return false; + // body could be a node Readable, but we don't want to import + // node built-ins, so we use duck typing + if (body._readableState && body._writableState && body._events) return false; + + // similarly, it could be a web ReadableStream + if (body[Symbol.toStringTag] === 'ReadableStream') return false; + } return true; } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 7209048a8fd8..def40b87e672 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -156,9 +156,8 @@ export async function respond(request, options, state = {}) { 'expires', 'vary' ]) { - if (key in response.headers) { - headers.set(key, /** @type {string} */ (response.headers.get(key))); - } + const value = response.headers.get(key); + if (value) headers.set(key, value); } return new Response(undefined, { From fb4d6082ce8a2585e9fc1881d9075d28065645b7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 16:56:54 -0500 Subject: [PATCH 16/36] lint --- packages/kit/src/runtime/server/index.js | 2 -- packages/kit/src/utils/http.js | 22 ---------------------- packages/kit/src/utils/http.spec.js | 14 -------------- 3 files changed, 38 deletions(-) delete mode 100644 packages/kit/src/utils/http.spec.js diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index def40b87e672..aadcc7bba432 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -2,8 +2,6 @@ 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 { hash } from '../hash.js'; -import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; /** @type {import('types/internal').Respond} */ diff --git a/packages/kit/src/utils/http.js b/packages/kit/src/utils/http.js index 99219cd41b3e..8b6f1a1b5458 100644 --- a/packages/kit/src/utils/http.js +++ b/packages/kit/src/utils/http.js @@ -1,25 +1,3 @@ -/** - * @param {Record} headers - * @param {string} key - * @returns {string | undefined} - * @throws {Error} - */ -export function get_single_valued_header(headers, key) { - const value = headers[key]; - if (Array.isArray(value)) { - if (value.length === 0) { - return undefined; - } - if (value.length > 1) { - throw new Error( - `Multiple headers provided for ${key}. Multiple may be provided only for set-cookie` - ); - } - return value[0]; - } - return value; -} - /** @param {Partial | undefined} object */ export function to_headers(object) { const headers = new Headers(); diff --git a/packages/kit/src/utils/http.spec.js b/packages/kit/src/utils/http.spec.js deleted file mode 100644 index 028ac2a4e832..000000000000 --- a/packages/kit/src/utils/http.spec.js +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from 'uvu'; -import * as assert from 'uvu/assert'; -import { get_single_valued_header } from './http.js'; - -test('get_single_valued_header', () => { - assert.equal('123', get_single_valued_header({ key: '123' }, 'key')); - assert.equal('world', get_single_valued_header({ hello: 'world' }, 'hello')); - assert.equal(undefined, get_single_valued_header({}, 'key')); - assert.equal('a', get_single_valued_header({ name: ['a'] }, 'name')); - assert.equal(undefined, get_single_valued_header({ name: ['a'] }, 'undefinedName')); - assert.throws(() => get_single_valued_header({ name: ['a', 'b', 'c'] }, 'name')); -}); - -test.run(); From aed1edc02e7059039bcf15f29520e68f8d19b6eb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 17:34:14 -0500 Subject: [PATCH 17/36] update docs --- documentation/docs/01-routing.md | 40 +++++++-------- documentation/docs/04-hooks.md | 67 +++++++++----------------- documentation/docs/05-modules.md | 8 +-- documentation/docs/10-adapters.md | 2 +- documentation/docs/14-configuration.md | 6 --- 5 files changed, 44 insertions(+), 79 deletions(-) diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md index 54d49fb53c2d..964a9930224e 100644 --- a/documentation/docs/01-routing.md +++ b/documentation/docs/01-routing.md @@ -54,28 +54,19 @@ Endpoints are modules written in `.js` (or `.ts`) files that export functions co // type of string[] is only for set-cookie // everything else must be a type of string type ResponseHeaders = Record; -type RequestHeaders = Record; -export type RawBody = null | Uint8Array; - -type ParameterizedBody = Body extends FormData - ? ReadOnlyFormData - : (string | RawBody | ReadOnlyFormData) & Body; - -export interface Request, Body = unknown> { +export interface RequestEvent> { + request: Request; url: URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; params: Record; - body: ParameterizedBody; locals: Locals; } -type DefaultBody = JSONResponse | Uint8Array; -export interface EndpointOutput { +type Body = JSONResponse | Uint8Array | string | ReadableStream | stream.Readable; + +export interface EndpointOutput { status?: number; - headers?: ResponseHeaders; + headers?: HeadersInit; body?: Body; } @@ -103,9 +94,7 @@ import db from '$lib/database'; export async function get({ params }) { // the `slug` parameter is available because this file // is called [slug].json.js - const { slug } = params; - - const article = await db.get(slug); + const article = await db.get(params.slug); if (article) { return { @@ -114,6 +103,10 @@ export async function get({ params }) { } }; } + + return { + status: 404 + }; } ``` @@ -152,12 +145,13 @@ return { #### Body parsing -The `body` property of the request object will be provided in the case of POST requests: +The `request` object is an instance of the standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) class. As such, accessing the request body is easy: -- Text data (with content-type `text/plain`) will be parsed to a `string` -- JSON data (with content-type `application/json`) will be parsed to a `JSONValue` (an `object`, `Array`, or primitive). -- Form data (with content-type `application/x-www-form-urlencoded` or `multipart/form-data`) will be parsed to a read-only version of the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. -- All other data will be provided as a `Uint8Array` +```js +export async function post({ request }) { + const data = await request.formData(); // or .json(), or .text(), etc +} +``` #### HTTP Method Overrides diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index 7a3dcec0d897..f23e13bfecb5 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -8,11 +8,11 @@ An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exp ### handle -This function runs every time SvelteKit receives a request — whether that happens while the app is running, or during [prerendering](#page-options-prerender) — and determines the response. It receives the `request` object and a function called `resolve`, which invokes SvelteKit's router and generates a response (rendering a page, or invoking an endpoint) accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example). +This function runs every time SvelteKit receives a request — whether that happens while the app is running, or during [prerendering](#page-options-prerender) — and determines the response. It receives an `event` object and a function called `resolve`, which invokes SvelteKit's router and generates a response (rendering a page, or invoking an endpoint) accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example). > Requests for static assets — which includes pages that were already prerendered — are _not_ handled by SvelteKit. -If unimplemented, defaults to `({ request, resolve }) => resolve(request)`. +If unimplemented, defaults to `({ event, resolve }) => resolve(event)`. ```ts // Declaration types for Hooks @@ -21,41 +21,23 @@ If unimplemented, defaults to `({ request, resolve }) => resolve(request)`. // type of string[] is only for set-cookie // everything else must be a type of string type ResponseHeaders = Record; -type RequestHeaders = Record; -export type RawBody = null | Uint8Array; - -type ParameterizedBody = Body extends FormData - ? ReadOnlyFormData - : (string | RawBody | ReadOnlyFormData) & Body; - -export interface Request, Body = unknown> { +export interface RequestEvent> { + request: Request; url: URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; params: Record; - body: ParameterizedBody; locals: Locals; } -type StrictBody = string | Uint8Array; - -export interface Response { - status: number; - headers: ResponseHeaders; - body?: StrictBody; -} - export interface ResolveOpts { ssr?: boolean; } -export interface Handle, Body = unknown> { +export interface Handle> { (input: { - request: ServerRequest; - resolve(request: ServerRequest, opts?: ResolveOpts): MaybePromise; - }): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + }): MaybePromise; } ``` @@ -67,14 +49,9 @@ export async function handle({ request, resolve }) { request.locals.user = await getUserInformation(request.headers.cookie); const response = await resolve(request); + response.headers.set('x-custom-header', 'potato'); - return { - ...response, - headers: { - ...response.headers, - 'x-custom-header': 'potato' - } - }; + return response; } ``` @@ -107,16 +84,16 @@ If unimplemented, SvelteKit will log the error with default formatting. ```ts // Declaration types for handleError hook -export interface HandleError, Body = unknown> { - (input: { error: Error & { frame?: string }; request: Request }): void; +export interface HandleError> { + (input: { error: Error & { frame?: string }; event: RequestEvent }): void; } ``` ```js /** @type {import('@sveltejs/kit').HandleError} */ -export async function handleError({ error, request }) { +export async function handleError({ error, event }) { // example integration with https://sentry.io/ - Sentry.captureException(error, { request }); + Sentry.captureException(error, { event }); } ``` @@ -124,29 +101,29 @@ export async function handleError({ error, request }) { ### getSession -This function takes the `request` object and returns a `session` object that is [accessible on the client](#modules-$app-stores) and therefore must be safe to expose to users. It runs whenever SvelteKit server-renders a page. +This function takes the `event` object and returns a `session` object that is [accessible on the client](#modules-$app-stores) and therefore must be safe to expose to users. It runs whenever SvelteKit server-renders a page. If unimplemented, session is `{}`. ```ts // Declaration types for getSession hook -export interface GetSession, Body = unknown, Session = any> { - (request: Request): Session | Promise; +export interface GetSession, Session = any> { + (event: RequestEvent): Session | Promise; } ``` ```js /** @type {import('@sveltejs/kit').GetSession} */ -export function getSession(request) { - return request.locals.user +export function getSession(event) { + return event.locals.user ? { user: { // only include properties needed client-side — // exclude anything else attached to the user // like access tokens etc - name: request.locals.user.name, - email: request.locals.user.email, - avatar: request.locals.user.avatar + name: event.locals.user.name, + email: event.locals.user.email, + avatar: event.locals.user.avatar } } : {}; diff --git a/documentation/docs/05-modules.md b/documentation/docs/05-modules.md index 13b979faa7da..d536dd0a0002 100644 --- a/documentation/docs/05-modules.md +++ b/documentation/docs/05-modules.md @@ -94,13 +94,13 @@ This module provides a helper function to sequence multiple `handle` calls. ```js import { sequence } from '@sveltejs/kit/hooks'; -async function first({ request, resolve }) { +async function first({ event, resolve }) { console.log('first'); - return await resolve(request); + return await resolve(event); } -async function second({ request, resolve }) { +async function second({ event, resolve }) { console.log('second'); - return await resolve(request); + return await resolve(event); } export const handle = sequence(first, second); diff --git a/documentation/docs/10-adapters.md b/documentation/docs/10-adapters.md index adc83623dfa9..01296837dfd3 100644 --- a/documentation/docs/10-adapters.md +++ b/documentation/docs/10-adapters.md @@ -92,7 +92,7 @@ Within the `adapt` method, there are a number of things that an adapter should d - Output code that: - Imports `App` from `${builder.getServerDirectory()}/app.js` - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` - - Listens for requests from the platform, converts them to a a [SvelteKit request](#hooks-handle), calls the `render` function to generate a [SvelteKit response](#hooks-handle) and responds with it + - Listens for requests from the platform, converts them to a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request if necessary), calls the `render` function to generate a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/install-fetch` helper for platforms that can use `node-fetch` - Bundle the output to avoid needing to install dependencies on the target platform, if necessary - Put the user's static files and the generated JS/CSS in the correct location for the target platform diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md index 5f057ecf3efa..40f60380f0ed 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/14-configuration.md @@ -26,11 +26,6 @@ const config = { template: 'src/app.html' }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -55,7 +50,6 @@ const config = { entries: ['*'], onError: 'fail' }, - protocol: null, router: true, serviceWorker: { register: true, From 90c30903908a35cabfe044c066667a1d36f9b337 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 17:50:45 -0500 Subject: [PATCH 18/36] update adapter-node docs --- packages/adapter-node/README.md | 36 +++++++++++++++++++++++++++++---- packages/adapter-node/index.js | 5 ++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index 71cf7d353f2d..55db2cdb1e96 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -17,8 +17,14 @@ export default { out: 'build', precompress: false, env: { + path: 'SOCKET_PATH', host: 'HOST', - port: 'PORT' + port: 'PORT', + base: undefined, + headers: { + protocol: undefined, + host: 'host' + } } }) } @@ -43,17 +49,39 @@ By default, the server will accept connections on `0.0.0.0` using port 3000. The HOST=127.0.0.1 PORT=4000 node build ``` -You can specify different environment variables if necessary using the `env` option: +Node HTTP servers don't give SvelteKit a reliable way to know the URL that is currently being requested. The simplest way to tell SvelteKit where the app is being served is to set the `BASE` environment variable: + +``` +BASE=https://my.site node build +``` + +With this, a request for the `/stuff` pathname will correctly resolve to `https://my.site/stuff`. Alternatively, you can specify headers that tell SvelteKit about the request protocol and host, from which it can construct the base URL: + +``` +PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build +``` + +> [`x-forwarded-proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) and [`x-forwarded-host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) are de facto standard headers that forward the original protocol and host if you're using a reverse proxy (think load balancers and CDNs). You should only set these variables if you trust the reverse proxy. + +All of these environment variables can be changed, if necessary, using the `env` option: ```js env: { host: 'MY_HOST_VARIABLE', - port: 'MY_PORT_VARIABLE' + port: 'MY_PORT_VARIABLE', + base: 'MY_BASEURL', + headers: { + protocol: 'MY_PROTOCOL_HEADER', + host: 'MY_HOST_HEADER' + } } ``` ``` -MY_HOST_VARIABLE=127.0.0.1 MY_PORT_VARIABLE=4000 node build +MY_HOST_VARIABLE=127.0.0.1 \ +MY_PORT_VARIABLE=4000 \ +MY_BASEURL=https://my.site \ +node build ``` ## Custom server diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 821272abb5a5..af1c59dd5127 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -18,7 +18,10 @@ export default function ({ host: host_env = 'HOST', port: port_env = 'PORT', base: base_env, - headers: { protocol: protocol_header_env, host: host_header_env = 'host' } + headers: { + protocol: protocol_header_env = 'PROTOCOL_HEADER', + host: host_header_env = 'HOST_HEADER' + } } = {} } = {}) { return { From c2e93991d25e639e4d0c25b5f5efcf4409099c91 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 17:51:36 -0500 Subject: [PATCH 19/36] docs --- documentation/docs/14-configuration.md | 28 -------------------------- 1 file changed, 28 deletions(-) diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md index 40f60380f0ed..d1fcc556e1a7 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/14-configuration.md @@ -105,30 +105,6 @@ Permissions-Policy: interest-cohort=() > This only applies to server-rendered responses — headers for prerendered pages (e.g. created with [adapter-static](/~https://github.com/sveltejs/kit/tree/master/packages/adapter-static)) are determined by the hosting platform. -### headers - -The current page or endpoint's `url` is, in some environments, derived from the request protocol (normally `https`) and the host, which is taken from the `Host` header by default. - -If your app is behind a reverse proxy (think load balancers and CDNs) then the `Host` header will be incorrect. In most cases, the underlying protocol and host are exposed via the [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) and [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) headers, which can be specified in your config: - -```js -// svelte.config.js -export default { - kit: { - headers: { - host: 'X-Forwarded-Host', - protocol: 'X-Forwarded-Proto' - } - } -}; -``` - -**You should only do this if you trust the reverse proxy**, which is why it isn't the default. - -### host - -A value that overrides the one derived from [`config.kit.headers.host`](#configuration-headers-host). - ### hydrate Whether to [hydrate](#page-options-hydrate) the server-rendered HTML with a client-side app. (It's rare that you would set this to `false` on an app-wide basis.) @@ -214,10 +190,6 @@ See [Prerendering](#page-options-prerender). An object containing zero or more o }; ``` -### protocol - -The protocol is assumed to be `'https'` (unless you're developing locally without the `--https` flag) unless [`config.kit.headers.protocol`](#configuration-headers-protocol) is set. If necessary, you can override it here. - ### router Enables or disables the client-side [router](#page-options-router) app-wide. From f05e724eb8e990c4078b7768a1dd08a55b813de1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 17:54:31 -0500 Subject: [PATCH 20/36] whoops --- documentation/docs/14-configuration.md | 28 -------------------------- 1 file changed, 28 deletions(-) diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md index 256ae46c90fb..d1fcc556e1a7 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/14-configuration.md @@ -105,34 +105,6 @@ Permissions-Policy: interest-cohort=() > This only applies to server-rendered responses — headers for prerendered pages (e.g. created with [adapter-static](/~https://github.com/sveltejs/kit/tree/master/packages/adapter-static)) are determined by the hosting platform. -# <<<<<<< HEAD - -### headers - -The current page or endpoint's `url` is, in some environments, derived from the request protocol (normally `https`) and the host, which is taken from the `Host` header by default. - -If your app is behind a reverse proxy (think load balancers and CDNs) then the `Host` header will be incorrect. In most cases, the underlying protocol and host are exposed via the [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) and [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) headers, which can be specified in your config: - -```js -// svelte.config.js -export default { - kit: { - headers: { - host: 'X-Forwarded-Host', - protocol: 'X-Forwarded-Proto' - } - } -}; -``` - -**You should only do this if you trust the reverse proxy**, which is why it isn't the default. - -### host - -A value that overrides the one derived from [`config.kit.headers.host`](#configuration-headers). - -> > > > > > > master - ### hydrate Whether to [hydrate](#page-options-hydrate) the server-rendered HTML with a client-side app. (It's rare that you would set this to `false` on an app-wide basis.) From 62c6a77ea5f824800d61902a234f90f9603cb3e5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 17:56:40 -0500 Subject: [PATCH 21/36] changesets --- .changeset/bright-icons-own.md | 5 +++++ .changeset/large-icons-complain.md | 5 +++++ .changeset/mighty-pandas-search.md | 10 ++++++++++ 3 files changed, 20 insertions(+) create mode 100644 .changeset/bright-icons-own.md create mode 100644 .changeset/large-icons-complain.md create mode 100644 .changeset/mighty-pandas-search.md diff --git a/.changeset/bright-icons-own.md b/.changeset/bright-icons-own.md new file mode 100644 index 000000000000..3ea59d3c1119 --- /dev/null +++ b/.changeset/bright-icons-own.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Allow endpoints to return a Response, or an object with Headers diff --git a/.changeset/large-icons-complain.md b/.changeset/large-icons-complain.md new file mode 100644 index 000000000000..bce27eefb5bd --- /dev/null +++ b/.changeset/large-icons-complain.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Expose standard Request object to endpoints diff --git a/.changeset/mighty-pandas-search.md b/.changeset/mighty-pandas-search.md new file mode 100644 index 000000000000..e19d96731dc3 --- /dev/null +++ b/.changeset/mighty-pandas-search.md @@ -0,0 +1,10 @@ +--- +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +change app.render signature to (request: Request) => Promise From d216ebbc6c63a31c91cfa8f1999841003ae728a4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:07:01 -0500 Subject: [PATCH 22/36] pointless commit to try and trick netlify into working --- packages/create-svelte/templates/default/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-svelte/templates/default/README.md b/packages/create-svelte/templates/default/README.md index cbee96e2b786..9c8dc797a0be 100644 --- a/packages/create-svelte/templates/default/README.md +++ b/packages/create-svelte/templates/default/README.md @@ -1,4 +1,4 @@ -# Default template +# Default SvelteKit template This README isn't part of the template; it is ignored, and replaced with [the shared README](../../shared/README.md) when a project is created. From 75abe9f4533d8b348035d593632bbd1274c18874 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:15:00 -0500 Subject: [PATCH 23/36] update template --- .../templates/default/src/hooks.ts | 19 +++++++++++-------- .../default/src/routes/todos/[uid].json.ts | 14 ++++++++------ .../default/src/routes/todos/_api.ts | 14 +++++++++----- .../default/src/routes/todos/index.json.ts | 14 ++++++++------ packages/kit/types/index.d.ts | 2 +- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/create-svelte/templates/default/src/hooks.ts b/packages/create-svelte/templates/default/src/hooks.ts index ce345fed108f..d767555dd9ea 100644 --- a/packages/create-svelte/templates/default/src/hooks.ts +++ b/packages/create-svelte/templates/default/src/hooks.ts @@ -2,19 +2,22 @@ import cookie from 'cookie'; import { v4 as uuid } from '@lukeed/uuid'; import type { Handle } from '@sveltejs/kit'; -export const handle: Handle = async ({ request, resolve }) => { - const cookies = cookie.parse(request.headers.cookie || ''); - request.locals.userid = cookies.userid || uuid(); +export const handle: Handle = async ({ event, resolve }) => { + const cookies = cookie.parse(event.request.headers.get('cookie') || ''); + event.locals.userid = cookies.userid || uuid(); - const response = await resolve(request); + const response = await resolve(event); if (!cookies.userid) { // if this is the first time the user has visited this app, // set a cookie so that we recognise them when they return - response.headers['set-cookie'] = cookie.serialize('userid', request.locals.userid, { - path: '/', - httpOnly: true - }); + response.headers.set( + 'set-cookie', + cookie.serialize('userid', event.locals.userid, { + path: '/', + httpOnly: true + }) + ); } return response; diff --git a/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts b/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts index 17891faf5979..0ca276dbcbf3 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts @@ -3,14 +3,16 @@ import type { RequestHandler } from '@sveltejs/kit'; import type { Locals } from '$lib/types'; // PATCH /todos/:uid.json -export const patch: RequestHandler = async (request) => { - return api(request, `todos/${request.locals.userid}/${request.params.uid}`, { - text: request.body.get('text'), - done: request.body.has('done') ? !!request.body.get('done') : undefined +export const patch: RequestHandler = async (event) => { + const data = await event.request.formData(); + + return api(event, `todos/${event.locals.userid}/${event.params.uid}`, { + text: data.get('text'), + done: data.has('done') ? !!data.get('done') : undefined }); }; // DELETE /todos/:uid.json -export const del: RequestHandler = async (request) => { - return api(request, `todos/${request.locals.userid}/${request.params.uid}`); +export const del: RequestHandler = async (event) => { + return api(event, `todos/${event.locals.userid}/${event.params.uid}`); }; diff --git a/packages/create-svelte/templates/default/src/routes/todos/_api.ts b/packages/create-svelte/templates/default/src/routes/todos/_api.ts index 2fd3385d1ed8..4e13eb029573 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/_api.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/_api.ts @@ -1,4 +1,4 @@ -import type { EndpointOutput, Request } from '@sveltejs/kit'; +import type { EndpointOutput, RequestEvent } from '@sveltejs/kit'; import type { Locals } from '$lib/types'; /* @@ -15,17 +15,17 @@ import type { Locals } from '$lib/types'; const base = 'https://api.svelte.dev'; export async function api( - request: Request, + event: RequestEvent, resource: string, data?: Record ): Promise { // user must have a cookie set - if (!request.locals.userid) { + if (!event.locals.userid) { return { status: 401 }; } const res = await fetch(`${base}/${resource}`, { - method: request.method, + method: event.request.method, headers: { 'content-type': 'application/json' }, @@ -36,7 +36,11 @@ export async function api( // behaviour is to show the URL corresponding to the form's "action" // attribute. in those cases, we want to redirect them back to the // /todos page, rather than showing the response - if (res.ok && request.method !== 'GET' && request.headers.accept !== 'application/json') { + if ( + res.ok && + event.request.method !== 'GET' && + event.request.headers.get('accept') !== 'application/json' + ) { return { status: 303, headers: { diff --git a/packages/create-svelte/templates/default/src/routes/todos/index.json.ts b/packages/create-svelte/templates/default/src/routes/todos/index.json.ts index 57d68a91dc2f..c300fd971491 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/index.json.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/index.json.ts @@ -3,9 +3,9 @@ import type { RequestHandler } from '@sveltejs/kit'; import type { Locals } from '$lib/types'; // GET /todos.json -export const get: RequestHandler = async (request) => { - // request.locals.userid comes from src/hooks.js - const response = await api(request, `todos/${request.locals.userid}`); +export const get: RequestHandler = async (event) => { + // event.locals.userid comes from src/hooks.js + const response = await api(event, `todos/${event.locals.userid}`); if (response.status === 404) { // user hasn't created a todo list. @@ -17,13 +17,15 @@ export const get: RequestHandler = async (request) => { }; // POST /todos.json -export const post: RequestHandler = async (request) => { - const response = await api(request, `todos/${request.locals.userid}`, { +export const post: RequestHandler = async (event) => { + const data = await event.request.formData(); + + const response = await api(event, `todos/${event.locals.userid}`, { // because index.svelte posts a FormData object, // request.body is _also_ a (readonly) FormData // object, which allows us to get form data // with the `body.get(key)` method - text: request.body.get('text') + text: data.get('text') }); return response; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3ea28f28c529..efd53489c127 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -7,4 +7,4 @@ export { App, SSRManifest } from './app'; export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; export { EndpointOutput, RequestHandler } from './endpoint'; export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page'; -export { ExternalFetch, GetSession, Handle, HandleError, ResolveOpts } from './hooks'; +export { ExternalFetch, GetSession, Handle, HandleError, RequestEvent, ResolveOpts } from './hooks'; From 8bbfc98439f4cb2c4570a0fd472704e042d116e5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:20:04 -0500 Subject: [PATCH 24/36] changeset --- .changeset/strong-schools-rule.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/strong-schools-rule.md diff --git a/.changeset/strong-schools-rule.md b/.changeset/strong-schools-rule.md new file mode 100644 index 000000000000..e90c67b134c9 --- /dev/null +++ b/.changeset/strong-schools-rule.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-node': patch +'@sveltejs/kit': patch +--- + +Remove protocol/host configuration options from Kit to adapter-node From 865d124a54dee3892ae3ed578d6858c807046988 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:30:07 -0500 Subject: [PATCH 25/36] work around zip-it-and-ship-it bug --- packages/create-svelte/templates/default/netlify.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/create-svelte/templates/default/netlify.toml b/packages/create-svelte/templates/default/netlify.toml index 88a39717479d..7dca1fcad039 100644 --- a/packages/create-svelte/templates/default/netlify.toml +++ b/packages/create-svelte/templates/default/netlify.toml @@ -4,3 +4,6 @@ [build.environment] NPM_FLAGS = "--version" # this prevents npm install from happening + +[functions] + node_bundler = "esbuild" \ No newline at end of file From 5293c71fe7bd6bd26d9ef2c2d1cb5aea324a0f2c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:42:17 -0500 Subject: [PATCH 26/36] Update .changeset/large-icons-complain.md --- .changeset/large-icons-complain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/large-icons-complain.md b/.changeset/large-icons-complain.md index bce27eefb5bd..91597f40feea 100644 --- a/.changeset/large-icons-complain.md +++ b/.changeset/large-icons-complain.md @@ -2,4 +2,4 @@ '@sveltejs/kit': patch --- -Expose standard Request object to endpoints +Breaking: Expose standard Request object to endpoints and hooks From 501dd330e3bb32450a66678205a2d69d3a5032c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:42:38 -0500 Subject: [PATCH 27/36] Update .changeset/mighty-pandas-search.md --- .changeset/mighty-pandas-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mighty-pandas-search.md b/.changeset/mighty-pandas-search.md index e19d96731dc3..39992e23c76f 100644 --- a/.changeset/mighty-pandas-search.md +++ b/.changeset/mighty-pandas-search.md @@ -7,4 +7,4 @@ '@sveltejs/kit': patch --- -change app.render signature to (request: Request) => Promise +Breaking: change app.render signature to (request: Request) => Promise From 9c20e7eb1b4304f22a1c5f0676724c73b3ed64c6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 18 Jan 2022 18:42:58 -0500 Subject: [PATCH 28/36] Update .changeset/strong-schools-rule.md --- .changeset/strong-schools-rule.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/strong-schools-rule.md b/.changeset/strong-schools-rule.md index e90c67b134c9..034c3d4aa68b 100644 --- a/.changeset/strong-schools-rule.md +++ b/.changeset/strong-schools-rule.md @@ -3,4 +3,4 @@ '@sveltejs/kit': patch --- -Remove protocol/host configuration options from Kit to adapter-node +Breaking: Remove protocol/host configuration options from Kit to adapter-node From f3286d7791b7ba7e525eb9ba1e0515a7d5ac2faf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 09:44:38 -0500 Subject: [PATCH 29/36] Update documentation/docs/04-hooks.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/04-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index f23e13bfecb5..cbdde3afb989 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -8,7 +8,7 @@ An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exp ### handle -This function runs every time SvelteKit receives a request — whether that happens while the app is running, or during [prerendering](#page-options-prerender) — and determines the response. It receives an `event` object and a function called `resolve`, which invokes SvelteKit's router and generates a response (rendering a page, or invoking an endpoint) accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example). +This function runs every time SvelteKit receives a request — whether that happens while the app is running, or during [prerendering](#page-options-prerender) — and determines the response. It receives an `event` object representing the request and a function called `resolve`, which invokes SvelteKit's router and generates a response (rendering a page, or invoking an endpoint) accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example). > Requests for static assets — which includes pages that were already prerendered — are _not_ handled by SvelteKit. From ca2760ef459abe9a2baa833f5f3e5bb4a64d8d6c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 09:45:01 -0500 Subject: [PATCH 30/36] Update packages/adapter-node/README.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- packages/adapter-node/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index 55db2cdb1e96..a343608b9a20 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -49,7 +49,7 @@ By default, the server will accept connections on `0.0.0.0` using port 3000. The HOST=127.0.0.1 PORT=4000 node build ``` -Node HTTP servers don't give SvelteKit a reliable way to know the URL that is currently being requested. The simplest way to tell SvelteKit where the app is being served is to set the `BASE` environment variable: +HTTP doesn't give SvelteKit a reliable way to know the URL that is currently being requested. The simplest way to tell SvelteKit where the app is being served is to set the `BASE` environment variable: ``` BASE=https://my.site node build From 2672f5071c25ba4341f52e954fc6b58d8038e7cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 09:57:00 -0500 Subject: [PATCH 31/36] reduce indentation --- packages/kit/src/utils/http.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/utils/http.js b/packages/kit/src/utils/http.js index 8b6f1a1b5458..497d20951364 100644 --- a/packages/kit/src/utils/http.js +++ b/packages/kit/src/utils/http.js @@ -5,15 +5,14 @@ export function to_headers(object) { if (object) { for (const key in object) { const value = object[key]; + if (!value) continue; - if (value) { - if (typeof value === 'string') { - headers.set(key, value); - } else { - value.forEach((value) => { - headers.append(key, value); - }); - } + if (typeof value === 'string') { + headers.set(key, value); + } else { + value.forEach((value) => { + headers.append(key, value); + }); } } } From 9b506f1fafa8ce34c7ab3800d68d7bf5d6642cee Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 10:28:51 -0500 Subject: [PATCH 32/36] add more types to adapters, to reflect these changes --- .../adapter-cloudflare-workers/files/entry.d.ts | 11 +++++++++++ packages/adapter-cloudflare-workers/files/entry.js | 2 +- packages/adapter-cloudflare-workers/index.js | 3 ++- packages/adapter-cloudflare/files/worker.d.ts | 11 +++++++++++ packages/adapter-cloudflare/files/worker.js | 6 +++--- packages/adapter-cloudflare/index.js | 3 ++- packages/adapter-cloudflare/tsconfig.json | 13 +++++++++++++ packages/adapter-netlify/index.js | 8 ++++++-- packages/adapter-netlify/rollup.config.js | 2 +- packages/adapter-netlify/src/handler.d.ts | 4 ++++ packages/adapter-netlify/src/handler.js | 4 +--- 11 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 packages/adapter-cloudflare-workers/files/entry.d.ts create mode 100644 packages/adapter-cloudflare/files/worker.d.ts create mode 100644 packages/adapter-cloudflare/tsconfig.json create mode 100644 packages/adapter-netlify/src/handler.d.ts diff --git a/packages/adapter-cloudflare-workers/files/entry.d.ts b/packages/adapter-cloudflare-workers/files/entry.d.ts new file mode 100644 index 000000000000..c0ee559ee90c --- /dev/null +++ b/packages/adapter-cloudflare-workers/files/entry.d.ts @@ -0,0 +1,11 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} + +declare module 'MANIFEST' { + import { SSRManifest } from '@sveltejs/kit'; + + export const manifest: SSRManifest; + export const prerendered: Set; +} diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index f38caa761f13..2ce945ce377c 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,5 +1,5 @@ import { App } from 'APP'; -import { manifest, prerendered } from './manifest.js'; +import { manifest, prerendered } from 'MANIFEST'; import { getAssetFromKV } from '@cloudflare/kv-asset-handler'; const app = new App(manifest); diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 279caa10e642..e47af92ff445 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -39,7 +39,8 @@ export default function () { builder.copy(`${files}/entry.js`, `${tmp}/entry.js`, { replace: { - APP: `${relativePath}/app.js` + APP: `${relativePath}/app.js`, + MANIFEST: './manifest.js' } }); diff --git a/packages/adapter-cloudflare/files/worker.d.ts b/packages/adapter-cloudflare/files/worker.d.ts new file mode 100644 index 000000000000..c0ee559ee90c --- /dev/null +++ b/packages/adapter-cloudflare/files/worker.d.ts @@ -0,0 +1,11 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} + +declare module 'MANIFEST' { + import { SSRManifest } from '@sveltejs/kit'; + + export const manifest: SSRManifest; + export const prerendered: Set; +} diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index 6c9ab94c17e7..9829bb501ffb 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -1,7 +1,7 @@ -import { App } from '../output/server/app.js'; -import { manifest, prerendered } from './manifest.js'; +import { App } from 'APP'; +import { manifest, prerendered } from 'MANIFEST'; -const app = /** @type {import('@sveltejs/kit').App} */ (new App(manifest)); +const app = new App(manifest); const prefix = `/${manifest.appDir}/`; diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 8f6061f4efe2..b3a256a7ae25 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -32,7 +32,8 @@ export default function (options = {}) { builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, { replace: { - APP: `${relativePath}/app.js` + APP: `${relativePath}/app.js`, + MANIFEST: './manifest.js' } }); diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json new file mode 100644 index 000000000000..dab7ee9050dc --- /dev/null +++ b/packages/adapter-cloudflare/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "noImplicitAny": true, + "target": "es2020", + "module": "es2020", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["index.js", "files"] +} diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index de939673c8b6..2aa36a2452c4 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -50,8 +50,12 @@ export default function ({ split = false } = {}) { /** @type {string[]} */ const redirects = []; + const replace = { + APP: './server/app.js' + }; + if (esm) { - builder.copy(`${files}/esm`, '.netlify'); + builder.copy(`${files}/esm`, '.netlify', { replace }); } else { glob('**/*.js', { cwd: '.netlify/server' }).forEach((file) => { const filepath = `.netlify/server/${file}`; @@ -60,7 +64,7 @@ export default function ({ split = false } = {}) { writeFileSync(filepath, output); }); - builder.copy(`${files}/cjs`, '.netlify'); + builder.copy(`${files}/cjs`, '.netlify', { replace }); writeFileSync(join('.netlify', 'package.json'), JSON.stringify({ type: 'commonjs' })); } diff --git a/packages/adapter-netlify/rollup.config.js b/packages/adapter-netlify/rollup.config.js index d211e9860e7f..2d88582be5e4 100644 --- a/packages/adapter-netlify/rollup.config.js +++ b/packages/adapter-netlify/rollup.config.js @@ -19,6 +19,6 @@ export default [ } ], plugins: [nodeResolve(), commonjs(), json()], - external: ['./server/app.js', ...require('module').builtinModules] + external: ['APP', ...require('module').builtinModules] } ]; diff --git a/packages/adapter-netlify/src/handler.d.ts b/packages/adapter-netlify/src/handler.d.ts new file mode 100644 index 000000000000..a34e560311af --- /dev/null +++ b/packages/adapter-netlify/src/handler.d.ts @@ -0,0 +1,4 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js index 00b8279c74b1..0835a3f6beff 100644 --- a/packages/adapter-netlify/src/handler.js +++ b/packages/adapter-netlify/src/handler.js @@ -1,13 +1,11 @@ import './shims'; -import { App } from './server/app.js'; +import { App } from 'APP'; /** - * * @param {import('@sveltejs/kit').SSRManifest} manifest * @returns {import('@netlify/functions').Handler} */ export function init(manifest) { - /** @type {import('@sveltejs/kit').App} */ const app = new App(manifest); return async (event) => { From 84f6136c567e9adb85005c23d90c02a87ea269d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 11:04:45 -0500 Subject: [PATCH 33/36] Update documentation/docs/10-adapters.md Co-authored-by: Ignatius Bagus --- documentation/docs/10-adapters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/10-adapters.md b/documentation/docs/10-adapters.md index 01296837dfd3..db8a2af158ba 100644 --- a/documentation/docs/10-adapters.md +++ b/documentation/docs/10-adapters.md @@ -92,7 +92,7 @@ Within the `adapt` method, there are a number of things that an adapter should d - Output code that: - Imports `App` from `${builder.getServerDirectory()}/app.js` - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` - - Listens for requests from the platform, converts them to a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request if necessary), calls the `render` function to generate a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it + - Listens for requests from the platform, converts them to a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `render` function to generate a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/install-fetch` helper for platforms that can use `node-fetch` - Bundle the output to avoid needing to install dependencies on the target platform, if necessary - Put the user's static files and the generated JS/CSS in the correct location for the target platform From e05cf2c2fd57e2c5cbd904c719faee6a0e966359 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 11:43:06 -0500 Subject: [PATCH 34/36] better error messages --- packages/kit/src/core/build/build_server.js | 9 ++++----- packages/kit/src/core/dev/plugin.js | 4 +++- packages/kit/src/runtime/server/index.js | 16 ++++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 9286173c9766..0e8f9b6cbd1d 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -71,7 +71,7 @@ export class App { // TODO remove for 1.0 // @ts-expect-error get request() { - throw new Error('request in handleError has been replaced with event'); + throw new Error('request in handleError has been replaced with event. See /~https://github.com/sveltejs/kit/pull/3384 for details'); } }); error.stack = this.options.get_stack(error); @@ -93,15 +93,14 @@ export class App { }; } - async render(request, { + render(request, { prerender } = {}) { if (!(request instanceof Request)) { - throw new Error('The first argument to app.render must be a Request object'); + throw new Error('The first argument to app.render must be a Request object. See /~https://github.com/sveltejs/kit/pull/3384 for details'); } - const { status, headers, body } = await respond(request, this.options, { prerender }); - return new Response(body, { status, headers }); + return respond(request, this.options, { prerender }); } } `; diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index 576d64f45cd0..7fa63353e2e4 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -239,7 +239,9 @@ export async function create_plugin(config, cwd) { // TODO remove for 1.0 // @ts-expect-error get request() { - throw new Error('request in handleError has been replaced with event'); + throw new Error( + 'request in handleError has been replaced with event. See /~https://github.com/sveltejs/kit/pull/3384 for details' + ); } }); }, diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index aadcc7bba432..ff095219af26 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -67,24 +67,28 @@ export async function respond(request, options, state = {}) { /** * @param {string} property * @param {string} replacement + * @param {string} suffix */ - const removed = (property, replacement) => ({ + const removed = (property, replacement, suffix = '') => ({ get: () => { - throw new Error(`event.${property} has been replaced by event.${replacement}`); + throw new Error(`event.${property} has been replaced by event.${replacement}` + suffix); } }); + const details = '. See /~https://github.com/sveltejs/kit/pull/3384 for details'; + const body_getter = { get: () => { throw new Error( - 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' + 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' + + details ); } }; Object.defineProperties(event, { - method: removed('method', 'request.method'), - headers: removed('headers', 'request.headers'), + method: removed('method', 'request.method', details), + headers: removed('headers', 'request.headers', details), origin: removed('origin', 'url.origin'), path: removed('path', 'url.pathname'), query: removed('query', 'url.searchParams'), @@ -188,7 +192,7 @@ export async function respond(request, options, state = {}) { // TODO remove for 1.0 // @ts-expect-error get request() { - throw new Error('request in handle has been replaced with event'); + throw new Error('request in handle has been replaced with event' + details); } }); } catch (/** @type {unknown} */ e) { From 3abcaec5840de11596c780512c1e279609c00c53 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 11:52:34 -0500 Subject: [PATCH 35/36] helpful errors for removed config options --- packages/kit/src/core/config/options.js | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 4e8862b997a4..96d1b8ad9ca1 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -66,6 +66,24 @@ const options = object( floc: boolean(false), + // TODO: remove this for the 1.0 release + headers: validate(undefined, (input, keypath) => { + if (typeof input !== undefined) { + throw new Error( + `${keypath} has been removed. See /~https://github.com/sveltejs/kit/pull/3384 for details` + ); + } + }), + + // TODO: remove this for the 1.0 release + host: validate(undefined, (input, keypath) => { + if (typeof input !== undefined) { + throw new Error( + `${keypath} has been removed. See /~https://github.com/sveltejs/kit/pull/3384 for details` + ); + } + }), + hydrate: boolean(true), inlineStyleThreshold: number(0), @@ -145,6 +163,7 @@ const options = object( return input; }), + // TODO: remove this for the 1.0 release force: validate(undefined, (input, keypath) => { if (typeof input !== undefined) { @@ -157,6 +176,7 @@ const options = object( ); } }), + onError: validate('fail', (input, keypath) => { if (typeof input === 'function') return input; if (['continue', 'fail'].includes(input)) return input; @@ -164,6 +184,7 @@ const options = object( `${keypath} should be either a custom function or one of "continue" or "fail"` ); }), + // TODO: remove this for the 1.0 release pages: validate(undefined, (input, keypath) => { if (typeof input !== undefined) { @@ -172,6 +193,15 @@ const options = object( }) }), + // TODO: remove this for the 1.0 release + protocol: validate(undefined, (input, keypath) => { + if (typeof input !== undefined) { + throw new Error( + `${keypath} has been removed. See /~https://github.com/sveltejs/kit/pull/3384 for details` + ); + } + }), + router: boolean(true), serviceWorker: object({ From 7a5a03ad16eb5efa1bc7fbcf381c026fd8a775d3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Jan 2022 14:56:19 -0500 Subject: [PATCH 36/36] fix tests --- packages/kit/src/core/config/index.spec.js | 6 ++++++ packages/kit/src/core/config/test/index.js | 3 +++ 2 files changed, 9 insertions(+) diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 022b41f08b1c..df7f0a0e527a 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -29,6 +29,8 @@ test('fills in defaults', () => { template: 'src/app.html' }, floc: false, + headers: undefined, + host: undefined, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -55,6 +57,7 @@ test('fills in defaults', () => { onError: 'fail', pages: undefined }, + protocol: undefined, router: true, ssr: null, target: null, @@ -135,6 +138,8 @@ test('fills in partial blanks', () => { template: 'src/app.html' }, floc: false, + headers: undefined, + host: undefined, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -161,6 +166,7 @@ test('fills in partial blanks', () => { onError: 'fail', pages: undefined }, + protocol: undefined, router: true, ssr: null, target: null, diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index d3048e71296e..02cb1fd55703 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -31,6 +31,8 @@ test('load default config (esm)', async () => { template: join(cwd, 'src/app.html') }, floc: false, + headers: undefined, + host: undefined, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -54,6 +56,7 @@ test('load default config (esm)', async () => { onError: 'fail', pages: undefined }, + protocol: undefined, router: true, ssr: null, target: null,