@sveltejs/kit
Advanced tools
Comparing version 1.15.0 to 1.20.2
{ | ||
"name": "@sveltejs/kit", | ||
"version": "1.15.0", | ||
"version": "1.20.2", | ||
"description": "The fastest way to build Svelte apps", | ||
"repository": { | ||
@@ -13,6 +14,6 @@ "type": "git", | ||
"dependencies": { | ||
"@sveltejs/vite-plugin-svelte": "^2.0.0", | ||
"@sveltejs/vite-plugin-svelte": "^2.4.1", | ||
"@types/cookie": "^0.5.1", | ||
"cookie": "^0.5.0", | ||
"devalue": "^4.3.0", | ||
"devalue": "^4.3.1", | ||
"esm-env": "^1.0.0", | ||
@@ -23,9 +24,9 @@ "kleur": "^4.1.5", | ||
"sade": "^1.8.1", | ||
"set-cookie-parser": "^2.5.1", | ||
"set-cookie-parser": "^2.6.0", | ||
"sirv": "^2.0.2", | ||
"tiny-glob": "^0.2.9", | ||
"undici": "5.21.0" | ||
"undici": "~5.22.0" | ||
}, | ||
"devDependencies": { | ||
"@playwright/test": "^1.29.2", | ||
"@playwright/test": "1.30.0", | ||
"@types/connect": "^3.4.35", | ||
@@ -37,12 +38,13 @@ "@types/marked": "^4.0.7", | ||
"@types/set-cookie-parser": "^2.4.2", | ||
"dts-buddy": "^0.0.10", | ||
"marked": "^4.2.3", | ||
"rollup": "^3.7.0", | ||
"svelte": "^3.56.0", | ||
"svelte-preprocess": "^5.0.0", | ||
"svelte-preprocess": "^5.0.3", | ||
"typescript": "^4.9.4", | ||
"uvu": "^0.5.6", | ||
"vite": "^4.2.0" | ||
"vite": "^4.3.6", | ||
"vitest": "^0.31.0" | ||
}, | ||
"peerDependencies": { | ||
"svelte": "^3.54.0", | ||
"svelte": "^3.54.0 || ^4.0.0-next.0", | ||
"vite": "^4.0.0" | ||
@@ -69,11 +71,15 @@ }, | ||
"./node": { | ||
"types": "./types/index.d.ts", | ||
"import": "./src/exports/node/index.js" | ||
}, | ||
"./node/polyfills": { | ||
"types": "./types/index.d.ts", | ||
"import": "./src/exports/node/polyfills.js" | ||
}, | ||
"./hooks": { | ||
"types": "./types/index.d.ts", | ||
"import": "./src/exports/hooks/index.js" | ||
}, | ||
"./vite": { | ||
"types": "./types/index.d.ts", | ||
"import": "./src/exports/vite/index.js" | ||
@@ -95,5 +101,5 @@ } | ||
"test:cross-platform:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:build", | ||
"test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\"", | ||
"test:unit": "vitest --config kit.vitest.config.js run", | ||
"postinstall": "node postinstall.js" | ||
} | ||
} |
@@ -27,3 +27,3 @@ import { existsSync, statSync, createReadStream, createWriteStream } from 'node:fs'; | ||
* }} opts | ||
* @returns {import('types').Builder} | ||
* @returns {import('@sveltejs/kit').Builder} | ||
*/ | ||
@@ -39,3 +39,3 @@ export function create_builder({ | ||
}) { | ||
/** @type {Map<import('types').RouteDefinition, import('types').RouteData>} */ | ||
/** @type {Map<import('@sveltejs/kit').RouteDefinition, import('types').RouteData>} */ | ||
const lookup = new Map(); | ||
@@ -52,3 +52,3 @@ | ||
/** @type {import('types').RouteDefinition} */ | ||
/** @type {import('@sveltejs/kit').RouteDefinition} */ | ||
const facade = { | ||
@@ -168,3 +168,3 @@ id: route.id, | ||
? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route))) | ||
: route_data | ||
: route_data.filter((route) => prerender_map.get(route.id) !== true) | ||
}); | ||
@@ -171,0 +171,0 @@ }, |
@@ -76,3 +76,3 @@ import fs from 'node:fs'; | ||
/** | ||
* @param {import('types').Config} config | ||
* @param {import('@sveltejs/kit').Config} config | ||
* @returns {import('types').ValidatedConfig} | ||
@@ -99,3 +99,3 @@ */ | ||
/** | ||
* @param {import('types').Config} config | ||
* @param {import('@sveltejs/kit').Config} config | ||
* @returns {import('types').ValidatedConfig} | ||
@@ -102,0 +102,0 @@ */ |
@@ -114,2 +114,7 @@ import { join } from 'node:path'; | ||
dangerZone: object({ | ||
// TODO 2.0: Remove this | ||
trackServerFetches: boolean(false) | ||
}), | ||
embedded: boolean(false), | ||
@@ -205,14 +210,44 @@ | ||
handleHttpError: validate('fail', (input, keypath) => { | ||
if (typeof input === 'function') return input; | ||
if (['fail', 'warn', 'ignore'].includes(input)) return input; | ||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); | ||
}), | ||
handleHttpError: validate( | ||
(/** @type {any} */ { message }) => { | ||
throw new Error( | ||
message + | ||
'\nTo suppress or handle this error, implement `handleHttpError` in https://kit.svelte.dev/docs/configuration#prerender' | ||
); | ||
}, | ||
(input, keypath) => { | ||
if (typeof input === 'function') return input; | ||
if (['fail', 'warn', 'ignore'].includes(input)) return input; | ||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); | ||
} | ||
), | ||
handleMissingId: validate('fail', (input, keypath) => { | ||
if (typeof input === 'function') return input; | ||
if (['fail', 'warn', 'ignore'].includes(input)) return input; | ||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); | ||
}), | ||
handleMissingId: validate( | ||
(/** @type {any} */ { message }) => { | ||
throw new Error( | ||
message + | ||
'\nTo suppress or handle this error, implement `handleMissingId` in https://kit.svelte.dev/docs/configuration#prerender' | ||
); | ||
}, | ||
(input, keypath) => { | ||
if (typeof input === 'function') return input; | ||
if (['fail', 'warn', 'ignore'].includes(input)) return input; | ||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); | ||
} | ||
), | ||
handleEntryGeneratorMismatch: validate( | ||
(/** @type {any} */ { message }) => { | ||
throw new Error( | ||
message + | ||
'\nTo suppress or handle this error, implement `handleEntryGeneratorMismatch` in https://kit.svelte.dev/docs/configuration#prerender' | ||
); | ||
}, | ||
(input, keypath) => { | ||
if (typeof input === 'function') return input; | ||
if (['fail', 'warn', 'ignore'].includes(input)) return input; | ||
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`); | ||
} | ||
), | ||
origin: validate('http://sveltekit-prerender', (input, keypath) => { | ||
@@ -219,0 +254,0 @@ assert_string(input, keypath); |
@@ -78,3 +78,3 @@ import { GENERATED_COMMENT } from '../constants.js'; | ||
properties.push(`${prefixed}: undefined;`); | ||
properties.push(`[key: string]: string | undefined;`); | ||
properties.push('[key: string]: string | undefined;'); | ||
} else { | ||
@@ -81,0 +81,0 @@ properties.push(`${prefixed}: string | undefined;`); |
@@ -46,13 +46,2 @@ import { s } from '../../utils/misc.js'; | ||
/** @typedef {{ index: number, path: string }} LookupEntry */ | ||
/** @type {Map<import('types').PageNode, LookupEntry>} */ | ||
const bundled_nodes = new Map(); | ||
build_data.manifest_data.nodes.forEach((node, i) => { | ||
bundled_nodes.set(node, { | ||
path: join_relative(relative_path, `/nodes/${i}.js`), | ||
index: i | ||
}); | ||
}); | ||
/** @type {(path: string) => string} */ | ||
@@ -70,12 +59,8 @@ const loader = (path) => `() => import('${path}')`; | ||
function get_nodes(indexes) { | ||
let string = indexes.map((n) => reindexed.get(n) ?? '').join(','); | ||
const string = indexes.map((n) => reindexed.get(n) ?? '').join(','); | ||
if (indexes.at(-1) === undefined) { | ||
// since JavaScript ignores trailing commas, we need to insert a dummy | ||
// comma so that the array has the correct length if the last item | ||
// is undefined | ||
string += ','; | ||
} | ||
return `[${string}]`; | ||
// since JavaScript ignores trailing commas, we need to insert a dummy | ||
// comma so that the array has the correct length if the last item | ||
// is undefined | ||
return `[${string},]`; | ||
} | ||
@@ -85,3 +70,3 @@ | ||
// String representation of | ||
/** @type {import('types').SSRManifest} */ | ||
/** @type {import('@sveltejs/kit').SSRManifest} */ | ||
return dedent` | ||
@@ -100,2 +85,4 @@ { | ||
${routes.map(route => { | ||
if (!route.page && !route.endpoint) return; | ||
route.params.forEach(param => { | ||
@@ -105,4 +92,2 @@ if (param.matcher) matchers.add(param.matcher); | ||
if (!route.page && !route.endpoint) return; | ||
return dedent` | ||
@@ -109,0 +94,0 @@ { |
@@ -5,3 +5,5 @@ import { join } from 'node:path'; | ||
import { | ||
validate_common_exports, | ||
validate_layout_exports, | ||
validate_layout_server_exports, | ||
validate_page_exports, | ||
validate_page_server_exports, | ||
@@ -14,2 +16,3 @@ validate_server_exports | ||
import { installPolyfills } from '../../exports/node/polyfills.js'; | ||
import { resolvePath } from '../../exports/index.js'; | ||
@@ -25,3 +28,3 @@ export default forked(import.meta.url, analyse); | ||
async function analyse({ manifest_path, env }) { | ||
/** @type {import('types').SSRManifest} */ | ||
/** @type {import('@sveltejs/kit').SSRManifest} */ | ||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; | ||
@@ -78,2 +81,4 @@ | ||
let config = undefined; | ||
/** @type {import('types').PrerenderEntryGenerator | undefined} */ | ||
let entries = undefined; | ||
@@ -102,2 +107,3 @@ if (route.endpoint) { | ||
config = mod.config; | ||
entries = mod.entries; | ||
} | ||
@@ -117,4 +123,4 @@ | ||
if (layout) { | ||
validate_common_exports(layout.server, layout.server_id); | ||
validate_common_exports(layout.universal, layout.universal_id); | ||
validate_layout_server_exports(layout.server, layout.server_id); | ||
validate_layout_exports(layout.universal, layout.universal_id); | ||
} | ||
@@ -128,3 +134,3 @@ } | ||
validate_page_server_exports(page.server, page.server_id); | ||
validate_common_exports(page.universal, page.universal_id); | ||
validate_page_exports(page.universal, page.universal_id); | ||
} | ||
@@ -135,2 +141,3 @@ | ||
config = get_config(nodes); | ||
entries ??= get_option(nodes, 'entries'); | ||
} | ||
@@ -147,3 +154,5 @@ | ||
}, | ||
prerender | ||
prerender, | ||
entries: | ||
entries && (await entries()).map((entry_object) => resolvePath(route.id, entry_object)) | ||
}); | ||
@@ -150,0 +159,0 @@ } |
@@ -16,2 +16,16 @@ import { resolve } from '../../utils/url.js'; | ||
const CRAWLABLE_META_NAME_ATTRS = new Set([ | ||
'og:url', | ||
'og:image', | ||
'og:image:url', | ||
'og:image:secure_url', | ||
'og:video', | ||
'og:video:url', | ||
'og:video:secure_url', | ||
'og:audio', | ||
'og:audio:url', | ||
'og:audio:secure_url', | ||
'twitter:image' | ||
]); | ||
/** | ||
@@ -85,2 +99,5 @@ * @param {string} html | ||
/** @type {Record<string, string>} */ | ||
const attributes = {}; | ||
if (tag === 'SCRIPT' || tag === 'STYLE') { | ||
@@ -100,5 +117,2 @@ while (i < html.length) { | ||
let href = ''; | ||
let rel = ''; | ||
while (i < html.length) { | ||
@@ -165,40 +179,3 @@ const start = i; | ||
value = decode(value); | ||
if (name === 'href') { | ||
if (tag === 'BASE') { | ||
base = resolve(base, value); | ||
} else { | ||
href = resolve(base, value); | ||
} | ||
} else if (name === 'id') { | ||
ids.push(value); | ||
} else if (name === 'name') { | ||
if (tag === 'A') ids.push(value); | ||
} else if (name === 'rel') { | ||
rel = value; | ||
} else if (name === 'src') { | ||
if (value) hrefs.push(resolve(base, value)); | ||
} else if (name === 'srcset') { | ||
const candidates = []; | ||
let insideURL = true; | ||
value = value.trim(); | ||
for (let i = 0; i < value.length; i++) { | ||
if ( | ||
value[i] === ',' && | ||
(!insideURL || (insideURL && WHITESPACE.test(value[i + 1]))) | ||
) { | ||
candidates.push(value.slice(0, i)); | ||
value = value.substring(i + 1).trim(); | ||
i = 0; | ||
insideURL = true; | ||
} else if (WHITESPACE.test(value[i])) { | ||
insideURL = false; | ||
} | ||
} | ||
candidates.push(value); | ||
for (const candidate of candidates) { | ||
const src = candidate.split(WHITESPACE)[0]; | ||
if (src) hrefs.push(resolve(base, src)); | ||
} | ||
} | ||
attributes[name] = value; | ||
} else { | ||
@@ -212,5 +189,53 @@ i -= 1; | ||
if (href && !/\bexternal\b/i.test(rel)) { | ||
hrefs.push(resolve(base, href)); | ||
const { href, id, name, property, rel, src, srcset, content } = attributes; | ||
if (href) { | ||
if (tag === 'BASE') { | ||
base = resolve(base, href); | ||
} else if (!rel || !/\bexternal\b/i.test(rel)) { | ||
hrefs.push(resolve(base, href)); | ||
} | ||
} | ||
if (id) { | ||
ids.push(id); | ||
} | ||
if (name && tag === 'A') { | ||
ids.push(name); | ||
} | ||
if (src) { | ||
hrefs.push(resolve(base, src)); | ||
} | ||
if (srcset) { | ||
let value = srcset; | ||
const candidates = []; | ||
let insideURL = true; | ||
value = value.trim(); | ||
for (let i = 0; i < value.length; i++) { | ||
if (value[i] === ',' && (!insideURL || (insideURL && WHITESPACE.test(value[i + 1])))) { | ||
candidates.push(value.slice(0, i)); | ||
value = value.substring(i + 1).trim(); | ||
i = 0; | ||
insideURL = true; | ||
} else if (WHITESPACE.test(value[i])) { | ||
insideURL = false; | ||
} | ||
} | ||
candidates.push(value); | ||
for (const candidate of candidates) { | ||
const src = candidate.split(WHITESPACE)[0]; | ||
if (src) hrefs.push(resolve(base, src)); | ||
} | ||
} | ||
if (tag === 'META' && content) { | ||
const attr = name ?? property; | ||
if (attr && CRAWLABLE_META_NAME_ATTRS.has(attr)) { | ||
hrefs.push(resolve(base, content)); | ||
} | ||
} | ||
} | ||
@@ -217,0 +242,0 @@ } |
@@ -30,3 +30,3 @@ import { readFileSync } from 'node:fs'; | ||
/** @type {import('types').SSRManifest} */ | ||
/** @type {import('@sveltejs/kit').SSRManifest} */ | ||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; | ||
@@ -33,0 +33,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'; | ||
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'; | ||
import { dirname, join } from 'node:path'; | ||
@@ -15,2 +15,3 @@ import { pathToFileURL } from 'node:url'; | ||
import { forked } from '../../utils/fork.js'; | ||
import * as devalue from 'devalue'; | ||
@@ -29,3 +30,3 @@ export default forked(import.meta.url, prerender); | ||
async function prerender({ out, manifest_path, metadata, verbose, env }) { | ||
/** @type {import('types').SSRManifest} */ | ||
/** @type {import('@sveltejs/kit').SSRManifest} */ | ||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; | ||
@@ -132,2 +133,10 @@ | ||
const handle_entry_generator_mismatch = normalise_error_handler( | ||
log, | ||
config.prerender.handleEntryGeneratorMismatch, | ||
({ generatedFromId, entry, matchedId }) => { | ||
return `The entries export from ${generatedFromId} generated entry ${entry}, which was matched by ${matchedId} - see the \`handleEntryGeneratorMismatch\` option in https://kit.svelte.dev/docs/configuration#prerender for more info.`; | ||
} | ||
); | ||
const q = queue(config.prerender.concurrency); | ||
@@ -170,4 +179,5 @@ | ||
* @param {string} [encoded] | ||
* @param {string} [generated_from_id] | ||
*/ | ||
function enqueue(referrer, decoded, encoded) { | ||
function enqueue(referrer, decoded, encoded, generated_from_id) { | ||
if (seen.has(decoded)) return; | ||
@@ -179,3 +189,3 @@ seen.add(decoded); | ||
return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer)); | ||
return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id)); | ||
} | ||
@@ -187,4 +197,5 @@ | ||
* @param {string?} referrer | ||
* @param {string} [generated_from_id] | ||
*/ | ||
async function visit(decoded, encoded, referrer) { | ||
async function visit(decoded, encoded, referrer, generated_from_id) { | ||
if (!decoded.startsWith(config.paths.base)) { | ||
@@ -215,2 +226,16 @@ handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' }); | ||
const encoded_id = response.headers.get('x-sveltekit-routeid'); | ||
const decoded_id = encoded_id && decode_uri(encoded_id); | ||
if ( | ||
decoded_id !== null && | ||
generated_from_id !== undefined && | ||
decoded_id !== generated_from_id | ||
) { | ||
handle_entry_generator_mismatch({ | ||
generatedFromId: generated_from_id, | ||
entry: decoded, | ||
matchedId: decoded_id | ||
}); | ||
} | ||
const body = Buffer.from(await response.arrayBuffer()); | ||
@@ -326,3 +351,7 @@ | ||
dest, | ||
`<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>` | ||
`<script>location.href=${devalue.uneval( | ||
location | ||
)};</script><meta http-equiv="refresh" content=${escape_html_attr( | ||
`0;url=${location}` | ||
)}>` | ||
); | ||
@@ -349,4 +378,19 @@ | ||
if (response.status === 200) { | ||
mkdirp(dirname(dest)); | ||
if (existsSync(dest) && statSync(dest).isDirectory()) { | ||
throw new Error( | ||
`Cannot save ${decoded} as it is already a directory. See https://kit.svelte.dev/docs/page-options#prerender-route-conflicts for more information` | ||
); | ||
} | ||
const dir = dirname(dest); | ||
if (existsSync(dir) && !statSync(dir).isDirectory()) { | ||
const parent = decoded.split('/').slice(0, -1).join('/'); | ||
throw new Error( | ||
`Cannot save ${decoded} as ${parent} is already a file. See https://kit.svelte.dev/docs/page-options#prerender-route-conflicts for more information` | ||
); | ||
} | ||
mkdirp(dir); | ||
log.info(`${response.status} ${decoded}`); | ||
@@ -375,5 +419,14 @@ writeFileSync(dest, body); | ||
/** @type {Array<{ id: string, entries: Array<string>}>} */ | ||
const route_level_entries = []; | ||
for (const [id, { entries }] of metadata.routes.entries()) { | ||
if (entries) { | ||
route_level_entries.push({ id, entries }); | ||
} | ||
} | ||
if ( | ||
config.prerender.entries.length > 1 || | ||
config.prerender.entries[0] !== '*' || | ||
route_level_entries.length > 0 || | ||
prerender_map.size > 0 | ||
@@ -399,2 +452,8 @@ ) { | ||
for (const { id, entries } of route_level_entries) { | ||
for (const entry of entries) { | ||
enqueue(null, config.paths.base + entry, undefined, id); | ||
} | ||
} | ||
await q.done(); | ||
@@ -401,0 +460,0 @@ |
@@ -230,2 +230,14 @@ import fs from 'node:fs'; | ||
/** | ||
* @param {string} type | ||
* @param {string} existing_file | ||
*/ | ||
function duplicate_files_error(type, existing_file) { | ||
return new Error( | ||
`Multiple ${type} files found in ${routes_base}${route.id} : ${path.basename( | ||
existing_file | ||
)} and ${file.name}` | ||
); | ||
} | ||
if (item.kind === 'component') { | ||
@@ -238,7 +250,17 @@ if (item.is_error) { | ||
} else if (item.is_layout) { | ||
if (!route.layout) route.layout = { depth, child_pages: [] }; | ||
if (!route.layout) { | ||
route.layout = { depth, child_pages: [] }; | ||
} else if (route.layout.component) { | ||
throw duplicate_files_error('layout component', route.layout.component); | ||
} | ||
route.layout.component = project_relative; | ||
if (item.uses_layout !== undefined) route.layout.parent_id = item.uses_layout; | ||
} else { | ||
if (!route.leaf) route.leaf = { depth }; | ||
if (!route.leaf) { | ||
route.leaf = { depth }; | ||
} else if (route.leaf.component) { | ||
throw duplicate_files_error('page component', route.leaf.component); | ||
} | ||
route.leaf.component = project_relative; | ||
@@ -248,8 +270,28 @@ if (item.uses_layout !== undefined) route.leaf.parent_id = item.uses_layout; | ||
} else if (item.is_layout) { | ||
if (!route.layout) route.layout = { depth, child_pages: [] }; | ||
if (!route.layout) { | ||
route.layout = { depth, child_pages: [] }; | ||
} else if (route.layout[item.kind]) { | ||
throw duplicate_files_error( | ||
item.kind + ' layout module', | ||
/** @type {string} */ (route.layout[item.kind]) | ||
); | ||
} | ||
route.layout[item.kind] = project_relative; | ||
} else if (item.is_page) { | ||
if (!route.leaf) route.leaf = { depth }; | ||
if (!route.leaf) { | ||
route.leaf = { depth }; | ||
} else if (route.leaf[item.kind]) { | ||
throw duplicate_files_error( | ||
item.kind + ' page module', | ||
/** @type {string} */ (route.leaf[item.kind]) | ||
); | ||
} | ||
route.leaf[item.kind] = project_relative; | ||
} else { | ||
if (route.endpoint) { | ||
throw duplicate_files_error('endpoint', route.endpoint.file); | ||
} | ||
route.endpoint = { | ||
@@ -420,3 +462,3 @@ file: project_relative | ||
const kind = !!(match[1] || match[4] || match[7]) ? 'server' : 'universal'; | ||
const kind = match[1] || match[4] || match[7] ? 'server' : 'universal'; | ||
@@ -478,3 +520,3 @@ return { | ||
// find all permutations created by optional parameters | ||
const split = normalized.split(/<\?(.+?)\>/g); | ||
const split = normalized.split(/<\?(.+?)>/g); | ||
@@ -481,0 +523,0 @@ let permutations = [/** @type {string} */ (split[0])]; |
@@ -11,3 +11,3 @@ import fs from 'node:fs'; | ||
// inside either `packages/kit` or `kit.svelte.dev` | ||
const descriptions_dir = fileURLToPath(new URL('../../../types/synthetic', import.meta.url)); | ||
const descriptions_dir = fileURLToPath(new URL('../../../src/types/synthetic', import.meta.url)); | ||
@@ -14,0 +14,0 @@ /** @param {string} filename */ |
@@ -24,3 +24,3 @@ import { relative_path, resolve_entry } from '../../utils/filesystem.js'; | ||
`import * as universal from ${s(relative_path(`${output}/nodes`, node.universal))};`, | ||
`export { universal };` | ||
'export { universal };' | ||
); | ||
@@ -27,0 +27,0 @@ } |
@@ -37,2 +37,3 @@ import fs from 'node:fs'; | ||
csrf_check_origin: ${s(config.kit.csrf.checkOrigin)}, | ||
track_server_fetches: ${s(config.kit.dangerZone.trackServerFetches)}, | ||
embedded: ${config.kit.embedded}, | ||
@@ -78,3 +79,3 @@ env_public_prefix: '${config.kit.env.publicPrefix}', | ||
export function write_server(config, output) { | ||
// TODO the casting shouldn't be necessary — investigate | ||
// TODO the casting shouldn't be necessary — investigate | ||
const hooks_file = /** @type {string} */ (resolve_entry(config.kit.files.hooks.server)); | ||
@@ -81,0 +82,0 @@ |
@@ -55,3 +55,3 @@ import fs from 'node:fs'; | ||
const has_meta_data = fs.existsSync(meta_data_file); | ||
let meta_data = has_meta_data | ||
const meta_data = has_meta_data | ||
? /** @type {Record<string, string[]>} */ (JSON.parse(fs.readFileSync(meta_data_file, 'utf-8'))) | ||
@@ -181,3 +181,3 @@ : {}; | ||
// now generate new types | ||
const imports = [`import type * as Kit from '@sveltejs/kit';`]; | ||
const imports = ["import type * as Kit from '@sveltejs/kit';"]; | ||
@@ -199,2 +199,8 @@ /** @type {string[]} */ | ||
if (route.params.length > 0) { | ||
exports.push( | ||
'export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;' | ||
); | ||
} | ||
declarations.push(`type RouteId = '${route.id}';`); | ||
@@ -206,20 +212,20 @@ | ||
// If T extends the empty object, void is also allowed as a return type | ||
`type MaybeWithVoid<T> = {} extends T ? T | void : T;`, | ||
'type MaybeWithVoid<T> = {} extends T ? T | void : T;', | ||
// Returns the key of the object whose values are required. | ||
`export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];`, | ||
'export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];', | ||
// Helper type to get the correct output type for load functions. It should be passed the parent type to check what types from App.PageData are still required. | ||
// If none, void is also allowed as a return type. | ||
`type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>`, | ||
'type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>', | ||
// null & {} == null, we need to prevent that in some situations | ||
`type EnsureDefined<T> = T extends null | undefined ? {} : T;`, | ||
'type EnsureDefined<T> = T extends null | undefined ? {} : T;', | ||
// Takes a union type and returns a union type where each type also has all properties | ||
// of all possible types (typed as undefined), making accessing them more ergonomic | ||
`type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;`, | ||
'type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;', | ||
// Re-export `Snapshot` from @sveltejs/kit — in future we could use this to infer <T> from the return type of `snapshot.capture` | ||
`export type Snapshot<T = any> = Kit.Snapshot<T>;` | ||
'export type Snapshot<T = any> = Kit.Snapshot<T>;' | ||
); | ||
@@ -256,6 +262,6 @@ } | ||
exports.push( | ||
`export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>` | ||
'export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>' | ||
); | ||
exports.push( | ||
`export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>` | ||
'export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>' | ||
); | ||
@@ -328,7 +334,7 @@ } | ||
if (route.endpoint) { | ||
exports.push(`export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;`); | ||
exports.push('export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;'); | ||
} | ||
if (route.leaf?.server || route.layout?.server || route.endpoint) { | ||
exports.push(`export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;`); | ||
exports.push('export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;'); | ||
} | ||
@@ -389,3 +395,3 @@ | ||
node.universal || (!is_page && all_pages_have_load) | ||
? `Partial<App.PageData> & Record<string, any> | void` | ||
? 'Partial<App.PageData> & Record<string, any> | void' | ||
: `OutputDataShape<${parent_type}>`; | ||
@@ -408,11 +414,11 @@ exports.push( | ||
exports.push( | ||
`type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;`, | ||
`type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];`, | ||
`type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;`, | ||
`type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];`, | ||
'type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;', | ||
'type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];', | ||
'type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;', | ||
'type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];', | ||
`type ActionsExport = typeof import('${from}').actions`, | ||
`export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>` | ||
'export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>' | ||
); | ||
type = `Expand<Kit.AwaitedActions<ActionsExport>> | null`; | ||
type = 'Expand<Kit.AwaitedActions<ActionsExport>> | null'; | ||
} | ||
@@ -445,3 +451,3 @@ exports.push(`export type ActionData = ${type};`); | ||
!is_page && all_pages_have_load | ||
? `Partial<App.PageData> & Record<string, any> | void` | ||
? 'Partial<App.PageData> & Record<string, any> | void' | ||
: `OutputDataShape<${parent_type}>`; | ||
@@ -694,3 +700,3 @@ exports.push( | ||
let a = declaration.type.pos; | ||
let b = declaration.type.end; | ||
const b = declaration.type.end; | ||
while (/\s/.test(content[a])) a += 1; | ||
@@ -767,3 +773,3 @@ | ||
let a = declaration.type.pos; | ||
let b = declaration.type.end; | ||
const b = declaration.type.end; | ||
while (/\s/.test(content[a])) a += 1; | ||
@@ -796,3 +802,3 @@ | ||
arg.name.end, | ||
`: import('./$types').RequestEvent` + (add_parens ? ')' : '') | ||
": import('./$types').RequestEvent" + (add_parens ? ')' : '') | ||
); | ||
@@ -799,0 +805,0 @@ } |
/** | ||
* @param {...import('types').Handle} handlers | ||
* @returns {import('types').Handle} | ||
* A helper function for sequencing multiple `handle` calls in a middleware-like manner. | ||
* The behavior for the `handle` options is as follows: | ||
* - `transformPageChunk` is applied in reverse order and merged | ||
* - `preload` is applied in forward order, the first option "wins" and no `preload` options after it are called | ||
* - `filterSerializedResponseHeaders` behaves the same as `preload` | ||
* | ||
* ```js | ||
* /// file: src/hooks.server.js | ||
* import { sequence } from '@sveltejs/kit/hooks'; | ||
* | ||
* /// type: import('@sveltejs/kit').Handle | ||
* async function first({ event, resolve }) { | ||
* console.log('first pre-processing'); | ||
* const result = await resolve(event, { | ||
* transformPageChunk: ({ html }) => { | ||
* // transforms are applied in reverse order | ||
* console.log('first transform'); | ||
* return html; | ||
* }, | ||
* preload: () => { | ||
* // this one wins as it's the first defined in the chain | ||
* console.log('first preload'); | ||
* } | ||
* }); | ||
* console.log('first post-processing'); | ||
* return result; | ||
* } | ||
* | ||
* /// type: import('@sveltejs/kit').Handle | ||
* async function second({ event, resolve }) { | ||
* console.log('second pre-processing'); | ||
* const result = await resolve(event, { | ||
* transformPageChunk: ({ html }) => { | ||
* console.log('second transform'); | ||
* return html; | ||
* }, | ||
* preload: () => { | ||
* console.log('second preload'); | ||
* }, | ||
* filterSerializedResponseHeaders: () => { | ||
* // this one wins as it's the first defined in the chain | ||
* console.log('second filterSerializedResponseHeaders'); | ||
* } | ||
* }); | ||
* console.log('second post-processing'); | ||
* return result; | ||
* } | ||
* | ||
* export const handle = sequence(first, second); | ||
* ``` | ||
* | ||
* The example above would print: | ||
* | ||
* ``` | ||
* first pre-processing | ||
* first preload | ||
* second pre-processing | ||
* second filterSerializedResponseHeaders | ||
* second transform | ||
* first transform | ||
* second post-processing | ||
* first post-processing | ||
* ``` | ||
* | ||
* @param {...import('@sveltejs/kit').Handle} handlers The chain of `handle` functions | ||
* @returns {import('@sveltejs/kit').Handle} | ||
*/ | ||
@@ -14,4 +78,4 @@ export function sequence(...handlers) { | ||
* @param {number} i | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('types').ResolveOptions | undefined} parent_options | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('@sveltejs/kit').ResolveOptions | undefined} parent_options | ||
* @returns {import('types').MaybePromise<Response>} | ||
@@ -25,3 +89,3 @@ */ | ||
resolve: (event, options) => { | ||
/** @param {{ html: string, done: boolean }} opts */ | ||
/** @type {import('@sveltejs/kit').ResolveOptions['transformPageChunk']} */ | ||
const transformPageChunk = async ({ html, done }) => { | ||
@@ -39,5 +103,17 @@ if (options?.transformPageChunk) { | ||
/** @type {import('@sveltejs/kit').ResolveOptions['filterSerializedResponseHeaders']} */ | ||
const filterSerializedResponseHeaders = | ||
parent_options?.filterSerializedResponseHeaders ?? | ||
options?.filterSerializedResponseHeaders; | ||
/** @type {import('@sveltejs/kit').ResolveOptions['preload']} */ | ||
const preload = parent_options?.preload ?? options?.preload; | ||
return i < length - 1 | ||
? apply_handle(i + 1, event, { transformPageChunk }) | ||
: resolve(event, { transformPageChunk }); | ||
? apply_handle(i + 1, event, { | ||
transformPageChunk, | ||
filterSerializedResponseHeaders, | ||
preload | ||
}) | ||
: resolve(event, { transformPageChunk, filterSerializedResponseHeaders, preload }); | ||
} | ||
@@ -44,0 +120,0 @@ }); |
import { HttpError, Redirect, ActionFailure } from '../runtime/control.js'; | ||
import { BROWSER, DEV } from 'esm-env'; | ||
import { get_route_segments } from '../utils/routing.js'; | ||
// For some reason we need to type the params as well here, | ||
// JSdoc doesn't seem to like @type with function overloads | ||
/** | ||
* @type {import('@sveltejs/kit').error} | ||
* @overload | ||
* @param {number} status | ||
* @param {any} message | ||
* @param {App.Error} body | ||
* @return {HttpError} | ||
*/ | ||
export function error(status, message) { | ||
/** | ||
* @overload | ||
* @param {number} status | ||
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body] | ||
* @return {HttpError} | ||
*/ | ||
/** | ||
* Creates an `HttpError` object with an HTTP status code and an optional message. | ||
* This object, if thrown during request handling, will cause SvelteKit to | ||
* return an error response without invoking `handleError`. | ||
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it. | ||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. | ||
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property. | ||
*/ | ||
export function error(status, body) { | ||
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) { | ||
@@ -16,6 +32,11 @@ throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`); | ||
return new HttpError(status, message); | ||
return new HttpError(status, body); | ||
} | ||
/** @type {import('@sveltejs/kit').redirect} */ | ||
/** | ||
* Create a `Redirect` object. If thrown during request handling, SvelteKit will return a redirect response. | ||
* Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it. | ||
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308. | ||
* @param {string} location The location to redirect to. | ||
*/ | ||
export function redirect(status, location) { | ||
@@ -29,3 +50,7 @@ if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) { | ||
/** @type {import('@sveltejs/kit').json} */ | ||
/** | ||
* Create a JSON `Response` object from the supplied data. | ||
* @param {any} data The value that will be serialized as JSON. | ||
* @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. `Content-Type: application/json` and `Content-Length` headers will be added automatically. | ||
*/ | ||
export function json(data, init) { | ||
@@ -56,3 +81,7 @@ // TODO deprecate this in favour of `Response.json` when it's | ||
/** @type {import('@sveltejs/kit').text} */ | ||
/** | ||
* Create a `Response` object from the supplied body. | ||
* @param {string} body The value that will be used as-is. | ||
* @param {ResponseInit} [init] Options such as `status` and `headers` that will be added to the response. A `Content-Length` header will be added automatically. | ||
*/ | ||
export function text(body, init) { | ||
@@ -71,5 +100,7 @@ const headers = new Headers(init?.headers); | ||
/** | ||
* Generates an `ActionFailure` object. | ||
* @param {number} status | ||
* @param {Record<string, any> | undefined} [data] | ||
* Create an `ActionFailure` object. | ||
* @template {Record<string, unknown> | undefined} [T=undefined] | ||
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. | ||
* @param {T} [data] Data associated with the failure (e.g. validation errors) | ||
* @returns {ActionFailure<T>} | ||
*/ | ||
@@ -79,1 +110,46 @@ export function fail(status, data) { | ||
} | ||
const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g; | ||
/** | ||
* Populate a route ID with params to resolve a pathname. | ||
* @example | ||
* ```js | ||
* resolvePath( | ||
* `/blog/[slug]/[...somethingElse]`, | ||
* { | ||
* slug: 'hello-world', | ||
* somethingElse: 'something/else' | ||
* } | ||
* ); // `/blog/hello-world/something/else` | ||
* ``` | ||
* @param {string} id | ||
* @param {Record<string, string | undefined>} params | ||
* @returns {string} | ||
*/ | ||
export function resolvePath(id, params) { | ||
const segments = get_route_segments(id); | ||
return ( | ||
'/' + | ||
segments | ||
.map((segment) => | ||
segment.replace(basic_param_pattern, (_, optional, name) => { | ||
const param_value = params[name]; | ||
// This is nested so TS correctly narrows the type | ||
if (!param_value) { | ||
if (optional) return ''; | ||
throw new Error(`Missing parameter '${name}' in route ${id}`); | ||
} | ||
if (param_value.startsWith('/') || param_value.endsWith('/')) | ||
throw new Error( | ||
`Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar` | ||
); | ||
return param_value; | ||
}) | ||
) | ||
.filter(Boolean) | ||
.join('/') | ||
); | ||
} |
@@ -95,3 +95,10 @@ import * as set_cookie_parser from 'set-cookie-parser'; | ||
/** @type {import('@sveltejs/kit/node').getRequest} */ | ||
/** | ||
* @param {{ | ||
* request: import('http').IncomingMessage; | ||
* base: string; | ||
* bodySizeLimit?: number; | ||
* }} options | ||
* @returns {Promise<Request>} | ||
*/ | ||
export async function getRequest({ request, base, bodySizeLimit }) { | ||
@@ -107,15 +114,27 @@ return new Request(base + request.url, { | ||
/** @type {import('@sveltejs/kit/node').setResponse} */ | ||
/** | ||
* @param {import('http').ServerResponse} res | ||
* @param {Response} response | ||
* @returns {Promise<void>} | ||
*/ | ||
export async function setResponse(res, response) { | ||
const headers = Object.fromEntries(response.headers); | ||
if (response.headers.has('set-cookie')) { | ||
const header = /** @type {string} */ (response.headers.get('set-cookie')); | ||
const split = set_cookie_parser.splitCookiesString(header); | ||
// @ts-expect-error | ||
headers['set-cookie'] = split; | ||
for (const [key, value] of response.headers) { | ||
try { | ||
res.setHeader( | ||
key, | ||
key === 'set-cookie' | ||
? set_cookie_parser.splitCookiesString( | ||
// This is absurd but necessary, TODO: investigate why | ||
/** @type {string}*/ (response.headers.get(key)) | ||
) | ||
: value | ||
); | ||
} catch (error) { | ||
res.getHeaderNames().forEach((name) => res.removeHeader(name)); | ||
res.writeHead(500).end(String(error)); | ||
return; | ||
} | ||
} | ||
res.writeHead(response.status, headers); | ||
res.writeHead(response.status); | ||
@@ -128,7 +147,6 @@ if (!response.body) { | ||
if (response.body.locked) { | ||
res.write( | ||
res.end( | ||
'Fatal error: Response body is locked. ' + | ||
`This can happen when the response was already read (for example through 'response.json()' or 'response.text()').` | ||
"This can happen when the response was already read (for example through 'response.json()' or 'response.text()')." | ||
); | ||
res.end(); | ||
return; | ||
@@ -135,0 +153,0 @@ } |
@@ -25,2 +25,10 @@ import { ReadableStream, TransformStream, WritableStream } from 'node:stream/web'; | ||
// TODO: remove this once we only support Node 18.11+ (the version multipart/form-data was added) | ||
/** | ||
* Make various web APIs available as globals: | ||
* - `crypto` | ||
* - `fetch` | ||
* - `Headers` | ||
* - `Request` | ||
* - `Response` | ||
*/ | ||
export function installPolyfills() { | ||
@@ -27,0 +35,0 @@ for (const name in globals) { |
@@ -5,2 +5,3 @@ import fs from 'node:fs'; | ||
import { s } from '../../../utils/misc.js'; | ||
import { normalizePath } from 'vite'; | ||
@@ -52,13 +53,6 @@ /** | ||
if (node.component && client_manifest) { | ||
const entry = find_deps(client_manifest, node.component, true); | ||
imported.push(...entry.imports); | ||
stylesheets.push(...entry.stylesheets); | ||
fonts.push(...entry.fonts); | ||
exports.push( | ||
`export const component = async () => (await import('../${ | ||
resolve_symlinks(server_manifest, node.component).chunk.file | ||
}')).default;`, | ||
`export const file = '${entry.file}';` // TODO what is this? | ||
}')).default;` | ||
); | ||
@@ -68,12 +62,4 @@ } | ||
if (node.universal) { | ||
if (client_manifest) { | ||
const entry = find_deps(client_manifest, node.universal, true); | ||
imported.push(...entry.imports); | ||
stylesheets.push(...entry.stylesheets); | ||
fonts.push(...entry.fonts); | ||
} | ||
imports.push(`import * as universal from '../${server_manifest[node.universal].file}';`); | ||
exports.push(`export { universal };`); | ||
exports.push('export { universal };'); | ||
exports.push(`export const universal_id = ${s(node.universal)};`); | ||
@@ -84,6 +70,18 @@ } | ||
imports.push(`import * as server from '../${server_manifest[node.server].file}';`); | ||
exports.push(`export { server };`); | ||
exports.push('export { server };'); | ||
exports.push(`export const server_id = ${s(node.server)};`); | ||
} | ||
if (client_manifest && (node.universal || node.component)) { | ||
const entry = find_deps( | ||
client_manifest, | ||
`${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`, | ||
true | ||
); | ||
imported.push(...entry.imports); | ||
stylesheets.push(...entry.stylesheets); | ||
fonts.push(...entry.fonts); | ||
} | ||
exports.push( | ||
@@ -90,0 +88,0 @@ `export const imports = ${s(imported)};`, |
@@ -38,3 +38,3 @@ import fs from 'node:fs'; | ||
// which is guaranteed to be `<base>/service-worker.js` | ||
const base = `location.pathname.split('/').slice(0, -1).join('/')`; | ||
const base = "location.pathname.split('/').slice(0, -1).join('/')"; | ||
@@ -83,4 +83,5 @@ fs.writeFileSync( | ||
}, | ||
configFile: false, | ||
define: vite_config.define, | ||
configFile: false, | ||
publicDir: false, | ||
resolve: { | ||
@@ -87,0 +88,0 @@ alias: [...get_config_aliases(kit), { find: '$service-worker', replacement: service_worker }] |
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
import { normalizePath } from 'vite'; | ||
@@ -75,3 +76,3 @@ /** | ||
while (!manifest[file]) { | ||
const next = path.relative('.', fs.realpathSync(file)); | ||
const next = normalizePath(path.relative('.', fs.realpathSync(file))); | ||
if (next === file) throw new Error(`Could not find file "${file}" in Vite manifest`); | ||
@@ -78,0 +79,0 @@ file = next; |
@@ -47,3 +47,3 @@ import fs from 'node:fs'; | ||
let manifest_data; | ||
/** @type {import('types').SSRManifest} */ | ||
/** @type {import('@sveltejs/kit').SSRManifest} */ | ||
let manifest; | ||
@@ -119,14 +119,7 @@ | ||
client: { | ||
start: { | ||
file: `${runtime_base}/client/start.js`, | ||
imports: [], | ||
stylesheets: [], | ||
fonts: [] | ||
}, | ||
app: { | ||
file: `${svelte_config.kit.outDir}/generated/client/app.js`, | ||
imports: [], | ||
stylesheets: [], | ||
fonts: [] | ||
} | ||
start: `${runtime_base}/client/start.js`, | ||
app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`, | ||
imports: [], | ||
stylesheets: [], | ||
fonts: [] | ||
}, | ||
@@ -150,3 +143,3 @@ nodes: manifest_data.nodes.map((node, index) => { | ||
result.component = async () => { | ||
const { module_node, module, url } = await resolve( | ||
const { module_node, module } = await resolve( | ||
/** @type {string} */ (node.component) | ||
@@ -156,3 +149,2 @@ ); | ||
module_nodes.push(module_node); | ||
result.file = url.endsWith('.svelte') ? url : url + '?import'; // TODO what is this for? | ||
@@ -191,15 +183,14 @@ return module.default; | ||
for (const dep of deps) { | ||
const url = new URL(dep.url, 'http://localhost/'); | ||
const url = new URL(dep.url, 'dummy:/'); | ||
const query = url.searchParams; | ||
if ( | ||
isCSSRequest(dep.file) || | ||
(query.has('svelte') && query.get('type') === 'style') | ||
(isCSSRequest(dep.file) || | ||
(query.has('svelte') && query.get('type') === 'style')) && | ||
!(query.has('raw') || query.has('url') || query.has('inline')) | ||
) { | ||
// setting `?inline` to load CSS modules as css string | ||
query.set('inline', ''); | ||
try { | ||
const mod = await loud_ssr_load_module( | ||
`${url.pathname}${url.search}${url.hash}` | ||
query.set('inline', ''); | ||
const mod = await vite.ssrLoadModule( | ||
`${decodeURI(url.pathname)}${url.search}${url.hash}` | ||
); | ||
@@ -243,3 +234,3 @@ styles[dep.url] = mod.default; | ||
matchers: async () => { | ||
/** @type {Record<string, import('types').ParamMatcher>} */ | ||
/** @type {Record<string, import('@sveltejs/kit').ParamMatcher>} */ | ||
const matchers = {}; | ||
@@ -364,3 +355,3 @@ | ||
// causing `instanceof` checks to fail | ||
const control_module_node = await import(`../../../runtime/control.js`); | ||
const control_module_node = await import('../../../runtime/control.js'); | ||
const control_module_vite = await vite.ssrLoadModule(`${runtime_base}/control.js`); | ||
@@ -367,0 +358,0 @@ |
@@ -104,3 +104,3 @@ import fs from 'node:fs'; | ||
`\n${colors.bold().red(path.relative('.', filename))}\n` + | ||
`\`<slot />\` missing — inner content will not be rendered`; | ||
'`<slot />` missing — inner content will not be rendered'; | ||
@@ -115,3 +115,6 @@ if (!warned.has(message)) { | ||
/** @return {Promise<import('vite').Plugin[]>} */ | ||
/** | ||
* Returns the SvelteKit Vite plugins. | ||
* @returns {Promise<import('vite').Plugin[]>} | ||
*/ | ||
export async function sveltekit() { | ||
@@ -193,2 +196,5 @@ const svelte_config = await load_config(); | ||
const sourcemapIgnoreList = /** @param {string} relative_path */ (relative_path) => | ||
relative_path.includes('node_modules') || relative_path.includes(kit.outDir); | ||
/** @type {import('vite').Plugin} */ | ||
@@ -237,5 +243,7 @@ const plugin_setup = { | ||
server: { | ||
cors: { preflightContinue: true }, | ||
fs: { | ||
allow: [...allow] | ||
}, | ||
sourcemapIgnoreList, | ||
watch: { | ||
@@ -246,4 +254,3 @@ ignored: [ | ||
] | ||
}, | ||
cors: { preflightContinue: true } | ||
} | ||
}, | ||
@@ -346,3 +353,3 @@ preview: { | ||
? `globalThis.__sveltekit_${version_hash}` | ||
: `globalThis.__sveltekit_dev`; | ||
: 'globalThis.__sveltekit_dev'; | ||
@@ -393,3 +400,3 @@ if (options?.ssr === false && process.env.TEST !== 'true') { | ||
// we use this alias so that we won't collide with user aliases | ||
case '\0__sveltekit/paths': | ||
case '\0__sveltekit/paths': { | ||
const { assets, base } = svelte_config.kit.paths; | ||
@@ -430,4 +437,5 @@ | ||
`; | ||
} | ||
case '\0__sveltekit/environment': | ||
case '\0__sveltekit/environment': { | ||
const { version } = svelte_config.kit; | ||
@@ -443,2 +451,3 @@ | ||
`; | ||
} | ||
} | ||
@@ -534,22 +543,7 @@ } | ||
/** | ||
* @param {string | undefined} file | ||
*/ | ||
function add_input(file) { | ||
if (!file) return; | ||
const resolved = path.resolve(file); | ||
const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); | ||
const name = relative.startsWith('..') | ||
? path.basename(file).replace(/^\+/, '') | ||
: relative.replace(/(\\|\/)\+/g, '-').replace(/[\\/]/g, '-'); | ||
input[`entry/${name}`] = resolved; | ||
} | ||
for (const node of manifest_data.nodes) { | ||
add_input(node.component); | ||
add_input(node.universal); | ||
} | ||
manifest_data.nodes.forEach((node, i) => { | ||
if (node.component || node.universal) { | ||
input[`nodes/${i}`] = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`; | ||
} | ||
}); | ||
} | ||
@@ -563,3 +557,7 @@ | ||
build: { | ||
copyPublicDir: !ssr, | ||
cssCodeSplit: true, | ||
cssMinify: initial_config.build?.minify == null ? true : !!initial_config.build.minify, | ||
// don't use the default name to avoid collisions with 'static/manifest.json' | ||
manifest: 'vite-manifest.json', | ||
outDir: `${out}/${ssr ? 'server' : 'client'}`, | ||
@@ -573,3 +571,4 @@ rollupOptions: { | ||
assetFileNames: `${prefix}/assets/[name].[hash][extname]`, | ||
hoistTransitiveImports: false | ||
hoistTransitiveImports: false, | ||
sourcemapIgnoreList | ||
}, | ||
@@ -579,6 +578,3 @@ preserveEntrySignatures: 'strict' | ||
ssrEmitAssets: true, | ||
copyPublicDir: !ssr, | ||
target: ssr ? 'node16.14' : undefined, | ||
// don't use the default name to avoid collisions with 'static/manifest.json' | ||
manifest: 'vite-manifest.json' | ||
target: ssr ? 'node16.14' : undefined | ||
}, | ||
@@ -678,3 +674,3 @@ publicDir: kit.files.assets, | ||
manifest_data, | ||
service_worker: !!service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? | ||
service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? | ||
client: null, | ||
@@ -737,13 +733,13 @@ server_manifest | ||
const deps_of = /** @param {string} f */ (f) => | ||
find_deps(client_manifest, posixify(path.relative('.', f)), false); | ||
const start = deps_of(`${runtime_directory}/client/start.js`); | ||
const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`); | ||
build_data.client = { | ||
start: find_deps( | ||
client_manifest, | ||
posixify(path.relative('.', `${runtime_directory}/client/start.js`)), | ||
false | ||
), | ||
app: find_deps( | ||
client_manifest, | ||
posixify(path.relative('.', `${kit.outDir}/generated/client-optimized/app.js`)), | ||
false | ||
) | ||
start: start.file, | ||
app: app.file, | ||
imports: [...start.imports, ...app.imports], | ||
stylesheets: [...start.stylesheets, ...app.stylesheets], | ||
fonts: [...start.fonts, ...app.fonts] | ||
}; | ||
@@ -750,0 +746,0 @@ |
import { BROWSER, DEV } from 'esm-env'; | ||
export { building, version } from '__sveltekit/environment'; | ||
/** | ||
* @type {import('$app/environment').browser} | ||
* `true` if the app is running in the browser. | ||
*/ | ||
@@ -9,6 +10,4 @@ export const browser = BROWSER; | ||
/** | ||
* @type {import('$app/environment').dev} | ||
* Whether the dev server is running. This is not guaranteed to correspond to `NODE_ENV` or `MODE`. | ||
*/ | ||
export const dev = DEV; | ||
export { building, version } from '__sveltekit/environment'; |
import * as devalue from 'devalue'; | ||
import { BROWSER, DEV } from 'esm-env'; | ||
import { client } from '../client/singletons.js'; | ||
import { invalidateAll } from './navigation.js'; | ||
import { BROWSER, DEV } from 'esm-env'; | ||
/** | ||
* @param {string} name | ||
* This action updates the `form` property of the current page with the given data and updates `$page.status`. | ||
* In case of an error, it redirects to the nearest error page. | ||
* @template {Record<string, unknown> | undefined} Success | ||
* @template {Record<string, unknown> | undefined} Failure | ||
* @param {import('@sveltejs/kit').ActionResult<Success, Failure>} result | ||
* @returns {Promise<void>} | ||
*/ | ||
function guard(name) { | ||
return () => { | ||
throw new Error(`Cannot call ${name}(...) on the server`); | ||
}; | ||
export function applyAction(result) { | ||
if (BROWSER) { | ||
return client.apply_action(result); | ||
} else { | ||
throw new Error('Cannot call applyAction(...) on the server'); | ||
} | ||
} | ||
/** @type {import('$app/forms').applyAction} */ | ||
export const applyAction = BROWSER ? client.apply_action : guard('applyAction'); | ||
/** @type {import('$app/forms').deserialize} */ | ||
/** | ||
* Use this function to deserialize the response from a form submission. | ||
* Usage: | ||
* | ||
* ```js | ||
* import { deserialize } from '$app/forms'; | ||
* | ||
* async function handleSubmit(event) { | ||
* const response = await fetch('/form?/action', { | ||
* method: 'POST', | ||
* body: new FormData(event.target) | ||
* }); | ||
* | ||
* const result = deserialize(await response.text()); | ||
* // ... | ||
* } | ||
* ``` | ||
* @template {Record<string, unknown> | undefined} Success | ||
* @template {Record<string, unknown> | undefined} Failure | ||
* @param {string} result | ||
* @returns {import('@sveltejs/kit').ActionResult<Success, Failure>} | ||
*/ | ||
export function deserialize(result) { | ||
@@ -27,9 +52,51 @@ const parsed = JSON.parse(result); | ||
/** @type {import('$app/forms').enhance} */ | ||
export function enhance(form, submit = () => {}) { | ||
if ( | ||
DEV && | ||
/** @type {HTMLFormElement} */ (HTMLFormElement.prototype.cloneNode.call(form)).method !== | ||
'post' | ||
) { | ||
/** | ||
* @param {string} old_name | ||
* @param {string} new_name | ||
* @param {string} call_location | ||
* @returns void | ||
*/ | ||
function warn_on_access(old_name, new_name, call_location) { | ||
if (!DEV) return; | ||
// TODO 2.0: Remove this code | ||
console.warn( | ||
`\`${old_name}\` has been deprecated in favor of \`${new_name}\`. \`${old_name}\` will be removed in a future version. (Called from ${call_location})` | ||
); | ||
} | ||
/** | ||
* Shallow clone an element, so that we can access e.g. `form.action` without worrying | ||
* that someone has added an `<input name="action">` (https://github.com/sveltejs/kit/issues/7593) | ||
* @template {HTMLElement} T | ||
* @param {T} element | ||
* @returns {T} | ||
*/ | ||
function clone(element) { | ||
return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element)); | ||
} | ||
/** | ||
* This action enhances a `<form>` element that otherwise would work without JavaScript. | ||
* | ||
* The `submit` function is called upon submission with the given FormData and the `action` that should be triggered. | ||
* If `cancel` is called, the form will not be submitted. | ||
* You can use the abort `controller` to cancel the submission in case another one starts. | ||
* If a function is returned, that function is called with the response from the server. | ||
* If nothing is returned, the fallback will be used. | ||
* | ||
* If this function or its return value isn't set, it | ||
* - falls back to updating the `form` prop with the returned data if the action is one same page as the form | ||
* - updates `$page.status` | ||
* - resets the `<form>` element and invalidates all data in case of successful submission with no redirect response | ||
* - redirects in case of a redirect response | ||
* - redirects to the nearest error page in case of an unexpected error | ||
* | ||
* If you provide a custom function with a callback and want to use the default behavior, invoke `update` in your callback. | ||
* @template {Record<string, unknown> | undefined} Success | ||
* @template {Record<string, unknown> | undefined} Failure | ||
* @param {HTMLFormElement} form_element The form element | ||
* @param {import('@sveltejs/kit').SubmitFunction<Success, Failure>} submit Submit callback | ||
*/ | ||
export function enhance(form_element, submit = () => {}) { | ||
if (DEV && clone(form_element).method !== 'post') { | ||
throw new Error('use:enhance can only be used on <form> fields with method="POST"'); | ||
@@ -41,3 +108,3 @@ } | ||
* action: URL; | ||
* result: import('types').ActionResult; | ||
* result: import('@sveltejs/kit').ActionResult; | ||
* reset?: boolean | ||
@@ -50,3 +117,3 @@ * }} opts | ||
// We call reset from the prototype to avoid DOM clobbering | ||
HTMLFormElement.prototype.reset.call(form); | ||
HTMLFormElement.prototype.reset.call(form_element); | ||
} | ||
@@ -73,13 +140,24 @@ await invalidateAll(); | ||
// We can't do submitter.formAction directly because that property is always set | ||
// We do cloneNode for avoid DOM clobbering - https://github.com/sveltejs/kit/issues/7593 | ||
event.submitter?.hasAttribute('formaction') | ||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction | ||
: /** @type {HTMLFormElement} */ (HTMLFormElement.prototype.cloneNode.call(form)).action | ||
: clone(form_element).action | ||
); | ||
const data = new FormData(form); | ||
const form_data = new FormData(form_element); | ||
if (DEV && clone(form_element).enctype !== 'multipart/form-data') { | ||
for (const value of form_data.values()) { | ||
if (value instanceof File) { | ||
// TODO 2.0: Upgrade to `throw Error` | ||
console.warn( | ||
'Your form contains <input type="file"> fields, but is missing the `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819. This will be upgraded to an error in v2.0.' | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
const submitter_name = event.submitter?.getAttribute('name'); | ||
if (submitter_name) { | ||
data.append(submitter_name, event.submitter?.getAttribute('value') ?? ''); | ||
form_data.append(submitter_name, event.submitter?.getAttribute('value') ?? ''); | ||
} | ||
@@ -92,2 +170,3 @@ | ||
// TODO 2.0: Remove `data` and `form` | ||
const callback = | ||
@@ -98,4 +177,12 @@ (await submit({ | ||
controller, | ||
data, | ||
form, | ||
get data() { | ||
warn_on_access('data', 'formData', 'use:enhance submit function'); | ||
return form_data; | ||
}, | ||
formData: form_data, | ||
get form() { | ||
warn_on_access('form', 'formElement', 'use:enhance submit function'); | ||
return form_element; | ||
}, | ||
formElement: form_element, | ||
submitter: event.submitter | ||
@@ -105,3 +192,3 @@ })) ?? fallback_callback; | ||
/** @type {import('types').ActionResult} */ | ||
/** @type {import('@sveltejs/kit').ActionResult} */ | ||
let result; | ||
@@ -117,3 +204,3 @@ | ||
cache: 'no-store', | ||
body: data, | ||
body: form_data, | ||
signal: controller.signal | ||
@@ -131,4 +218,12 @@ }); | ||
action, | ||
data, | ||
form, | ||
get data() { | ||
warn_on_access('data', 'formData', 'callback returned from use:enhance submit function'); | ||
return form_data; | ||
}, | ||
formData: form_data, | ||
get form() { | ||
warn_on_access('form', 'formElement', 'callback returned from use:enhance submit function'); | ||
return form_element; | ||
}, | ||
formElement: form_element, | ||
update: (opts) => fallback_callback({ action, result, reset: opts?.reset }), | ||
@@ -141,3 +236,3 @@ // @ts-expect-error generic constraints stuff we don't care about | ||
// @ts-expect-error | ||
HTMLFormElement.prototype.addEventListener.call(form, 'submit', handle_submit); | ||
HTMLFormElement.prototype.addEventListener.call(form_element, 'submit', handle_submit); | ||
@@ -147,5 +242,5 @@ return { | ||
// @ts-expect-error | ||
HTMLFormElement.prototype.removeEventListener.call(form, 'submit', handle_submit); | ||
HTMLFormElement.prototype.removeEventListener.call(form_element, 'submit', handle_submit); | ||
} | ||
}; | ||
} |
@@ -1,22 +0,112 @@ | ||
import { BROWSER } from 'esm-env'; | ||
import { client } from '../client/singletons.js'; | ||
import { client_method } from '../client/singletons.js'; | ||
/** | ||
* @param {string} name | ||
* If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling. | ||
* This is generally discouraged, since it breaks user expectations. | ||
* @returns {void} | ||
*/ | ||
function guard(name) { | ||
return () => { | ||
throw new Error(`Cannot call ${name}(...) on the server`); | ||
}; | ||
} | ||
export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scroll_handling'); | ||
export const disableScrollHandling = BROWSER | ||
? client.disable_scroll_handling | ||
: guard('disableScrollHandling'); | ||
export const goto = BROWSER ? client.goto : guard('goto'); | ||
export const invalidate = BROWSER ? client.invalidate : guard('invalidate'); | ||
export const invalidateAll = BROWSER ? client.invalidateAll : guard('invalidateAll'); | ||
export const preloadData = BROWSER ? client.preload_data : guard('preloadData'); | ||
export const preloadCode = BROWSER ? client.preload_code : guard('preloadCode'); | ||
export const beforeNavigate = BROWSER ? client.before_navigate : () => {}; | ||
export const afterNavigate = BROWSER ? client.after_navigate : () => {}; | ||
/** | ||
* Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. | ||
* For external URLs, use `window.location = url` instead of calling `goto(url)`. | ||
* | ||
* @type {(url: string | URL, opts?: { | ||
* replaceState?: boolean; | ||
* noScroll?: boolean; | ||
* keepFocus?: boolean; | ||
* invalidateAll?: boolean; | ||
* state?: any | ||
* }) => Promise<void>} | ||
* @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. | ||
* @param {Object} [opts] Options related to the navigation | ||
* @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` | ||
* @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation | ||
* @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body | ||
* @param {boolean} [invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation. | ||
* @param {any} [opts.state] The state of the new/updated history entry | ||
* @returns {Promise<void>} | ||
*/ | ||
export const goto = /* @__PURE__ */ client_method('goto'); | ||
/** | ||
* Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated. | ||
* | ||
* If the argument is given as a `string` or `URL`, it must resolve to the same URL that was passed to `fetch` or `depends` (including query parameters). | ||
* To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL. | ||
* | ||
* The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned. | ||
* This can be useful if you want to invalidate based on a pattern instead of a exact match. | ||
* | ||
* ```ts | ||
* // Example: Match '/path' regardless of the query parameters | ||
* import { invalidate } from '$app/navigation'; | ||
* | ||
* invalidate((url) => url.pathname === '/path'); | ||
* ``` | ||
* @type {(url: string | URL | ((url: URL) => boolean)) => Promise<void>} | ||
* @param {string | URL | ((url: URL) => boolean)} url The invalidated URL | ||
* @returns {Promise<void>} | ||
*/ | ||
export const invalidate = /* @__PURE__ */ client_method('invalidate'); | ||
/** | ||
* Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated. | ||
* @type {() => Promise<void>} | ||
* @returns {Promise<void>} | ||
*/ | ||
export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all'); | ||
/** | ||
* Programmatically preloads the given page, which means | ||
* 1. ensuring that the code for the page is loaded, and | ||
* 2. calling the page's load function with the appropriate options. | ||
* | ||
* This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `<a>` element with `data-sveltekit-preload-data`. | ||
* If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous. | ||
* Returns a Promise that resolves when the preload is complete. | ||
* | ||
* @type {(href: string) => Promise<void>} | ||
* @param {string} href Page to preload | ||
* @returns {Promise<void>} | ||
*/ | ||
export const preloadData = /* @__PURE__ */ client_method('preload_data'); | ||
/** | ||
* Programmatically imports the code for routes that haven't yet been fetched. | ||
* Typically, you might call this to speed up subsequent navigation. | ||
* | ||
* You can specify routes by any matching pathname such as `/about` (to match `src/routes/about/+page.svelte`) or `/blog/*` (to match `src/routes/blog/[slug]/+page.svelte`). | ||
* | ||
* Unlike `preloadData`, this won't call `load` functions. | ||
* Returns a Promise that resolves when the modules have been imported. | ||
* | ||
* @type {(...urls: string[]) => Promise<void>} | ||
* @param {...string[]} urls | ||
* @returns {Promise<void>} | ||
*/ | ||
export const preloadCode = /* @__PURE__ */ client_method('preload_code'); | ||
/** | ||
* A navigation interceptor that triggers before we navigate to a new URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. | ||
* Calling `cancel()` will prevent the navigation from completing. If the navigation would have directly unloaded the current page, calling `cancel` will trigger the native | ||
* browser unload confirmation dialog. In these cases, `navigation.willUnload` is `true`. | ||
* | ||
* When a navigation isn't client side, `navigation.to.route.id` will be `null`. | ||
* | ||
* `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. | ||
* @type {(callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void) => void} | ||
* @param {(navigation: import('@sveltejs/kit').BeforeNavigate) => void} callback | ||
* @returns {void} | ||
*/ | ||
export const beforeNavigate = /* @__PURE__ */ client_method('before_navigate'); | ||
/** | ||
* A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. | ||
* | ||
* `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. | ||
* @type {(callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void) => void} | ||
* @param {(navigation: import('@sveltejs/kit').AfterNavigate) => void} callback | ||
* @returns {void} | ||
*/ | ||
export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); |
@@ -6,3 +6,4 @@ import { getContext } from 'svelte'; | ||
/** | ||
* @type {import('$app/stores').getStores} | ||
* A function that returns all of the contextual stores. On the server, this must be called during component initialization. | ||
* Only use this if you need to defer store subscription until after the component has mounted, for some reason. | ||
*/ | ||
@@ -13,8 +14,11 @@ export const getStores = () => { | ||
return { | ||
/** @type {typeof page} */ | ||
page: { | ||
subscribe: stores.page.subscribe | ||
}, | ||
/** @type {typeof navigating} */ | ||
navigating: { | ||
subscribe: stores.navigating.subscribe | ||
}, | ||
/** @type {typeof updated} */ | ||
updated: stores.updated | ||
@@ -24,5 +28,10 @@ }; | ||
/** @type {typeof import('$app/stores').page} */ | ||
/** | ||
* A readable store whose value contains page data. | ||
* | ||
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time. | ||
* | ||
* @type {import('svelte/store').Readable<import('@sveltejs/kit').Page>} | ||
*/ | ||
export const page = { | ||
/** @param {(value: any) => void} fn */ | ||
subscribe(fn) { | ||
@@ -34,3 +43,10 @@ const store = __SVELTEKIT_DEV__ ? get_store('page') : getStores().page; | ||
/** @type {typeof import('$app/stores').navigating} */ | ||
/** | ||
* A readable store. | ||
* When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. | ||
* When navigating finishes, its value reverts to `null`. | ||
* | ||
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time. | ||
* @type {import('svelte/store').Readable<import('@sveltejs/kit').Navigation | null>} | ||
*/ | ||
export const navigating = { | ||
@@ -43,3 +59,8 @@ subscribe(fn) { | ||
/** @type {typeof import('$app/stores').updated} */ | ||
/** | ||
* A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. | ||
* | ||
* On the server, this store can only be subscribed to during component initialization. In the browser, it can be subscribed to at any time. | ||
* @type {import('svelte/store').Readable<boolean> & { check(): Promise<boolean> }} | ||
*/ | ||
export const updated = { | ||
@@ -58,4 +79,4 @@ subscribe(fn) { | ||
browser | ||
? `Cannot check updated store before subscribing` | ||
: `Can only check updated store in browser` | ||
? 'Cannot check updated store before subscribing' | ||
: 'Can only check updated store in browser' | ||
); | ||
@@ -62,0 +83,0 @@ } |
@@ -34,5 +34,5 @@ import { DEV } from 'esm-env'; | ||
import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js'; | ||
import { validate_common_exports } from '../../utils/exports.js'; | ||
import { validate_page_exports } from '../../utils/exports.js'; | ||
import { compact } from '../../utils/array.js'; | ||
import { validate_depends } from '../shared.js'; | ||
import { INVALIDATED_PARAM, validate_depends } from '../shared.js'; | ||
@@ -90,6 +90,6 @@ let errored = false; | ||
const callbacks = { | ||
/** @type {Array<(navigation: import('types').BeforeNavigate) => void>} */ | ||
/** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */ | ||
before_navigate: [], | ||
/** @type {Array<(navigation: import('types').AfterNavigate) => void>} */ | ||
/** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */ | ||
after_navigate: [] | ||
@@ -143,3 +143,3 @@ }; | ||
/** @type {import('types').Page} */ | ||
/** @type {import('@sveltejs/kit').Page} */ | ||
let page; | ||
@@ -168,3 +168,17 @@ | ||
load_cache = null; | ||
await update(intent, url, []); | ||
const nav_token = (token = {}); | ||
const navigation_result = intent && (await load_route(intent)); | ||
if (nav_token !== token) return; | ||
if (navigation_result) { | ||
if (navigation_result.type === 'redirect') { | ||
return goto(new URL(navigation_result.location, url).href, {}, [url.pathname], nav_token); | ||
} else { | ||
if (navigation_result.props.page !== undefined) { | ||
page = navigation_result.props.page; | ||
} | ||
root.$set(navigation_result.props); | ||
} | ||
} | ||
} | ||
@@ -263,182 +277,5 @@ | ||
/** | ||
* Returns `true` if update completes, `false` if it is aborted | ||
* @param {import('./types').NavigationIntent | undefined} intent | ||
* @param {URL} url | ||
* @param {string[]} redirect_chain | ||
* @param {number} [previous_history_index] | ||
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts] | ||
* @param {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token | ||
* @param {() => void} [callback] | ||
*/ | ||
async function update( | ||
intent, | ||
url, | ||
redirect_chain, | ||
previous_history_index, | ||
opts, | ||
nav_token = {}, | ||
callback | ||
) { | ||
token = nav_token; | ||
let navigation_result = intent && (await load_route(intent)); | ||
if (!navigation_result) { | ||
if (is_external_url(url, base)) { | ||
return await native_navigation(url); | ||
} | ||
navigation_result = await server_fallback( | ||
url, | ||
{ id: null }, | ||
await handle_error(new Error(`Not found: ${url.pathname}`), { | ||
url, | ||
params: {}, | ||
route: { id: null } | ||
}), | ||
404 | ||
); | ||
} | ||
// if this is an internal navigation intent, use the normalized | ||
// URL for the rest of the function | ||
url = intent?.url || url; | ||
// abort if user navigated during update | ||
if (token !== nav_token) return false; | ||
if (navigation_result.type === 'redirect') { | ||
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) { | ||
navigation_result = await load_root_error_page({ | ||
status: 500, | ||
error: await handle_error(new Error('Redirect loop'), { | ||
url, | ||
params: {}, | ||
route: { id: null } | ||
}), | ||
url, | ||
route: { id: null } | ||
}); | ||
} else { | ||
goto( | ||
new URL(navigation_result.location, url).href, | ||
{}, | ||
[...redirect_chain, url.pathname], | ||
nav_token | ||
); | ||
return false; | ||
} | ||
} else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) { | ||
const updated = await stores.updated.check(); | ||
if (updated) { | ||
await native_navigation(url); | ||
} | ||
} | ||
// reset invalidation only after a finished navigation. If there are redirects or | ||
// additional invalidations, they should get the same invalidation treatment | ||
invalidated.length = 0; | ||
force_invalidation = false; | ||
updating = true; | ||
// `previous_history_index` will be undefined for invalidation | ||
if (previous_history_index) { | ||
update_scroll_positions(previous_history_index); | ||
capture_snapshot(previous_history_index); | ||
} | ||
// ensure the url pathname matches the page's trailing slash option | ||
if ( | ||
navigation_result.props.page?.url && | ||
navigation_result.props.page.url.pathname !== url.pathname | ||
) { | ||
url.pathname = navigation_result.props.page?.url.pathname; | ||
} | ||
if (opts && opts.details) { | ||
const { details } = opts; | ||
const change = details.replaceState ? 0 : 1; | ||
details.state[INDEX_KEY] = current_history_index += change; | ||
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); | ||
if (!details.replaceState) { | ||
// if we navigated back, then pushed a new state, we can | ||
// release memory by pruning the scroll/snapshot lookup | ||
let i = current_history_index + 1; | ||
while (snapshots[i] || scroll_positions[i]) { | ||
delete snapshots[i]; | ||
delete scroll_positions[i]; | ||
i += 1; | ||
} | ||
} | ||
} | ||
// reset preload synchronously after the history state has been set to avoid race conditions | ||
load_cache = null; | ||
if (started) { | ||
current = navigation_result.state; | ||
// reset url before updating page store | ||
if (navigation_result.props.page) { | ||
navigation_result.props.page.url = url; | ||
} | ||
root.$set(navigation_result.props); | ||
} else { | ||
initialize(navigation_result); | ||
} | ||
// opts must be passed if we're navigating | ||
if (opts) { | ||
const { scroll, keepfocus } = opts; | ||
const { activeElement } = document; | ||
// need to render the DOM before we can scroll to the rendered elements and do focus management | ||
await tick(); | ||
// we reset scroll before dealing with focus, to avoid a flash of unscrolled content | ||
if (autoscroll) { | ||
const deep_linked = | ||
url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))); | ||
if (scroll) { | ||
scrollTo(scroll.x, scroll.y); | ||
} else if (deep_linked) { | ||
// Here we use `scrollIntoView` on the element instead of `scrollTo` | ||
// because it natively supports the `scroll-margin` and `scroll-behavior` | ||
// CSS properties. | ||
deep_linked.scrollIntoView(); | ||
} else { | ||
scrollTo(0, 0); | ||
} | ||
} | ||
const changed_focus = | ||
// reset focus only if any manual focus management didn't override it | ||
document.activeElement !== activeElement && | ||
// also refocus when activeElement is body already because the | ||
// focus event might not have been fired on it yet | ||
document.activeElement !== document.body; | ||
if (!keepfocus && !changed_focus) { | ||
await reset_focus(); | ||
} | ||
} else { | ||
await tick(); | ||
} | ||
autoscroll = true; | ||
if (navigation_result.props.page) { | ||
page = navigation_result.props.page; | ||
} | ||
if (callback) callback(); | ||
updating = false; | ||
} | ||
/** @param {import('./types').NavigationFinished} result */ | ||
function initialize(result) { | ||
if (DEV && document.querySelector('vite-error-overlay')) return; | ||
if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return; | ||
@@ -450,3 +287,3 @@ current = result.state; | ||
page = /** @type {import('types').Page} */ (result.props.page); | ||
page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page); | ||
@@ -461,3 +298,3 @@ root = new app.root({ | ||
/** @type {import('types').AfterNavigate} */ | ||
/** @type {import('@sveltejs/kit').AfterNavigate} */ | ||
const navigation = { | ||
@@ -505,2 +342,3 @@ from: null, | ||
url.pathname = normalize_path(url.pathname, slash); | ||
// eslint-disable-next-line | ||
url.search = url.search; // turn `/?` into `/` | ||
@@ -605,3 +443,3 @@ | ||
if (DEV) { | ||
validate_common_exports(node.universal); | ||
validate_page_exports(node.universal); | ||
} | ||
@@ -620,3 +458,3 @@ | ||
/** @type {import('types').LoadEvent} */ | ||
/** @type {import('@sveltejs/kit').LoadEvent} */ | ||
const load_input = { | ||
@@ -1076,3 +914,3 @@ route: { | ||
* url: URL; | ||
* type: import('types').NavigationType; | ||
* type: import('@sveltejs/kit').NavigationType; | ||
* intent?: import('./types').NavigationIntent; | ||
@@ -1085,3 +923,3 @@ * delta?: number; | ||
/** @type {import('types').Navigation} */ | ||
/** @type {import('@sveltejs/kit').Navigation} */ | ||
const navigation = { | ||
@@ -1131,3 +969,3 @@ from: { | ||
* } | null; | ||
* type: import('types').NavigationType; | ||
* type: import('@sveltejs/kit').NavigationType; | ||
* delta?: number; | ||
@@ -1147,3 +985,3 @@ * nav_token?: {}; | ||
delta, | ||
nav_token, | ||
nav_token = {}, | ||
accepted, | ||
@@ -1171,21 +1009,157 @@ blocked | ||
await update( | ||
intent, | ||
url, | ||
redirect_chain, | ||
previous_history_index, | ||
{ | ||
scroll, | ||
keepfocus, | ||
details | ||
}, | ||
nav_token, | ||
() => { | ||
navigating = false; | ||
callbacks.after_navigate.forEach((fn) => | ||
fn(/** @type {import('types').AfterNavigate} */ (navigation)) | ||
token = nav_token; | ||
let navigation_result = intent && (await load_route(intent)); | ||
if (!navigation_result) { | ||
if (is_external_url(url, base)) { | ||
return await native_navigation(url); | ||
} | ||
navigation_result = await server_fallback( | ||
url, | ||
{ id: null }, | ||
await handle_error(new Error(`Not found: ${url.pathname}`), { | ||
url, | ||
params: {}, | ||
route: { id: null } | ||
}), | ||
404 | ||
); | ||
} | ||
// if this is an internal navigation intent, use the normalized | ||
// URL for the rest of the function | ||
url = intent?.url || url; | ||
// abort if user navigated during update | ||
if (token !== nav_token) return false; | ||
if (navigation_result.type === 'redirect') { | ||
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) { | ||
navigation_result = await load_root_error_page({ | ||
status: 500, | ||
error: await handle_error(new Error('Redirect loop'), { | ||
url, | ||
params: {}, | ||
route: { id: null } | ||
}), | ||
url, | ||
route: { id: null } | ||
}); | ||
} else { | ||
goto( | ||
new URL(navigation_result.location, url).href, | ||
{}, | ||
[...redirect_chain, url.pathname], | ||
nav_token | ||
); | ||
stores.navigating.set(null); | ||
return false; | ||
} | ||
} else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) { | ||
const updated = await stores.updated.check(); | ||
if (updated) { | ||
await native_navigation(url); | ||
} | ||
} | ||
// reset invalidation only after a finished navigation. If there are redirects or | ||
// additional invalidations, they should get the same invalidation treatment | ||
invalidated.length = 0; | ||
force_invalidation = false; | ||
updating = true; | ||
update_scroll_positions(previous_history_index); | ||
capture_snapshot(previous_history_index); | ||
// ensure the url pathname matches the page's trailing slash option | ||
if ( | ||
navigation_result.props.page?.url && | ||
navigation_result.props.page.url.pathname !== url.pathname | ||
) { | ||
url.pathname = navigation_result.props.page?.url.pathname; | ||
} | ||
if (details) { | ||
const change = details.replaceState ? 0 : 1; | ||
details.state[INDEX_KEY] = current_history_index += change; | ||
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); | ||
if (!details.replaceState) { | ||
// if we navigated back, then pushed a new state, we can | ||
// release memory by pruning the scroll/snapshot lookup | ||
let i = current_history_index + 1; | ||
while (snapshots[i] || scroll_positions[i]) { | ||
delete snapshots[i]; | ||
delete scroll_positions[i]; | ||
i += 1; | ||
} | ||
} | ||
} | ||
// reset preload synchronously after the history state has been set to avoid race conditions | ||
load_cache = null; | ||
if (started) { | ||
current = navigation_result.state; | ||
// reset url before updating page store | ||
if (navigation_result.props.page) { | ||
navigation_result.props.page.url = url; | ||
} | ||
root.$set(navigation_result.props); | ||
} else { | ||
initialize(navigation_result); | ||
} | ||
const { activeElement } = document; | ||
// need to render the DOM before we can scroll to the rendered elements and do focus management | ||
await tick(); | ||
// we reset scroll before dealing with focus, to avoid a flash of unscrolled content | ||
if (autoscroll) { | ||
const deep_linked = | ||
url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))); | ||
if (scroll) { | ||
scrollTo(scroll.x, scroll.y); | ||
} else if (deep_linked) { | ||
// Here we use `scrollIntoView` on the element instead of `scrollTo` | ||
// because it natively supports the `scroll-margin` and `scroll-behavior` | ||
// CSS properties. | ||
deep_linked.scrollIntoView(); | ||
} else { | ||
scrollTo(0, 0); | ||
} | ||
} | ||
const changed_focus = | ||
// reset focus only if any manual focus management didn't override it | ||
document.activeElement !== activeElement && | ||
// also refocus when activeElement is body already because the | ||
// focus event might not have been fired on it yet | ||
document.activeElement !== document.body; | ||
if (!keepfocus && !changed_focus) { | ||
reset_focus(); | ||
} | ||
autoscroll = true; | ||
if (navigation_result.props.page) { | ||
page = navigation_result.props.page; | ||
} | ||
navigating = false; | ||
if (type === 'popstate') { | ||
restore_snapshot(current_history_index); | ||
} | ||
callbacks.after_navigate.forEach((fn) => | ||
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (navigation)) | ||
); | ||
stores.navigating.set(null); | ||
updating = false; | ||
} | ||
@@ -1218,3 +1192,3 @@ | ||
debugger; | ||
debugger; // eslint-disable-line | ||
} | ||
@@ -1285,4 +1259,4 @@ | ||
const { url, external } = get_link_info(a, base); | ||
if (external) return; | ||
const { url, external, download } = get_link_info(a, base); | ||
if (external || download) return; | ||
@@ -1320,4 +1294,4 @@ const options = get_router_options(a); | ||
for (const a of container.querySelectorAll('a')) { | ||
const { url, external } = get_link_info(a, base); | ||
if (external) continue; | ||
const { url, external, download } = get_link_info(a, base); | ||
if (external || download) continue; | ||
@@ -1343,3 +1317,3 @@ const options = get_router_options(a); | ||
* @param {unknown} error | ||
* @param {import('types').NavigationEvent} event | ||
* @param {import('@sveltejs/kit').NavigationEvent} event | ||
* @returns {import('types').MaybePromise<App.Error>} | ||
@@ -1411,3 +1385,3 @@ */ | ||
invalidateAll: () => { | ||
invalidate_all: () => { | ||
force_invalidation = true; | ||
@@ -1494,3 +1468,3 @@ return invalidate(); | ||
// it's due to an external or full-page-reload link, for which we don't want to call the hook again. | ||
/** @type {import('types').BeforeNavigate} */ | ||
/** @type {import('@sveltejs/kit').BeforeNavigate} */ | ||
const navigation = { | ||
@@ -1541,3 +1515,3 @@ from: { | ||
const { url, external, target } = get_link_info(a, base); | ||
const { url, external, target, download } = get_link_info(a, base); | ||
if (!url) return; | ||
@@ -1570,2 +1544,4 @@ | ||
if (download) return; | ||
// Ignore the following but fire beforeNavigate | ||
@@ -1589,5 +1565,13 @@ if (external || options.reload) { | ||
if (hash !== undefined && nonhash === location.href.split('#')[0]) { | ||
// If we are trying to navigate to the same hash, we should only | ||
// attempt to scroll to that element and avoid any history changes. | ||
// Otherwise, this can cause Firefox to incorrectly assign a null | ||
// history state value without any signal that we can detect. | ||
if (current.url.hash === url.hash) { | ||
event.preventDefault(); | ||
a.ownerDocument.getElementById(hash)?.scrollIntoView(); | ||
return; | ||
} | ||
// set this flag to distinguish between navigations triggered by | ||
// clicking a hash link and those triggered by popstate | ||
// TODO why not update history here directly? | ||
hash_navigating = true; | ||
@@ -1601,3 +1585,7 @@ | ||
return; | ||
if (!options.replace_state) return; | ||
// hashchange event shouldn't occur if the router is replacing state. | ||
hash_navigating = false; | ||
event.preventDefault(); | ||
} | ||
@@ -1693,3 +1681,2 @@ | ||
const delta = event.state[INDEX_KEY] - current_history_index; | ||
let blocked = false; | ||
@@ -1707,3 +1694,2 @@ await navigate({ | ||
history.go(-delta); | ||
blocked = true; | ||
}, | ||
@@ -1713,6 +1699,2 @@ type: 'popstate', | ||
}); | ||
if (!blocked) { | ||
restore_snapshot(current_history_index); | ||
} | ||
} | ||
@@ -1738,3 +1720,3 @@ }); | ||
for (const link of document.querySelectorAll('link')) { | ||
if (link.rel === 'icon') link.href = link.href; | ||
if (link.rel === 'icon') link.href = link.href; // eslint-disable-line | ||
} | ||
@@ -1799,10 +1781,26 @@ | ||
/** @type {Array<import('./types').BranchNode | undefined>} */ | ||
const branch = await Promise.all(branch_promises); | ||
const parsed_route = routes.find(({ id }) => id === route.id); | ||
// server-side will have compacted the branch, reinstate empty slots | ||
// so that error boundaries can be lined up correctly | ||
if (parsed_route) { | ||
const layouts = parsed_route.layouts; | ||
for (let i = 0; i < layouts.length; i++) { | ||
if (!layouts[i]) { | ||
branch.splice(i, 0, undefined); | ||
} | ||
} | ||
} | ||
result = await get_navigation_result_from_branch({ | ||
url, | ||
params, | ||
branch: await Promise.all(branch_promises), | ||
branch, | ||
status, | ||
error, | ||
form, | ||
route: routes.find(({ id }) => id === route.id) ?? null | ||
route: parsed_route ?? null | ||
}); | ||
@@ -1838,9 +1836,6 @@ } catch (error) { | ||
data_url.pathname = add_data_suffix(url.pathname); | ||
if (DEV && url.searchParams.has('x-sveltekit-invalidated')) { | ||
throw new Error('Cannot used reserved query parameter "x-sveltekit-invalidated"'); | ||
if (DEV && url.searchParams.has(INVALIDATED_PARAM)) { | ||
throw new Error(`Cannot used reserved query parameter "${INVALIDATED_PARAM}"`); | ||
} | ||
data_url.searchParams.append( | ||
'x-sveltekit-invalidated', | ||
invalid.map((x) => (x ? '1' : '')).join('_') | ||
); | ||
data_url.searchParams.append(INVALIDATED_PARAM, invalid.map((i) => (i ? '1' : '0')).join('')); | ||
@@ -1855,2 +1850,4 @@ const res = await native_fetch(data_url.href); | ||
// TODO: fix eslint error | ||
// eslint-disable-next-line | ||
return new Promise(async (resolve) => { | ||
@@ -1958,3 +1955,4 @@ /** | ||
root.tabIndex = -1; | ||
root.focus({ preventScroll: true }); | ||
// @ts-expect-error | ||
root.focus({ preventScroll: true, focusVisible: false }); | ||
@@ -1968,8 +1966,40 @@ // restore `tabindex` as to prevent `root` from stealing input from elements | ||
return new Promise((resolve) => { | ||
// capture current selection, so we can compare the state after | ||
// snapshot restoration and afterNavigate callbacks have run | ||
const selection = getSelection(); | ||
if (selection && selection.type !== 'None') { | ||
/** @type {Range[]} */ | ||
const ranges = []; | ||
for (let i = 0; i < selection.rangeCount; i += 1) { | ||
ranges.push(selection.getRangeAt(i)); | ||
} | ||
setTimeout(() => { | ||
if (selection.rangeCount !== ranges.length) return; | ||
for (let i = 0; i < selection.rangeCount; i += 1) { | ||
const a = ranges[i]; | ||
const b = selection.getRangeAt(i); | ||
// we need to do a deep comparison rather than just `a !== b` because | ||
// Safari behaves differently to other browsers | ||
if ( | ||
a.commonAncestorContainer !== b.commonAncestorContainer || | ||
a.startContainer !== b.startContainer || | ||
a.endContainer !== b.endContainer || | ||
a.startOffset !== b.startOffset || | ||
a.endOffset !== b.endOffset | ||
) { | ||
return; | ||
} | ||
} | ||
// if the selection hasn't changed (as a result of an element being (auto)focused, | ||
// or a programmatic selection, we reset everything as part of the navigation) | ||
// fixes https://github.com/sveltejs/kit/issues/8439 | ||
resolve(getSelection()?.removeAllRanges()); | ||
selection.removeAllRanges(); | ||
}); | ||
}); | ||
} | ||
} | ||
@@ -1976,0 +2006,0 @@ } |
@@ -32,10 +32,8 @@ import { DEV } from 'esm-env'; | ||
const stack_array = /** @type {string} */ (new Error().stack).split('\n'); | ||
// We need to do some Firefox-specific cutoff because it (impressively) maintains the stack | ||
// across events and for example traces a `fetch` call triggered from a button back | ||
// to the creation of the event listener and the element creation itself, | ||
// We need to do a cutoff because Safari and Firefox maintain the stack | ||
// across events and for example traces a `fetch` call triggered from a button | ||
// back to the creation of the event listener and the element creation itself, | ||
// where at some point client.js will show up, leading to false positives. | ||
const firefox_cutoff = stack_array.findIndex((a) => a.includes('*listen@')); | ||
const stack = stack_array | ||
.slice(0, firefox_cutoff !== -1 ? firefox_cutoff : undefined) | ||
.join('\n'); | ||
const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load')); | ||
const stack = stack_array.slice(0, cutoff + 2).join('\n'); | ||
@@ -42,0 +40,0 @@ const heuristic = can_inspect_stack_trace |
import { writable } from 'svelte/store'; | ||
import { create_updated_store, notifiable_store } from './utils.js'; | ||
import { BROWSER } from 'esm-env'; | ||
@@ -16,7 +17,38 @@ /** @type {import('./types').Client} */ | ||
/** | ||
* @template {keyof typeof client} T | ||
* @param {T} key | ||
* @returns {typeof client[T]} | ||
*/ | ||
export function client_method(key) { | ||
if (!BROWSER) { | ||
if (key === 'before_navigate' || key === 'after_navigate') { | ||
// @ts-expect-error doesn't recognize that both keys here return void so expects a async function | ||
return () => {}; | ||
} else { | ||
/** @type {Record<string, string>} */ | ||
const name_lookup = { | ||
disable_scroll_handling: 'disableScrollHandling', | ||
preload_data: 'preloadData', | ||
preload_code: 'preloadCode', | ||
invalidate_all: 'invalidateAll' | ||
}; | ||
return () => { | ||
throw new Error(`Cannot call ${name_lookup[key] ?? key}(...) on the server`); | ||
}; | ||
} | ||
} else { | ||
// @ts-expect-error | ||
return (...args) => client[key](...args); | ||
} | ||
} | ||
export const stores = { | ||
url: notifiable_store({}), | ||
page: notifiable_store({}), | ||
navigating: writable(/** @type {import('types').Navigation | null} */ (null)), | ||
updated: create_updated_store() | ||
url: /* @__PURE__ */ notifiable_store({}), | ||
page: /* @__PURE__ */ notifiable_store({}), | ||
navigating: /* @__PURE__ */ writable( | ||
/** @type {import('@sveltejs/kit').Navigation | null} */ (null) | ||
), | ||
updated: /* @__PURE__ */ create_updated_store() | ||
}; |
@@ -13,3 +13,3 @@ import { DEV } from 'esm-env'; | ||
console.warn( | ||
`Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>` | ||
'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>' | ||
); | ||
@@ -16,0 +16,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { applyAction } from '$app/forms'; | ||
import { applyAction } from '../app/forms'; | ||
import { | ||
@@ -10,14 +10,6 @@ afterNavigate, | ||
preloadData | ||
} from '$app/navigation'; | ||
} from '../app/navigation'; | ||
import { SvelteComponent } from 'svelte'; | ||
import { | ||
ClientHooks, | ||
CSRPageNode, | ||
CSRPageNodeLoader, | ||
CSRRoute, | ||
Page, | ||
ParamMatcher, | ||
TrailingSlash, | ||
Uses | ||
} from 'types'; | ||
import { ClientHooks, CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types'; | ||
import { Page, ParamMatcher } from '@sveltejs/kit'; | ||
@@ -58,3 +50,3 @@ export interface SvelteKitApp { | ||
invalidate: typeof invalidate; | ||
invalidateAll: typeof invalidateAll; | ||
invalidate_all: typeof invalidateAll; | ||
preload_code: typeof preloadCode; | ||
@@ -61,0 +53,0 @@ preload_data: typeof preloadData; |
@@ -35,6 +35,6 @@ import { BROWSER, DEV } from 'esm-env'; | ||
'preload-data': ['', 'off', 'tap', 'hover'], | ||
keepfocus: ['', 'off'], | ||
noscroll: ['', 'off'], | ||
reload: ['', 'off'], | ||
replacestate: ['', 'off'] | ||
keepfocus: ['', 'true', 'off', 'false'], | ||
noscroll: ['', 'true', 'off', 'false'], | ||
reload: ['', 'true', 'off', 'false'], | ||
replacestate: ['', 'true', 'off', 'false'] | ||
}); | ||
@@ -137,6 +137,7 @@ | ||
is_external_url(url, base) || | ||
(a.getAttribute('rel') || '').split(/\s+/).includes('external') || | ||
a.hasAttribute('download'); | ||
(a.getAttribute('rel') || '').split(/\s+/).includes('external'); | ||
return { url, external, target }; | ||
const download = url?.origin === location.origin && a.hasAttribute('download'); | ||
return { url, external, target, download }; | ||
} | ||
@@ -180,9 +181,23 @@ | ||
/** @param {string | null} value */ | ||
function get_option_state(value) { | ||
switch (value) { | ||
case '': | ||
case 'true': | ||
return true; | ||
case 'off': | ||
case 'false': | ||
return false; | ||
default: | ||
return null; | ||
} | ||
} | ||
return { | ||
preload_code: levels[preload_code ?? 'off'], | ||
preload_data: levels[preload_data ?? 'off'], | ||
keep_focus: keep_focus === 'off' ? false : keep_focus === '' ? true : null, | ||
noscroll: noscroll === 'off' ? false : noscroll === '' ? true : null, | ||
reload: reload === 'off' ? false : reload === '' ? true : null, | ||
replace_state: replace_state === 'off' ? false : replace_state === '' ? true : null | ||
keep_focus: get_option_state(keep_focus), | ||
noscroll: get_option_state(noscroll), | ||
reload: get_option_state(reload), | ||
replace_state: get_option_state(replace_state) | ||
}; | ||
@@ -224,2 +239,9 @@ } | ||
if (DEV || !BROWSER) { | ||
return { | ||
subscribe, | ||
check: async () => false | ||
}; | ||
} | ||
const interval = __SVELTEKIT_APP_VERSION_POLL_INTERVAL__; | ||
@@ -232,4 +254,2 @@ | ||
async function check() { | ||
if (DEV || !BROWSER) return false; | ||
clearTimeout(timeout); | ||
@@ -239,10 +259,14 @@ | ||
const res = await fetch(`${assets}/${__SVELTEKIT_APP_VERSION_FILE__}`, { | ||
headers: { | ||
pragma: 'no-cache', | ||
'cache-control': 'no-cache' | ||
try { | ||
const res = await fetch(`${assets}/${__SVELTEKIT_APP_VERSION_FILE__}`, { | ||
headers: { | ||
pragma: 'no-cache', | ||
'cache-control': 'no-cache' | ||
} | ||
}); | ||
if (!res.ok) { | ||
return false; | ||
} | ||
}); | ||
if (res.ok) { | ||
const data = await res.json(); | ||
@@ -257,4 +281,4 @@ const updated = data.version !== version; | ||
return updated; | ||
} else { | ||
throw new Error(`Version check failed: ${res.status}`); | ||
} catch { | ||
return false; | ||
} | ||
@@ -261,0 +285,0 @@ } |
@@ -1,2 +0,2 @@ | ||
export let HttpError = class HttpError { | ||
export class HttpError { | ||
/** | ||
@@ -20,5 +20,5 @@ * @param {number} status | ||
} | ||
}; | ||
} | ||
export let Redirect = class Redirect { | ||
export class Redirect { | ||
/** | ||
@@ -32,3 +32,3 @@ * @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status | ||
} | ||
}; | ||
} | ||
@@ -38,3 +38,3 @@ /** | ||
*/ | ||
export let ActionFailure = class ActionFailure { | ||
export class ActionFailure { | ||
/** | ||
@@ -48,3 +48,3 @@ * @param {number} status | ||
} | ||
}; | ||
} | ||
@@ -64,5 +64,8 @@ /** | ||
export function replace_implementations(implementations) { | ||
ActionFailure = implementations.ActionFailure; | ||
HttpError = implementations.HttpError; | ||
Redirect = implementations.Redirect; | ||
// @ts-expect-error | ||
ActionFailure = implementations.ActionFailure; // eslint-disable-line no-class-assign | ||
// @ts-expect-error | ||
HttpError = implementations.HttpError; // eslint-disable-line no-class-assign | ||
// @ts-expect-error | ||
Redirect = implementations.Redirect; // eslint-disable-line no-class-assign | ||
} |
declare module '__SERVER__/internal.js' { | ||
export const options: import('types').SSROptions; | ||
export const get_hooks: () => Promise<{ | ||
handle?: import('types').Handle; | ||
handleError?: import('types').HandleServerError; | ||
handleFetch?: import('types').HandleFetch; | ||
handle?: import('@sveltejs/kit').Handle; | ||
handleError?: import('@sveltejs/kit').HandleServerError; | ||
handleFetch?: import('@sveltejs/kit').HandleFetch; | ||
}>; | ||
} |
@@ -40,7 +40,7 @@ import { parse, serialize } from 'cookie'; | ||
/** @type {import('types').Cookies} */ | ||
/** @type {import('@sveltejs/kit').Cookies} */ | ||
const cookies = { | ||
// The JSDoc param annotations appearing below for get, set and delete | ||
// are necessary to expose the `cookie` library types to | ||
// typescript users. `@type {import('types').Cookies}` above is not | ||
// typescript users. `@type {import('@sveltejs/kit').Cookies}` above is not | ||
// sufficient to do so. | ||
@@ -111,28 +111,3 @@ | ||
set(name, value, opts = {}) { | ||
let path = opts.path ?? default_path; | ||
new_cookies[name] = { | ||
name, | ||
value, | ||
options: { | ||
...defaults, | ||
...opts, | ||
path | ||
} | ||
}; | ||
if (__SVELTEKIT_DEV__) { | ||
const serialized = serialize(name, value, new_cookies[name].options); | ||
if (new TextEncoder().encode(serialized).byteLength > MAX_COOKIE_SIZE) { | ||
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`); | ||
} | ||
cookie_paths[name] ??= new Set(); | ||
if (!value) { | ||
cookie_paths[name].delete(path); | ||
} else { | ||
cookie_paths[name].add(path); | ||
} | ||
} | ||
set_internal(name, value, { ...defaults, ...opts }); | ||
}, | ||
@@ -198,3 +173,36 @@ | ||
return { cookies, new_cookies, get_cookie_header }; | ||
/** | ||
* @param {string} name | ||
* @param {string} value | ||
* @param {import('cookie').CookieSerializeOptions} opts | ||
*/ | ||
function set_internal(name, value, opts) { | ||
const path = opts.path ?? default_path; | ||
new_cookies[name] = { | ||
name, | ||
value, | ||
options: { | ||
...opts, | ||
path | ||
} | ||
}; | ||
if (__SVELTEKIT_DEV__) { | ||
const serialized = serialize(name, value, new_cookies[name].options); | ||
if (new TextEncoder().encode(serialized).byteLength > MAX_COOKIE_SIZE) { | ||
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`); | ||
} | ||
cookie_paths[name] ??= new Set(); | ||
if (!value) { | ||
cookie_paths[name].delete(path); | ||
} else { | ||
cookie_paths[name].add(path); | ||
} | ||
} | ||
} | ||
return { cookies, new_cookies, get_cookie_header, set_internal }; | ||
} | ||
@@ -201,0 +209,0 @@ |
@@ -11,11 +11,9 @@ import { HttpError, Redirect } from '../../control.js'; | ||
export const INVALIDATED_PARAM = 'x-sveltekit-invalidated'; | ||
const encoder = new TextEncoder(); | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSRRoute} route | ||
* @param {import('types').SSROptions} options | ||
* @param {import('types').SSRManifest} manifest | ||
* @param {import('@sveltejs/kit').SSRManifest} manifest | ||
* @param {import('types').SSRState} state | ||
@@ -82,3 +80,4 @@ * @param {boolean[] | undefined} invalidated_data_nodes | ||
return data; | ||
} | ||
}, | ||
track_server_fetches: options.track_server_fetches | ||
}); | ||
@@ -189,3 +188,3 @@ } catch (e) { | ||
* async iterable containing their resolutions | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSROptions} options | ||
@@ -192,0 +191,0 @@ * @param {Array<import('types').ServerDataSkippedNode | import('types').ServerDataNode | import('types').ServerErrorNode | null | undefined>} nodes |
@@ -6,3 +6,3 @@ import { negotiate } from '../../utils/http.js'; | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSREndpoint} mod | ||
@@ -43,4 +43,4 @@ * @param {import('types').SSRState} state | ||
try { | ||
const response = await handler( | ||
/** @type {import('types').RequestEvent<Record<string, any>>} */ (event) | ||
let response = await handler( | ||
/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event) | ||
); | ||
@@ -55,2 +55,9 @@ | ||
if (state.prerendering) { | ||
// the returned Response might have immutable Headers | ||
// so we should clone them before trying to mutate them | ||
response = new Response(response.body, { | ||
status: response.status, | ||
statusText: response.statusText, | ||
headers: new Headers(response.headers) | ||
}); | ||
response.headers.set('x-sveltekit-prerender', String(prerender)); | ||
@@ -73,3 +80,3 @@ } | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
*/ | ||
@@ -76,0 +83,0 @@ export function is_endpoint_request(event) { |
@@ -7,16 +7,15 @@ import * as set_cookie_parser from 'set-cookie-parser'; | ||
* @param {{ | ||
* event: import('types').RequestEvent; | ||
* event: import('@sveltejs/kit').RequestEvent; | ||
* options: import('types').SSROptions; | ||
* manifest: import('types').SSRManifest; | ||
* manifest: import('@sveltejs/kit').SSRManifest; | ||
* state: import('types').SSRState; | ||
* get_cookie_header: (url: URL, header: string | null) => string; | ||
* set_internal: (name: string, value: string, opts: import('cookie').CookieSerializeOptions) => void; | ||
* }} opts | ||
* @returns {typeof fetch} | ||
*/ | ||
export function create_fetch({ event, options, manifest, state, get_cookie_header }) { | ||
export function create_fetch({ event, options, manifest, state, get_cookie_header, set_internal }) { | ||
return async (info, init) => { | ||
const original_request = normalize_fetch_input(info, init, event.url); | ||
const request_body = init?.body; | ||
// some runtimes (e.g. Cloudflare) error if you access `request.mode`, | ||
@@ -72,5 +71,2 @@ // annoyingly, so we need to read the value from the `init` object instead | ||
/** @type {Response} */ | ||
let response; | ||
// handle fetch requests for static assets. e.g. prebaked data, etc. | ||
@@ -116,11 +112,2 @@ // we need to support everything the browser's fetch supports | ||
if (request_body && typeof request_body !== 'string' && !ArrayBuffer.isView(request_body)) { | ||
// TODO is this still necessary? we just bail out below | ||
// 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 or TypedArray'); | ||
} | ||
if (!request.headers.has('accept')) { | ||
@@ -137,3 +124,4 @@ request.headers.set('accept', '*/*'); | ||
response = await respond(request, options, manifest, { | ||
/** @type {Response} */ | ||
const response = await respond(request, options, manifest, { | ||
...state, | ||
@@ -149,3 +137,3 @@ depth: state.depth + 1 | ||
// options.sameSite is string, something more specific is required - type cast is safe | ||
event.cookies.set( | ||
set_internal( | ||
name, | ||
@@ -152,0 +140,0 @@ value, |
import { respond } from './respond.js'; | ||
import { set_private_env, set_public_env } from '../shared-server.js'; | ||
import { options, get_hooks } from '__SERVER__/internal.js'; | ||
import { DEV } from 'esm-env'; | ||
@@ -9,6 +10,6 @@ export class Server { | ||
/** @type {import('types').SSRManifest} */ | ||
/** @type {import('@sveltejs/kit').SSRManifest} */ | ||
#manifest; | ||
/** @param {import('types').SSRManifest} manifest */ | ||
/** @param {import('@sveltejs/kit').SSRManifest} manifest */ | ||
constructor(manifest) { | ||
@@ -39,10 +40,23 @@ /** @type {import('types').SSROptions} */ | ||
if (!this.#options.hooks) { | ||
const module = await get_hooks(); | ||
try { | ||
const module = await get_hooks(); | ||
this.#options.hooks = { | ||
handle: module.handle || (({ event, resolve }) => resolve(event)), | ||
// @ts-expect-error | ||
handleError: module.handleError || (({ error }) => console.error(error?.stack)), | ||
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)) | ||
}; | ||
this.#options.hooks = { | ||
handle: module.handle || (({ event, resolve }) => resolve(event)), | ||
handleError: module.handleError || (({ error }) => console.error(error)), | ||
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)) | ||
}; | ||
} catch (error) { | ||
if (DEV) { | ||
this.#options.hooks = { | ||
handle: () => { | ||
throw error; | ||
}, | ||
handleError: ({ error }) => console.error(error), | ||
handleFetch: ({ request, fetch }) => fetch(request) | ||
}; | ||
} else { | ||
throw error; | ||
} | ||
} | ||
} | ||
@@ -49,0 +63,0 @@ } |
@@ -8,3 +8,3 @@ import * as devalue from 'devalue'; | ||
/** @param {import('types').RequestEvent} event */ | ||
/** @param {import('@sveltejs/kit').RequestEvent} event */ | ||
export function is_action_json_request(event) { | ||
@@ -20,3 +20,3 @@ const accept = negotiate(event.request.headers.get('accept') ?? '*/*', [ | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSROptions} options | ||
@@ -77,7 +77,3 @@ * @param {import('types').SSRNode['server'] | undefined} server | ||
if (err instanceof Redirect) { | ||
return action_json({ | ||
type: 'redirect', | ||
status: err.status, | ||
location: err.location | ||
}); | ||
return action_json_redirect(err); | ||
} | ||
@@ -102,3 +98,3 @@ | ||
return error instanceof ActionFailure | ||
? new Error(`Cannot "throw fail()". Use "return fail()"`) | ||
? new Error('Cannot "throw fail()". Use "return fail()"') | ||
: error; | ||
@@ -108,3 +104,14 @@ } | ||
/** | ||
* @param {import('types').ActionResult} data | ||
* @param {import('@sveltejs/kit').Redirect} redirect | ||
*/ | ||
export function action_json_redirect(redirect) { | ||
return action_json({ | ||
type: 'redirect', | ||
status: redirect.status, | ||
location: redirect.location | ||
}); | ||
} | ||
/** | ||
* @param {import('@sveltejs/kit').ActionResult} data | ||
* @param {ResponseInit} [init] | ||
@@ -117,3 +124,3 @@ */ | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
*/ | ||
@@ -125,5 +132,5 @@ export function is_action_request(event) { | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSRNode['server'] | undefined} server | ||
* @returns {Promise<import('types').ActionResult>} | ||
* @returns {Promise<import('@sveltejs/kit').ActionResult>} | ||
*/ | ||
@@ -188,3 +195,3 @@ export async function handle_action_request(event, server) { | ||
/** | ||
* @param {import('types').Actions} actions | ||
* @param {import('@sveltejs/kit').Actions} actions | ||
*/ | ||
@@ -194,3 +201,3 @@ function check_named_default_separate(actions) { | ||
throw new Error( | ||
`When using named actions, the default action cannot be used. See the docs for more info: https://kit.svelte.dev/docs/form-actions#named-actions` | ||
'When using named actions, the default action cannot be used. See the docs for more info: https://kit.svelte.dev/docs/form-actions#named-actions' | ||
); | ||
@@ -201,3 +208,3 @@ } | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {NonNullable<import('types').SSRNode['server']['actions']>} actions | ||
@@ -237,3 +244,3 @@ * @throws {Redirect | ActionFailure | HttpError | Error} | ||
if (data instanceof Redirect) { | ||
throw new Error(`Cannot \`return redirect(...)\` — use \`throw redirect(...)\` instead`); | ||
throw new Error('Cannot `return redirect(...)` — use `throw redirect(...)` instead'); | ||
} | ||
@@ -243,3 +250,3 @@ | ||
throw new Error( | ||
`Cannot \`return error(...)\` — use \`throw error(...)\` or \`return fail(...)\` instead` | ||
'Cannot `return error(...)` — use `throw error(...)` or `return fail(...)` instead' | ||
); | ||
@@ -246,0 +253,0 @@ } |
@@ -181,4 +181,9 @@ import { escape_html_attr } from '../../../utils/escape.js'; | ||
get_meta() { | ||
const content = escape_html_attr(this.get_header(true)); | ||
return `<meta http-equiv="content-security-policy" content=${content}>`; | ||
const content = this.get_header(true); | ||
if (!content) { | ||
return; | ||
} | ||
return `<meta http-equiv="content-security-policy" content=${escape_html_attr(content)}>`; | ||
} | ||
@@ -185,0 +190,0 @@ } |
@@ -25,6 +25,6 @@ import { text } from '../../../exports/index.js'; | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').PageNodeIndexes} page | ||
* @param {import('types').SSROptions} options | ||
* @param {import('types').SSRManifest} manifest | ||
* @param {import('@sveltejs/kit').SSRManifest} manifest | ||
* @param {import('types').SSRState} state | ||
@@ -58,3 +58,3 @@ * @param {import('types').RequiredResolveOptions} resolve_opts | ||
/** @type {import('types').ActionResult | undefined} */ | ||
/** @type {import('@sveltejs/kit').ActionResult | undefined} */ | ||
let action_result = undefined; | ||
@@ -123,3 +123,3 @@ | ||
/** @type {Array<import('./types.js').Loaded | null>} */ | ||
let branch = []; | ||
const branch = []; | ||
@@ -156,3 +156,4 @@ /** @type {Error | null} */ | ||
return data; | ||
} | ||
}, | ||
track_server_fetches: options.track_server_fetches | ||
}); | ||
@@ -159,0 +160,0 @@ } catch (e) { |
@@ -9,10 +9,18 @@ import { disable_search, make_trackable } from '../../../utils/url.js'; | ||
* @param {{ | ||
* event: import('types').RequestEvent; | ||
* event: import('@sveltejs/kit').RequestEvent; | ||
* state: import('types').SSRState; | ||
* node: import('types').SSRNode | undefined; | ||
* parent: () => Promise<Record<string, any>>; | ||
* track_server_fetches: boolean; | ||
* }} opts | ||
* @returns {Promise<import('types').ServerDataNode | null>} | ||
*/ | ||
export async function load_server_data({ event, state, node, parent }) { | ||
export async function load_server_data({ | ||
event, | ||
state, | ||
node, | ||
parent, | ||
// TODO 2.0: Remove this | ||
track_server_fetches | ||
}) { | ||
if (!node?.server) return null; | ||
@@ -55,3 +63,6 @@ | ||
uses.dependencies.add(url.href); | ||
// TODO 2.0: Remove this | ||
if (track_server_fetches) { | ||
uses.dependencies.add(url.href); | ||
} | ||
@@ -137,3 +148,3 @@ return event.fetch(info, init); | ||
* @param {{ | ||
* event: import('types').RequestEvent; | ||
* event: import('@sveltejs/kit').RequestEvent; | ||
* fetched: import('./types').Fetched[]; | ||
@@ -185,7 +196,7 @@ * node: import('types').SSRNode | undefined; | ||
/** | ||
* @param {Pick<import('types').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>} event | ||
* @param {import("types").SSRState} state | ||
* @param {import("./types").Fetched[]} fetched | ||
* @param {Pick<import('@sveltejs/kit').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>} event | ||
* @param {import('types').SSRState} state | ||
* @param {import('./types').Fetched[]} fetched | ||
* @param {boolean} csr | ||
* @param {Pick<Required<import("types").ResolveOptions>, 'filterSerializedResponseHeaders'>} resolve_opts | ||
* @param {Pick<Required<import('@sveltejs/kit').ResolveOptions>, 'filterSerializedResponseHeaders'>} resolve_opts | ||
*/ | ||
@@ -258,3 +269,3 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) { | ||
response_body: body, | ||
response: response | ||
response | ||
}); | ||
@@ -261,0 +272,0 @@ } |
@@ -31,3 +31,3 @@ import * as devalue from 'devalue'; | ||
* options: import('types').SSROptions; | ||
* manifest: import('types').SSRManifest; | ||
* manifest: import('@sveltejs/kit').SSRManifest; | ||
* state: import('types').SSRState; | ||
@@ -37,5 +37,5 @@ * page_config: { ssr: boolean; csr: boolean }; | ||
* error: App.Error | null; | ||
* event: import('types').RequestEvent; | ||
* event: import('@sveltejs/kit').RequestEvent; | ||
* resolve_opts: import('types').RequiredResolveOptions; | ||
* action_result?: import('types').ActionResult; | ||
* action_result?: import('@sveltejs/kit').ActionResult; | ||
* }} opts | ||
@@ -68,5 +68,5 @@ */ | ||
const modulepreloads = new Set([...client.start.imports, ...client.app.imports]); | ||
const stylesheets = new Set(client.app.stylesheets); | ||
const fonts = new Set(client.app.fonts); | ||
const modulepreloads = new Set(client.imports); | ||
const stylesheets = new Set(client.stylesheets); | ||
const fonts = new Set(client.fonts); | ||
@@ -101,20 +101,9 @@ /** @type {Set<string>} */ | ||
if (paths.relative !== false && !state.prerendering?.fallback) { | ||
const segments = event.url.pathname.slice(paths.base.length).split('/'); | ||
const segments = event.url.pathname.slice(paths.base.length).split('/').slice(2); | ||
if (segments.length === 1 && paths.base !== '') { | ||
// if we're on `/my-base-path`, relative links need to start `./my-base-path` rather than `.` | ||
base = `./${paths.base.split('/').at(-1)}`; | ||
base = segments.map(() => '..').join('/') || '.'; | ||
base_expression = `new URL(${s(base)}, location).pathname`; | ||
} else { | ||
base = | ||
segments | ||
.slice(2) | ||
.map(() => '..') | ||
.join('/') || '.'; | ||
// resolve e.g. '../..' against current location, then remove trailing slash | ||
base_expression = `new URL(${s(base)}, location).pathname.slice(0, -1)`; | ||
// resolve e.g. '../..' against current location, then remove trailing slash | ||
base_expression = `new URL(${s(base)}, location).pathname.slice(0, -1)`; | ||
} | ||
if (!paths.assets || (paths.assets[0] === '/' && paths.assets !== SVELTE_KIT_ASSETS)) { | ||
@@ -175,3 +164,3 @@ assets = base; | ||
console.warn( | ||
`Avoid calling \`fetch\` eagerly during server side rendering — put your \`fetch\` calls inside \`onMount\` or a \`load\` function instead` | ||
'Avoid calling `fetch` eagerly during server side rendering — put your `fetch` calls inside `onMount` or a `load` function instead' | ||
); | ||
@@ -276,3 +265,3 @@ warned = true; | ||
const global = __SVELTEKIT_DEV__ ? `__sveltekit_dev` : `__sveltekit_${options.version_hash}`; | ||
const global = __SVELTEKIT_DEV__ ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`; | ||
@@ -312,10 +301,9 @@ const { data, chunks } = get_data( | ||
const properties = [ | ||
`env: ${s(public_env)}`, | ||
paths.assets && `assets: ${s(paths.assets)}`, | ||
`base: ${base_expression}`, | ||
`element: document.currentScript.parentElement` | ||
`env: ${s(public_env)}` | ||
].filter(Boolean); | ||
if (chunks) { | ||
blocks.push(`const deferred = new Map();`); | ||
blocks.push('const deferred = new Map();'); | ||
@@ -339,4 +327,6 @@ properties.push(`defer: (id) => new Promise((fulfil, reject) => { | ||
const args = [`app`, `${global}.element`]; | ||
const args = ['app', 'element']; | ||
blocks.push('const element = document.currentScript.parentElement;'); | ||
if (page_config.ssr) { | ||
@@ -360,3 +350,3 @@ const serialized = { form: 'null', error: 'null' }; | ||
`node_ids: [${branch.map(({ node }) => node.index).join(', ')}]`, | ||
`data`, | ||
'data', | ||
`form: ${serialized.form}`, | ||
@@ -378,4 +368,4 @@ `error: ${serialized.error}` | ||
blocks.push(`Promise.all([ | ||
import(${s(prefixed(client.start.file))}), | ||
import(${s(prefixed(client.app.file))}) | ||
import(${s(prefixed(client.start))}), | ||
import(${s(prefixed(client.app))}) | ||
]).then(([kit, app]) => { | ||
@@ -386,3 +376,3 @@ kit.start(${args.join(', ')}); | ||
if (options.service_worker) { | ||
const opts = __SVELTEKIT_DEV__ ? `, { type: 'module' }` : ''; | ||
const opts = __SVELTEKIT_DEV__ ? ", { type: 'module' }" : ''; | ||
@@ -514,3 +504,3 @@ // we use an anonymous function instead of an arrow function to support | ||
* async iterable containing their resolutions | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSROptions} options | ||
@@ -517,0 +507,0 @@ * @param {Array<import('types').ServerDataNode | null>} nodes |
@@ -13,5 +13,5 @@ import { render_response } from './render.js'; | ||
* @param {{ | ||
* event: import('types').RequestEvent; | ||
* event: import('@sveltejs/kit').RequestEvent; | ||
* options: import('types').SSROptions; | ||
* manifest: import('types').SSRManifest; | ||
* manifest: import('@sveltejs/kit').SSRManifest; | ||
* state: import('types').SSRState; | ||
@@ -48,3 +48,4 @@ * status: number; | ||
node: default_layout, | ||
parent: async () => ({}) | ||
parent: async () => ({}), | ||
track_server_fetches: options.track_server_fetches | ||
}); | ||
@@ -51,0 +52,0 @@ |
@@ -18,3 +18,3 @@ import { DEV } from 'esm-env'; | ||
import { exec } from '../../utils/routing.js'; | ||
import { INVALIDATED_PARAM, redirect_json_response, render_data } from './data/index.js'; | ||
import { redirect_json_response, render_data } from './data/index.js'; | ||
import { add_cookies_to_headers, get_cookies } from './cookie.js'; | ||
@@ -24,3 +24,5 @@ import { create_fetch } from './fetch.js'; | ||
import { | ||
validate_common_exports, | ||
validate_layout_exports, | ||
validate_layout_server_exports, | ||
validate_page_exports, | ||
validate_page_server_exports, | ||
@@ -31,2 +33,4 @@ validate_server_exports | ||
import { error, json, text } from '../../exports/index.js'; | ||
import { action_json_redirect, is_action_json_request } from './page/actions.js'; | ||
import { INVALIDATED_PARAM } from '../shared.js'; | ||
@@ -47,3 +51,3 @@ /* global __SVELTEKIT_ADAPTER_NAME__ */ | ||
* @param {import('types').SSROptions} options | ||
* @param {import('types').SSRManifest} manifest | ||
* @param {import('@sveltejs/kit').SSRManifest} manifest | ||
* @param {import('types').SSRState} state | ||
@@ -54,9 +58,12 @@ * @returns {Promise<Response>} | ||
/** URL but stripped from the potential `/__data.json` suffix and its search param */ | ||
let url = new URL(request.url); | ||
const url = new URL(request.url); | ||
if (options.csrf_check_origin) { | ||
const forbidden = | ||
request.method === 'POST' && | ||
request.headers.get('origin') !== url.origin && | ||
is_form_content_type(request); | ||
is_form_content_type(request) && | ||
(request.method === 'POST' || | ||
request.method === 'PUT' || | ||
request.method === 'PATCH' || | ||
request.method === 'DELETE') && | ||
request.headers.get('origin') !== url.origin; | ||
@@ -98,3 +105,6 @@ if (forbidden) { | ||
url.pathname = strip_data_suffix(url.pathname) || '/'; | ||
invalidated_data_nodes = url.searchParams.get(INVALIDATED_PARAM)?.split('_').map(Boolean); | ||
invalidated_data_nodes = url.searchParams | ||
.get(INVALIDATED_PARAM) | ||
?.split('') | ||
.map((node) => node === '1'); | ||
url.searchParams.delete(INVALIDATED_PARAM); | ||
@@ -129,3 +139,3 @@ } | ||
/** @type {import('types').RequestEvent} */ | ||
/** @type {import('@sveltejs/kit').RequestEvent} */ | ||
const event = { | ||
@@ -155,3 +165,3 @@ // @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself | ||
throw new Error( | ||
`Use \`event.cookies.set(name, value, options)\` instead of \`event.setHeaders\` to set cookies` | ||
'Use `event.cookies.set(name, value, options)` instead of `event.setHeaders` to set cookies' | ||
); | ||
@@ -182,4 +192,8 @@ } else if (lower in headers) { | ||
// determine whether we need to redirect to add/remove a trailing slash | ||
if (route && !is_data_request) { | ||
if (route.page) { | ||
if (route) { | ||
// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`, | ||
// regardless of the `trailingSlash` route option | ||
if (url.pathname === base || url.pathname === base + '/') { | ||
trailing_slash = 'always'; | ||
} else if (route.page) { | ||
const nodes = await Promise.all([ | ||
@@ -197,4 +211,7 @@ // we use == here rather than === because [undefined] serializes as "[null]" | ||
if (layout) { | ||
validate_common_exports(layout.server, /** @type {string} */ (layout.server_id)); | ||
validate_common_exports( | ||
validate_layout_server_exports( | ||
layout.server, | ||
/** @type {string} */ (layout.server_id) | ||
); | ||
validate_layout_exports( | ||
layout.universal, | ||
@@ -208,3 +225,3 @@ /** @type {string} */ (layout.universal_id) | ||
validate_page_server_exports(page.server, /** @type {string} */ (page.server_id)); | ||
validate_common_exports(page.universal, /** @type {string} */ (page.universal_id)); | ||
validate_page_exports(page.universal, /** @type {string} */ (page.universal_id)); | ||
} | ||
@@ -223,19 +240,21 @@ } | ||
const normalized = normalize_path(url.pathname, trailing_slash ?? 'never'); | ||
if (!is_data_request) { | ||
const normalized = normalize_path(url.pathname, trailing_slash ?? 'never'); | ||
if (normalized !== url.pathname && !state.prerendering?.fallback) { | ||
return new Response(undefined, { | ||
status: 308, | ||
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) | ||
} | ||
}); | ||
if (normalized !== url.pathname && !state.prerendering?.fallback) { | ||
return new Response(undefined, { | ||
status: 308, | ||
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) | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
const { cookies, new_cookies, get_cookie_header } = get_cookies( | ||
const { cookies, new_cookies, get_cookie_header, set_internal } = get_cookies( | ||
request, | ||
@@ -248,3 +267,10 @@ url, | ||
event.cookies = cookies; | ||
event.fetch = create_fetch({ event, options, manifest, state, get_cookie_header }); | ||
event.fetch = create_fetch({ | ||
event, | ||
options, | ||
manifest, | ||
state, | ||
get_cookie_header, | ||
set_internal | ||
}); | ||
@@ -322,2 +348,4 @@ if (state.prerendering && !state.prerendering.fallback) disable_search(url); | ||
? redirect_json_response(e) | ||
: route?.page && is_action_json_request(event) | ||
? action_json_redirect(e) | ||
: redirect_response(e.status, e.location); | ||
@@ -332,4 +360,4 @@ add_cookies_to_headers(response.headers, Object.values(cookies_to_add)); | ||
* | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('types').ResolveOptions} [opts] | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('@sveltejs/kit').ResolveOptions} [opts] | ||
*/ | ||
@@ -336,0 +364,0 @@ async function resolve(event, opts) { |
@@ -37,10 +37,8 @@ import { DEV } from 'esm-env'; | ||
export function allowed_methods(mod) { | ||
const allowed = []; | ||
const allowed = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].filter( | ||
(method) => method in mod | ||
); | ||
for (const method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) { | ||
if (method in mod) allowed.push(method); | ||
} | ||
if ('GET' in mod || 'HEAD' in mod) allowed.push('HEAD'); | ||
if (mod.GET || mod.HEAD) allowed.push('HEAD'); | ||
return allowed; | ||
@@ -71,3 +69,3 @@ } | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSROptions} options | ||
@@ -81,3 +79,3 @@ * @param {unknown} error | ||
// ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it | ||
// ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it | ||
const type = negotiate(event.request.headers.get('accept') || 'text/html', [ | ||
@@ -98,3 +96,3 @@ 'application/json', | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {import('types').SSROptions} options | ||
@@ -141,3 +139,3 @@ * @param {any} error | ||
/** | ||
* @param {import('types').RequestEvent} event | ||
* @param {import('@sveltejs/kit').RequestEvent} event | ||
* @param {Error & { path: string }} error | ||
@@ -172,7 +170,7 @@ */ | ||
if (node.uses?.parent) uses.push(`"parent":1`); | ||
if (node.uses?.route) uses.push(`"route":1`); | ||
if (node.uses?.url) uses.push(`"url":1`); | ||
if (node.uses?.parent) uses.push('"parent":1'); | ||
if (node.uses?.route) uses.push('"route":1'); | ||
if (node.uses?.url) uses.push('"url":1'); | ||
return `"uses":{${uses.join(',')}}`; | ||
} |
@@ -13,1 +13,3 @@ /** | ||
} | ||
export const INVALIDATED_PARAM = 'x-sveltekit-invalidated'; |
@@ -1,3 +0,1 @@ | ||
import { HttpError, Redirect } from '../runtime/control.js'; | ||
/** | ||
@@ -21,3 +19,5 @@ * @param {unknown} err | ||
export function normalize_error(error) { | ||
return /** @type {Redirect | HttpError | Error} */ (error); | ||
return /** @type {import('../runtime/control.js').Redirect | import('../runtime/control.js').HttpError | Error} */ ( | ||
error | ||
); | ||
} |
/** | ||
* @param {string[]} expected | ||
* @param {Set<string>} expected | ||
*/ | ||
function validator(expected) { | ||
const set = new Set(expected); | ||
/** | ||
@@ -15,7 +13,9 @@ * @param {any} module | ||
for (const key in module) { | ||
if (key[0] === '_' || set.has(key)) continue; // key is valid in this module | ||
if (key[0] === '_' || expected.has(key)) continue; // key is valid in this module | ||
const values = [...expected.values()]; | ||
const hint = | ||
hint_for_supported_files(key, file?.slice(file.lastIndexOf('.'))) ?? | ||
`valid exports are ${expected.join(', ')}, or anything with a '_' prefix`; | ||
`valid exports are ${values.join(', ')}, or anything with a '_' prefix`; | ||
@@ -35,13 +35,21 @@ throw new Error(`Invalid export '${key}'${file ? ` in ${file}` : ''} (${hint})`); | ||
function hint_for_supported_files(key, ext = '.js') { | ||
let supported_files = []; | ||
const supported_files = []; | ||
if (valid_common_exports.includes(key)) { | ||
if (valid_layout_exports.has(key)) { | ||
supported_files.push(`+layout${ext}`); | ||
} | ||
if (valid_page_exports.has(key)) { | ||
supported_files.push(`+page${ext}`); | ||
} | ||
if (valid_page_server_exports.includes(key)) { | ||
if (valid_layout_server_exports.has(key)) { | ||
supported_files.push(`+layout.server${ext}`); | ||
} | ||
if (valid_page_server_exports.has(key)) { | ||
supported_files.push(`+page.server${ext}`); | ||
} | ||
if (valid_server_exports.includes(key)) { | ||
if (valid_server_exports.has(key)) { | ||
supported_files.push(`+server${ext}`); | ||
@@ -51,8 +59,9 @@ } | ||
if (supported_files.length > 0) { | ||
return `'${key}' is a valid export in ${supported_files.join(` or `)}`; | ||
return `'${key}' is a valid export in ${supported_files.slice(0, -1).join(', ')}${ | ||
supported_files.length > 1 ? ' or ' : '' | ||
}${supported_files.at(-1)}`; | ||
} | ||
} | ||
const valid_common_exports = ['load', 'prerender', 'csr', 'ssr', 'trailingSlash', 'config']; | ||
const valid_page_server_exports = [ | ||
const valid_layout_exports = new Set([ | ||
'load', | ||
@@ -62,7 +71,9 @@ 'prerender', | ||
'ssr', | ||
'actions', | ||
'trailingSlash', | ||
'config' | ||
]; | ||
const valid_server_exports = [ | ||
]); | ||
const valid_page_exports = new Set([...valid_layout_exports, 'entries']); | ||
const valid_layout_server_exports = new Set([...valid_layout_exports]); | ||
const valid_page_server_exports = new Set([...valid_layout_server_exports, 'actions', 'entries']); | ||
const valid_server_exports = new Set([ | ||
'GET', | ||
@@ -76,7 +87,10 @@ 'POST', | ||
'trailingSlash', | ||
'config' | ||
]; | ||
'config', | ||
'entries' | ||
]); | ||
export const validate_common_exports = validator(valid_common_exports); | ||
export const validate_layout_exports = validator(valid_layout_exports); | ||
export const validate_page_exports = validator(valid_page_exports); | ||
export const validate_layout_server_exports = validator(valid_layout_server_exports); | ||
export const validate_page_server_exports = validator(valid_page_server_exports); | ||
export const validate_server_exports = validator(valid_server_exports); |
import { fileURLToPath } from 'node:url'; | ||
import child_process from 'node:child_process'; | ||
import { Worker, parentPort } from 'node:worker_threads'; | ||
@@ -14,19 +14,17 @@ /** | ||
export function forked(module, callback) { | ||
if (process.env.SVELTEKIT_FORK && process.send) { | ||
process.send({ type: 'ready', module }); | ||
process.on( | ||
if (process.env.SVELTEKIT_FORK && parentPort) { | ||
parentPort.on( | ||
'message', | ||
/** @param {any} data */ async (data) => { | ||
if (data?.type === 'args' && data.module === module) { | ||
if (process.send) { | ||
process.send({ | ||
type: 'result', | ||
module, | ||
payload: await callback(data.payload) | ||
}); | ||
} | ||
parentPort?.postMessage({ | ||
type: 'result', | ||
module, | ||
payload: await callback(data.payload) | ||
}); | ||
} | ||
} | ||
); | ||
parentPort.postMessage({ type: 'ready', module }); | ||
} | ||
@@ -40,16 +38,14 @@ | ||
return new Promise((fulfil, reject) => { | ||
const child = child_process.fork(fileURLToPath(module), { | ||
stdio: 'inherit', | ||
const worker = new Worker(fileURLToPath(module), { | ||
env: { | ||
...process.env, | ||
SVELTEKIT_FORK: 'true' | ||
}, | ||
serialization: 'advanced' | ||
} | ||
}); | ||
child.on( | ||
worker.on( | ||
'message', | ||
/** @param {any} data */ (data) => { | ||
if (data?.type === 'ready' && data.module === module) { | ||
child.send({ | ||
worker.postMessage({ | ||
type: 'args', | ||
@@ -62,3 +58,3 @@ module, | ||
if (data?.type === 'result' && data.module === module) { | ||
child.kill(); | ||
worker.terminate(); | ||
fulfil(data.payload); | ||
@@ -69,3 +65,3 @@ } | ||
child.on('exit', (code) => { | ||
worker.on('exit', (code) => { | ||
if (code) { | ||
@@ -72,0 +68,0 @@ reject(new Error(`Failed with code ${code}`)); |
@@ -62,5 +62,5 @@ /** | ||
*/ | ||
export function is_content_type(request, ...types) { | ||
function is_content_type(request, ...types) { | ||
const type = request.headers.get('content-type')?.split(';', 1)[0].trim() ?? ''; | ||
return types.includes(type); | ||
return types.includes(type.toLowerCase()); | ||
} | ||
@@ -72,3 +72,10 @@ | ||
export function is_form_content_type(request) { | ||
return is_content_type(request, 'application/x-www-form-urlencoded', 'multipart/form-data'); | ||
// These content types must be protected against CSRF | ||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype | ||
return is_content_type( | ||
request, | ||
'application/x-www-form-urlencoded', | ||
'multipart/form-data', | ||
'text/plain' | ||
); | ||
} |
/** | ||
* @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option | ||
* @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value | ||
* @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash' | 'entries'} Option | ||
* @template {(import('types').SSRNode['universal'] | import('types').SSRNode['server'])[Option]} Value | ||
* | ||
@@ -12,3 +12,3 @@ * @param {Array<import('types').SSRNode | undefined>} nodes | ||
return nodes.reduce((value, node) => { | ||
return /** @type {any} TypeScript's too dumb to understand this */ ( | ||
return /** @type {Value} TypeScript's too dumb to understand this */ ( | ||
node?.universal?.[option] ?? node?.server?.[option] ?? value | ||
@@ -15,0 +15,0 @@ ); |
@@ -99,3 +99,14 @@ const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/; | ||
const optional_param_regex = /\/\[\[\w+?(?:=\w+)?\]\]/; | ||
/** | ||
* Removes optional params from a route ID. | ||
* @param {string} id | ||
* @returns The route id with optional params removed | ||
*/ | ||
export function remove_optional_params(id) { | ||
return id.replace(optional_param_regex, ''); | ||
} | ||
/** | ||
* Returns `false` for `(group)` segments | ||
@@ -122,3 +133,3 @@ * @param {string} segment | ||
* @param {import('types').RouteParam[]} params | ||
* @param {Record<string, import('types').ParamMatcher>} matchers | ||
* @param {Record<string, import('@sveltejs/kit').ParamMatcher>} matchers | ||
*/ | ||
@@ -162,3 +173,3 @@ export function exec(match, params, matchers) { | ||
const next_value = values[i + 1]; | ||
if (next_param && !next_param.rest && next_param.optional && next_value) { | ||
if (next_param && !next_param.rest && next_param.optional && next_value && param.chained) { | ||
buffered = 0; | ||
@@ -165,0 +176,0 @@ } |
/** | ||
* @returns {import("types").Deferred & { promise: Promise<any> }}} | ||
* @returns {import('types').Deferred & { promise: Promise<any> }}} | ||
*/ | ||
@@ -26,3 +26,3 @@ function defer() { | ||
export function create_async_iterator() { | ||
let deferred = [defer()]; | ||
const deferred = [defer()]; | ||
@@ -29,0 +29,0 @@ return { |
@@ -96,5 +96,11 @@ import { BROWSER } from 'esm-env'; | ||
* which excludes things like `origin` | ||
* @type {Array<keyof URL>} | ||
*/ | ||
const tracked_url_properties = ['href', 'pathname', 'search', 'searchParams', 'toString', 'toJSON']; | ||
const tracked_url_properties = /** @type {const} */ ([ | ||
'href', | ||
'pathname', | ||
'search', | ||
'searchParams', | ||
'toString', | ||
'toJSON' | ||
]); | ||
@@ -109,8 +115,6 @@ /** | ||
for (const property of tracked_url_properties) { | ||
let value = tracked[property]; | ||
Object.defineProperty(tracked, property, { | ||
get() { | ||
callback(); | ||
return value; | ||
return url[property]; | ||
}, | ||
@@ -117,0 +121,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
656080
118
19438
15
12
+ Added@ampproject/remapping@2.3.0(transitive)
+ Added@jridgewell/gen-mapping@0.3.5(transitive)
+ Added@jridgewell/resolve-uri@3.1.2(transitive)
+ Added@jridgewell/set-array@1.2.1(transitive)
+ Added@jridgewell/trace-mapping@0.3.25(transitive)
+ Added@types/estree@1.0.5(transitive)
+ Addedacorn@8.12.1(transitive)
+ Addedaria-query@5.3.1(transitive)
+ Addedaxobject-query@4.1.0(transitive)
+ Addedcode-red@1.0.4(transitive)
+ Addedcss-tree@2.3.1(transitive)
+ Addedestree-walker@3.0.3(transitive)
+ Addedis-reference@3.0.2(transitive)
+ Addedlocate-character@3.0.0(transitive)
+ Addedmdn-data@2.0.30(transitive)
+ Addedperiscopic@3.1.0(transitive)
+ Addedpostcss@8.4.47(transitive)
+ Addedsvelte@4.2.19(transitive)
+ Addedundici@5.22.1(transitive)
+ Addedvite@4.5.5(transitive)
- Removedpostcss@8.4.45(transitive)
- Removedsvelte@3.59.2(transitive)
- Removedundici@5.21.0(transitive)
- Removedvite@4.5.3(transitive)
Updateddevalue@^4.3.1
Updatedset-cookie-parser@^2.6.0
Updatedundici@~5.22.0