Spaces:
Runtime error
Runtime error
| /** @param {Partial<import('types').ResponseHeaders> | undefined} object */ | |
| function to_headers(object) { | |
| const headers = new Headers(); | |
| if (object) { | |
| for (const key in object) { | |
| const value = object[key]; | |
| if (!value) continue; | |
| if (Array.isArray(value)) { | |
| value.forEach((value) => { | |
| headers.append(key, /** @type {string} */ (value)); | |
| }); | |
| } else { | |
| headers.set(key, /** @type {string} */ (value)); | |
| } | |
| } | |
| } | |
| return headers; | |
| } | |
| /** | |
| * Hash using djb2 | |
| * @param {import('types').StrictBody} value | |
| */ | |
| function hash(value) { | |
| let hash = 5381; | |
| let i = value.length; | |
| if (typeof value === 'string') { | |
| while (i) hash = (hash * 33) ^ value.charCodeAt(--i); | |
| } else { | |
| while (i) hash = (hash * 33) ^ value[--i]; | |
| } | |
| return (hash >>> 0).toString(36); | |
| } | |
| /** @param {Record<string, any>} obj */ | |
| function lowercase_keys(obj) { | |
| /** @type {Record<string, any>} */ | |
| const clone = {}; | |
| for (const key in obj) { | |
| clone[key.toLowerCase()] = obj[key]; | |
| } | |
| return clone; | |
| } | |
| /** @param {Record<string, string>} params */ | |
| function decode_params(params) { | |
| for (const key in params) { | |
| // input has already been decoded by decodeURI | |
| // now handle the rest that decodeURIComponent would do | |
| params[key] = params[key] | |
| .replace(/%23/g, '#') | |
| .replace(/%3[Bb]/g, ';') | |
| .replace(/%2[Cc]/g, ',') | |
| .replace(/%2[Ff]/g, '/') | |
| .replace(/%3[Ff]/g, '?') | |
| .replace(/%3[Aa]/g, ':') | |
| .replace(/%40/g, '@') | |
| .replace(/%26/g, '&') | |
| .replace(/%3[Dd]/g, '=') | |
| .replace(/%2[Bb]/g, '+') | |
| .replace(/%24/g, '$'); | |
| } | |
| return params; | |
| } | |
| /** @param {any} body */ | |
| function is_pojo(body) { | |
| if (typeof body !== 'object') return false; | |
| if (body) { | |
| if (body instanceof Uint8Array) return false; | |
| if (body instanceof ReadableStream) return false; | |
| // if body is a node Readable, throw an error | |
| // TODO remove this for 1.0 | |
| if (body._readableState && typeof body.pipe === 'function') { | |
| throw new Error('Node streams are no longer supported — use a ReadableStream instead'); | |
| } | |
| } | |
| return true; | |
| } | |
| /** @param {import('types').RequestEvent} event */ | |
| function normalize_request_method(event) { | |
| const method = event.request.method.toLowerCase(); | |
| return method === 'delete' ? 'del' : method; // 'delete' is a reserved word | |
| } | |
| /** @param {string} body */ | |
| function error(body) { | |
| return new Response(body, { | |
| status: 500 | |
| }); | |
| } | |
| /** @param {unknown} s */ | |
| function is_string(s) { | |
| return typeof s === 'string' || s instanceof String; | |
| } | |
| const text_types = new Set([ | |
| 'application/xml', | |
| 'application/json', | |
| 'application/x-www-form-urlencoded', | |
| 'multipart/form-data' | |
| ]); | |
| const bodyless_status_codes = new Set([101, 204, 205, 304]); | |
| /** | |
| * Decides how the body should be parsed based on its mime type | |
| * | |
| * @param {string | undefined | null} content_type The `content-type` header of a request/response. | |
| * @returns {boolean} | |
| */ | |
| function is_text(content_type) { | |
| if (!content_type) return true; // defaults to json | |
| const type = content_type.split(';')[0].toLowerCase(); // get the mime type | |
| return type.startsWith('text/') || type.endsWith('+xml') || text_types.has(type); | |
| } | |
| /** | |
| * @param {import('types').RequestEvent} event | |
| * @param {{ [method: string]: import('types').RequestHandler }} mod | |
| * @returns {Promise<Response>} | |
| */ | |
| async function render_endpoint(event, mod) { | |
| const method = normalize_request_method(event); | |
| /** @type {import('types').RequestHandler} */ | |
| let handler = mod[method]; | |
| if (!handler && method === 'head') { | |
| handler = mod.get; | |
| } | |
| if (!handler) { | |
| const allowed = []; | |
| for (const method in ['get', 'post', 'put', 'patch']) { | |
| if (mod[method]) allowed.push(method.toUpperCase()); | |
| } | |
| if (mod.del) allowed.push('DELETE'); | |
| if (mod.get || mod.head) allowed.push('HEAD'); | |
| return event.request.headers.get('x-sveltekit-load') | |
| ? // TODO would be nice to avoid these requests altogether, | |
| // by noting whether or not page endpoints export `get` | |
| new Response(undefined, { | |
| status: 204 | |
| }) | |
| : new Response(`${event.request.method} method not allowed`, { | |
| status: 405, | |
| headers: { | |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 | |
| // "The server must generate an Allow header field in a 405 status code response" | |
| allow: allowed.join(', ') | |
| } | |
| }); | |
| } | |
| 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}`); | |
| } | |
| // TODO remove for 1.0 | |
| // @ts-expect-error | |
| if (response.fallthrough) { | |
| throw new Error( | |
| 'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching' | |
| ); | |
| } | |
| const { status = 200, body = {} } = response; | |
| const headers = | |
| response.headers instanceof Headers | |
| ? new Headers(response.headers) | |
| : to_headers(response.headers); | |
| const type = headers.get('content-type'); | |
| if (!is_text(type) && !(body instanceof Uint8Array || is_string(body))) { | |
| return error( | |
| `${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type` | |
| ); | |
| } | |
| /** @type {import('types').StrictBody} */ | |
| let normalized_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').StrictBody} */ (body); | |
| } | |
| 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)}"`); | |
| } | |
| } | |
| return new Response( | |
| method !== 'head' && !bodyless_status_codes.has(status) ? normalized_body : undefined, | |
| { | |
| status, | |
| headers | |
| } | |
| ); | |
| } | |
| var chars$1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$'; | |
| var unsafeChars = /[<>\b\f\n\r\t\0\u2028\u2029]/g; | |
| var reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/; | |
| var escaped = { | |
| '<': '\\u003C', | |
| '>': '\\u003E', | |
| '/': '\\u002F', | |
| '\\': '\\\\', | |
| '\b': '\\b', | |
| '\f': '\\f', | |
| '\n': '\\n', | |
| '\r': '\\r', | |
| '\t': '\\t', | |
| '\0': '\\0', | |
| '\u2028': '\\u2028', | |
| '\u2029': '\\u2029' | |
| }; | |
| var objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0'); | |
| function devalue(value) { | |
| var counts = new Map(); | |
| function walk(thing) { | |
| if (typeof thing === 'function') { | |
| throw new Error("Cannot stringify a function"); | |
| } | |
| if (counts.has(thing)) { | |
| counts.set(thing, counts.get(thing) + 1); | |
| return; | |
| } | |
| counts.set(thing, 1); | |
| if (!isPrimitive(thing)) { | |
| var type = getType(thing); | |
| switch (type) { | |
| case 'Number': | |
| case 'String': | |
| case 'Boolean': | |
| case 'Date': | |
| case 'RegExp': | |
| return; | |
| case 'Array': | |
| thing.forEach(walk); | |
| break; | |
| case 'Set': | |
| case 'Map': | |
| Array.from(thing).forEach(walk); | |
| break; | |
| default: | |
| var proto = Object.getPrototypeOf(thing); | |
| if (proto !== Object.prototype && | |
| proto !== null && | |
| Object.getOwnPropertyNames(proto).sort().join('\0') !== objectProtoOwnPropertyNames) { | |
| throw new Error("Cannot stringify arbitrary non-POJOs"); | |
| } | |
| if (Object.getOwnPropertySymbols(thing).length > 0) { | |
| throw new Error("Cannot stringify POJOs with symbolic keys"); | |
| } | |
| Object.keys(thing).forEach(function (key) { return walk(thing[key]); }); | |
| } | |
| } | |
| } | |
| walk(value); | |
| var names = new Map(); | |
| Array.from(counts) | |
| .filter(function (entry) { return entry[1] > 1; }) | |
| .sort(function (a, b) { return b[1] - a[1]; }) | |
| .forEach(function (entry, i) { | |
| names.set(entry[0], getName(i)); | |
| }); | |
| function stringify(thing) { | |
| if (names.has(thing)) { | |
| return names.get(thing); | |
| } | |
| if (isPrimitive(thing)) { | |
| return stringifyPrimitive(thing); | |
| } | |
| var type = getType(thing); | |
| switch (type) { | |
| case 'Number': | |
| case 'String': | |
| case 'Boolean': | |
| return "Object(" + stringify(thing.valueOf()) + ")"; | |
| case 'RegExp': | |
| return "new RegExp(" + stringifyString(thing.source) + ", \"" + thing.flags + "\")"; | |
| case 'Date': | |
| return "new Date(" + thing.getTime() + ")"; | |
| case 'Array': | |
| var members = thing.map(function (v, i) { return i in thing ? stringify(v) : ''; }); | |
| var tail = thing.length === 0 || (thing.length - 1 in thing) ? '' : ','; | |
| return "[" + members.join(',') + tail + "]"; | |
| case 'Set': | |
| case 'Map': | |
| return "new " + type + "([" + Array.from(thing).map(stringify).join(',') + "])"; | |
| default: | |
| var obj = "{" + Object.keys(thing).map(function (key) { return safeKey(key) + ":" + stringify(thing[key]); }).join(',') + "}"; | |
| var proto = Object.getPrototypeOf(thing); | |
| if (proto === null) { | |
| return Object.keys(thing).length > 0 | |
| ? "Object.assign(Object.create(null)," + obj + ")" | |
| : "Object.create(null)"; | |
| } | |
| return obj; | |
| } | |
| } | |
| var str = stringify(value); | |
| if (names.size) { | |
| var params_1 = []; | |
| var statements_1 = []; | |
| var values_1 = []; | |
| names.forEach(function (name, thing) { | |
| params_1.push(name); | |
| if (isPrimitive(thing)) { | |
| values_1.push(stringifyPrimitive(thing)); | |
| return; | |
| } | |
| var type = getType(thing); | |
| switch (type) { | |
| case 'Number': | |
| case 'String': | |
| case 'Boolean': | |
| values_1.push("Object(" + stringify(thing.valueOf()) + ")"); | |
| break; | |
| case 'RegExp': | |
| values_1.push(thing.toString()); | |
| break; | |
| case 'Date': | |
| values_1.push("new Date(" + thing.getTime() + ")"); | |
| break; | |
| case 'Array': | |
| values_1.push("Array(" + thing.length + ")"); | |
| thing.forEach(function (v, i) { | |
| statements_1.push(name + "[" + i + "]=" + stringify(v)); | |
| }); | |
| break; | |
| case 'Set': | |
| values_1.push("new Set"); | |
| statements_1.push(name + "." + Array.from(thing).map(function (v) { return "add(" + stringify(v) + ")"; }).join('.')); | |
| break; | |
| case 'Map': | |
| values_1.push("new Map"); | |
| statements_1.push(name + "." + Array.from(thing).map(function (_a) { | |
| var k = _a[0], v = _a[1]; | |
| return "set(" + stringify(k) + ", " + stringify(v) + ")"; | |
| }).join('.')); | |
| break; | |
| default: | |
| values_1.push(Object.getPrototypeOf(thing) === null ? 'Object.create(null)' : '{}'); | |
| Object.keys(thing).forEach(function (key) { | |
| statements_1.push("" + name + safeProp(key) + "=" + stringify(thing[key])); | |
| }); | |
| } | |
| }); | |
| statements_1.push("return " + str); | |
| return "(function(" + params_1.join(',') + "){" + statements_1.join(';') + "}(" + values_1.join(',') + "))"; | |
| } | |
| else { | |
| return str; | |
| } | |
| } | |
| function getName(num) { | |
| var name = ''; | |
| do { | |
| name = chars$1[num % chars$1.length] + name; | |
| num = ~~(num / chars$1.length) - 1; | |
| } while (num >= 0); | |
| return reserved.test(name) ? name + "_" : name; | |
| } | |
| function isPrimitive(thing) { | |
| return Object(thing) !== thing; | |
| } | |
| function stringifyPrimitive(thing) { | |
| if (typeof thing === 'string') | |
| return stringifyString(thing); | |
| if (thing === void 0) | |
| return 'void 0'; | |
| if (thing === 0 && 1 / thing < 0) | |
| return '-0'; | |
| var str = String(thing); | |
| if (typeof thing === 'number') | |
| return str.replace(/^(-)?0\./, '$1.'); | |
| return str; | |
| } | |
| function getType(thing) { | |
| return Object.prototype.toString.call(thing).slice(8, -1); | |
| } | |
| function escapeUnsafeChar(c) { | |
| return escaped[c] || c; | |
| } | |
| function escapeUnsafeChars(str) { | |
| return str.replace(unsafeChars, escapeUnsafeChar); | |
| } | |
| function safeKey(key) { | |
| return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? key : escapeUnsafeChars(JSON.stringify(key)); | |
| } | |
| function safeProp(key) { | |
| return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? "." + key : "[" + escapeUnsafeChars(JSON.stringify(key)) + "]"; | |
| } | |
| function stringifyString(str) { | |
| var result = '"'; | |
| for (var i = 0; i < str.length; i += 1) { | |
| var char = str.charAt(i); | |
| var code = char.charCodeAt(0); | |
| if (char === '"') { | |
| result += '\\"'; | |
| } | |
| else if (char in escaped) { | |
| result += escaped[char]; | |
| } | |
| else if (code >= 0xd800 && code <= 0xdfff) { | |
| var next = str.charCodeAt(i + 1); | |
| // If this is the beginning of a [high, low] surrogate pair, | |
| // add the next two characters, otherwise escape | |
| if (code <= 0xdbff && (next >= 0xdc00 && next <= 0xdfff)) { | |
| result += char + str[++i]; | |
| } | |
| else { | |
| result += "\\u" + code.toString(16).toUpperCase(); | |
| } | |
| } | |
| else { | |
| result += char; | |
| } | |
| } | |
| result += '"'; | |
| return result; | |
| } | |
| function noop() { } | |
| function safe_not_equal(a, b) { | |
| return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); | |
| } | |
| Promise.resolve(); | |
| const subscriber_queue = []; | |
| /** | |
| * Creates a `Readable` store that allows reading by subscription. | |
| * @param value initial value | |
| * @param {StartStopNotifier}start start and stop notifications for subscriptions | |
| */ | |
| function readable(value, start) { | |
| return { | |
| subscribe: writable(value, start).subscribe | |
| }; | |
| } | |
| /** | |
| * Create a `Writable` store that allows both updating and reading by subscription. | |
| * @param {*=}value initial value | |
| * @param {StartStopNotifier=}start start and stop notifications for subscriptions | |
| */ | |
| function writable(value, start = noop) { | |
| let stop; | |
| const subscribers = new Set(); | |
| function set(new_value) { | |
| if (safe_not_equal(value, new_value)) { | |
| value = new_value; | |
| if (stop) { // store is ready | |
| const run_queue = !subscriber_queue.length; | |
| for (const subscriber of subscribers) { | |
| subscriber[1](); | |
| subscriber_queue.push(subscriber, value); | |
| } | |
| if (run_queue) { | |
| for (let i = 0; i < subscriber_queue.length; i += 2) { | |
| subscriber_queue[i][0](subscriber_queue[i + 1]); | |
| } | |
| subscriber_queue.length = 0; | |
| } | |
| } | |
| } | |
| } | |
| function update(fn) { | |
| set(fn(value)); | |
| } | |
| function subscribe(run, invalidate = noop) { | |
| const subscriber = [run, invalidate]; | |
| subscribers.add(subscriber); | |
| if (subscribers.size === 1) { | |
| stop = start(set) || noop; | |
| } | |
| run(value); | |
| return () => { | |
| subscribers.delete(subscriber); | |
| if (subscribers.size === 0) { | |
| stop(); | |
| stop = null; | |
| } | |
| }; | |
| } | |
| return { set, update, subscribe }; | |
| } | |
| /** | |
| * @param {unknown} err | |
| * @return {Error} | |
| */ | |
| function coalesce_to_error(err) { | |
| return err instanceof Error || | |
| (err && /** @type {any} */ (err).name && /** @type {any} */ (err).message) | |
| ? /** @type {Error} */ (err) | |
| : new Error(JSON.stringify(err)); | |
| } | |
| /** | |
| * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser. | |
| * | |
| * The first closes the script element, so everything after is treated as raw HTML. | |
| * The second disables further parsing until `-->`, so the script element might be unexpectedly | |
| * kept open until until an unrelated HTML comment in the page. | |
| * | |
| * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018 | |
| * browsers. | |
| * | |
| * @see tests for unsafe parsing examples. | |
| * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements | |
| * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions | |
| * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state | |
| * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state | |
| * @see https://github.com/tc39/proposal-json-superset | |
| * @type {Record<string, string>} | |
| */ | |
| const render_json_payload_script_dict = { | |
| '<': '\\u003C', | |
| '\u2028': '\\u2028', | |
| '\u2029': '\\u2029' | |
| }; | |
| const render_json_payload_script_regex = new RegExp( | |
| `[${Object.keys(render_json_payload_script_dict).join('')}]`, | |
| 'g' | |
| ); | |
| /** | |
| * Generates a raw HTML string containing a safe script element carrying JSON data and associated attributes. | |
| * | |
| * It escapes all the special characters needed to guarantee the element is unbroken, but care must | |
| * be taken to ensure it is inserted in the document at an acceptable position for a script element, | |
| * and that the resulting string isn't further modified. | |
| * | |
| * Attribute names must be type-checked so we don't need to escape them. | |
| * | |
| * @param {import('types').PayloadScriptAttributes} attrs A list of attributes to be added to the element. | |
| * @param {import('types').JSONValue} payload The data to be carried by the element. Must be serializable to JSON. | |
| * @returns {string} The raw HTML of a script element carrying the JSON payload. | |
| * @example const html = render_json_payload_script({ type: 'data', url: '/data.json' }, { foo: 'bar' }); | |
| */ | |
| function render_json_payload_script(attrs, payload) { | |
| const safe_payload = JSON.stringify(payload).replace( | |
| render_json_payload_script_regex, | |
| (match) => render_json_payload_script_dict[match] | |
| ); | |
| let safe_attrs = ''; | |
| for (const [key, value] of Object.entries(attrs)) { | |
| if (value === undefined) continue; | |
| safe_attrs += ` sveltekit:data-${key}=${escape_html_attr(value)}`; | |
| } | |
| return `<script type="application/json"${safe_attrs}>${safe_payload}</script>`; | |
| } | |
| /** | |
| * When inside a double-quoted attribute value, only `&` and `"` hold special meaning. | |
| * @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state | |
| * @type {Record<string, string>} | |
| */ | |
| const escape_html_attr_dict = { | |
| '&': '&', | |
| '"': '"' | |
| }; | |
| const escape_html_attr_regex = new RegExp( | |
| // special characters | |
| `[${Object.keys(escape_html_attr_dict).join('')}]|` + | |
| // high surrogate without paired low surrogate | |
| '[\\ud800-\\udbff](?![\\udc00-\\udfff])|' + | |
| // a valid surrogate pair, the only match with 2 code units | |
| // we match it so that we can match unpaired low surrogates in the same pass | |
| // TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff] | |
| '[\\ud800-\\udbff][\\udc00-\\udfff]|' + | |
| // unpaired low surrogate (see previous match) | |
| '[\\udc00-\\udfff]', | |
| 'g' | |
| ); | |
| /** | |
| * Formats a string to be used as an attribute's value in raw HTML. | |
| * | |
| * It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes | |
| * characters that are special in attributes, and surrounds the whole string in double-quotes. | |
| * | |
| * @param {string} str | |
| * @returns {string} Escaped string surrounded by double-quotes. | |
| * @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`; | |
| */ | |
| function escape_html_attr(str) { | |
| const escaped_str = str.replace(escape_html_attr_regex, (match) => { | |
| if (match.length === 2) { | |
| // valid surrogate pair | |
| return match; | |
| } | |
| return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`; | |
| }); | |
| return `"${escaped_str}"`; | |
| } | |
| const s = JSON.stringify; | |
| const encoder = new TextEncoder(); | |
| /** | |
| * SHA-256 hashing function adapted from https://bitwiseshiftleft.github.io/sjcl | |
| * modified and redistributed under BSD license | |
| * @param {string} data | |
| */ | |
| function sha256(data) { | |
| if (!key[0]) precompute(); | |
| const out = init.slice(0); | |
| const array = encode$1(data); | |
| for (let i = 0; i < array.length; i += 16) { | |
| const w = array.subarray(i, i + 16); | |
| let tmp; | |
| let a; | |
| let b; | |
| let out0 = out[0]; | |
| let out1 = out[1]; | |
| let out2 = out[2]; | |
| let out3 = out[3]; | |
| let out4 = out[4]; | |
| let out5 = out[5]; | |
| let out6 = out[6]; | |
| let out7 = out[7]; | |
| /* Rationale for placement of |0 : | |
| * If a value can overflow is original 32 bits by a factor of more than a few | |
| * million (2^23 ish), there is a possibility that it might overflow the | |
| * 53-bit mantissa and lose precision. | |
| * | |
| * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that | |
| * propagates around the loop, and on the hash state out[]. I don't believe | |
| * that the clamps on out4 and on out0 are strictly necessary, but it's close | |
| * (for out4 anyway), and better safe than sorry. | |
| * | |
| * The clamps on out[] are necessary for the output to be correct even in the | |
| * common case and for short inputs. | |
| */ | |
| for (let i = 0; i < 64; i++) { | |
| // load up the input word for this round | |
| if (i < 16) { | |
| tmp = w[i]; | |
| } else { | |
| a = w[(i + 1) & 15]; | |
| b = w[(i + 14) & 15]; | |
| tmp = w[i & 15] = | |
| (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + | |
| ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + | |
| w[i & 15] + | |
| w[(i + 9) & 15]) | | |
| 0; | |
| } | |
| tmp = | |
| tmp + | |
| out7 + | |
| ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + | |
| (out6 ^ (out4 & (out5 ^ out6))) + | |
| key[i]; // | 0; | |
| // shift register | |
| out7 = out6; | |
| out6 = out5; | |
| out5 = out4; | |
| out4 = (out3 + tmp) | 0; | |
| out3 = out2; | |
| out2 = out1; | |
| out1 = out0; | |
| out0 = | |
| (tmp + | |
| ((out1 & out2) ^ (out3 & (out1 ^ out2))) + | |
| ((out1 >>> 2) ^ | |
| (out1 >>> 13) ^ | |
| (out1 >>> 22) ^ | |
| (out1 << 30) ^ | |
| (out1 << 19) ^ | |
| (out1 << 10))) | | |
| 0; | |
| } | |
| out[0] = (out[0] + out0) | 0; | |
| out[1] = (out[1] + out1) | 0; | |
| out[2] = (out[2] + out2) | 0; | |
| out[3] = (out[3] + out3) | 0; | |
| out[4] = (out[4] + out4) | 0; | |
| out[5] = (out[5] + out5) | 0; | |
| out[6] = (out[6] + out6) | 0; | |
| out[7] = (out[7] + out7) | 0; | |
| } | |
| const bytes = new Uint8Array(out.buffer); | |
| reverse_endianness(bytes); | |
| return base64(bytes); | |
| } | |
| /** The SHA-256 initialization vector */ | |
| const init = new Uint32Array(8); | |
| /** The SHA-256 hash key */ | |
| const key = new Uint32Array(64); | |
| /** Function to precompute init and key. */ | |
| function precompute() { | |
| /** @param {number} x */ | |
| function frac(x) { | |
| return (x - Math.floor(x)) * 0x100000000; | |
| } | |
| let prime = 2; | |
| for (let i = 0; i < 64; prime++) { | |
| let is_prime = true; | |
| for (let factor = 2; factor * factor <= prime; factor++) { | |
| if (prime % factor === 0) { | |
| is_prime = false; | |
| break; | |
| } | |
| } | |
| if (is_prime) { | |
| if (i < 8) { | |
| init[i] = frac(prime ** (1 / 2)); | |
| } | |
| key[i] = frac(prime ** (1 / 3)); | |
| i++; | |
| } | |
| } | |
| } | |
| /** @param {Uint8Array} bytes */ | |
| function reverse_endianness(bytes) { | |
| for (let i = 0; i < bytes.length; i += 4) { | |
| const a = bytes[i + 0]; | |
| const b = bytes[i + 1]; | |
| const c = bytes[i + 2]; | |
| const d = bytes[i + 3]; | |
| bytes[i + 0] = d; | |
| bytes[i + 1] = c; | |
| bytes[i + 2] = b; | |
| bytes[i + 3] = a; | |
| } | |
| } | |
| /** @param {string} str */ | |
| function encode$1(str) { | |
| const encoded = encoder.encode(str); | |
| const length = encoded.length * 8; | |
| // result should be a multiple of 512 bits in length, | |
| // with room for a 1 (after the data) and two 32-bit | |
| // words containing the original input bit length | |
| const size = 512 * Math.ceil((length + 65) / 512); | |
| const bytes = new Uint8Array(size / 8); | |
| bytes.set(encoded); | |
| // append a 1 | |
| bytes[encoded.length] = 0b10000000; | |
| reverse_endianness(bytes); | |
| // add the input bit length | |
| const words = new Uint32Array(bytes.buffer); | |
| words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us | |
| words[words.length - 1] = length; | |
| return words; | |
| } | |
| /* | |
| Based on https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 | |
| MIT License | |
| Copyright (c) 2020 Egor Nepomnyaschih | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all | |
| copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| SOFTWARE. | |
| */ | |
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); | |
| /** @param {Uint8Array} bytes */ | |
| function base64(bytes) { | |
| const l = bytes.length; | |
| let result = ''; | |
| let i; | |
| for (i = 2; i < l; i += 3) { | |
| result += chars[bytes[i - 2] >> 2]; | |
| result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; | |
| result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)]; | |
| result += chars[bytes[i] & 0x3f]; | |
| } | |
| if (i === l + 1) { | |
| // 1 octet yet to write | |
| result += chars[bytes[i - 2] >> 2]; | |
| result += chars[(bytes[i - 2] & 0x03) << 4]; | |
| result += '=='; | |
| } | |
| if (i === l) { | |
| // 2 octets yet to write | |
| result += chars[bytes[i - 2] >> 2]; | |
| result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; | |
| result += chars[(bytes[i - 1] & 0x0f) << 2]; | |
| result += '='; | |
| } | |
| return result; | |
| } | |
| /** @type {Promise<void>} */ | |
| let csp_ready; | |
| const array = new Uint8Array(16); | |
| function generate_nonce() { | |
| crypto.getRandomValues(array); | |
| return base64(array); | |
| } | |
| const quoted = new Set([ | |
| 'self', | |
| 'unsafe-eval', | |
| 'unsafe-hashes', | |
| 'unsafe-inline', | |
| 'none', | |
| 'strict-dynamic', | |
| 'report-sample' | |
| ]); | |
| const crypto_pattern = /^(nonce|sha\d\d\d)-/; | |
| class Csp { | |
| /** @type {boolean} */ | |
| #use_hashes; | |
| /** @type {boolean} */ | |
| #dev; | |
| /** @type {boolean} */ | |
| #script_needs_csp; | |
| /** @type {boolean} */ | |
| #style_needs_csp; | |
| /** @type {import('types').CspDirectives} */ | |
| #directives; | |
| /** @type {import('types').Csp.Source[]} */ | |
| #script_src; | |
| /** @type {import('types').Csp.Source[]} */ | |
| #style_src; | |
| /** | |
| * @param {{ | |
| * mode: string, | |
| * directives: import('types').CspDirectives | |
| * }} config | |
| * @param {{ | |
| * dev: boolean; | |
| * prerender: boolean; | |
| * needs_nonce: boolean; | |
| * }} opts | |
| */ | |
| constructor({ mode, directives }, { dev, prerender, needs_nonce }) { | |
| this.#use_hashes = mode === 'hash' || (mode === 'auto' && prerender); | |
| this.#directives = dev ? { ...directives } : directives; // clone in dev so we can safely mutate | |
| this.#dev = dev; | |
| const d = this.#directives; | |
| if (dev) { | |
| // remove strict-dynamic in dev... | |
| // TODO reinstate this if we can figure out how to make strict-dynamic work | |
| // if (d['default-src']) { | |
| // d['default-src'] = d['default-src'].filter((name) => name !== 'strict-dynamic'); | |
| // if (d['default-src'].length === 0) delete d['default-src']; | |
| // } | |
| // if (d['script-src']) { | |
| // d['script-src'] = d['script-src'].filter((name) => name !== 'strict-dynamic'); | |
| // if (d['script-src'].length === 0) delete d['script-src']; | |
| // } | |
| const effective_style_src = d['style-src'] || d['default-src']; | |
| // ...and add unsafe-inline so we can inject <style> elements | |
| if (effective_style_src && !effective_style_src.includes('unsafe-inline')) { | |
| d['style-src'] = [...effective_style_src, 'unsafe-inline']; | |
| } | |
| } | |
| this.#script_src = []; | |
| this.#style_src = []; | |
| const effective_script_src = d['script-src'] || d['default-src']; | |
| const effective_style_src = d['style-src'] || d['default-src']; | |
| this.#script_needs_csp = | |
| !!effective_script_src && | |
| effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0; | |
| this.#style_needs_csp = | |
| !dev && | |
| !!effective_style_src && | |
| effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0; | |
| this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes; | |
| this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes; | |
| if (this.script_needs_nonce || this.style_needs_nonce || needs_nonce) { | |
| this.nonce = generate_nonce(); | |
| } | |
| } | |
| /** @param {string} content */ | |
| add_script(content) { | |
| if (this.#script_needs_csp) { | |
| if (this.#use_hashes) { | |
| this.#script_src.push(`sha256-${sha256(content)}`); | |
| } else if (this.#script_src.length === 0) { | |
| this.#script_src.push(`nonce-${this.nonce}`); | |
| } | |
| } | |
| } | |
| /** @param {string} content */ | |
| add_style(content) { | |
| if (this.#style_needs_csp) { | |
| if (this.#use_hashes) { | |
| this.#style_src.push(`sha256-${sha256(content)}`); | |
| } else if (this.#style_src.length === 0) { | |
| this.#style_src.push(`nonce-${this.nonce}`); | |
| } | |
| } | |
| } | |
| /** @param {boolean} [is_meta] */ | |
| get_header(is_meta = false) { | |
| const header = []; | |
| // due to browser inconsistencies, we can't append sources to default-src | |
| // (specifically, Firefox appears to not ignore nonce-{nonce} directives | |
| // on default-src), so we ensure that script-src and style-src exist | |
| const directives = { ...this.#directives }; | |
| if (this.#style_src.length > 0) { | |
| directives['style-src'] = [ | |
| ...(directives['style-src'] || directives['default-src'] || []), | |
| ...this.#style_src | |
| ]; | |
| } | |
| if (this.#script_src.length > 0) { | |
| directives['script-src'] = [ | |
| ...(directives['script-src'] || directives['default-src'] || []), | |
| ...this.#script_src | |
| ]; | |
| } | |
| for (const key in directives) { | |
| if (is_meta && (key === 'frame-ancestors' || key === 'report-uri' || key === 'sandbox')) { | |
| // these values cannot be used with a <meta> tag | |
| // TODO warn? | |
| continue; | |
| } | |
| // @ts-expect-error gimme a break typescript, `key` is obviously a member of directives | |
| const value = /** @type {string[] | true} */ (directives[key]); | |
| if (!value) continue; | |
| const directive = [key]; | |
| if (Array.isArray(value)) { | |
| value.forEach((value) => { | |
| if (quoted.has(value) || crypto_pattern.test(value)) { | |
| directive.push(`'${value}'`); | |
| } else { | |
| directive.push(value); | |
| } | |
| }); | |
| } | |
| header.push(directive.join(' ')); | |
| } | |
| return header.join('; '); | |
| } | |
| get_meta() { | |
| const content = escape_html_attr(this.get_header(true)); | |
| return `<meta http-equiv="content-security-policy" content=${content}>`; | |
| } | |
| } | |
| const absolute = /^([a-z]+:)?\/?\//; | |
| const scheme = /^[a-z]+:/; | |
| /** | |
| * @param {string} base | |
| * @param {string} path | |
| */ | |
| function resolve(base, path) { | |
| if (scheme.test(path)) return path; | |
| const base_match = absolute.exec(base); | |
| const path_match = absolute.exec(path); | |
| if (!base_match) { | |
| throw new Error(`bad base path: "${base}"`); | |
| } | |
| const baseparts = path_match ? [] : base.slice(base_match[0].length).split('/'); | |
| const pathparts = path_match ? path.slice(path_match[0].length).split('/') : path.split('/'); | |
| baseparts.pop(); | |
| for (let i = 0; i < pathparts.length; i += 1) { | |
| const part = pathparts[i]; | |
| if (part === '.') continue; | |
| else if (part === '..') baseparts.pop(); | |
| else baseparts.push(part); | |
| } | |
| const prefix = (path_match && path_match[0]) || (base_match && base_match[0]) || ''; | |
| return `${prefix}${baseparts.join('/')}`; | |
| } | |
| /** @param {string} path */ | |
| function is_root_relative(path) { | |
| return path[0] === '/' && path[1] !== '/'; | |
| } | |
| /** | |
| * @param {string} path | |
| * @param {import('types').TrailingSlash} trailing_slash | |
| */ | |
| function normalize_path(path, trailing_slash) { | |
| if (path === '/' || trailing_slash === 'ignore') return path; | |
| if (trailing_slash === 'never') { | |
| return path.endsWith('/') ? path.slice(0, -1) : path; | |
| } else if (trailing_slash === 'always' && !path.endsWith('/')) { | |
| return path + '/'; | |
| } | |
| return path; | |
| } | |
| class LoadURL extends URL { | |
| /** @returns {string} */ | |
| get hash() { | |
| throw new Error( | |
| 'url.hash is inaccessible from load. Consider accessing hash from the page store within the script tag of your component.' | |
| ); | |
| } | |
| } | |
| class PrerenderingURL extends URL { | |
| /** @returns {string} */ | |
| get search() { | |
| throw new Error('Cannot access url.search on a page with prerendering enabled'); | |
| } | |
| /** @returns {URLSearchParams} */ | |
| get searchParams() { | |
| throw new Error('Cannot access url.searchParams on a page with prerendering enabled'); | |
| } | |
| } | |
| // TODO rename this function/module | |
| const updated = { | |
| ...readable(false), | |
| check: () => false | |
| }; | |
| /** | |
| * Creates the HTML response. | |
| * @param {{ | |
| * branch: Array<import('./types').Loaded>; | |
| * options: import('types').SSROptions; | |
| * state: import('types').SSRState; | |
| * $session: any; | |
| * page_config: { hydrate: boolean, router: boolean }; | |
| * status: number; | |
| * error: Error | null; | |
| * event: import('types').RequestEvent; | |
| * resolve_opts: import('types').RequiredResolveOptions; | |
| * stuff: Record<string, any>; | |
| * }} opts | |
| */ | |
| async function render_response({ | |
| branch, | |
| options, | |
| state, | |
| $session, | |
| page_config, | |
| status, | |
| error = null, | |
| event, | |
| resolve_opts, | |
| stuff | |
| }) { | |
| if (state.prerendering) { | |
| if (options.csp.mode === 'nonce') { | |
| throw new Error('Cannot use prerendering if config.kit.csp.mode === "nonce"'); | |
| } | |
| if (options.template_contains_nonce) { | |
| throw new Error('Cannot use prerendering if page template contains %sveltekit.nonce%'); | |
| } | |
| } | |
| const stylesheets = new Set(options.manifest._.entry.css); | |
| const modulepreloads = new Set(options.manifest._.entry.js); | |
| /** @type {Map<string, string>} */ | |
| const styles = new Map(); | |
| /** @type {Array<import('./types').Fetched>} */ | |
| const serialized_data = []; | |
| let shadow_props; | |
| let rendered; | |
| let is_private = false; | |
| /** @type {import('types').NormalizedLoadOutputCache | undefined} */ | |
| let cache; | |
| if (error) { | |
| error.stack = options.get_stack(error); | |
| } | |
| if (resolve_opts.ssr) { | |
| branch.forEach(({ node, props, loaded, fetched, uses_credentials }) => { | |
| if (node.css) node.css.forEach((url) => stylesheets.add(url)); | |
| if (node.js) node.js.forEach((url) => modulepreloads.add(url)); | |
| if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v)); | |
| // TODO probably better if `fetched` wasn't populated unless `hydrate` | |
| if (fetched && page_config.hydrate) serialized_data.push(...fetched); | |
| if (props) shadow_props = props; | |
| cache = loaded?.cache; | |
| is_private = cache?.private ?? uses_credentials; | |
| }); | |
| const session = writable($session); | |
| /** @type {Record<string, any>} */ | |
| const props = { | |
| stores: { | |
| page: writable(null), | |
| navigating: writable(null), | |
| /** @type {import('svelte/store').Writable<App.Session>} */ | |
| session: { | |
| ...session, | |
| subscribe: (fn) => { | |
| is_private = cache?.private ?? true; | |
| return session.subscribe(fn); | |
| } | |
| }, | |
| updated | |
| }, | |
| /** @type {import('types').Page} */ | |
| page: { | |
| error, | |
| params: event.params, | |
| routeId: event.routeId, | |
| status, | |
| stuff, | |
| url: state.prerendering ? new PrerenderingURL(event.url) : event.url | |
| }, | |
| components: branch.map(({ node }) => node.module.default) | |
| }; | |
| // TODO remove this for 1.0 | |
| /** | |
| * @param {string} property | |
| * @param {string} replacement | |
| */ | |
| const print_error = (property, replacement) => { | |
| Object.defineProperty(props.page, property, { | |
| get: () => { | |
| throw new Error(`$page.${property} has been replaced by $page.url.${replacement}`); | |
| } | |
| }); | |
| }; | |
| print_error('origin', 'origin'); | |
| print_error('path', 'pathname'); | |
| print_error('query', 'searchParams'); | |
| // props_n (instead of props[n]) makes it easy to avoid | |
| // unnecessary updates for layout components | |
| for (let i = 0; i < branch.length; i += 1) { | |
| props[`props_${i}`] = await branch[i].loaded.props; | |
| } | |
| rendered = options.root.render(props); | |
| } else { | |
| rendered = { head: '', html: '', css: { code: '', map: null } }; | |
| } | |
| let { head, html: body } = rendered; | |
| const inlined_style = Array.from(styles.values()).join('\n'); | |
| await csp_ready; | |
| const csp = new Csp(options.csp, { | |
| dev: options.dev, | |
| prerender: !!state.prerendering, | |
| needs_nonce: options.template_contains_nonce | |
| }); | |
| const target = hash(body); | |
| // prettier-ignore | |
| const init_app = ` | |
| import { start } from ${s(options.prefix + options.manifest._.entry.file)}; | |
| start({ | |
| target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode, | |
| paths: ${s(options.paths)}, | |
| session: ${try_serialize($session, (error) => { | |
| throw new Error(`Failed to serialize session data: ${error.message}`); | |
| })}, | |
| route: ${!!page_config.router}, | |
| spa: ${!resolve_opts.ssr}, | |
| trailing_slash: ${s(options.trailing_slash)}, | |
| hydrate: ${resolve_opts.ssr && page_config.hydrate ? `{ | |
| status: ${status}, | |
| error: ${serialize_error(error)}, | |
| nodes: [${branch.map(({ node }) => node.index).join(', ')}], | |
| params: ${devalue(event.params)}, | |
| routeId: ${s(event.routeId)} | |
| }` : 'null'} | |
| }); | |
| `; | |
| const init_service_worker = ` | |
| if ('serviceWorker' in navigator) { | |
| addEventListener('load', () => { | |
| navigator.serviceWorker.register('${options.service_worker}'); | |
| }); | |
| } | |
| `; | |
| if (inlined_style) { | |
| const attributes = []; | |
| if (options.dev) attributes.push(' data-sveltekit'); | |
| if (csp.style_needs_nonce) attributes.push(` nonce="${csp.nonce}"`); | |
| csp.add_style(inlined_style); | |
| head += `\n\t<style${attributes.join('')}>${inlined_style}</style>`; | |
| } | |
| // prettier-ignore | |
| head += Array.from(stylesheets) | |
| .map((dep) => { | |
| const attributes = [ | |
| 'rel="stylesheet"', | |
| `href="${options.prefix + dep}"` | |
| ]; | |
| if (csp.style_needs_nonce) { | |
| attributes.push(`nonce="${csp.nonce}"`); | |
| } | |
| if (styles.has(dep)) { | |
| // don't load stylesheets that are already inlined | |
| // include them in disabled state so that Vite can detect them and doesn't try to add them | |
| attributes.push('disabled', 'media="(max-width: 0)"'); | |
| } | |
| return `\n\t<link ${attributes.join(' ')}>`; | |
| }) | |
| .join(''); | |
| if (page_config.router || page_config.hydrate) { | |
| head += Array.from(modulepreloads) | |
| .map((dep) => `\n\t<link rel="modulepreload" href="${options.prefix + dep}">`) | |
| .join(''); | |
| const attributes = ['type="module"', `data-sveltekit-hydrate="${target}"`]; | |
| csp.add_script(init_app); | |
| if (csp.script_needs_nonce) { | |
| attributes.push(`nonce="${csp.nonce}"`); | |
| } | |
| body += `\n\t\t<script ${attributes.join(' ')}>${init_app}</script>`; | |
| body += serialized_data | |
| .map(({ url, body, response }) => | |
| render_json_payload_script( | |
| { type: 'data', url, body: typeof body === 'string' ? hash(body) : undefined }, | |
| response | |
| ) | |
| ) | |
| .join('\n\t'); | |
| if (shadow_props) { | |
| body += render_json_payload_script({ type: 'props' }, shadow_props); | |
| } | |
| } | |
| if (options.service_worker) { | |
| // always include service worker unless it's turned off explicitly | |
| csp.add_script(init_service_worker); | |
| head += ` | |
| <script${csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''}>${init_service_worker}</script>`; | |
| } | |
| if (state.prerendering) { | |
| const http_equiv = []; | |
| const csp_headers = csp.get_meta(); | |
| if (csp_headers) { | |
| http_equiv.push(csp_headers); | |
| } | |
| if (cache) { | |
| http_equiv.push(`<meta http-equiv="cache-control" content="max-age=${cache.maxage}">`); | |
| } | |
| if (http_equiv.length > 0) { | |
| head = http_equiv.join('\n') + head; | |
| } | |
| } | |
| const segments = event.url.pathname.slice(options.paths.base.length).split('/').slice(2); | |
| const assets = | |
| options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); | |
| const html = await resolve_opts.transformPage({ | |
| html: options.template({ head, body, assets, nonce: /** @type {string} */ (csp.nonce) }) | |
| }); | |
| const headers = new Headers({ | |
| 'content-type': 'text/html', | |
| etag: `"${hash(html)}"` | |
| }); | |
| if (cache) { | |
| headers.set('cache-control', `${is_private ? 'private' : 'public'}, max-age=${cache.maxage}`); | |
| } | |
| if (!options.floc) { | |
| headers.set('permissions-policy', 'interest-cohort=()'); | |
| } | |
| if (!state.prerendering) { | |
| const csp_header = csp.get_header(); | |
| if (csp_header) { | |
| headers.set('content-security-policy', csp_header); | |
| } | |
| } | |
| return new Response(html, { | |
| status, | |
| headers | |
| }); | |
| } | |
| /** | |
| * @param {any} data | |
| * @param {(error: Error) => void} [fail] | |
| */ | |
| function try_serialize(data, fail) { | |
| try { | |
| return devalue(data); | |
| } catch (err) { | |
| if (fail) fail(coalesce_to_error(err)); | |
| return null; | |
| } | |
| } | |
| // Ensure we return something truthy so the client will not re-render the page over the error | |
| /** @param {(Error & {frame?: string} & {loc?: object}) | undefined | null} error */ | |
| function serialize_error(error) { | |
| if (!error) return null; | |
| let serialized = try_serialize(error); | |
| if (!serialized) { | |
| const { name, message, stack } = error; | |
| serialized = try_serialize({ ...error, name, message, stack }); | |
| } | |
| if (!serialized) { | |
| serialized = '{}'; | |
| } | |
| return serialized; | |
| } | |
| /*! | |
| * cookie | |
| * Copyright(c) 2012-2014 Roman Shtylman | |
| * Copyright(c) 2015 Douglas Christopher Wilson | |
| * MIT Licensed | |
| */ | |
| /** | |
| * Module exports. | |
| * @public | |
| */ | |
| var parse_1 = parse$1; | |
| var serialize_1 = serialize; | |
| /** | |
| * Module variables. | |
| * @private | |
| */ | |
| var __toString = Object.prototype.toString; | |
| /** | |
| * RegExp to match field-content in RFC 7230 sec 3.2 | |
| * | |
| * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] | |
| * field-vchar = VCHAR / obs-text | |
| * obs-text = %x80-FF | |
| */ | |
| var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; | |
| /** | |
| * Parse a cookie header. | |
| * | |
| * Parse the given cookie header string into an object | |
| * The object has the various cookies as keys(names) => values | |
| * | |
| * @param {string} str | |
| * @param {object} [options] | |
| * @return {object} | |
| * @public | |
| */ | |
| function parse$1(str, options) { | |
| if (typeof str !== 'string') { | |
| throw new TypeError('argument str must be a string'); | |
| } | |
| var obj = {}; | |
| var opt = options || {}; | |
| var dec = opt.decode || decode; | |
| var index = 0; | |
| while (index < str.length) { | |
| var eqIdx = str.indexOf('=', index); | |
| // no more cookie pairs | |
| if (eqIdx === -1) { | |
| break | |
| } | |
| var endIdx = str.indexOf(';', index); | |
| if (endIdx === -1) { | |
| endIdx = str.length; | |
| } else if (endIdx < eqIdx) { | |
| // backtrack on prior semicolon | |
| index = str.lastIndexOf(';', eqIdx - 1) + 1; | |
| continue | |
| } | |
| var key = str.slice(index, eqIdx).trim(); | |
| // only assign once | |
| if (undefined === obj[key]) { | |
| var val = str.slice(eqIdx + 1, endIdx).trim(); | |
| // quoted values | |
| if (val.charCodeAt(0) === 0x22) { | |
| val = val.slice(1, -1); | |
| } | |
| obj[key] = tryDecode(val, dec); | |
| } | |
| index = endIdx + 1; | |
| } | |
| return obj; | |
| } | |
| /** | |
| * Serialize data into a cookie header. | |
| * | |
| * Serialize the a name value pair into a cookie string suitable for | |
| * http headers. An optional options object specified cookie parameters. | |
| * | |
| * serialize('foo', 'bar', { httpOnly: true }) | |
| * => "foo=bar; httpOnly" | |
| * | |
| * @param {string} name | |
| * @param {string} val | |
| * @param {object} [options] | |
| * @return {string} | |
| * @public | |
| */ | |
| function serialize(name, val, options) { | |
| var opt = options || {}; | |
| var enc = opt.encode || encode; | |
| if (typeof enc !== 'function') { | |
| throw new TypeError('option encode is invalid'); | |
| } | |
| if (!fieldContentRegExp.test(name)) { | |
| throw new TypeError('argument name is invalid'); | |
| } | |
| var value = enc(val); | |
| if (value && !fieldContentRegExp.test(value)) { | |
| throw new TypeError('argument val is invalid'); | |
| } | |
| var str = name + '=' + value; | |
| if (null != opt.maxAge) { | |
| var maxAge = opt.maxAge - 0; | |
| if (isNaN(maxAge) || !isFinite(maxAge)) { | |
| throw new TypeError('option maxAge is invalid') | |
| } | |
| str += '; Max-Age=' + Math.floor(maxAge); | |
| } | |
| if (opt.domain) { | |
| if (!fieldContentRegExp.test(opt.domain)) { | |
| throw new TypeError('option domain is invalid'); | |
| } | |
| str += '; Domain=' + opt.domain; | |
| } | |
| if (opt.path) { | |
| if (!fieldContentRegExp.test(opt.path)) { | |
| throw new TypeError('option path is invalid'); | |
| } | |
| str += '; Path=' + opt.path; | |
| } | |
| if (opt.expires) { | |
| var expires = opt.expires; | |
| if (!isDate(expires) || isNaN(expires.valueOf())) { | |
| throw new TypeError('option expires is invalid'); | |
| } | |
| str += '; Expires=' + expires.toUTCString(); | |
| } | |
| if (opt.httpOnly) { | |
| str += '; HttpOnly'; | |
| } | |
| if (opt.secure) { | |
| str += '; Secure'; | |
| } | |
| if (opt.priority) { | |
| var priority = typeof opt.priority === 'string' | |
| ? opt.priority.toLowerCase() | |
| : opt.priority; | |
| switch (priority) { | |
| case 'low': | |
| str += '; Priority=Low'; | |
| break | |
| case 'medium': | |
| str += '; Priority=Medium'; | |
| break | |
| case 'high': | |
| str += '; Priority=High'; | |
| break | |
| default: | |
| throw new TypeError('option priority is invalid') | |
| } | |
| } | |
| if (opt.sameSite) { | |
| var sameSite = typeof opt.sameSite === 'string' | |
| ? opt.sameSite.toLowerCase() : opt.sameSite; | |
| switch (sameSite) { | |
| case true: | |
| str += '; SameSite=Strict'; | |
| break; | |
| case 'lax': | |
| str += '; SameSite=Lax'; | |
| break; | |
| case 'strict': | |
| str += '; SameSite=Strict'; | |
| break; | |
| case 'none': | |
| str += '; SameSite=None'; | |
| break; | |
| default: | |
| throw new TypeError('option sameSite is invalid'); | |
| } | |
| } | |
| return str; | |
| } | |
| /** | |
| * URL-decode string value. Optimized to skip native call when no %. | |
| * | |
| * @param {string} str | |
| * @returns {string} | |
| */ | |
| function decode (str) { | |
| return str.indexOf('%') !== -1 | |
| ? decodeURIComponent(str) | |
| : str | |
| } | |
| /** | |
| * URL-encode value. | |
| * | |
| * @param {string} str | |
| * @returns {string} | |
| */ | |
| function encode (val) { | |
| return encodeURIComponent(val) | |
| } | |
| /** | |
| * Determine if value is a Date. | |
| * | |
| * @param {*} val | |
| * @private | |
| */ | |
| function isDate (val) { | |
| return __toString.call(val) === '[object Date]' || | |
| val instanceof Date | |
| } | |
| /** | |
| * Try decoding a string using a decoding function. | |
| * | |
| * @param {string} str | |
| * @param {function} decode | |
| * @private | |
| */ | |
| function tryDecode(str, decode) { | |
| try { | |
| return decode(str); | |
| } catch (e) { | |
| return str; | |
| } | |
| } | |
| var setCookie = {exports: {}}; | |
| var defaultParseOptions = { | |
| decodeValues: true, | |
| map: false, | |
| silent: false, | |
| }; | |
| function isNonEmptyString(str) { | |
| return typeof str === "string" && !!str.trim(); | |
| } | |
| function parseString(setCookieValue, options) { | |
| var parts = setCookieValue.split(";").filter(isNonEmptyString); | |
| var nameValue = parts.shift().split("="); | |
| var name = nameValue.shift(); | |
| var value = nameValue.join("="); // everything after the first =, joined by a "=" if there was more than one part | |
| options = options | |
| ? Object.assign({}, defaultParseOptions, options) | |
| : defaultParseOptions; | |
| try { | |
| value = options.decodeValues ? decodeURIComponent(value) : value; // decode cookie value | |
| } catch (e) { | |
| console.error( | |
| "set-cookie-parser encountered an error while decoding a cookie with value '" + | |
| value + | |
| "'. Set options.decodeValues to false to disable this feature.", | |
| e | |
| ); | |
| } | |
| var cookie = { | |
| name: name, // grab everything before the first = | |
| value: value, | |
| }; | |
| parts.forEach(function (part) { | |
| var sides = part.split("="); | |
| var key = sides.shift().trimLeft().toLowerCase(); | |
| var value = sides.join("="); | |
| if (key === "expires") { | |
| cookie.expires = new Date(value); | |
| } else if (key === "max-age") { | |
| cookie.maxAge = parseInt(value, 10); | |
| } else if (key === "secure") { | |
| cookie.secure = true; | |
| } else if (key === "httponly") { | |
| cookie.httpOnly = true; | |
| } else if (key === "samesite") { | |
| cookie.sameSite = value; | |
| } else { | |
| cookie[key] = value; | |
| } | |
| }); | |
| return cookie; | |
| } | |
| function parse(input, options) { | |
| options = options | |
| ? Object.assign({}, defaultParseOptions, options) | |
| : defaultParseOptions; | |
| if (!input) { | |
| if (!options.map) { | |
| return []; | |
| } else { | |
| return {}; | |
| } | |
| } | |
| if (input.headers && input.headers["set-cookie"]) { | |
| // fast-path for node.js (which automatically normalizes header names to lower-case | |
| input = input.headers["set-cookie"]; | |
| } else if (input.headers) { | |
| // slow-path for other environments - see #25 | |
| var sch = | |
| input.headers[ | |
| Object.keys(input.headers).find(function (key) { | |
| return key.toLowerCase() === "set-cookie"; | |
| }) | |
| ]; | |
| // warn if called on a request-like object with a cookie header rather than a set-cookie header - see #34, 36 | |
| if (!sch && input.headers.cookie && !options.silent) { | |
| console.warn( | |
| "Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning." | |
| ); | |
| } | |
| input = sch; | |
| } | |
| if (!Array.isArray(input)) { | |
| input = [input]; | |
| } | |
| options = options | |
| ? Object.assign({}, defaultParseOptions, options) | |
| : defaultParseOptions; | |
| if (!options.map) { | |
| return input.filter(isNonEmptyString).map(function (str) { | |
| return parseString(str, options); | |
| }); | |
| } else { | |
| var cookies = {}; | |
| return input.filter(isNonEmptyString).reduce(function (cookies, str) { | |
| var cookie = parseString(str, options); | |
| cookies[cookie.name] = cookie; | |
| return cookies; | |
| }, cookies); | |
| } | |
| } | |
| /* | |
| Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas | |
| that are within a single set-cookie field-value, such as in the Expires portion. | |
| This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 | |
| Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 | |
| React Native's fetch does this for *every* header, including set-cookie. | |
| Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 | |
| Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation | |
| */ | |
| function splitCookiesString(cookiesString) { | |
| if (Array.isArray(cookiesString)) { | |
| return cookiesString; | |
| } | |
| if (typeof cookiesString !== "string") { | |
| return []; | |
| } | |
| var cookiesStrings = []; | |
| var pos = 0; | |
| var start; | |
| var ch; | |
| var lastComma; | |
| var nextStart; | |
| var cookiesSeparatorFound; | |
| function skipWhitespace() { | |
| while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { | |
| pos += 1; | |
| } | |
| return pos < cookiesString.length; | |
| } | |
| function notSpecialChar() { | |
| ch = cookiesString.charAt(pos); | |
| return ch !== "=" && ch !== ";" && ch !== ","; | |
| } | |
| while (pos < cookiesString.length) { | |
| start = pos; | |
| cookiesSeparatorFound = false; | |
| while (skipWhitespace()) { | |
| ch = cookiesString.charAt(pos); | |
| if (ch === ",") { | |
| // ',' is a cookie separator if we have later first '=', not ';' or ',' | |
| lastComma = pos; | |
| pos += 1; | |
| skipWhitespace(); | |
| nextStart = pos; | |
| while (pos < cookiesString.length && notSpecialChar()) { | |
| pos += 1; | |
| } | |
| // currently special character | |
| if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { | |
| // we found cookies separator | |
| cookiesSeparatorFound = true; | |
| // pos is inside the next cookie, so back up and return it. | |
| pos = nextStart; | |
| cookiesStrings.push(cookiesString.substring(start, lastComma)); | |
| start = pos; | |
| } else { | |
| // in param ',' or param separator ';', | |
| // we continue from that comma | |
| pos = lastComma + 1; | |
| } | |
| } else { | |
| pos += 1; | |
| } | |
| } | |
| if (!cookiesSeparatorFound || pos >= cookiesString.length) { | |
| cookiesStrings.push(cookiesString.substring(start, cookiesString.length)); | |
| } | |
| } | |
| return cookiesStrings; | |
| } | |
| setCookie.exports = parse; | |
| setCookie.exports.parse = parse; | |
| var parseString_1 = setCookie.exports.parseString = parseString; | |
| var splitCookiesString_1 = setCookie.exports.splitCookiesString = splitCookiesString; | |
| /** | |
| * @param {import('types').LoadOutput} loaded | |
| * @returns {import('types').NormalizedLoadOutput} | |
| */ | |
| function normalize(loaded) { | |
| // TODO remove for 1.0 | |
| // @ts-expect-error | |
| if (loaded.fallthrough) { | |
| throw new Error( | |
| 'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching' | |
| ); | |
| } | |
| // TODO remove for 1.0 | |
| if ('maxage' in loaded) { | |
| throw new Error('maxage should be replaced with cache: { maxage }'); | |
| } | |
| const has_error_status = | |
| loaded.status && loaded.status >= 400 && loaded.status <= 599 && !loaded.redirect; | |
| if (loaded.error || has_error_status) { | |
| const status = loaded.status; | |
| if (!loaded.error && has_error_status) { | |
| return { status: status || 500, error: new Error() }; | |
| } | |
| const error = typeof loaded.error === 'string' ? new Error(loaded.error) : loaded.error; | |
| if (!(error instanceof Error)) { | |
| return { | |
| status: 500, | |
| error: new Error( | |
| `"error" property returned from load() must be a string or instance of Error, received type "${typeof error}"` | |
| ) | |
| }; | |
| } | |
| if (!status || status < 400 || status > 599) { | |
| console.warn('"error" returned from load() without a valid status code — defaulting to 500'); | |
| return { status: 500, error }; | |
| } | |
| return { status, error }; | |
| } | |
| if (loaded.redirect) { | |
| if (!loaded.status || Math.floor(loaded.status / 100) !== 3) { | |
| throw new Error( | |
| '"redirect" property returned from load() must be accompanied by a 3xx status code' | |
| ); | |
| } | |
| if (typeof loaded.redirect !== 'string') { | |
| throw new Error('"redirect" property returned from load() must be a string'); | |
| } | |
| } | |
| if (loaded.dependencies) { | |
| if ( | |
| !Array.isArray(loaded.dependencies) || | |
| loaded.dependencies.some((dep) => typeof dep !== 'string') | |
| ) { | |
| throw new Error('"dependencies" property returned from load() must be of type string[]'); | |
| } | |
| } | |
| // TODO remove before 1.0 | |
| if (/** @type {any} */ (loaded).context) { | |
| throw new Error( | |
| 'You are returning "context" from a load function. ' + | |
| '"context" was renamed to "stuff", please adjust your code accordingly.' | |
| ); | |
| } | |
| return /** @type {import('types').NormalizedLoadOutput} */ (loaded); | |
| } | |
| /** | |
| * @param {string} hostname | |
| * @param {string} [constraint] | |
| */ | |
| function domain_matches(hostname, constraint) { | |
| if (!constraint) return true; | |
| const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint; | |
| if (hostname === normalized) return true; | |
| return hostname.endsWith('.' + normalized); | |
| } | |
| /** | |
| * @param {string} path | |
| * @param {string} [constraint] | |
| */ | |
| function path_matches(path, constraint) { | |
| if (!constraint) return true; | |
| const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint; | |
| if (path === normalized) return true; | |
| return path.startsWith(normalized + '/'); | |
| } | |
| /** | |
| * Calls the user's `load` function. | |
| * @param {{ | |
| * event: import('types').RequestEvent; | |
| * options: import('types').SSROptions; | |
| * state: import('types').SSRState; | |
| * route: import('types').SSRPage | null; | |
| * node: import('types').SSRNode; | |
| * $session: any; | |
| * stuff: Record<string, any>; | |
| * is_error: boolean; | |
| * is_leaf: boolean; | |
| * status?: number; | |
| * error?: Error; | |
| * }} opts | |
| * @returns {Promise<import('./types').Loaded>} | |
| */ | |
| async function load_node({ | |
| event, | |
| options, | |
| state, | |
| route, | |
| node, | |
| $session, | |
| stuff, | |
| is_error, | |
| is_leaf, | |
| status, | |
| error | |
| }) { | |
| const { module } = node; | |
| let uses_credentials = false; | |
| /** @type {Array<import('./types').Fetched>} */ | |
| const fetched = []; | |
| const cookies = parse_1(event.request.headers.get('cookie') || ''); | |
| /** @type {import('set-cookie-parser').Cookie[]} */ | |
| const new_cookies = []; | |
| /** @type {import('types').LoadOutput} */ | |
| let loaded; | |
| const should_prerender = node.module.prerender ?? options.prerender.default; | |
| /** @type {import('types').ShadowData} */ | |
| const shadow = is_leaf | |
| ? await load_shadow_data( | |
| /** @type {import('types').SSRPage} */ (route), | |
| event, | |
| options, | |
| should_prerender | |
| ) | |
| : {}; | |
| if (shadow.cookies) { | |
| shadow.cookies.forEach((header) => { | |
| new_cookies.push(parseString_1(header)); | |
| }); | |
| } | |
| if (shadow.error) { | |
| loaded = { | |
| status: shadow.status, | |
| error: shadow.error | |
| }; | |
| } else if (shadow.redirect) { | |
| loaded = { | |
| status: shadow.status, | |
| redirect: shadow.redirect | |
| }; | |
| } else if (module.load) { | |
| /** @type {import('types').LoadEvent} */ | |
| const load_input = { | |
| url: state.prerendering ? new PrerenderingURL(event.url) : new LoadURL(event.url), | |
| params: event.params, | |
| props: shadow.body || {}, | |
| routeId: event.routeId, | |
| get session() { | |
| if (node.module.prerender ?? options.prerender.default) { | |
| throw Error( | |
| 'Attempted to access session from a prerendered page. Session would never be populated.' | |
| ); | |
| } | |
| uses_credentials = true; | |
| return $session; | |
| }, | |
| /** | |
| * @param {RequestInfo} resource | |
| * @param {RequestInit} opts | |
| */ | |
| fetch: async (resource, opts = {}) => { | |
| /** @type {string} */ | |
| let requested; | |
| if (typeof resource === 'string') { | |
| requested = resource; | |
| } else { | |
| requested = resource.url; | |
| opts = { | |
| method: resource.method, | |
| headers: resource.headers, | |
| body: resource.body, | |
| mode: resource.mode, | |
| credentials: resource.credentials, | |
| cache: resource.cache, | |
| redirect: resource.redirect, | |
| referrer: resource.referrer, | |
| integrity: resource.integrity, | |
| ...opts | |
| }; | |
| } | |
| opts.headers = new Headers(opts.headers); | |
| // merge headers from request | |
| for (const [key, value] of event.request.headers) { | |
| if ( | |
| key !== 'authorization' && | |
| key !== 'cookie' && | |
| key !== 'host' && | |
| key !== 'if-none-match' && | |
| !opts.headers.has(key) | |
| ) { | |
| opts.headers.set(key, value); | |
| } | |
| } | |
| const resolved = resolve(event.url.pathname, requested.split('?')[0]); | |
| /** @type {Response} */ | |
| let response; | |
| /** @type {import('types').PrerenderDependency} */ | |
| let dependency; | |
| // handle fetch requests for static assets. e.g. prebaked data, etc. | |
| // we need to support everything the browser's fetch supports | |
| const prefix = options.paths.assets || options.paths.base; | |
| const filename = decodeURIComponent( | |
| resolved.startsWith(prefix) ? resolved.slice(prefix.length) : resolved | |
| ).slice(1); | |
| const filename_html = `${filename}/index.html`; // path may also match path/index.html | |
| const is_asset = options.manifest.assets.has(filename); | |
| const is_asset_html = options.manifest.assets.has(filename_html); | |
| if (is_asset || is_asset_html) { | |
| const file = is_asset ? filename : filename_html; | |
| if (options.read) { | |
| const type = is_asset | |
| ? options.manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))] | |
| : 'text/html'; | |
| response = new Response(options.read(file), { | |
| headers: type ? { 'content-type': type } : {} | |
| }); | |
| } else { | |
| response = await fetch( | |
| `${event.url.origin}/${file}`, | |
| /** @type {RequestInit} */ (opts) | |
| ); | |
| } | |
| } else if (is_root_relative(resolved)) { | |
| if (opts.credentials !== 'omit') { | |
| uses_credentials = true; | |
| const authorization = event.request.headers.get('authorization'); | |
| // combine cookies from the initiating request with any that were | |
| // added via set-cookie | |
| const combined_cookies = { ...cookies }; | |
| for (const cookie of new_cookies) { | |
| if (!domain_matches(event.url.hostname, cookie.domain)) continue; | |
| if (!path_matches(resolved, cookie.path)) continue; | |
| combined_cookies[cookie.name] = cookie.value; | |
| } | |
| const cookie = Object.entries(combined_cookies) | |
| .map(([name, value]) => `${name}=${value}`) | |
| .join('; '); | |
| if (cookie) { | |
| opts.headers.set('cookie', cookie); | |
| } | |
| if (authorization && !opts.headers.has('authorization')) { | |
| opts.headers.set('authorization', authorization); | |
| } | |
| } | |
| if (opts.body && typeof opts.body !== 'string') { | |
| // per https://developer.mozilla.org/en-US/docs/Web/API/Request/Request, this can be a | |
| // Blob, BufferSource, FormData, URLSearchParams, USVString, or ReadableStream object. | |
| // non-string bodies are irksome to deal with, but luckily aren't particularly useful | |
| // in this context anyway, so we take the easy route and ban them | |
| throw new Error('Request body must be a string'); | |
| } | |
| response = await respond( | |
| new Request(new URL(requested, event.url).href, { ...opts }), | |
| options, | |
| { | |
| ...state, | |
| initiator: route | |
| } | |
| ); | |
| if (state.prerendering) { | |
| dependency = { response, body: null }; | |
| state.prerendering.dependencies.set(resolved, dependency); | |
| } | |
| } else { | |
| // external | |
| if (resolved.startsWith('//')) { | |
| requested = event.url.protocol + requested; | |
| } | |
| // external fetch | |
| // allow cookie passthrough for "same-origin" | |
| // if SvelteKit is serving my.domain.com: | |
| // - domain.com WILL NOT receive cookies | |
| // - my.domain.com WILL receive cookies | |
| // - api.domain.dom WILL NOT receive cookies | |
| // - sub.my.domain.com WILL receive cookies | |
| // ports do not affect the resolution | |
| // leading dot prevents mydomain.com matching domain.com | |
| if ( | |
| `.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) && | |
| opts.credentials !== 'omit' | |
| ) { | |
| uses_credentials = true; | |
| const cookie = event.request.headers.get('cookie'); | |
| if (cookie) opts.headers.set('cookie', cookie); | |
| } | |
| // we need to delete the connection header, as explained here: | |
| // https://github.com/nodejs/undici/issues/1470#issuecomment-1140798467 | |
| // TODO this may be a case for being selective about which headers we let through | |
| opts.headers.delete('connection'); | |
| const external_request = new Request(requested, /** @type {RequestInit} */ (opts)); | |
| response = await options.hooks.externalFetch.call(null, external_request); | |
| } | |
| const set_cookie = response.headers.get('set-cookie'); | |
| if (set_cookie) { | |
| new_cookies.push( | |
| ...splitCookiesString_1(set_cookie) | |
| .map((str) => parseString_1(str)) | |
| ); | |
| } | |
| const proxy = new Proxy(response, { | |
| get(response, key, _receiver) { | |
| async function text() { | |
| const body = await response.text(); | |
| /** @type {import('types').ResponseHeaders} */ | |
| const headers = {}; | |
| for (const [key, value] of response.headers) { | |
| // TODO skip others besides set-cookie and etag? | |
| if (key !== 'set-cookie' && key !== 'etag') { | |
| headers[key] = value; | |
| } | |
| } | |
| if (!opts.body || typeof opts.body === 'string') { | |
| const status_number = Number(response.status); | |
| if (isNaN(status_number)) { | |
| throw new Error( | |
| `response.status is not a number. value: "${ | |
| response.status | |
| }" type: ${typeof response.status}` | |
| ); | |
| } | |
| fetched.push({ | |
| url: requested, | |
| body: opts.body, | |
| response: { | |
| status: status_number, | |
| statusText: response.statusText, | |
| headers, | |
| body | |
| } | |
| }); | |
| } | |
| if (dependency) { | |
| dependency.body = body; | |
| } | |
| return body; | |
| } | |
| if (key === 'arrayBuffer') { | |
| return async () => { | |
| const buffer = await response.arrayBuffer(); | |
| if (dependency) { | |
| dependency.body = new Uint8Array(buffer); | |
| } | |
| // TODO should buffer be inlined into the page (albeit base64'd)? | |
| // any conditions in which it shouldn't be? | |
| return buffer; | |
| }; | |
| } | |
| if (key === 'text') { | |
| return text; | |
| } | |
| if (key === 'json') { | |
| return async () => { | |
| return JSON.parse(await text()); | |
| }; | |
| } | |
| // TODO arrayBuffer? | |
| return Reflect.get(response, key, response); | |
| } | |
| }); | |
| return proxy; | |
| }, | |
| stuff: { ...stuff }, | |
| status: is_error ? status ?? null : null, | |
| error: is_error ? error ?? null : null | |
| }; | |
| if (options.dev) { | |
| // TODO remove this for 1.0 | |
| Object.defineProperty(load_input, 'page', { | |
| get: () => { | |
| throw new Error('`page` in `load` functions has been replaced by `url` and `params`'); | |
| } | |
| }); | |
| } | |
| loaded = await module.load.call(null, load_input); | |
| if (!loaded) { | |
| // TODO do we still want to enforce this now that there's no fallthrough? | |
| throw new Error(`load function must return a value${options.dev ? ` (${node.entry})` : ''}`); | |
| } | |
| } else if (shadow.body) { | |
| loaded = { | |
| props: shadow.body | |
| }; | |
| } else { | |
| loaded = {}; | |
| } | |
| // generate __data.json files when prerendering | |
| if (shadow.body && state.prerendering) { | |
| const pathname = `${event.url.pathname.replace(/\/$/, '')}/__data.json`; | |
| const dependency = { | |
| response: new Response(undefined), | |
| body: JSON.stringify(shadow.body) | |
| }; | |
| state.prerendering.dependencies.set(pathname, dependency); | |
| } | |
| return { | |
| node, | |
| props: shadow.body, | |
| loaded: normalize(loaded), | |
| stuff: loaded.stuff || stuff, | |
| fetched, | |
| set_cookie_headers: new_cookies.map((new_cookie) => { | |
| const { name, value, ...options } = new_cookie; | |
| // @ts-expect-error | |
| return serialize_1(name, value, options); | |
| }), | |
| uses_credentials | |
| }; | |
| } | |
| /** | |
| * | |
| * @param {import('types').SSRPage} route | |
| * @param {import('types').RequestEvent} event | |
| * @param {import('types').SSROptions} options | |
| * @param {boolean} prerender | |
| * @returns {Promise<import('types').ShadowData>} | |
| */ | |
| async function load_shadow_data(route, event, options, prerender) { | |
| if (!route.shadow) return {}; | |
| try { | |
| const mod = await route.shadow(); | |
| if (prerender && (mod.post || mod.put || mod.del || mod.patch)) { | |
| throw new Error('Cannot prerender pages that have endpoints with mutative methods'); | |
| } | |
| const method = normalize_request_method(event); | |
| const is_get = method === 'head' || method === 'get'; | |
| const handler = method === 'head' ? mod.head || mod.get : mod[method]; | |
| if (!handler && !is_get) { | |
| return { | |
| status: 405, | |
| error: new Error(`${method} method not allowed`) | |
| }; | |
| } | |
| /** @type {import('types').ShadowData} */ | |
| const data = { | |
| status: 200, | |
| cookies: [], | |
| body: {} | |
| }; | |
| if (!is_get) { | |
| const result = await handler(event); | |
| // TODO remove for 1.0 | |
| // @ts-expect-error | |
| if (result.fallthrough) { | |
| throw new Error( | |
| 'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching' | |
| ); | |
| } | |
| const { status, headers, body } = validate_shadow_output(result); | |
| data.status = status; | |
| add_cookies(/** @type {string[]} */ (data.cookies), headers); | |
| // Redirects are respected... | |
| if (status >= 300 && status < 400) { | |
| data.redirect = /** @type {string} */ ( | |
| headers instanceof Headers ? headers.get('location') : headers.location | |
| ); | |
| return data; | |
| } | |
| // ...but 4xx and 5xx status codes _don't_ result in the error page | |
| // rendering for non-GET requests — instead, we allow the page | |
| // to render with any validation errors etc that were returned | |
| data.body = body; | |
| } | |
| const get = (method === 'head' && mod.head) || mod.get; | |
| if (get) { | |
| const result = await get(event); | |
| // TODO remove for 1.0 | |
| // @ts-expect-error | |
| if (result.fallthrough) { | |
| throw new Error( | |
| 'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching' | |
| ); | |
| } | |
| const { status, headers, body } = validate_shadow_output(result); | |
| add_cookies(/** @type {string[]} */ (data.cookies), headers); | |
| data.status = status; | |
| if (status >= 400) { | |
| data.error = new Error('Failed to load data'); | |
| return data; | |
| } | |
| if (status >= 300) { | |
| data.redirect = /** @type {string} */ ( | |
| headers instanceof Headers ? headers.get('location') : headers.location | |
| ); | |
| return data; | |
| } | |
| data.body = { ...body, ...data.body }; | |
| } | |
| return data; | |
| } catch (e) { | |
| const error = coalesce_to_error(e); | |
| options.handle_error(error, event); | |
| return { | |
| status: 500, | |
| error | |
| }; | |
| } | |
| } | |
| /** | |
| * @param {string[]} target | |
| * @param {Partial<import('types').ResponseHeaders>} headers | |
| */ | |
| function add_cookies(target, headers) { | |
| const cookies = headers['set-cookie']; | |
| if (cookies) { | |
| if (Array.isArray(cookies)) { | |
| target.push(...cookies); | |
| } else { | |
| target.push(/** @type {string} */ (cookies)); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {import('types').ShadowEndpointOutput} result | |
| */ | |
| function validate_shadow_output(result) { | |
| const { status = 200, body = {} } = result; | |
| let headers = result.headers || {}; | |
| if (headers instanceof Headers) { | |
| if (headers.has('set-cookie')) { | |
| throw new Error( | |
| 'Endpoint request handler cannot use Headers interface with Set-Cookie headers' | |
| ); | |
| } | |
| } else { | |
| headers = lowercase_keys(/** @type {Record<string, string>} */ (headers)); | |
| } | |
| if (!is_pojo(body)) { | |
| throw new Error('Body returned from endpoint request handler must be a plain object'); | |
| } | |
| return { status, headers, body }; | |
| } | |
| /** | |
| * @typedef {import('./types.js').Loaded} Loaded | |
| * @typedef {import('types').SSROptions} SSROptions | |
| * @typedef {import('types').SSRState} SSRState | |
| */ | |
| /** | |
| * @param {{ | |
| * event: import('types').RequestEvent; | |
| * options: SSROptions; | |
| * state: SSRState; | |
| * $session: any; | |
| * status: number; | |
| * error: Error; | |
| * resolve_opts: import('types').RequiredResolveOptions; | |
| * }} opts | |
| */ | |
| async function respond_with_error({ | |
| event, | |
| options, | |
| state, | |
| $session, | |
| status, | |
| error, | |
| resolve_opts | |
| }) { | |
| try { | |
| const branch = []; | |
| let stuff = {}; | |
| if (resolve_opts.ssr) { | |
| 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 | |
| const layout_loaded = /** @type {Loaded} */ ( | |
| await load_node({ | |
| event, | |
| options, | |
| state, | |
| route: null, | |
| node: default_layout, | |
| $session, | |
| stuff: {}, | |
| is_error: false, | |
| is_leaf: false | |
| }) | |
| ); | |
| const error_loaded = /** @type {Loaded} */ ( | |
| await load_node({ | |
| event, | |
| options, | |
| state, | |
| route: null, | |
| node: default_error, | |
| $session, | |
| stuff: layout_loaded ? layout_loaded.stuff : {}, | |
| is_error: true, | |
| is_leaf: false, | |
| status, | |
| error | |
| }) | |
| ); | |
| branch.push(layout_loaded, error_loaded); | |
| stuff = error_loaded.stuff; | |
| } | |
| return await render_response({ | |
| options, | |
| state, | |
| $session, | |
| page_config: { | |
| hydrate: options.hydrate, | |
| router: options.router | |
| }, | |
| stuff, | |
| status, | |
| error, | |
| branch, | |
| event, | |
| resolve_opts | |
| }); | |
| } catch (err) { | |
| const error = coalesce_to_error(err); | |
| options.handle_error(error, event); | |
| return new Response(error.stack, { | |
| status: 500 | |
| }); | |
| } | |
| } | |
| /** | |
| * @typedef {import('./types.js').Loaded} Loaded | |
| * @typedef {import('types').SSRNode} SSRNode | |
| * @typedef {import('types').SSROptions} SSROptions | |
| * @typedef {import('types').SSRState} SSRState | |
| */ | |
| /** | |
| * Gets the nodes, calls `load` for each of them, and then calls render to build the HTML response. | |
| * @param {{ | |
| * event: import('types').RequestEvent; | |
| * options: SSROptions; | |
| * state: SSRState; | |
| * $session: any; | |
| * resolve_opts: import('types').RequiredResolveOptions; | |
| * route: import('types').SSRPage; | |
| * }} opts | |
| * @returns {Promise<Response>} | |
| */ | |
| async function respond$1(opts) { | |
| const { event, options, state, $session, route, resolve_opts } = opts; | |
| /** @type {Array<SSRNode | undefined>} */ | |
| let nodes; | |
| if (!resolve_opts.ssr) { | |
| return await render_response({ | |
| ...opts, | |
| branch: [], | |
| page_config: { | |
| hydrate: true, | |
| router: true | |
| }, | |
| status: 200, | |
| error: null, | |
| event, | |
| stuff: {} | |
| }); | |
| } | |
| try { | |
| nodes = await Promise.all( | |
| // we use == here rather than === because [undefined] serializes as "[null]" | |
| route.a.map((n) => (n == undefined ? n : options.manifest._.nodes[n]())) | |
| ); | |
| } catch (err) { | |
| const error = coalesce_to_error(err); | |
| options.handle_error(error, event); | |
| return await respond_with_error({ | |
| event, | |
| options, | |
| state, | |
| $session, | |
| status: 500, | |
| error, | |
| resolve_opts | |
| }); | |
| } | |
| // the leaf node will be present. only layouts may be undefined | |
| const leaf = /** @type {SSRNode} */ (nodes[nodes.length - 1]).module; | |
| let page_config = get_page_config(leaf, options); | |
| if (state.prerendering) { | |
| // if the page isn't marked as prerenderable (or is explicitly | |
| // marked NOT prerenderable, if `prerender.default` is `true`), | |
| // then bail out at this point | |
| const should_prerender = leaf.prerender ?? options.prerender.default; | |
| if (!should_prerender) { | |
| return new Response(undefined, { | |
| status: 204 | |
| }); | |
| } | |
| } | |
| /** @type {Array<Loaded>} */ | |
| let branch = []; | |
| /** @type {number} */ | |
| let status = 200; | |
| /** @type {Error | null} */ | |
| let error = null; | |
| /** @type {string[]} */ | |
| let set_cookie_headers = []; | |
| let stuff = {}; | |
| ssr: { | |
| for (let i = 0; i < nodes.length; i += 1) { | |
| const node = nodes[i]; | |
| /** @type {Loaded | undefined} */ | |
| let loaded; | |
| if (node) { | |
| try { | |
| loaded = await load_node({ | |
| ...opts, | |
| node, | |
| stuff, | |
| is_error: false, | |
| is_leaf: i === nodes.length - 1 | |
| }); | |
| set_cookie_headers = set_cookie_headers.concat(loaded.set_cookie_headers); | |
| if (loaded.loaded.redirect) { | |
| return with_cookies( | |
| new Response(undefined, { | |
| status: loaded.loaded.status, | |
| headers: { | |
| location: loaded.loaded.redirect | |
| } | |
| }), | |
| set_cookie_headers | |
| ); | |
| } | |
| if (loaded.loaded.error) { | |
| ({ status, error } = loaded.loaded); | |
| } | |
| } catch (err) { | |
| const e = coalesce_to_error(err); | |
| options.handle_error(e, event); | |
| status = 500; | |
| error = e; | |
| } | |
| if (loaded && !error) { | |
| branch.push(loaded); | |
| } | |
| if (error) { | |
| while (i--) { | |
| if (route.b[i]) { | |
| const index = /** @type {number} */ (route.b[i]); | |
| const error_node = await options.manifest._.nodes[index](); | |
| /** @type {Loaded} */ | |
| let node_loaded; | |
| let j = i; | |
| while (!(node_loaded = branch[j])) { | |
| j -= 1; | |
| } | |
| try { | |
| const error_loaded = /** @type {import('./types').Loaded} */ ( | |
| await load_node({ | |
| ...opts, | |
| node: error_node, | |
| stuff: node_loaded.stuff, | |
| is_error: true, | |
| is_leaf: false, | |
| status, | |
| error | |
| }) | |
| ); | |
| if (error_loaded.loaded.error) { | |
| continue; | |
| } | |
| page_config = get_page_config(error_node.module, options); | |
| branch = branch.slice(0, j + 1).concat(error_loaded); | |
| stuff = { ...node_loaded.stuff, ...error_loaded.stuff }; | |
| break ssr; | |
| } catch (err) { | |
| const e = coalesce_to_error(err); | |
| options.handle_error(e, event); | |
| continue; | |
| } | |
| } | |
| } | |
| // TODO backtrack until we find an __error.svelte component | |
| // that we can use as the leaf node | |
| // for now just return regular error page | |
| return with_cookies( | |
| await respond_with_error({ | |
| event, | |
| options, | |
| state, | |
| $session, | |
| status, | |
| error, | |
| resolve_opts | |
| }), | |
| set_cookie_headers | |
| ); | |
| } | |
| } | |
| if (loaded && loaded.loaded.stuff) { | |
| stuff = { | |
| ...stuff, | |
| ...loaded.loaded.stuff | |
| }; | |
| } | |
| } | |
| } | |
| try { | |
| return with_cookies( | |
| await render_response({ | |
| ...opts, | |
| stuff, | |
| event, | |
| page_config, | |
| status, | |
| error, | |
| branch: branch.filter(Boolean) | |
| }), | |
| set_cookie_headers | |
| ); | |
| } catch (err) { | |
| const error = coalesce_to_error(err); | |
| options.handle_error(error, event); | |
| return with_cookies( | |
| await respond_with_error({ | |
| ...opts, | |
| status: 500, | |
| error | |
| }), | |
| set_cookie_headers | |
| ); | |
| } | |
| } | |
| /** | |
| * @param {import('types').SSRComponent} leaf | |
| * @param {SSROptions} options | |
| */ | |
| function get_page_config(leaf, options) { | |
| // TODO remove for 1.0 | |
| if ('ssr' in leaf) { | |
| throw new Error( | |
| '`export const ssr` has been removed — use the handle hook instead: https://kit.svelte.dev/docs/hooks#handle' | |
| ); | |
| } | |
| return { | |
| router: 'router' in leaf ? !!leaf.router : options.router, | |
| hydrate: 'hydrate' in leaf ? !!leaf.hydrate : options.hydrate | |
| }; | |
| } | |
| /** | |
| * @param {Response} response | |
| * @param {string[]} set_cookie_headers | |
| */ | |
| function with_cookies(response, set_cookie_headers) { | |
| if (set_cookie_headers.length) { | |
| set_cookie_headers.forEach((value) => { | |
| response.headers.append('set-cookie', value); | |
| }); | |
| } | |
| return response; | |
| } | |
| /** | |
| * @param {import('types').RequestEvent} event | |
| * @param {import('types').SSRPage} route | |
| * @param {import('types').SSROptions} options | |
| * @param {import('types').SSRState} state | |
| * @param {import('types').RequiredResolveOptions} resolve_opts | |
| * @returns {Promise<Response>} | |
| */ | |
| async function render_page(event, route, options, state, resolve_opts) { | |
| if (state.initiator === route) { | |
| // infinite request cycle detected | |
| return new Response(`Not found: ${event.url.pathname}`, { | |
| status: 404 | |
| }); | |
| } | |
| if (route.shadow) { | |
| const type = negotiate(event.request.headers.get('accept') || 'text/html', [ | |
| 'text/html', | |
| 'application/json' | |
| ]); | |
| if (type === 'application/json') { | |
| return render_endpoint(event, await route.shadow()); | |
| } | |
| } | |
| const $session = await options.hooks.getSession(event); | |
| return respond$1({ | |
| event, | |
| options, | |
| state, | |
| $session, | |
| resolve_opts, | |
| route | |
| }); | |
| } | |
| /** | |
| * @param {string} accept | |
| * @param {string[]} types | |
| */ | |
| function negotiate(accept, types) { | |
| const parts = accept | |
| .split(',') | |
| .map((str, i) => { | |
| const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str); | |
| if (match) { | |
| const [, type, subtype, q = '1'] = match; | |
| return { type, subtype, q: +q, i }; | |
| } | |
| throw new Error(`Invalid Accept header: ${accept}`); | |
| }) | |
| .sort((a, b) => { | |
| if (a.q !== b.q) { | |
| return b.q - a.q; | |
| } | |
| if ((a.subtype === '*') !== (b.subtype === '*')) { | |
| return a.subtype === '*' ? 1 : -1; | |
| } | |
| if ((a.type === '*') !== (b.type === '*')) { | |
| return a.type === '*' ? 1 : -1; | |
| } | |
| return a.i - b.i; | |
| }); | |
| let accepted; | |
| let min_priority = Infinity; | |
| for (const mimetype of types) { | |
| const [type, subtype] = mimetype.split('/'); | |
| const priority = parts.findIndex( | |
| (part) => | |
| (part.type === type || part.type === '*') && | |
| (part.subtype === subtype || part.subtype === '*') | |
| ); | |
| if (priority !== -1 && priority < min_priority) { | |
| accepted = mimetype; | |
| min_priority = priority; | |
| } | |
| } | |
| return accepted; | |
| } | |
| /** | |
| * @param {RegExpMatchArray} match | |
| * @param {string[]} names | |
| * @param {string[]} types | |
| * @param {Record<string, import('types').ParamMatcher>} matchers | |
| */ | |
| function exec(match, names, types, matchers) { | |
| /** @type {Record<string, string>} */ | |
| const params = {}; | |
| for (let i = 0; i < names.length; i += 1) { | |
| const name = names[i]; | |
| const type = types[i]; | |
| const value = match[i + 1] || ''; | |
| if (type) { | |
| const matcher = matchers[type]; | |
| if (!matcher) throw new Error(`Missing "${type}" param matcher`); // TODO do this ahead of time? | |
| if (!matcher(value)) return; | |
| } | |
| params[name] = value; | |
| } | |
| return params; | |
| } | |
| const DATA_SUFFIX = '/__data.json'; | |
| /** @param {{ html: string }} opts */ | |
| const default_transform = ({ html }) => html; | |
| /** @type {import('types').Respond} */ | |
| async function respond(request, options, state) { | |
| let url = new URL(request.url); | |
| const { parameter, allowed } = options.method_override; | |
| const method_override = url.searchParams.get(parameter)?.toUpperCase(); | |
| if (method_override) { | |
| if (request.method === 'POST') { | |
| if (allowed.includes(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`; | |
| return new Response(body, { | |
| status: 400 | |
| }); | |
| } | |
| } else { | |
| throw new Error(`${parameter}=${method_override} is only allowed with POST requests`); | |
| } | |
| } | |
| let decoded; | |
| try { | |
| decoded = decodeURI(url.pathname); | |
| } catch { | |
| return new Response('Malformed URI', { status: 400 }); | |
| } | |
| /** @type {import('types').SSRRoute | null} */ | |
| let route = null; | |
| /** @type {Record<string, string>} */ | |
| let params = {}; | |
| if (options.paths.base && !state.prerendering?.fallback) { | |
| if (!decoded.startsWith(options.paths.base)) { | |
| return new Response('Not found', { status: 404 }); | |
| } | |
| decoded = decoded.slice(options.paths.base.length) || '/'; | |
| } | |
| const is_data_request = decoded.endsWith(DATA_SUFFIX); | |
| if (is_data_request) { | |
| const data_suffix_length = DATA_SUFFIX.length - (options.trailing_slash === 'always' ? 1 : 0); | |
| decoded = decoded.slice(0, -data_suffix_length) || '/'; | |
| url = new URL(url.origin + url.pathname.slice(0, -data_suffix_length) + url.search); | |
| } | |
| if (!state.prerendering?.fallback) { | |
| const matchers = await options.manifest._.matchers(); | |
| for (const candidate of options.manifest._.routes) { | |
| const match = candidate.pattern.exec(decoded); | |
| if (!match) continue; | |
| const matched = exec(match, candidate.names, candidate.types, matchers); | |
| if (matched) { | |
| route = candidate; | |
| params = decode_params(matched); | |
| break; | |
| } | |
| } | |
| } | |
| if (route) { | |
| if (route.type === 'page') { | |
| const normalized = normalize_path(url.pathname, options.trailing_slash); | |
| if (normalized !== url.pathname && !state.prerendering?.fallback) { | |
| return new Response(undefined, { | |
| status: 301, | |
| headers: { | |
| 'x-sveltekit-normalize': '1', | |
| location: | |
| // ensure paths starting with '//' are not treated as protocol-relative | |
| (normalized.startsWith('//') ? url.origin + normalized : normalized) + | |
| (url.search === '?' ? '' : url.search) | |
| } | |
| }); | |
| } | |
| } else if (is_data_request) { | |
| // requesting /__data.json should fail for a standalone endpoint | |
| return new Response(undefined, { | |
| status: 404 | |
| }); | |
| } | |
| } | |
| /** @type {import('types').RequestEvent} */ | |
| const event = { | |
| get clientAddress() { | |
| if (!state.getClientAddress) { | |
| throw new Error( | |
| `${ | |
| import.meta.env.VITE_SVELTEKIT_ADAPTER_NAME | |
| } does not specify getClientAddress. Please raise an issue` | |
| ); | |
| } | |
| Object.defineProperty(event, 'clientAddress', { | |
| value: state.getClientAddress() | |
| }); | |
| return event.clientAddress; | |
| }, | |
| locals: {}, | |
| params, | |
| platform: state.platform, | |
| request, | |
| routeId: route && route.id, | |
| url | |
| }; | |
| // TODO remove this for 1.0 | |
| /** | |
| * @param {string} property | |
| * @param {string} replacement | |
| * @param {string} suffix | |
| */ | |
| const removed = (property, replacement, suffix = '') => ({ | |
| get: () => { | |
| 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()`' + | |
| details | |
| ); | |
| } | |
| }; | |
| Object.defineProperties(event, { | |
| 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'), | |
| body: body_getter, | |
| rawBody: body_getter | |
| }); | |
| /** @type {import('types').RequiredResolveOptions} */ | |
| let resolve_opts = { | |
| ssr: true, | |
| transformPage: default_transform | |
| }; | |
| // TODO match route before calling handle? | |
| try { | |
| const response = await options.hooks.handle({ | |
| event, | |
| resolve: async (event, opts) => { | |
| if (opts) { | |
| resolve_opts = { | |
| ssr: opts.ssr !== false, | |
| transformPage: opts.transformPage || default_transform | |
| }; | |
| } | |
| if (state.prerendering?.fallback) { | |
| return await render_response({ | |
| event, | |
| options, | |
| state, | |
| $session: await options.hooks.getSession(event), | |
| page_config: { router: true, hydrate: true }, | |
| stuff: {}, | |
| status: 200, | |
| error: null, | |
| branch: [], | |
| resolve_opts: { | |
| ...resolve_opts, | |
| ssr: false | |
| } | |
| }); | |
| } | |
| if (route) { | |
| /** @type {Response} */ | |
| let response; | |
| if (is_data_request && route.type === 'page' && route.shadow) { | |
| response = await render_endpoint(event, await route.shadow()); | |
| // loading data for a client-side transition is a special case | |
| if (request.headers.has('x-sveltekit-load')) { | |
| // since redirects are opaque to the browser, we need to repackage | |
| // 3xx responses as 200s with a custom header | |
| if (response.status >= 300 && response.status < 400) { | |
| const location = response.headers.get('location'); | |
| if (location) { | |
| const headers = new Headers(response.headers); | |
| headers.set('x-sveltekit-location', location); | |
| response = new Response(undefined, { | |
| status: 204, | |
| headers | |
| }); | |
| } | |
| } | |
| } | |
| } else { | |
| response = | |
| route.type === 'endpoint' | |
| ? await render_endpoint(event, await route.load()) | |
| : await render_page(event, route, options, state, resolve_opts); | |
| } | |
| if (response) { | |
| // 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'); | |
| // 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); | |
| } | |
| 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' | |
| ]) { | |
| const value = response.headers.get(key); | |
| if (value) headers.set(key, value); | |
| } | |
| return new Response(undefined, { | |
| status: 304, | |
| headers | |
| }); | |
| } | |
| } | |
| return response; | |
| } | |
| } | |
| // 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(event); | |
| return await respond_with_error({ | |
| event, | |
| options, | |
| state, | |
| $session, | |
| status: 404, | |
| error: new Error(`Not found: ${event.url.pathname}`), | |
| resolve_opts | |
| }); | |
| } | |
| if (state.prerendering) { | |
| return new Response('not found', { status: 404 }); | |
| } | |
| // we can't load the endpoint from our own manifest, | |
| // so we need to make an actual HTTP request | |
| return await fetch(request); | |
| }, | |
| // TODO remove for 1.0 | |
| // @ts-expect-error | |
| get request() { | |
| throw new Error('request in handle has been replaced with event' + details); | |
| } | |
| }); | |
| // TODO for 1.0, change the error message to point to docs rather than PR | |
| if (response && !(response instanceof Response)) { | |
| throw new Error('handle must return a Response object' + details); | |
| } | |
| return response; | |
| } catch (/** @type {unknown} */ e) { | |
| const error = coalesce_to_error(e); | |
| options.handle_error(error, event); | |
| try { | |
| const $session = await options.hooks.getSession(event); | |
| return await respond_with_error({ | |
| event, | |
| options, | |
| state, | |
| $session, | |
| status: 500, | |
| error, | |
| resolve_opts | |
| }); | |
| } catch (/** @type {unknown} */ e) { | |
| const error = coalesce_to_error(e); | |
| return new Response(options.dev ? error.stack : error.message, { | |
| status: 500 | |
| }); | |
| } | |
| } | |
| } | |
| export { respond }; | |