mirror of
https://gitverse.ru/kpa39l/telepost.git
synced 2026-09-29 18:05:08 +00:00
Initial commit
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import colors from 'kleur';
|
||||
import sade from 'sade';
|
||||
import { load_config } from './core/config/index.js';
|
||||
import { coalesce_to_error } from './utils/error.js';
|
||||
|
||||
/** @param {unknown} e */
|
||||
function handle_error(e) {
|
||||
const error = coalesce_to_error(e);
|
||||
|
||||
if (error.name === 'SyntaxError') throw error;
|
||||
|
||||
console.error(colors.bold().red(`> ${error.message}`));
|
||||
if (error.stack) {
|
||||
console.error(colors.gray(error.stack.split('\n').slice(1).join('\n')));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
||||
const prog = sade('svelte-kit').version(pkg.version);
|
||||
|
||||
prog
|
||||
.command('sync')
|
||||
.describe('Synchronise generated type definitions')
|
||||
.option('--mode', 'Specify a mode for loading environment variables', 'development')
|
||||
.action(async ({ mode }) => {
|
||||
if (!fs.existsSync('svelte.config.js')) {
|
||||
console.warn(`Missing ${path.resolve('svelte.config.js')} — skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await load_config();
|
||||
const sync = await import('./core/sync/sync.js');
|
||||
await sync.all_types(config, mode);
|
||||
} catch (error) {
|
||||
handle_error(error);
|
||||
}
|
||||
});
|
||||
|
||||
prog.parse(process.argv, { unknown: (arg) => `Unknown option: ${arg}` });
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* A fake asset path used in `vite dev` and `vite preview`, so that we can
|
||||
* serve local assets while verifying that requests are correctly prefixed
|
||||
*/
|
||||
export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets';
|
||||
|
||||
export const GENERATED_COMMENT = '// this file is generated — do not edit it\n';
|
||||
|
||||
export const ENDPOINT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
||||
|
||||
export const PAGE_METHODS = ['GET', 'POST', 'HEAD'];
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import { existsSync, statSync, createReadStream, createWriteStream } from 'node:fs';
|
||||
import { extname, resolve } from 'node:path';
|
||||
import { pipeline } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import zlib from 'node:zlib';
|
||||
import { copy, rimraf, mkdirp } from '../../utils/filesystem.js';
|
||||
import { generate_manifest } from '../generate_manifest/index.js';
|
||||
import { get_route_segments } from '../../utils/routing.js';
|
||||
import { get_env } from '../../exports/vite/utils.js';
|
||||
import generate_fallback from '../postbuild/fallback.js';
|
||||
import { write } from '../sync/utils.js';
|
||||
import { list_files } from '../utils.js';
|
||||
|
||||
const pipe = promisify(pipeline);
|
||||
const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.wasm'];
|
||||
|
||||
/**
|
||||
* Creates the Builder which is passed to adapters for building the application.
|
||||
* @param {{
|
||||
* config: import('types').ValidatedConfig;
|
||||
* build_data: import('types').BuildData;
|
||||
* server_metadata: import('types').ServerMetadata;
|
||||
* route_data: import('types').RouteData[];
|
||||
* prerendered: import('types').Prerendered;
|
||||
* prerender_map: import('types').PrerenderMap;
|
||||
* log: import('types').Logger;
|
||||
* vite_config: import('vite').ResolvedConfig;
|
||||
* }} opts
|
||||
* @returns {import('@sveltejs/kit').Builder}
|
||||
*/
|
||||
export function create_builder({
|
||||
config,
|
||||
build_data,
|
||||
server_metadata,
|
||||
route_data,
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
vite_config
|
||||
}) {
|
||||
/** @type {Map<import('@sveltejs/kit').RouteDefinition, import('types').RouteData>} */
|
||||
const lookup = new Map();
|
||||
|
||||
/**
|
||||
* Rather than exposing the internal `RouteData` type, which is subject to change,
|
||||
* we expose a stable type that adapters can use to group/filter routes
|
||||
*/
|
||||
const routes = route_data.map((route) => {
|
||||
const { config, methods, page, api } = /** @type {import('types').ServerMetadataRoute} */ (
|
||||
server_metadata.routes.get(route.id)
|
||||
);
|
||||
|
||||
/** @type {import('@sveltejs/kit').RouteDefinition} */
|
||||
const facade = {
|
||||
id: route.id,
|
||||
api,
|
||||
page,
|
||||
segments: get_route_segments(route.id).map((segment) => ({
|
||||
dynamic: segment.includes('['),
|
||||
rest: segment.includes('[...'),
|
||||
content: segment
|
||||
})),
|
||||
pattern: route.pattern,
|
||||
prerender: prerender_map.get(route.id) ?? false,
|
||||
methods,
|
||||
config
|
||||
};
|
||||
|
||||
lookup.set(facade, route);
|
||||
|
||||
return facade;
|
||||
});
|
||||
|
||||
return {
|
||||
log,
|
||||
rimraf,
|
||||
mkdirp,
|
||||
copy,
|
||||
|
||||
config,
|
||||
prerendered,
|
||||
routes,
|
||||
|
||||
async compress(directory) {
|
||||
if (!existsSync(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = list_files(directory, (file) => extensions.includes(extname(file))).map(
|
||||
(file) => resolve(directory, file)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
files.flatMap((file) => [compress_file(file, 'gz'), compress_file(file, 'br')])
|
||||
);
|
||||
},
|
||||
|
||||
async createEntries(fn) {
|
||||
const seen = new Set();
|
||||
|
||||
for (let i = 0; i < route_data.length; i += 1) {
|
||||
const route = route_data[i];
|
||||
if (prerender_map.get(route.id) === true) continue;
|
||||
const { id, filter, complete } = fn(routes[i]);
|
||||
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
|
||||
const group = [route];
|
||||
|
||||
// figure out which lower priority routes should be considered fallbacks
|
||||
for (let j = i + 1; j < route_data.length; j += 1) {
|
||||
if (prerender_map.get(routes[j].id) === true) continue;
|
||||
if (filter(routes[j])) {
|
||||
group.push(route_data[j]);
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = new Set(group);
|
||||
|
||||
// heuristic: if /foo/[bar] is included, /foo/[bar].json should
|
||||
// also be included, since the page likely needs the endpoint
|
||||
// TODO is this still necessary, given the new way of doing things?
|
||||
filtered.forEach((route) => {
|
||||
if (route.page) {
|
||||
const endpoint = route_data.find((candidate) => candidate.id === route.id + '.json');
|
||||
|
||||
if (endpoint) {
|
||||
filtered.add(endpoint);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.size > 0) {
|
||||
await complete({
|
||||
generateManifest: ({ relativePath }) =>
|
||||
generate_manifest({
|
||||
build_data,
|
||||
relative_path: relativePath,
|
||||
routes: Array.from(filtered)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async generateFallback(dest) {
|
||||
const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`;
|
||||
const env = get_env(config.kit.env, vite_config.mode);
|
||||
|
||||
const fallback = await generate_fallback({
|
||||
manifest_path,
|
||||
env: { ...env.private, ...env.public }
|
||||
});
|
||||
|
||||
write(dest, fallback);
|
||||
},
|
||||
|
||||
generateManifest({ relativePath, routes: subset }) {
|
||||
return generate_manifest({
|
||||
build_data,
|
||||
relative_path: relativePath,
|
||||
routes: subset
|
||||
? subset.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route)))
|
||||
: route_data.filter((route) => prerender_map.get(route.id) !== true)
|
||||
});
|
||||
},
|
||||
|
||||
getBuildDirectory(name) {
|
||||
return `${config.kit.outDir}/${name}`;
|
||||
},
|
||||
|
||||
getClientDirectory() {
|
||||
return `${config.kit.outDir}/output/client`;
|
||||
},
|
||||
|
||||
getServerDirectory() {
|
||||
return `${config.kit.outDir}/output/server`;
|
||||
},
|
||||
|
||||
getAppPath() {
|
||||
return build_data.app_path;
|
||||
},
|
||||
|
||||
writeClient(dest) {
|
||||
return copy(`${config.kit.outDir}/output/client`, dest, {
|
||||
// avoid making vite build artefacts public
|
||||
filter: (basename) => basename !== '.vite'
|
||||
});
|
||||
},
|
||||
|
||||
writePrerendered(dest) {
|
||||
const source = `${config.kit.outDir}/output/prerendered`;
|
||||
return [...copy(`${source}/pages`, dest), ...copy(`${source}/dependencies`, dest)];
|
||||
},
|
||||
|
||||
writeServer(dest) {
|
||||
return copy(`${config.kit.outDir}/output/server`, dest);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {'gz' | 'br'} format
|
||||
*/
|
||||
async function compress_file(file, format = 'gz') {
|
||||
const compress =
|
||||
format == 'br'
|
||||
? zlib.createBrotliCompress({
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
||||
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size
|
||||
}
|
||||
})
|
||||
: zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION });
|
||||
|
||||
const source = createReadStream(file);
|
||||
const destination = createWriteStream(`${file}.${format}`);
|
||||
|
||||
await pipe(source, compress, destination);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import colors from 'kleur';
|
||||
import { create_builder } from './builder.js';
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').BuildData} build_data
|
||||
* @param {import('types').ServerMetadata} server_metadata
|
||||
* @param {import('types').Prerendered} prerendered
|
||||
* @param {import('types').PrerenderMap} prerender_map
|
||||
* @param {import('types').Logger} log
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
*/
|
||||
export async function adapt(
|
||||
config,
|
||||
build_data,
|
||||
server_metadata,
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
vite_config
|
||||
) {
|
||||
const { name, adapt } = config.kit.adapter;
|
||||
|
||||
console.log(colors.bold().cyan(`\n> Using ${name}`));
|
||||
|
||||
const builder = create_builder({
|
||||
config,
|
||||
build_data,
|
||||
server_metadata,
|
||||
route_data: build_data.manifest_data.routes.filter((route) => route.page || route.endpoint),
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
vite_config
|
||||
});
|
||||
|
||||
await adapt(builder);
|
||||
|
||||
log.success('done');
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>%sveltekit.error.message%</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
--bg: white;
|
||||
--fg: #222;
|
||||
--divider: #ccc;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 32rem;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: 200;
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
top: -0.05rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
border-left: 1px solid var(--divider);
|
||||
padding: 0 0 0 1rem;
|
||||
margin: 0 0 0 1rem;
|
||||
min-height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message h1 {
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
--bg: #222;
|
||||
--fg: #ddd;
|
||||
--divider: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error">
|
||||
<span class="status">%sveltekit.status%</span>
|
||||
<div class="message">
|
||||
<h1>%sveltekit.error.message%</h1>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as url from 'node:url';
|
||||
import options from './options.js';
|
||||
|
||||
/**
|
||||
* Loads the template (src/app.html by default) and validates that it has the
|
||||
* required content.
|
||||
* @param {string} cwd
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function load_template(cwd, { kit }) {
|
||||
const { env, files } = kit;
|
||||
|
||||
const relative = path.relative(cwd, files.appTemplate);
|
||||
|
||||
if (!fs.existsSync(files.appTemplate)) {
|
||||
throw new Error(`${relative} does not exist`);
|
||||
}
|
||||
|
||||
const contents = fs.readFileSync(files.appTemplate, 'utf8');
|
||||
|
||||
const expected_tags = ['%sveltekit.head%', '%sveltekit.body%'];
|
||||
expected_tags.forEach((tag) => {
|
||||
if (contents.indexOf(tag) === -1) {
|
||||
throw new Error(`${relative} is missing ${tag}`);
|
||||
}
|
||||
});
|
||||
|
||||
for (const match of contents.matchAll(/%sveltekit\.env\.([^%]+)%/g)) {
|
||||
if (!match[1].startsWith(env.publicPrefix)) {
|
||||
throw new Error(
|
||||
`Environment variables in ${relative} must start with ${env.publicPrefix} (saw %sveltekit.env.${match[1]}%)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the error page (src/error.html by default) if it exists.
|
||||
* Falls back to a generic error page content.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function load_error_page(config) {
|
||||
let { errorTemplate } = config.kit.files;
|
||||
|
||||
// Don't do this inside resolving the config, because that would mean
|
||||
// adding/removing error.html isn't detected and would require a restart.
|
||||
if (!fs.existsSync(config.kit.files.errorTemplate)) {
|
||||
errorTemplate = url.fileURLToPath(new URL('./default-error.html', import.meta.url));
|
||||
}
|
||||
|
||||
return fs.readFileSync(errorTemplate, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and validates svelte.config.js
|
||||
* @param {{ cwd?: string }} options
|
||||
* @returns {Promise<import('types').ValidatedConfig>}
|
||||
*/
|
||||
export async function load_config({ cwd = process.cwd() } = {}) {
|
||||
const config_file = path.join(cwd, 'svelte.config.js');
|
||||
|
||||
if (!fs.existsSync(config_file)) {
|
||||
return process_config({}, { cwd });
|
||||
}
|
||||
|
||||
const config = await import(`${url.pathToFileURL(config_file).href}?ts=${Date.now()}`);
|
||||
|
||||
return process_config(config.default, { cwd });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').Config} config
|
||||
* @returns {import('types').ValidatedConfig}
|
||||
*/
|
||||
function process_config(config, { cwd = process.cwd() } = {}) {
|
||||
const validated = validate_config(config);
|
||||
|
||||
validated.kit.outDir = path.resolve(cwd, validated.kit.outDir);
|
||||
|
||||
for (const key in validated.kit.files) {
|
||||
if (key === 'hooks') {
|
||||
validated.kit.files.hooks.client = path.resolve(cwd, validated.kit.files.hooks.client);
|
||||
validated.kit.files.hooks.server = path.resolve(cwd, validated.kit.files.hooks.server);
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
validated.kit.files[key] = path.resolve(cwd, validated.kit.files[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').Config} config
|
||||
* @returns {import('types').ValidatedConfig}
|
||||
*/
|
||||
export function validate_config(config) {
|
||||
if (typeof config !== 'object') {
|
||||
throw new Error(
|
||||
'svelte.config.js must have a configuration object as its default export. See https://kit.svelte.dev/docs/configuration'
|
||||
);
|
||||
}
|
||||
|
||||
return options(config, 'config');
|
||||
}
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
/** @typedef {import('./types.js').Validator} Validator */
|
||||
|
||||
const directives = object({
|
||||
'child-src': string_array(),
|
||||
'default-src': string_array(),
|
||||
'frame-src': string_array(),
|
||||
'worker-src': string_array(),
|
||||
'connect-src': string_array(),
|
||||
'font-src': string_array(),
|
||||
'img-src': string_array(),
|
||||
'manifest-src': string_array(),
|
||||
'media-src': string_array(),
|
||||
'object-src': string_array(),
|
||||
'prefetch-src': string_array(),
|
||||
'script-src': string_array(),
|
||||
'script-src-elem': string_array(),
|
||||
'script-src-attr': string_array(),
|
||||
'style-src': string_array(),
|
||||
'style-src-elem': string_array(),
|
||||
'style-src-attr': string_array(),
|
||||
'base-uri': string_array(),
|
||||
sandbox: string_array(),
|
||||
'form-action': string_array(),
|
||||
'frame-ancestors': string_array(),
|
||||
'navigate-to': string_array(),
|
||||
'report-uri': string_array(),
|
||||
'report-to': string_array(),
|
||||
'require-trusted-types-for': string_array(),
|
||||
'trusted-types': string_array(),
|
||||
'upgrade-insecure-requests': boolean(false),
|
||||
'require-sri-for': string_array(),
|
||||
'block-all-mixed-content': boolean(false),
|
||||
'plugin-types': string_array(),
|
||||
referrer: string_array()
|
||||
});
|
||||
|
||||
/** @type {Validator} */
|
||||
const options = object(
|
||||
{
|
||||
extensions: validate(['.svelte'], (input, keypath) => {
|
||||
if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {
|
||||
throw new Error(`${keypath} must be an array of strings`);
|
||||
}
|
||||
|
||||
input.forEach((extension) => {
|
||||
if (extension[0] !== '.') {
|
||||
throw new Error(`Each member of ${keypath} must start with '.' — saw '${extension}'`);
|
||||
}
|
||||
|
||||
if (!/^(\.[a-z0-9]+)+$/i.test(extension)) {
|
||||
throw new Error(`File extensions must be alphanumeric — saw '${extension}'`);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
kit: object({
|
||||
adapter: validate(null, (input, keypath) => {
|
||||
if (typeof input !== 'object' || !input.adapt) {
|
||||
let message = `${keypath} should be an object with an "adapt" method`;
|
||||
|
||||
if (Array.isArray(input) || typeof input === 'string') {
|
||||
// for the early adapter adopters
|
||||
message += ', rather than the name of an adapter';
|
||||
}
|
||||
|
||||
throw new Error(`${message}. See https://kit.svelte.dev/docs/adapters`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
alias: validate({}, (input, keypath) => {
|
||||
if (typeof input !== 'object') {
|
||||
throw new Error(`${keypath} should be an object`);
|
||||
}
|
||||
|
||||
for (const key in input) {
|
||||
assert_string(input[key], `${keypath}.${key}`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
appDir: validate('_app', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (input) {
|
||||
if (input.startsWith('/') || input.endsWith('/')) {
|
||||
throw new Error(
|
||||
"config.kit.appDir cannot start or end with '/'. See https://kit.svelte.dev/docs/configuration"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${keypath} cannot be empty`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
csp: object({
|
||||
mode: list(['auto', 'hash', 'nonce']),
|
||||
directives,
|
||||
reportOnly: directives
|
||||
}),
|
||||
|
||||
csrf: object({
|
||||
checkOrigin: boolean(true)
|
||||
}),
|
||||
|
||||
dangerZone: object({
|
||||
// TODO 2.0: Remove this
|
||||
trackServerFetches: boolean(false)
|
||||
}),
|
||||
|
||||
embedded: boolean(false),
|
||||
|
||||
env: object({
|
||||
dir: string(process.cwd()),
|
||||
publicPrefix: string('PUBLIC_'),
|
||||
privatePrefix: string('')
|
||||
}),
|
||||
|
||||
files: object({
|
||||
assets: string('static'),
|
||||
hooks: object({
|
||||
client: string(join('src', 'hooks.client')),
|
||||
server: string(join('src', 'hooks.server'))
|
||||
}),
|
||||
lib: string(join('src', 'lib')),
|
||||
params: string(join('src', 'params')),
|
||||
routes: string(join('src', 'routes')),
|
||||
serviceWorker: string(join('src', 'service-worker')),
|
||||
appTemplate: string(join('src', 'app.html')),
|
||||
errorTemplate: string(join('src', 'error.html'))
|
||||
}),
|
||||
|
||||
inlineStyleThreshold: number(0),
|
||||
|
||||
moduleExtensions: string_array(['.js', '.ts']),
|
||||
|
||||
outDir: string('.svelte-kit'),
|
||||
|
||||
output: object({
|
||||
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs'], 'modulepreload')
|
||||
}),
|
||||
|
||||
paths: object({
|
||||
base: validate('', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (input !== '' && (input.endsWith('/') || !input.startsWith('/'))) {
|
||||
throw new Error(
|
||||
`${keypath} option must either be the empty string or a root-relative path that starts but doesn't end with '/'. See https://kit.svelte.dev/docs/configuration#paths`
|
||||
);
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
assets: validate('', (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (input) {
|
||||
if (!/^[a-z]+:\/\//.test(input)) {
|
||||
throw new Error(
|
||||
`${keypath} option must be an absolute path, if specified. See https://kit.svelte.dev/docs/configuration#paths`
|
||||
);
|
||||
}
|
||||
|
||||
if (input.endsWith('/')) {
|
||||
throw new Error(
|
||||
`${keypath} option must not end with '/'. See https://kit.svelte.dev/docs/configuration#paths`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}),
|
||||
relative: validate(undefined, (input, keypath) => {
|
||||
if (typeof input !== 'boolean') {
|
||||
throw new Error(`${keypath} option must be a boolean or undefined`);
|
||||
}
|
||||
|
||||
return input;
|
||||
})
|
||||
}),
|
||||
|
||||
prerender: object({
|
||||
concurrency: number(1),
|
||||
crawl: boolean(true),
|
||||
entries: validate(['*'], (input, keypath) => {
|
||||
if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {
|
||||
throw new Error(`${keypath} must be an array of strings`);
|
||||
}
|
||||
|
||||
input.forEach((page) => {
|
||||
if (page !== '*' && page[0] !== '/') {
|
||||
throw new Error(
|
||||
`Each member of ${keypath} must be either '*' or an absolute path beginning with '/' — saw '${page}'`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return input;
|
||||
}),
|
||||
|
||||
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(
|
||||
(/** @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) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
let origin;
|
||||
|
||||
try {
|
||||
origin = new URL(input).origin;
|
||||
} catch (e) {
|
||||
throw new Error(`${keypath} must be a valid origin`);
|
||||
}
|
||||
|
||||
if (input !== origin) {
|
||||
throw new Error(`${keypath} must be a valid origin (${origin} rather than ${input})`);
|
||||
}
|
||||
|
||||
return origin;
|
||||
})
|
||||
}),
|
||||
|
||||
serviceWorker: object({
|
||||
register: boolean(true),
|
||||
files: fun((filename) => !/\.DS_Store/.test(filename))
|
||||
}),
|
||||
|
||||
typescript: object({
|
||||
config: fun((config) => config)
|
||||
}),
|
||||
|
||||
version: object({
|
||||
name: string(Date.now().toString()),
|
||||
pollInterval: number(0)
|
||||
})
|
||||
})
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {Record<string, Validator>} children
|
||||
* @param {boolean} [allow_unknown]
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function object(children, allow_unknown = false) {
|
||||
return (input, keypath) => {
|
||||
/** @type {Record<string, any>} */
|
||||
const output = {};
|
||||
|
||||
if ((input && typeof input !== 'object') || Array.isArray(input)) {
|
||||
throw new Error(`${keypath} should be an object`);
|
||||
}
|
||||
|
||||
for (const key in input) {
|
||||
if (!(key in children)) {
|
||||
if (allow_unknown) {
|
||||
output[key] = input[key];
|
||||
} else {
|
||||
let message = `Unexpected option ${keypath}.${key}`;
|
||||
|
||||
// special case
|
||||
if (keypath === 'config.kit' && key in options) {
|
||||
message += ` (did you mean config.${key}?)`;
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in children) {
|
||||
const validator = children[key];
|
||||
output[key] = validator(input && input[key], `${keypath}.${key}`);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} fallback
|
||||
* @param {(value: any, keypath: string) => any} fn
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function validate(fallback, fn) {
|
||||
return (input, keypath) => {
|
||||
return input === undefined ? fallback : fn(input, keypath);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | null} fallback
|
||||
* @param {boolean} allow_empty
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function string(fallback, allow_empty = true) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
assert_string(input, keypath);
|
||||
|
||||
if (!allow_empty && input === '') {
|
||||
throw new Error(`${keypath} cannot be empty`);
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[] | undefined} [fallback]
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function string_array(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (!Array.isArray(input) || input.some((value) => typeof value !== 'string')) {
|
||||
throw new Error(`${keypath} must be an array of strings, if specified`);
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} fallback
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function number(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (typeof input !== 'number') {
|
||||
throw new Error(`${keypath} should be a number, if specified`);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} fallback
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function boolean(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (typeof input !== 'boolean') {
|
||||
throw new Error(`${keypath} should be true or false, if specified`);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} options
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function list(options, fallback = options[0]) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (!options.includes(input)) {
|
||||
// prettier-ignore
|
||||
const msg = options.length > 2
|
||||
? `${keypath} should be one of ${options.slice(0, -1).map(input => `"${input}"`).join(', ')} or "${options[options.length - 1]}"`
|
||||
: `${keypath} should be either "${options[0]}" or "${options[1]}"`;
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(...args: any) => any} fallback
|
||||
* @returns {Validator}
|
||||
*/
|
||||
function fun(fallback) {
|
||||
return validate(fallback, (input, keypath) => {
|
||||
if (typeof input !== 'function') {
|
||||
throw new Error(`${keypath} should be a function, if specified`);
|
||||
}
|
||||
return input;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {string} keypath
|
||||
*/
|
||||
function assert_string(input, keypath) {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error(`${keypath} should be a string, if specified`);
|
||||
}
|
||||
}
|
||||
|
||||
export default options;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type Validator<T = any> = (input: T, keypath: string) => T;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { GENERATED_COMMENT } from '../constants.js';
|
||||
import { dedent } from './sync/utils.js';
|
||||
import { runtime_base } from './utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {'public' | 'private'} EnvType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {string}
|
||||
*/
|
||||
export function create_static_module(id, env) {
|
||||
/** @type {string[]} */
|
||||
const declarations = [];
|
||||
|
||||
for (const key in env) {
|
||||
if (!valid_identifier.test(key) || reserved.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const comment = `/** @type {import('${id}').${key}} */`;
|
||||
const declaration = `export const ${key} = ${JSON.stringify(env[key])};`;
|
||||
|
||||
declarations.push(`${comment}\n${declaration}`);
|
||||
}
|
||||
|
||||
return GENERATED_COMMENT + declarations.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EnvType} type
|
||||
* @param {Record<string, string> | undefined} dev_values If in a development mode, values to pre-populate the module with.
|
||||
*/
|
||||
export function create_dynamic_module(type, dev_values) {
|
||||
if (dev_values) {
|
||||
const keys = Object.entries(dev_values).map(
|
||||
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
||||
);
|
||||
return `export const env = {\n${keys.join(',\n')}\n}`;
|
||||
}
|
||||
return `export { ${type}_env as env } from '${runtime_base}/shared-server.js';`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EnvType} id
|
||||
* @param {import('types').Env} env
|
||||
* @returns {string}
|
||||
*/
|
||||
export function create_static_types(id, env) {
|
||||
const declarations = Object.keys(env[id])
|
||||
.filter((k) => valid_identifier.test(k))
|
||||
.map((k) => `export const ${k}: string;`);
|
||||
|
||||
return dedent`
|
||||
declare module '$env/static/${id}' {
|
||||
${declarations.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EnvType} id
|
||||
* @param {import('types').Env} env
|
||||
* @param {{
|
||||
* public_prefix: string;
|
||||
* private_prefix: string;
|
||||
* }} prefixes
|
||||
* @returns {string}
|
||||
*/
|
||||
export function create_dynamic_types(id, env, { public_prefix, private_prefix }) {
|
||||
const properties = Object.keys(env[id])
|
||||
.filter((k) => valid_identifier.test(k))
|
||||
.map((k) => `${k}: string;`);
|
||||
|
||||
const public_prefixed = `[key: \`${public_prefix}\${string}\`]`;
|
||||
const private_prefixed = `[key: \`${private_prefix}\${string}\`]`;
|
||||
|
||||
if (id === 'private') {
|
||||
if (public_prefix) {
|
||||
properties.push(`${public_prefixed}: undefined;`);
|
||||
}
|
||||
properties.push(`${private_prefixed}: string | undefined;`);
|
||||
} else {
|
||||
if (private_prefix) {
|
||||
properties.push(`${private_prefixed}: undefined;`);
|
||||
}
|
||||
properties.push(`${public_prefixed}: string | undefined;`);
|
||||
}
|
||||
|
||||
return dedent`
|
||||
declare module '$env/dynamic/${id}' {
|
||||
export const env: {
|
||||
${properties.join('\n')}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export const reserved = new Set([
|
||||
'do',
|
||||
'if',
|
||||
'in',
|
||||
'for',
|
||||
'let',
|
||||
'new',
|
||||
'try',
|
||||
'var',
|
||||
'case',
|
||||
'else',
|
||||
'enum',
|
||||
'eval',
|
||||
'null',
|
||||
'this',
|
||||
'true',
|
||||
'void',
|
||||
'with',
|
||||
'await',
|
||||
'break',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'false',
|
||||
'super',
|
||||
'throw',
|
||||
'while',
|
||||
'yield',
|
||||
'delete',
|
||||
'export',
|
||||
'import',
|
||||
'public',
|
||||
'return',
|
||||
'static',
|
||||
'switch',
|
||||
'typeof',
|
||||
'default',
|
||||
'extends',
|
||||
'finally',
|
||||
'package',
|
||||
'private',
|
||||
'continue',
|
||||
'debugger',
|
||||
'function',
|
||||
'arguments',
|
||||
'interface',
|
||||
'protected',
|
||||
'implements',
|
||||
'instanceof'
|
||||
]);
|
||||
|
||||
export const valid_identifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { get_mime_lookup } from '../utils.js';
|
||||
import { resolve_symlinks } from '../../exports/vite/build/utils.js';
|
||||
import { compact } from '../../utils/array.js';
|
||||
import { join_relative } from '../../utils/filesystem.js';
|
||||
import { dedent } from '../sync/utils.js';
|
||||
|
||||
/**
|
||||
* Generates the data used to write the server-side manifest.js file. This data is used in the Vite
|
||||
* build process, to power routing, etc.
|
||||
* @param {{
|
||||
* build_data: import('types').BuildData;
|
||||
* relative_path: string;
|
||||
* routes: import('types').RouteData[];
|
||||
* }} opts
|
||||
*/
|
||||
export function generate_manifest({ build_data, relative_path, routes }) {
|
||||
/**
|
||||
* @type {Map<any, number>} The new index of each node in the filtered nodes array
|
||||
*/
|
||||
const reindexed = new Map();
|
||||
/**
|
||||
* All nodes actually used in the routes definition (prerendered routes are omitted).
|
||||
* Root layout/error is always included as they are needed for 404 and root errors.
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const used_nodes = new Set([0, 1]);
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.page) {
|
||||
for (const i of route.page.layouts) used_nodes.add(i);
|
||||
for (const i of route.page.errors) used_nodes.add(i);
|
||||
used_nodes.add(route.page.leaf);
|
||||
}
|
||||
}
|
||||
|
||||
const node_paths = compact(
|
||||
build_data.manifest_data.nodes.map((_, i) => {
|
||||
if (used_nodes.has(i)) {
|
||||
reindexed.set(i, reindexed.size);
|
||||
return join_relative(relative_path, `/nodes/${i}.js`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/** @type {(path: string) => string} */
|
||||
const loader = (path) => `__memo(() => import('${path}'))`;
|
||||
|
||||
const assets = build_data.manifest_data.assets.map((asset) => asset.file);
|
||||
if (build_data.service_worker) {
|
||||
assets.push(build_data.service_worker);
|
||||
}
|
||||
|
||||
const matchers = new Set();
|
||||
|
||||
/** @param {Array<number | undefined>} indexes */
|
||||
function get_nodes(indexes) {
|
||||
const string = indexes.map((n) => reindexed.get(n) ?? '').join(',');
|
||||
|
||||
// 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},]`;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
// String representation of
|
||||
/** @template {import('@sveltejs/kit').SSRManifest} T */
|
||||
const manifest_expr = dedent`
|
||||
{
|
||||
appDir: ${s(build_data.app_dir)},
|
||||
appPath: ${s(build_data.app_path)},
|
||||
assets: new Set(${s(assets)}),
|
||||
mimeTypes: ${s(get_mime_lookup(build_data.manifest_data))},
|
||||
_: {
|
||||
client: ${s(build_data.client)},
|
||||
nodes: [
|
||||
${(node_paths).map(loader).join(',\n')}
|
||||
],
|
||||
routes: [
|
||||
${routes.map(route => {
|
||||
if (!route.page && !route.endpoint) return;
|
||||
|
||||
route.params.forEach(param => {
|
||||
if (param.matcher) matchers.add(param.matcher);
|
||||
});
|
||||
|
||||
return dedent`
|
||||
{
|
||||
id: ${s(route.id)},
|
||||
pattern: ${route.pattern},
|
||||
params: ${s(route.params)},
|
||||
page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${reindexed.get(route.page.leaf)} }` : 'null'},
|
||||
endpoint: ${route.endpoint ? loader(join_relative(relative_path, resolve_symlinks(build_data.server_manifest, route.endpoint.file).chunk.file)) : 'null'}
|
||||
}
|
||||
`;
|
||||
}).filter(Boolean).join(',\n')}
|
||||
],
|
||||
matchers: async () => {
|
||||
${Array.from(
|
||||
matchers,
|
||||
type => `const { match: ${type} } = await import ('${(join_relative(relative_path, `/entries/matchers/${type}.js`))}')`
|
||||
).join('\n')}
|
||||
return { ${Array.from(matchers).join(', ')} };
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Memoize the loaders to prevent Node from doing unnecessary work
|
||||
// on every dynamic import call
|
||||
return dedent`
|
||||
(() => {
|
||||
function __memo(fn) {
|
||||
let value;
|
||||
return () => value ??= (value = fn());
|
||||
}
|
||||
|
||||
return ${manifest_expr}
|
||||
})()
|
||||
`;
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { get_option } from '../../utils/options.js';
|
||||
import {
|
||||
validate_layout_exports,
|
||||
validate_layout_server_exports,
|
||||
validate_page_exports,
|
||||
validate_page_server_exports,
|
||||
validate_server_exports
|
||||
} from '../../utils/exports.js';
|
||||
import { load_config } from '../config/index.js';
|
||||
import { forked } from '../../utils/fork.js';
|
||||
import { should_polyfill } from '../../utils/platform.js';
|
||||
import { installPolyfills } from '../../exports/node/polyfills.js';
|
||||
import { resolvePath } from '../../exports/index.js';
|
||||
import { ENDPOINT_METHODS } from '../../constants.js';
|
||||
import { filter_private_env, filter_public_env } from '../../utils/env.js';
|
||||
|
||||
export default forked(import.meta.url, analyse);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* manifest_path: string;
|
||||
* env: Record<string, string>
|
||||
* }} opts
|
||||
*/
|
||||
async function analyse({ manifest_path, env }) {
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
|
||||
|
||||
/** @type {import('types').ValidatedKitConfig} */
|
||||
const config = (await load_config()).kit;
|
||||
|
||||
const server_root = join(config.outDir, 'output');
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href);
|
||||
|
||||
if (should_polyfill) {
|
||||
installPolyfills();
|
||||
}
|
||||
|
||||
// configure `import { building } from '$app/environment'` —
|
||||
// essential we do this before analysing the code
|
||||
internal.set_building(true);
|
||||
|
||||
// set env, in case it's used in initialisation
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
|
||||
internal.set_private_env(filter_private_env(env, { public_prefix, private_prefix }));
|
||||
internal.set_public_env(filter_public_env(env, { public_prefix, private_prefix }));
|
||||
|
||||
/** @type {import('types').ServerMetadata} */
|
||||
const metadata = {
|
||||
nodes: [],
|
||||
routes: new Map()
|
||||
};
|
||||
|
||||
// analyse nodes
|
||||
for (const loader of manifest._.nodes) {
|
||||
const node = await loader();
|
||||
|
||||
metadata.nodes[node.index] = {
|
||||
has_server_load: node.server?.load !== undefined || node.server?.trailingSlash !== undefined
|
||||
};
|
||||
}
|
||||
|
||||
// analyse routes
|
||||
for (const route of manifest._.routes) {
|
||||
/** @type {Array<'GET' | 'POST'>} */
|
||||
const page_methods = [];
|
||||
|
||||
/** @type {(import('types').HttpMethod | '*')[]} */
|
||||
const api_methods = [];
|
||||
|
||||
/** @type {import('types').PrerenderOption | undefined} */
|
||||
let prerender = undefined;
|
||||
/** @type {any} */
|
||||
let config = undefined;
|
||||
/** @type {import('types').PrerenderEntryGenerator | undefined} */
|
||||
let entries = undefined;
|
||||
|
||||
if (route.endpoint) {
|
||||
const mod = await route.endpoint();
|
||||
if (mod.prerender !== undefined) {
|
||||
validate_server_exports(mod, route.id);
|
||||
|
||||
if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
|
||||
throw new Error(
|
||||
`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
|
||||
);
|
||||
}
|
||||
|
||||
prerender = mod.prerender;
|
||||
}
|
||||
|
||||
for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) {
|
||||
if (mod[method]) api_methods.push(method);
|
||||
}
|
||||
|
||||
if (mod.fallback) {
|
||||
api_methods.push('*');
|
||||
}
|
||||
|
||||
config = mod.config;
|
||||
entries = mod.entries;
|
||||
}
|
||||
|
||||
if (route.page) {
|
||||
const nodes = await Promise.all(
|
||||
[...route.page.layouts, route.page.leaf].map((n) => {
|
||||
if (n !== undefined) return manifest._.nodes[n]();
|
||||
})
|
||||
);
|
||||
|
||||
const layouts = nodes.slice(0, -1);
|
||||
const page = nodes.at(-1);
|
||||
|
||||
for (const layout of layouts) {
|
||||
if (layout) {
|
||||
validate_layout_server_exports(layout.server, layout.server_id);
|
||||
validate_layout_exports(layout.universal, layout.universal_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (page) {
|
||||
page_methods.push('GET');
|
||||
if (page.server?.actions) page_methods.push('POST');
|
||||
|
||||
validate_page_server_exports(page.server, page.server_id);
|
||||
validate_page_exports(page.universal, page.universal_id);
|
||||
}
|
||||
|
||||
prerender = get_option(nodes, 'prerender') ?? false;
|
||||
|
||||
config = get_config(nodes);
|
||||
entries ??= get_option(nodes, 'entries');
|
||||
}
|
||||
|
||||
metadata.routes.set(route.id, {
|
||||
config,
|
||||
methods: Array.from(new Set([...page_methods, ...api_methods])),
|
||||
page: {
|
||||
methods: page_methods
|
||||
},
|
||||
api: {
|
||||
methods: api_methods
|
||||
},
|
||||
prerender,
|
||||
entries:
|
||||
entries && (await entries()).map((entry_object) => resolvePath(route.id, entry_object))
|
||||
});
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a shallow merge (first level) of the config object
|
||||
* @param {Array<import('types').SSRNode | undefined>} nodes
|
||||
*/
|
||||
function get_config(nodes) {
|
||||
let current = {};
|
||||
for (const node of nodes) {
|
||||
const config = node?.universal?.config ?? node?.server?.config;
|
||||
if (config) {
|
||||
current = {
|
||||
...current,
|
||||
...config
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(current).length ? current : undefined;
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
import { resolve } from '../../utils/url.js';
|
||||
import { decode } from './entities.js';
|
||||
|
||||
const DOCTYPE = 'DOCTYPE';
|
||||
const CDATA_OPEN = '[CDATA[';
|
||||
const CDATA_CLOSE = ']]>';
|
||||
const COMMENT_OPEN = '--';
|
||||
const COMMENT_CLOSE = '-->';
|
||||
|
||||
const TAG_OPEN = /[a-zA-Z]/;
|
||||
const TAG_CHAR = /[a-zA-Z0-9]/;
|
||||
const ATTRIBUTE_NAME = /[^\t\n\f />"'=]/;
|
||||
|
||||
const WHITESPACE = /[\s\n\r]/;
|
||||
|
||||
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'
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {string} base
|
||||
*/
|
||||
export function crawl(html, base) {
|
||||
/** @type {string[]} */
|
||||
const ids = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const hrefs = [];
|
||||
|
||||
let i = 0;
|
||||
main: while (i < html.length) {
|
||||
const char = html[i];
|
||||
|
||||
if (char === '<') {
|
||||
if (html[i + 1] === '!') {
|
||||
i += 2;
|
||||
|
||||
if (html.slice(i, i + DOCTYPE.length).toUpperCase() === DOCTYPE) {
|
||||
i += DOCTYPE.length;
|
||||
while (i < html.length) {
|
||||
if (html[i++] === '>') {
|
||||
continue main;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip cdata
|
||||
if (html.slice(i, i + CDATA_OPEN.length) === CDATA_OPEN) {
|
||||
i += CDATA_OPEN.length;
|
||||
while (i < html.length) {
|
||||
if (html.slice(i, i + CDATA_CLOSE.length) === CDATA_CLOSE) {
|
||||
i += CDATA_CLOSE.length;
|
||||
continue main;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// skip comments
|
||||
if (html.slice(i, i + COMMENT_OPEN.length) === COMMENT_OPEN) {
|
||||
i += COMMENT_OPEN.length;
|
||||
while (i < html.length) {
|
||||
if (html.slice(i, i + COMMENT_CLOSE.length) === COMMENT_CLOSE) {
|
||||
i += COMMENT_CLOSE.length;
|
||||
continue main;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse opening tags
|
||||
const start = ++i;
|
||||
if (TAG_OPEN.test(html[start])) {
|
||||
while (i < html.length) {
|
||||
if (!TAG_CHAR.test(html[i])) {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const tag = html.slice(start, i).toUpperCase();
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const attributes = {};
|
||||
|
||||
if (tag === 'SCRIPT' || tag === 'STYLE') {
|
||||
while (i < html.length) {
|
||||
if (
|
||||
html[i] === '<' &&
|
||||
html[i + 1] === '/' &&
|
||||
html.slice(i + 2, i + 2 + tag.length).toUpperCase() === tag
|
||||
) {
|
||||
continue main;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
while (i < html.length) {
|
||||
const start = i;
|
||||
|
||||
const char = html[start];
|
||||
if (char === '>') break;
|
||||
|
||||
if (ATTRIBUTE_NAME.test(char)) {
|
||||
i += 1;
|
||||
|
||||
while (i < html.length) {
|
||||
if (!ATTRIBUTE_NAME.test(html[i])) {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
const name = html.slice(start, i).toLowerCase();
|
||||
|
||||
while (WHITESPACE.test(html[i])) i += 1;
|
||||
|
||||
if (html[i] === '=') {
|
||||
i += 1;
|
||||
while (WHITESPACE.test(html[i])) i += 1;
|
||||
|
||||
let value;
|
||||
|
||||
if (html[i] === "'" || html[i] === '"') {
|
||||
const quote = html[i++];
|
||||
|
||||
const start = i;
|
||||
let escaped = false;
|
||||
|
||||
while (i < html.length) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else {
|
||||
const char = html[i];
|
||||
|
||||
if (html[i] === quote) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = true;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
value = html.slice(start, i);
|
||||
} else {
|
||||
const start = i;
|
||||
while (html[i] !== '>' && !WHITESPACE.test(html[i])) i += 1;
|
||||
value = html.slice(start, i);
|
||||
|
||||
i -= 1;
|
||||
}
|
||||
|
||||
value = decode(value);
|
||||
attributes[name] = value;
|
||||
} else {
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return { ids, hrefs };
|
||||
}
|
||||
+2252
File diff suppressed because it is too large
Load Diff
+54
@@ -0,0 +1,54 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { installPolyfills } from '../../exports/node/polyfills.js';
|
||||
import { load_config } from '../config/index.js';
|
||||
import { forked } from '../../utils/fork.js';
|
||||
|
||||
export default forked(import.meta.url, generate_fallback);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* manifest_path: string;
|
||||
* env: Record<string, string>
|
||||
* }} opts
|
||||
*/
|
||||
async function generate_fallback({ manifest_path, env }) {
|
||||
/** @type {import('types').ValidatedKitConfig} */
|
||||
const config = (await load_config()).kit;
|
||||
|
||||
installPolyfills();
|
||||
|
||||
const server_root = join(config.outDir, 'output');
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const { set_building } = await import(pathToFileURL(`${server_root}/server/internal.js`).href);
|
||||
|
||||
/** @type {import('types').ServerModule} */
|
||||
const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href);
|
||||
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
|
||||
|
||||
set_building(true);
|
||||
|
||||
const server = new Server(manifest);
|
||||
await server.init({ env });
|
||||
|
||||
const response = await server.respond(new Request(config.prerender.origin + '/[fallback]'), {
|
||||
getClientAddress: () => {
|
||||
throw new Error('Cannot read clientAddress during prerendering');
|
||||
},
|
||||
prerendering: {
|
||||
fallback: true,
|
||||
dependencies: new Map()
|
||||
},
|
||||
read: (file) => readFileSync(join(config.files.assets, file))
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
throw new Error(`Could not create a fallback page — failed with status ${response.status}`);
|
||||
}
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { installPolyfills } from '../../exports/node/polyfills.js';
|
||||
import { mkdirp, posixify, walk } from '../../utils/filesystem.js';
|
||||
import { should_polyfill } from '../../utils/platform.js';
|
||||
import { decode_uri, is_root_relative, resolve } from '../../utils/url.js';
|
||||
import { escape_html_attr } from '../../utils/escape.js';
|
||||
import { logger } from '../utils.js';
|
||||
import { load_config } from '../config/index.js';
|
||||
import { get_route_segments } from '../../utils/routing.js';
|
||||
import { queue } from './queue.js';
|
||||
import { crawl } from './crawl.js';
|
||||
import { forked } from '../../utils/fork.js';
|
||||
import * as devalue from 'devalue';
|
||||
|
||||
export default forked(import.meta.url, prerender);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* out: string;
|
||||
* manifest_path: string;
|
||||
* metadata: import('types').ServerMetadata;
|
||||
* verbose: boolean;
|
||||
* env: Record<string, string>
|
||||
* }} opts
|
||||
*/
|
||||
async function prerender({ out, manifest_path, metadata, verbose, env }) {
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const internal = await import(pathToFileURL(`${out}/server/internal.js`).href);
|
||||
|
||||
/** @type {import('types').ServerModule} */
|
||||
const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
|
||||
|
||||
// configure `import { building } from '$app/environment'` —
|
||||
// essential we do this before analysing the code
|
||||
internal.set_building(true);
|
||||
|
||||
/**
|
||||
* @template {{message: string}} T
|
||||
* @template {Omit<T, 'message'>} K
|
||||
* @param {import('types').Logger} log
|
||||
* @param {'fail' | 'warn' | 'ignore' | ((details: T) => void)} input
|
||||
* @param {(details: K) => string} format
|
||||
* @returns {(details: K) => void}
|
||||
*/
|
||||
function normalise_error_handler(log, input, format) {
|
||||
switch (input) {
|
||||
case 'fail':
|
||||
return (details) => {
|
||||
throw new Error(format(details));
|
||||
};
|
||||
case 'warn':
|
||||
return (details) => {
|
||||
log.error(format(details));
|
||||
};
|
||||
case 'ignore':
|
||||
return () => {};
|
||||
default:
|
||||
// @ts-expect-error TS thinks T might be of a different kind, but it's not
|
||||
return (details) => input({ ...details, message: format(details) });
|
||||
}
|
||||
}
|
||||
|
||||
const OK = 2;
|
||||
const REDIRECT = 3;
|
||||
|
||||
/** @type {import('types').Prerendered} */
|
||||
const prerendered = {
|
||||
pages: new Map(),
|
||||
assets: new Map(),
|
||||
redirects: new Map(),
|
||||
paths: []
|
||||
};
|
||||
|
||||
/** @type {import('types').PrerenderMap} */
|
||||
const prerender_map = new Map();
|
||||
|
||||
for (const [id, { prerender }] of metadata.routes) {
|
||||
if (prerender !== undefined) {
|
||||
prerender_map.set(id, prerender);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const prerendered_routes = new Set();
|
||||
|
||||
/** @type {import('types').ValidatedKitConfig} */
|
||||
const config = (await load_config()).kit;
|
||||
|
||||
/** @type {import('types').Logger} */
|
||||
const log = logger({ verbose });
|
||||
|
||||
if (should_polyfill) {
|
||||
installPolyfills();
|
||||
}
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const saved = new Map();
|
||||
|
||||
const server = new Server(manifest);
|
||||
await server.init({ env });
|
||||
|
||||
const handle_http_error = normalise_error_handler(
|
||||
log,
|
||||
config.prerender.handleHttpError,
|
||||
({ status, path, referrer, referenceType }) => {
|
||||
const message =
|
||||
status === 404 && !path.startsWith(config.paths.base)
|
||||
? `${path} does not begin with \`base\`, which is configured in \`paths.base\` and can be imported from \`$app/paths\` - see https://kit.svelte.dev/docs/configuration#paths for more info`
|
||||
: path;
|
||||
|
||||
return `${status} ${message}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
|
||||
}
|
||||
);
|
||||
|
||||
const handle_missing_id = normalise_error_handler(
|
||||
log,
|
||||
config.prerender.handleMissingId,
|
||||
({ path, id, referrers }) => {
|
||||
return (
|
||||
`The following pages contain links to ${path}#${id}, but no element with id="${id}" exists on ${path} - see the \`handleMissingId\` option in https://kit.svelte.dev/docs/configuration#prerender for more info:` +
|
||||
referrers.map((l) => `\n - ${l}`).join('')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {boolean} is_html
|
||||
*/
|
||||
function output_filename(path, is_html) {
|
||||
const file = path.slice(config.paths.base.length + 1) || 'index.html';
|
||||
|
||||
if (is_html && !file.endsWith('.html')) {
|
||||
return file + (file.endsWith('/') ? 'index.html' : '.html');
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
const files = new Set(walk(`${out}/client`).map(posixify));
|
||||
|
||||
const immutable = `${config.appDir}/immutable`;
|
||||
if (existsSync(`${out}/server/${immutable}`)) {
|
||||
for (const file of walk(`${out}/server/${immutable}`)) {
|
||||
files.add(posixify(`${config.appDir}/immutable/${file}`));
|
||||
}
|
||||
}
|
||||
const seen = new Set();
|
||||
const written = new Set();
|
||||
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const expected_hashlinks = new Map();
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
const actual_hashlinks = new Map();
|
||||
|
||||
/**
|
||||
* @param {string | null} referrer
|
||||
* @param {string} decoded
|
||||
* @param {string} [encoded]
|
||||
* @param {string} [generated_from_id]
|
||||
*/
|
||||
function enqueue(referrer, decoded, encoded, generated_from_id) {
|
||||
if (seen.has(decoded)) return;
|
||||
seen.add(decoded);
|
||||
|
||||
const file = decoded.slice(config.paths.base.length + 1);
|
||||
if (files.has(file)) return;
|
||||
|
||||
return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} decoded
|
||||
* @param {string} encoded
|
||||
* @param {string?} referrer
|
||||
* @param {string} [generated_from_id]
|
||||
*/
|
||||
async function visit(decoded, encoded, referrer, generated_from_id) {
|
||||
if (!decoded.startsWith(config.paths.base)) {
|
||||
handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {Map<string, import('types').PrerenderDependency>} */
|
||||
const dependencies = new Map();
|
||||
|
||||
const response = await server.respond(new Request(config.prerender.origin + encoded), {
|
||||
getClientAddress() {
|
||||
throw new Error('Cannot read clientAddress during prerendering');
|
||||
},
|
||||
prerendering: {
|
||||
dependencies
|
||||
},
|
||||
read: (file) => {
|
||||
// stuff we just wrote
|
||||
const filepath = saved.get(file);
|
||||
if (filepath) return readFileSync(filepath);
|
||||
|
||||
// stuff in `static`
|
||||
return readFileSync(join(config.files.assets, file));
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
save('pages', response, body, decoded, encoded, referrer, 'linked');
|
||||
|
||||
for (const [dependency_path, result] of dependencies) {
|
||||
// this seems circuitous, but using new URL allows us to not care
|
||||
// whether dependency_path is encoded or not
|
||||
const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname;
|
||||
const decoded_dependency_path = decode_uri(encoded_dependency_path);
|
||||
|
||||
const headers = Object.fromEntries(result.response.headers);
|
||||
|
||||
const prerender = headers['x-sveltekit-prerender'];
|
||||
if (prerender) {
|
||||
const encoded_route_id = headers['x-sveltekit-routeid'];
|
||||
if (encoded_route_id != null) {
|
||||
const route_id = decode_uri(encoded_route_id);
|
||||
const existing_value = prerender_map.get(route_id);
|
||||
if (existing_value !== 'auto') {
|
||||
prerender_map.set(route_id, prerender === 'true' ? true : 'auto');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const body = result.body ?? new Uint8Array(await result.response.arrayBuffer());
|
||||
|
||||
save(
|
||||
'dependencies',
|
||||
result.response,
|
||||
body,
|
||||
decoded_dependency_path,
|
||||
encoded_dependency_path,
|
||||
decoded,
|
||||
'fetched'
|
||||
);
|
||||
}
|
||||
|
||||
// avoid triggering `filterSerializeResponseHeaders` guard
|
||||
const headers = Object.fromEntries(response.headers);
|
||||
|
||||
if (config.prerender.crawl && headers['content-type'] === 'text/html') {
|
||||
const { ids, hrefs } = crawl(body.toString(), decoded);
|
||||
|
||||
actual_hashlinks.set(decoded, ids);
|
||||
|
||||
for (const href of hrefs) {
|
||||
if (!is_root_relative(href)) continue;
|
||||
|
||||
const { pathname, search, hash } = new URL(href, 'http://localhost');
|
||||
|
||||
if (search) {
|
||||
// TODO warn that query strings have no effect on statically-exported pages
|
||||
}
|
||||
|
||||
if (hash) {
|
||||
const key = decode_uri(pathname + hash);
|
||||
|
||||
if (!expected_hashlinks.has(key)) {
|
||||
expected_hashlinks.set(key, new Set());
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */ (expected_hashlinks.get(key)).add(decoded);
|
||||
}
|
||||
|
||||
enqueue(decoded, decode_uri(pathname), pathname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'pages' | 'dependencies'} category
|
||||
* @param {Response} response
|
||||
* @param {string | Uint8Array} body
|
||||
* @param {string} decoded
|
||||
* @param {string} encoded
|
||||
* @param {string | null} referrer
|
||||
* @param {'linked' | 'fetched'} referenceType
|
||||
*/
|
||||
function save(category, response, body, decoded, encoded, referrer, referenceType) {
|
||||
const response_type = Math.floor(response.status / 100);
|
||||
const headers = Object.fromEntries(response.headers);
|
||||
|
||||
const type = headers['content-type'];
|
||||
const is_html = response_type === REDIRECT || type === 'text/html';
|
||||
|
||||
const file = output_filename(decoded, is_html);
|
||||
const dest = `${config.outDir}/output/prerendered/${category}/${file}`;
|
||||
|
||||
if (written.has(file)) return;
|
||||
|
||||
const encoded_route_id = response.headers.get('x-sveltekit-routeid');
|
||||
const route_id = encoded_route_id != null ? decode_uri(encoded_route_id) : null;
|
||||
if (route_id !== null) prerendered_routes.add(route_id);
|
||||
|
||||
if (response_type === REDIRECT) {
|
||||
const location = headers['location'];
|
||||
|
||||
if (location) {
|
||||
const resolved = resolve(encoded, location);
|
||||
if (is_root_relative(resolved)) {
|
||||
enqueue(decoded, decode_uri(resolved), resolved);
|
||||
}
|
||||
|
||||
if (!headers['x-sveltekit-normalize']) {
|
||||
mkdirp(dirname(dest));
|
||||
|
||||
log.warn(`${response.status} ${decoded} -> ${location}`);
|
||||
|
||||
writeFileSync(
|
||||
dest,
|
||||
`<script>location.href=${devalue.uneval(
|
||||
location
|
||||
)};</script><meta http-equiv="refresh" content=${escape_html_attr(
|
||||
`0;url=${location}`
|
||||
)}>`
|
||||
);
|
||||
|
||||
written.add(file);
|
||||
|
||||
if (!prerendered.redirects.has(decoded)) {
|
||||
prerendered.redirects.set(decoded, {
|
||||
status: response.status,
|
||||
location: resolved
|
||||
});
|
||||
|
||||
prerendered.paths.push(decoded);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn(`location header missing on redirect received from ${decoded}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
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}`);
|
||||
writeFileSync(dest, body);
|
||||
written.add(file);
|
||||
|
||||
if (is_html) {
|
||||
prerendered.pages.set(decoded, {
|
||||
file
|
||||
});
|
||||
} else {
|
||||
prerendered.assets.set(decoded, {
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
prerendered.paths.push(decoded);
|
||||
} else if (response_type !== OK) {
|
||||
handle_http_error({ status: response.status, path: decoded, referrer, referenceType });
|
||||
}
|
||||
|
||||
manifest.assets.add(file);
|
||||
saved.set(file, dest);
|
||||
}
|
||||
|
||||
/** @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
|
||||
) {
|
||||
// Only log if we're actually going to do something to not confuse users
|
||||
log.info('Prerendering');
|
||||
}
|
||||
|
||||
for (const entry of config.prerender.entries) {
|
||||
if (entry === '*') {
|
||||
for (const [id, prerender] of prerender_map) {
|
||||
if (prerender) {
|
||||
if (id.includes('[')) continue;
|
||||
const path = `/${get_route_segments(id).join('/')}`;
|
||||
enqueue(null, config.paths.base + path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enqueue(null, config.paths.base + entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { id, entries } of route_level_entries) {
|
||||
for (const entry of entries) {
|
||||
enqueue(null, config.paths.base + entry, undefined, id);
|
||||
}
|
||||
}
|
||||
|
||||
await q.done();
|
||||
|
||||
// handle invalid fragment links
|
||||
for (const [key, referrers] of expected_hashlinks) {
|
||||
const index = key.indexOf('#');
|
||||
const path = key.slice(0, index);
|
||||
const id = key.slice(index + 1);
|
||||
|
||||
const hashlinks = actual_hashlinks.get(path);
|
||||
// ignore fragment links to pages that were not prerendered
|
||||
if (!hashlinks) continue;
|
||||
|
||||
if (!hashlinks.includes(id)) {
|
||||
handle_missing_id({ id, path, referrers: Array.from(referrers) });
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const not_prerendered = [];
|
||||
|
||||
for (const [route_id, prerender] of prerender_map) {
|
||||
if (prerender === true && !prerendered_routes.has(route_id)) {
|
||||
not_prerendered.push(route_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (not_prerendered.length > 0) {
|
||||
throw new Error(
|
||||
`The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${not_prerendered.map(
|
||||
(id) => ` - ${id}`
|
||||
)}\n\nSee https://kit.svelte.dev/docs/page-options#prerender-troubleshooting for info on how to solve this`
|
||||
);
|
||||
}
|
||||
|
||||
return { prerendered, prerender_map };
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* fn: () => Promise<any>,
|
||||
* fulfil: (value: any) => void,
|
||||
* reject: (error: Error) => void
|
||||
* }} Task
|
||||
*/
|
||||
|
||||
/** @param {number} concurrency */
|
||||
export function queue(concurrency) {
|
||||
/** @type {Task[]} */
|
||||
const tasks = [];
|
||||
|
||||
let current = 0;
|
||||
|
||||
/** @type {(value?: any) => void} */
|
||||
let fulfil;
|
||||
|
||||
/** @type {(error: Error) => void} */
|
||||
let reject;
|
||||
|
||||
let closed = false;
|
||||
|
||||
const done = new Promise((f, r) => {
|
||||
fulfil = f;
|
||||
reject = r;
|
||||
});
|
||||
|
||||
done.catch(() => {
|
||||
// this is necessary in case a catch handler is never added
|
||||
// to the done promise by the user
|
||||
});
|
||||
|
||||
function dequeue() {
|
||||
if (current < concurrency) {
|
||||
const task = tasks.shift();
|
||||
|
||||
if (task) {
|
||||
current += 1;
|
||||
const promise = Promise.resolve(task.fn());
|
||||
|
||||
promise
|
||||
.then(task.fulfil, (err) => {
|
||||
task.reject(err);
|
||||
reject(err);
|
||||
})
|
||||
.then(() => {
|
||||
current -= 1;
|
||||
dequeue();
|
||||
});
|
||||
} else if (current === 0) {
|
||||
closed = true;
|
||||
fulfil();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** @param {() => any} fn */
|
||||
add: (fn) => {
|
||||
if (closed) throw new Error('Cannot add tasks to a queue that has ended');
|
||||
|
||||
const promise = new Promise((fulfil, reject) => {
|
||||
tasks.push({ fn, fulfil, reject });
|
||||
});
|
||||
|
||||
dequeue();
|
||||
return promise;
|
||||
},
|
||||
|
||||
done: () => {
|
||||
if (current === 0) {
|
||||
closed = true;
|
||||
fulfil();
|
||||
}
|
||||
|
||||
return done;
|
||||
}
|
||||
};
|
||||
}
|
||||
Generated
Vendored
+570
@@ -0,0 +1,570 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import colors from 'kleur';
|
||||
import { lookup } from 'mrmime';
|
||||
import { list_files, runtime_directory } from '../../utils.js';
|
||||
import { posixify } from '../../../utils/filesystem.js';
|
||||
import { parse_route_id } from '../../../utils/routing.js';
|
||||
import { sort_routes } from './sort.js';
|
||||
|
||||
/**
|
||||
* Generates the manifest data used for the client-side manifest and types generation.
|
||||
* @param {{
|
||||
* config: import('types').ValidatedConfig;
|
||||
* fallback?: string;
|
||||
* cwd?: string;
|
||||
* }} opts
|
||||
* @returns {import('types').ManifestData}
|
||||
*/
|
||||
export default function create_manifest_data({
|
||||
config,
|
||||
fallback = `${runtime_directory}/components`,
|
||||
cwd = process.cwd()
|
||||
}) {
|
||||
const assets = create_assets(config);
|
||||
const matchers = create_matchers(config, cwd);
|
||||
const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);
|
||||
|
||||
for (const route of routes) {
|
||||
for (const param of route.params) {
|
||||
if (param.matcher && !matchers[param.matcher]) {
|
||||
throw new Error(`No matcher found for parameter '${param.matcher}' in route ${route.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
assets,
|
||||
matchers,
|
||||
nodes,
|
||||
routes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function create_assets(config) {
|
||||
return list_files(config.kit.files.assets).map((file) => ({
|
||||
file,
|
||||
size: fs.statSync(path.resolve(config.kit.files.assets, file)).size,
|
||||
type: lookup(file) || null
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} cwd
|
||||
*/
|
||||
function create_matchers(config, cwd) {
|
||||
const params_base = path.relative(cwd, config.kit.files.params);
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const matchers = {};
|
||||
if (fs.existsSync(config.kit.files.params)) {
|
||||
for (const file of fs.readdirSync(config.kit.files.params)) {
|
||||
const ext = path.extname(file);
|
||||
if (!config.kit.moduleExtensions.includes(ext)) continue;
|
||||
const type = file.slice(0, -ext.length);
|
||||
|
||||
if (/^\w+$/.test(type)) {
|
||||
const matcher_file = path.join(params_base, file);
|
||||
|
||||
// Disallow same matcher with different extensions
|
||||
if (matchers[type]) {
|
||||
throw new Error(`Duplicate matchers: ${matcher_file} and ${matchers[type]}`);
|
||||
} else {
|
||||
matchers[type] = matcher_file;
|
||||
}
|
||||
} else {
|
||||
// Allow for matcher test collocation
|
||||
if (type.endsWith('.test') || type.endsWith('.spec')) continue;
|
||||
|
||||
throw new Error(
|
||||
`Matcher names can only have underscores and alphanumeric characters — "${file}" is invalid`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} cwd
|
||||
* @param {string} fallback
|
||||
*/
|
||||
function create_routes_and_nodes(cwd, config, fallback) {
|
||||
/** @type {import('types').RouteData[]} */
|
||||
const routes = [];
|
||||
|
||||
const routes_base = posixify(path.relative(cwd, config.kit.files.routes));
|
||||
|
||||
const valid_extensions = [...config.extensions, ...config.kit.moduleExtensions];
|
||||
|
||||
/** @type {import('types').PageNode[]} */
|
||||
const nodes = [];
|
||||
|
||||
if (fs.existsSync(config.kit.files.routes)) {
|
||||
/**
|
||||
* @param {number} depth
|
||||
* @param {string} id
|
||||
* @param {string} segment
|
||||
* @param {import('types').RouteData | null} parent
|
||||
*/
|
||||
const walk = (depth, id, segment, parent) => {
|
||||
const unescaped = id.replace(/\[([ux])\+([^\]]+)\]/gi, (match, type, code) => {
|
||||
if (match !== match.toLowerCase()) {
|
||||
throw new Error(`Character escape sequence in ${id} must be lowercase`);
|
||||
}
|
||||
|
||||
if (!/[0-9a-f]+/.test(code)) {
|
||||
throw new Error(`Invalid character escape sequence in ${id}`);
|
||||
}
|
||||
|
||||
if (type === 'x') {
|
||||
if (code.length !== 2) {
|
||||
throw new Error(`Hexadecimal escape sequence in ${id} must be two characters`);
|
||||
}
|
||||
|
||||
return String.fromCharCode(parseInt(code, 16));
|
||||
} else {
|
||||
if (code.length < 4 || code.length > 6) {
|
||||
throw new Error(
|
||||
`Unicode escape sequence in ${id} must be between four and six characters`
|
||||
);
|
||||
}
|
||||
|
||||
return String.fromCharCode(parseInt(code, 16));
|
||||
}
|
||||
});
|
||||
|
||||
if (/\]\[/.test(unescaped)) {
|
||||
throw new Error(`Invalid route ${id} — parameters must be separated`);
|
||||
}
|
||||
|
||||
if (count_occurrences('[', id) !== count_occurrences(']', id)) {
|
||||
throw new Error(`Invalid route ${id} — brackets are unbalanced`);
|
||||
}
|
||||
|
||||
if (/#/.test(segment)) {
|
||||
// Vite will barf on files with # in them
|
||||
throw new Error(`Route ${id} should be renamed to ${id.replace(/#/g, '[x+23]')}`);
|
||||
}
|
||||
|
||||
if (/\[\.\.\.\w+\]\/\[\[/.test(id)) {
|
||||
throw new Error(
|
||||
`Invalid route ${id} — an [[optional]] route segment cannot follow a [...rest] route segment`
|
||||
);
|
||||
}
|
||||
|
||||
if (/\[\[\.\.\./.test(id)) {
|
||||
throw new Error(
|
||||
`Invalid route ${id} — a rest route segment is always optional, remove the outer square brackets`
|
||||
);
|
||||
}
|
||||
|
||||
const { pattern, params } = parse_route_id(id);
|
||||
|
||||
/** @type {import('types').RouteData} */
|
||||
const route = {
|
||||
id,
|
||||
parent,
|
||||
|
||||
segment,
|
||||
pattern,
|
||||
params,
|
||||
|
||||
layout: null,
|
||||
error: null,
|
||||
leaf: null,
|
||||
page: null,
|
||||
endpoint: null
|
||||
};
|
||||
|
||||
// important to do this before walking children, so that child
|
||||
// routes appear later
|
||||
routes.push(route);
|
||||
|
||||
// if we don't do this, the route map becomes unwieldy to console.log
|
||||
Object.defineProperty(route, 'parent', { enumerable: false });
|
||||
|
||||
const dir = path.join(cwd, routes_base, id);
|
||||
|
||||
// We can't use withFileTypes because of a NodeJs bug which returns wrong results
|
||||
// with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
|
||||
const files = fs.readdirSync(dir).map((name) => ({
|
||||
is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
|
||||
name
|
||||
}));
|
||||
|
||||
// process files first
|
||||
for (const file of files) {
|
||||
if (file.is_dir) continue;
|
||||
|
||||
const ext = valid_extensions.find((ext) => file.name.endsWith(ext));
|
||||
if (!ext) continue;
|
||||
|
||||
if (!file.name.startsWith('+')) {
|
||||
const name = file.name.slice(0, -ext.length);
|
||||
// check if it is a valid route filename but missing the + prefix
|
||||
const typo =
|
||||
/^(?:(page(?:@(.*))?)|(layout(?:@(.*))?)|(error))$/.test(name) ||
|
||||
/^(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/.test(
|
||||
name
|
||||
);
|
||||
if (typo) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Missing route file prefix. Did you mean +${file.name}?` +
|
||||
` at ${path.join(dir, file.name)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.name.endsWith('.d.ts')) {
|
||||
let name = file.name.slice(0, -5);
|
||||
const ext = valid_extensions.find((ext) => name.endsWith(ext));
|
||||
if (ext) name = name.slice(0, -ext.length);
|
||||
|
||||
const valid =
|
||||
/^\+(?:(page(?:@(.*))?)|(layout(?:@(.*))?)|(error))$/.test(name) ||
|
||||
/^\+(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/.test(
|
||||
name
|
||||
);
|
||||
|
||||
if (valid) continue;
|
||||
}
|
||||
|
||||
const project_relative = posixify(path.relative(cwd, path.join(dir, file.name)));
|
||||
|
||||
const item = analyze(
|
||||
project_relative,
|
||||
file.name,
|
||||
config.extensions,
|
||||
config.kit.moduleExtensions
|
||||
);
|
||||
|
||||
/**
|
||||
* @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') {
|
||||
if (item.is_error) {
|
||||
route.error = {
|
||||
depth,
|
||||
component: project_relative
|
||||
};
|
||||
} else if (item.is_layout) {
|
||||
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 };
|
||||
} else if (route.leaf.component) {
|
||||
throw duplicate_files_error('page component', route.leaf.component);
|
||||
}
|
||||
|
||||
route.leaf.component = project_relative;
|
||||
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: [] };
|
||||
} 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 };
|
||||
} 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 = {
|
||||
file: project_relative
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// then handle children
|
||||
for (const file of files) {
|
||||
if (file.is_dir) {
|
||||
walk(depth + 1, path.posix.join(id, file.name), file.name, route);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(0, '/', '', null);
|
||||
|
||||
if (routes.length === 1) {
|
||||
const root = routes[0];
|
||||
if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
|
||||
throw new Error(
|
||||
'No routes found. If you are using a custom src/routes directory, make sure it is specified in svelte.config.js'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no routes directory, we'll just create a single empty route. This ensures the root layout and
|
||||
// error components are included in the manifest, which is needed for subsequent build/dev commands to work
|
||||
routes.push({
|
||||
id: '/',
|
||||
segment: '',
|
||||
pattern: /^$/,
|
||||
params: [],
|
||||
parent: null,
|
||||
layout: null,
|
||||
error: null,
|
||||
leaf: null,
|
||||
page: null,
|
||||
endpoint: null
|
||||
});
|
||||
}
|
||||
|
||||
prevent_conflicts(routes);
|
||||
|
||||
const root = routes[0];
|
||||
|
||||
if (!root.layout?.component) {
|
||||
if (!root.layout) root.layout = { depth: 0, child_pages: [] };
|
||||
root.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
|
||||
}
|
||||
|
||||
if (!root.error?.component) {
|
||||
if (!root.error) root.error = { depth: 0 };
|
||||
root.error.component = posixify(path.relative(cwd, `${fallback}/error.svelte`));
|
||||
}
|
||||
|
||||
// we do layouts/errors first as they are more likely to be reused,
|
||||
// and smaller indexes take fewer bytes. also, this guarantees that
|
||||
// the default error/layout are 0/1
|
||||
for (const route of routes) {
|
||||
if (route.layout) {
|
||||
if (!route.layout?.component) {
|
||||
route.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
|
||||
}
|
||||
nodes.push(route.layout);
|
||||
}
|
||||
if (route.error) nodes.push(route.error);
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.leaf) nodes.push(route.leaf);
|
||||
}
|
||||
|
||||
const indexes = new Map(nodes.map((node, i) => [node, i]));
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.leaf) continue;
|
||||
|
||||
route.page = {
|
||||
layouts: [],
|
||||
errors: [],
|
||||
leaf: /** @type {number} */ (indexes.get(route.leaf))
|
||||
};
|
||||
|
||||
/** @type {import('types').RouteData | null} */
|
||||
let current_route = route;
|
||||
let current_node = route.leaf;
|
||||
let parent_id = route.leaf.parent_id;
|
||||
|
||||
while (current_route) {
|
||||
if (parent_id === undefined || current_route.segment === parent_id) {
|
||||
if (current_route.layout || current_route.error) {
|
||||
route.page.layouts.unshift(
|
||||
current_route.layout ? indexes.get(current_route.layout) : undefined
|
||||
);
|
||||
route.page.errors.unshift(
|
||||
current_route.error ? indexes.get(current_route.error) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
if (current_route.layout) {
|
||||
/** @type {import('types').PageNode[]} */ (current_route.layout.child_pages).push(
|
||||
route.leaf
|
||||
);
|
||||
current_node.parent = current_node = current_route.layout;
|
||||
parent_id = current_node.parent_id;
|
||||
} else {
|
||||
parent_id = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
current_route = current_route.parent;
|
||||
}
|
||||
|
||||
if (parent_id !== undefined) {
|
||||
throw new Error(`${current_node.component} references missing segment "${parent_id}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
routes: sort_routes(routes)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} project_relative
|
||||
* @param {string} file
|
||||
* @param {string[]} component_extensions
|
||||
* @param {string[]} module_extensions
|
||||
* @returns {import('./types.js').RouteFile}
|
||||
*/
|
||||
function analyze(project_relative, file, component_extensions, module_extensions) {
|
||||
const component_extension = component_extensions.find((ext) => file.endsWith(ext));
|
||||
if (component_extension) {
|
||||
const name = file.slice(0, -component_extension.length);
|
||||
const pattern = /^\+(?:(page(?:@(.*))?)|(layout(?:@(.*))?)|(error))$/;
|
||||
const match = pattern.exec(name);
|
||||
if (!match) {
|
||||
throw new Error(`Files prefixed with + are reserved (saw ${project_relative})`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'component',
|
||||
is_page: !!match[1],
|
||||
is_layout: !!match[3],
|
||||
is_error: !!match[5],
|
||||
uses_layout: match[2] ?? match[4]
|
||||
};
|
||||
}
|
||||
|
||||
const module_extension = module_extensions.find((ext) => file.endsWith(ext));
|
||||
if (module_extension) {
|
||||
const name = file.slice(0, -module_extension.length);
|
||||
const pattern =
|
||||
/^\+(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/;
|
||||
const match = pattern.exec(name);
|
||||
if (!match) {
|
||||
throw new Error(`Files prefixed with + are reserved (saw ${project_relative})`);
|
||||
} else if (match[3] || match[6]) {
|
||||
throw new Error(
|
||||
// prettier-ignore
|
||||
`Only Svelte files can reference named layouts. Remove '${match[3] || match[6]}' from ${file} (at ${project_relative})`
|
||||
);
|
||||
}
|
||||
|
||||
const kind = match[1] || match[4] || match[7] ? 'server' : 'universal';
|
||||
|
||||
return {
|
||||
kind,
|
||||
is_page: !!match[2],
|
||||
is_layout: !!match[5]
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Files and directories prefixed with + are reserved (saw ${project_relative})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} needle
|
||||
* @param {string} haystack
|
||||
*/
|
||||
function count_occurrences(needle, haystack) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < haystack.length; i += 1) {
|
||||
if (haystack[i] === needle) count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** @param {import('types').RouteData[]} routes */
|
||||
function prevent_conflicts(routes) {
|
||||
/** @type {Map<string, string>} */
|
||||
const lookup = new Map();
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.leaf && !route.endpoint) continue;
|
||||
|
||||
const normalized = normalize_route_id(route.id);
|
||||
|
||||
// find all permutations created by optional parameters
|
||||
const split = normalized.split(/<\?(.+?)>/g);
|
||||
|
||||
let permutations = [/** @type {string} */ (split[0])];
|
||||
|
||||
// turn `x/[[optional]]/y` into `x/y` and `x/[required]/y`
|
||||
for (let i = 1; i < split.length; i += 2) {
|
||||
const matcher = split[i];
|
||||
const next = split[i + 1];
|
||||
|
||||
permutations = permutations.reduce((a, b) => {
|
||||
a.push(b + next);
|
||||
if (!(matcher === '*' && b.endsWith('//'))) a.push(b + `<${matcher}>${next}`);
|
||||
return a;
|
||||
}, /** @type {string[]} */ ([]));
|
||||
}
|
||||
|
||||
for (const permutation of permutations) {
|
||||
// remove leading/trailing/duplicated slashes caused by prior
|
||||
// manipulation of optional parameters and (groups)
|
||||
const key = permutation
|
||||
.replace(/\/{2,}/, '/')
|
||||
.replace(/^\//, '')
|
||||
.replace(/\/$/, '');
|
||||
|
||||
if (lookup.has(key)) {
|
||||
throw new Error(
|
||||
`The "${lookup.get(key)}" and "${route.id}" routes conflict with each other`
|
||||
);
|
||||
}
|
||||
|
||||
lookup.set(key, route.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function normalize_route_id(id) {
|
||||
return (
|
||||
id
|
||||
// remove groups
|
||||
.replace(/(?<=^|\/)\(.+?\)(?=$|\/)/g, '')
|
||||
|
||||
.replace(/\[[ux]\+([0-9a-f]+)\]/g, (_, x) =>
|
||||
String.fromCharCode(parseInt(x, 16)).replace(/\//g, '%2f')
|
||||
)
|
||||
|
||||
// replace `[param]` with `<*>`, `[param=x]` with `<x>`, and `[[param]]` with `<?*>`
|
||||
.replace(
|
||||
/\[(?:(\[)|(\.\.\.))?.+?(=.+?)?\]\]?/g,
|
||||
(_, optional, rest, matcher) => `<${optional ? '?' : ''}${rest ?? ''}${matcher ?? '*'}>`
|
||||
)
|
||||
);
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { get_route_segments } from '../../../utils/routing.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'static' | 'required' | 'optional' | 'rest';
|
||||
* content: string;
|
||||
* matched: boolean;
|
||||
* }} Part
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Part[]} Segment
|
||||
*/
|
||||
|
||||
const EMPTY = { type: 'static', content: '', matched: false };
|
||||
|
||||
/** @param {import('types').RouteData[]} routes */
|
||||
export function sort_routes(routes) {
|
||||
/** @type {Map<string, Part[]>} */
|
||||
const segment_cache = new Map();
|
||||
|
||||
/** @param {string} segment */
|
||||
function get_parts(segment) {
|
||||
if (!segment_cache.has(segment)) {
|
||||
segment_cache.set(segment, split(segment));
|
||||
}
|
||||
|
||||
return segment_cache.get(segment);
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function split(id) {
|
||||
/** @type {Part[]} */
|
||||
const parts = [];
|
||||
|
||||
let i = 0;
|
||||
while (i <= id.length) {
|
||||
const start = id.indexOf('[', i);
|
||||
if (start === -1) {
|
||||
parts.push({ type: 'static', content: id.slice(i), matched: false });
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push({ type: 'static', content: id.slice(i, start), matched: false });
|
||||
|
||||
const type = id[start + 1] === '[' ? 'optional' : id[start + 1] === '.' ? 'rest' : 'required';
|
||||
const delimiter = type === 'optional' ? ']]' : ']';
|
||||
const end = id.indexOf(delimiter, start);
|
||||
|
||||
if (end === -1) {
|
||||
throw new Error(`Invalid route ID ${id}`);
|
||||
}
|
||||
|
||||
const content = id.slice(start, (i = end + delimiter.length));
|
||||
|
||||
parts.push({
|
||||
type,
|
||||
content,
|
||||
matched: content.includes('=')
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
return routes.sort((route_a, route_b) => {
|
||||
const segments_a = split_route_id(route_a.id).map(get_parts);
|
||||
const segments_b = split_route_id(route_b.id).map(get_parts);
|
||||
|
||||
for (let i = 0; i < Math.max(segments_a.length, segments_b.length); i += 1) {
|
||||
const segment_a = segments_a[i] ?? [EMPTY];
|
||||
const segment_b = segments_b[i] ?? [EMPTY];
|
||||
|
||||
for (let j = 0; j < Math.max(segment_a.length, segment_b.length); j += 1) {
|
||||
const a = segment_a[j];
|
||||
const b = segment_b[j];
|
||||
|
||||
// first part of each segment is always static
|
||||
// (though it may be the empty string), then
|
||||
// it alternates between dynamic and static
|
||||
// (i.e. [foo][bar] is disallowed)
|
||||
const dynamic = j % 2 === 1;
|
||||
|
||||
if (dynamic) {
|
||||
if (!a) return -1;
|
||||
if (!b) return +1;
|
||||
|
||||
// get the next static chunk, so we can handle [...rest] edge cases
|
||||
const next_a = segment_a[j + 1].content || segments_a[i + 1]?.[0].content;
|
||||
const next_b = segment_b[j + 1].content || segments_b[i + 1]?.[0].content;
|
||||
|
||||
// `[...rest]/x` outranks `[...rest]`
|
||||
if (a.type === 'rest' && b.type === 'rest') {
|
||||
if (next_a && next_b) continue;
|
||||
if (next_a) return -1;
|
||||
if (next_b) return +1;
|
||||
}
|
||||
|
||||
// `[...rest]/x` outranks `[required]` or `[required]/[required]`
|
||||
// but not `[required]/x`
|
||||
if (a.type === 'rest') {
|
||||
return next_a && !next_b ? -1 : +1;
|
||||
}
|
||||
|
||||
if (b.type === 'rest') {
|
||||
return next_b && !next_a ? +1 : -1;
|
||||
}
|
||||
|
||||
// part with matcher outranks one without
|
||||
if (a.matched !== b.matched) {
|
||||
return a.matched ? -1 : +1;
|
||||
}
|
||||
|
||||
if (a.type !== b.type) {
|
||||
// `[...rest]` has already been accounted for, so here
|
||||
// we're comparing between `[required]` and `[[optional]]`
|
||||
if (a.type === 'required') return -1;
|
||||
if (b.type === 'required') return +1;
|
||||
}
|
||||
} else if (a.content !== b.content) {
|
||||
// shallower path outranks deeper path
|
||||
if (a === EMPTY) return -1;
|
||||
if (b === EMPTY) return +1;
|
||||
|
||||
return sort_static(a.content, b.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return route_a.id < route_b.id ? +1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
function split_route_id(id) {
|
||||
return get_route_segments(
|
||||
id
|
||||
// remove all [[optional]] parts unless they're at the very end
|
||||
.replace(/\[\[[^\]]+\]\](?!$)/g, '')
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort two strings lexicographically, except `foobar` outranks `foo`
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function sort_static(a, b) {
|
||||
if (a === b) return 0;
|
||||
|
||||
for (let i = 0; true; i += 1) {
|
||||
const char_a = a[i];
|
||||
const char_b = b[i];
|
||||
|
||||
if (char_a !== char_b) {
|
||||
if (char_a === undefined) return +1;
|
||||
if (char_b === undefined) return -1;
|
||||
return char_a < char_b ? -1 : +1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
import { PageNode } from 'types';
|
||||
|
||||
interface Part {
|
||||
dynamic: boolean;
|
||||
optional: boolean;
|
||||
rest: boolean;
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
interface RouteTreeNode {
|
||||
error: PageNode | undefined;
|
||||
layout: PageNode | undefined;
|
||||
}
|
||||
|
||||
export type RouteTree = Map<string, RouteTreeNode>;
|
||||
|
||||
interface RouteComponent {
|
||||
kind: 'component';
|
||||
is_page: boolean;
|
||||
is_layout: boolean;
|
||||
is_error: boolean;
|
||||
uses_layout: string | undefined;
|
||||
}
|
||||
|
||||
interface RouteSharedModule {
|
||||
kind: 'universal';
|
||||
is_page: boolean;
|
||||
is_layout: boolean;
|
||||
}
|
||||
|
||||
interface RouteServerModule {
|
||||
kind: 'server';
|
||||
is_page: boolean;
|
||||
is_layout: boolean;
|
||||
}
|
||||
|
||||
export type RouteFile = RouteComponent | RouteSharedModule | RouteServerModule;
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import path from 'node:path';
|
||||
import create_manifest_data from './create_manifest_data/index.js';
|
||||
import { write_client_manifest } from './write_client_manifest.js';
|
||||
import { write_root } from './write_root.js';
|
||||
import { write_tsconfig } from './write_tsconfig.js';
|
||||
import { write_types, write_all_types } from './write_types/index.js';
|
||||
import { write_ambient } from './write_ambient.js';
|
||||
import { write_server } from './write_server.js';
|
||||
|
||||
/**
|
||||
* Initialize SvelteKit's generated files.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} mode
|
||||
*/
|
||||
export function init(config, mode) {
|
||||
write_tsconfig(config.kit);
|
||||
write_ambient(config.kit, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SvelteKit's generated files
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export async function create(config) {
|
||||
const manifest_data = create_manifest_data({ config });
|
||||
|
||||
const output = path.join(config.kit.outDir, 'generated');
|
||||
|
||||
write_client_manifest(config.kit, manifest_data, `${output}/client`);
|
||||
write_server(config, output);
|
||||
write_root(manifest_data, output);
|
||||
await write_all_types(config, manifest_data);
|
||||
|
||||
return { manifest_data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SvelteKit's generated files in response to a single file content update.
|
||||
* Do not call this when the file in question was created/deleted.
|
||||
*
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} file
|
||||
*/
|
||||
export async function update(config, manifest_data, file) {
|
||||
await write_types(config, manifest_data, file);
|
||||
|
||||
return { manifest_data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run sync.init and sync.create in series, returning the result from sync.create.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} mode The Vite mode
|
||||
*/
|
||||
export async function all(config, mode) {
|
||||
init(config, mode);
|
||||
return await create(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run sync.init and then generate all type files.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} mode The Vite mode
|
||||
*/
|
||||
export async function all_types(config, mode) {
|
||||
init(config, mode);
|
||||
const manifest_data = create_manifest_data({ config });
|
||||
await write_all_types(config, manifest_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate __SERVER__/internal.js in response to src/{app.html,error.html,service-worker.js} changing
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
export function server(config) {
|
||||
write_server(config, path.join(config.kit.outDir, 'generated'));
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/** @type {import('typescript')} */
|
||||
// @ts-ignore
|
||||
export let ts = undefined;
|
||||
try {
|
||||
ts = (await import('typescript')).default;
|
||||
} catch {}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { VERSION } from 'svelte/compiler';
|
||||
import { mkdirp } from '../../utils/filesystem.js';
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const previous_contents = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {string} code
|
||||
*/
|
||||
export function write_if_changed(file, code) {
|
||||
if (code !== previous_contents.get(file)) {
|
||||
write(file, code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {string} code
|
||||
*/
|
||||
export function write(file, code) {
|
||||
previous_contents.set(file, code);
|
||||
mkdirp(path.dirname(file));
|
||||
fs.writeFileSync(file, code);
|
||||
}
|
||||
|
||||
/** @type {WeakMap<TemplateStringsArray, { strings: string[], indents: string[] }>} */
|
||||
const dedent_map = new WeakMap();
|
||||
|
||||
/**
|
||||
* Allows indenting template strings without the extra indentation ending up in the result.
|
||||
* Still allows indentation of lines relative to one another in the template string.
|
||||
* @param {TemplateStringsArray} strings
|
||||
* @param {any[]} values
|
||||
*/
|
||||
export function dedent(strings, ...values) {
|
||||
let dedented = dedent_map.get(strings);
|
||||
|
||||
if (!dedented) {
|
||||
const indentation = /** @type {RegExpExecArray} */ (/\n?([ \t]*)/.exec(strings[0]))[1];
|
||||
const pattern = new RegExp(`^${indentation}`, 'gm');
|
||||
|
||||
dedented = {
|
||||
strings: strings.map((str) => str.replace(pattern, '')),
|
||||
indents: []
|
||||
};
|
||||
|
||||
let current = '\n';
|
||||
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
const string = dedented.strings[i];
|
||||
const match = /\n([ \t]*)$/.exec(string);
|
||||
|
||||
if (match) current = match[0];
|
||||
dedented.indents[i] = current;
|
||||
}
|
||||
|
||||
dedent_map.set(strings, dedented);
|
||||
}
|
||||
|
||||
let str = dedented.strings[0];
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
str += String(values[i]).replace(/\n/g, dedented.indents[i]) + dedented.strings[i + 1];
|
||||
}
|
||||
|
||||
str = str.trim();
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export function isSvelte5Plus() {
|
||||
return Number(VERSION[0]) >= 5;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { get_env } from '../../exports/vite/utils.js';
|
||||
import { GENERATED_COMMENT } from '../../constants.js';
|
||||
import { create_dynamic_types, create_static_types } from '../env.js';
|
||||
import { write_if_changed } from './utils.js';
|
||||
|
||||
// TODO these types should be described in a neutral place, rather than
|
||||
// inside either `packages/kit` or `kit.svelte.dev`
|
||||
const descriptions_dir = fileURLToPath(new URL('../../../src/types/synthetic', import.meta.url));
|
||||
|
||||
/** @param {string} filename */
|
||||
function read_description(filename) {
|
||||
const content = fs.readFileSync(`${descriptions_dir}/${filename}`, 'utf8');
|
||||
return `/**\n${content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => ` * ${line}`)
|
||||
.join('\n')}\n */`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').Env} env
|
||||
* @param {{
|
||||
* public_prefix: string;
|
||||
* private_prefix: string;
|
||||
* }} prefixes
|
||||
*/
|
||||
const template = (env, prefixes) => `
|
||||
${GENERATED_COMMENT}
|
||||
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
${read_description('$env+static+private.md')}
|
||||
${create_static_types('private', env)}
|
||||
|
||||
${read_description('$env+static+public.md')}
|
||||
${create_static_types('public', env)}
|
||||
|
||||
${read_description('$env+dynamic+private.md')}
|
||||
${create_dynamic_types('private', env, prefixes)}
|
||||
|
||||
${read_description('$env+dynamic+public.md')}
|
||||
${create_dynamic_types('public', env, prefixes)}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Writes ambient declarations including types reference to @sveltejs/kit,
|
||||
* and the existing environment variables in process.env to
|
||||
* $env/static/private and $env/static/public
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* @param {string} mode The Vite mode
|
||||
*/
|
||||
export function write_ambient(config, mode) {
|
||||
const env = get_env(config.env, mode);
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
|
||||
|
||||
write_if_changed(
|
||||
path.join(config.outDir, 'ambient.d.ts'),
|
||||
template(env, { public_prefix, private_prefix })
|
||||
);
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import path from 'node:path';
|
||||
import { relative_path, resolve_entry } from '../../utils/filesystem.js';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { dedent, isSvelte5Plus, write_if_changed } from './utils.js';
|
||||
import colors from 'kleur';
|
||||
|
||||
/**
|
||||
* Writes the client manifest to disk. The manifest is used to power the router. It contains the
|
||||
* list of routes and corresponding Svelte components (i.e. pages and layouts).
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} output
|
||||
* @param {Array<{ has_server_load: boolean }>} [metadata]
|
||||
*/
|
||||
export function write_client_manifest(kit, manifest_data, output, metadata) {
|
||||
/**
|
||||
* Creates a module that exports a `CSRPageNode`
|
||||
* @param {import('types').PageNode} node
|
||||
*/
|
||||
function generate_node(node) {
|
||||
const declarations = [];
|
||||
|
||||
if (node.universal) {
|
||||
declarations.push(
|
||||
`import * as universal from ${s(relative_path(`${output}/nodes`, node.universal))};`,
|
||||
'export { universal };'
|
||||
);
|
||||
}
|
||||
|
||||
if (node.component) {
|
||||
declarations.push(
|
||||
`export { default as component } from ${s(
|
||||
relative_path(`${output}/nodes`, node.component)
|
||||
)};`
|
||||
);
|
||||
}
|
||||
|
||||
return declarations.join('\n');
|
||||
}
|
||||
|
||||
/** @type {Map<import('types').PageNode, number>} */
|
||||
const indices = new Map();
|
||||
const nodes = manifest_data.nodes
|
||||
.map((node, i) => {
|
||||
indices.set(node, i);
|
||||
|
||||
write_if_changed(`${output}/nodes/${i}.js`, generate_node(node));
|
||||
return `() => import('./nodes/${i}')`;
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
const layouts_with_server_load = new Set();
|
||||
|
||||
const dictionary = dedent`
|
||||
{
|
||||
${manifest_data.routes
|
||||
.map((route) => {
|
||||
if (route.page) {
|
||||
const errors = route.page.errors.slice(1).map((n) => n ?? '');
|
||||
const layouts = route.page.layouts.slice(1).map((n) => n ?? '');
|
||||
|
||||
while (layouts.at(-1) === '') layouts.pop();
|
||||
while (errors.at(-1) === '') errors.pop();
|
||||
|
||||
let leaf_has_server_load = false;
|
||||
if (route.leaf) {
|
||||
if (metadata) {
|
||||
const i = /** @type {number} */ (indices.get(route.leaf));
|
||||
|
||||
leaf_has_server_load = metadata[i].has_server_load;
|
||||
} else if (route.leaf.server) {
|
||||
leaf_has_server_load = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode whether or not the route uses server data
|
||||
// using the ones' complement, to save space
|
||||
const array = [`${leaf_has_server_load ? '~' : ''}${route.page.leaf}`];
|
||||
|
||||
// Encode whether or not the layout uses server data.
|
||||
// It's a different method compared to pages because layouts
|
||||
// are reused across pages, so we save space by doing it this way.
|
||||
route.page.layouts.forEach((layout) => {
|
||||
if (layout == undefined) return;
|
||||
|
||||
let layout_has_server_load = false;
|
||||
|
||||
if (metadata) {
|
||||
layout_has_server_load = metadata[layout].has_server_load;
|
||||
} else if (manifest_data.nodes[layout].server) {
|
||||
layout_has_server_load = true;
|
||||
}
|
||||
|
||||
if (layout_has_server_load) {
|
||||
layouts_with_server_load.add(layout);
|
||||
}
|
||||
});
|
||||
|
||||
// only include non-root layout/error nodes if they exist
|
||||
if (layouts.length > 0 || errors.length > 0) array.push(`[${layouts.join(',')}]`);
|
||||
if (errors.length > 0) array.push(`[${errors.join(',')}]`);
|
||||
|
||||
return `${s(route.id)}: [${array.join(',')}]`;
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(',\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
const hooks_file = resolve_entry(kit.files.hooks.client);
|
||||
|
||||
const typo = resolve_entry('src/+hooks.client');
|
||||
if (typo) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` +
|
||||
` at ${path.resolve(typo)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
write_if_changed(
|
||||
`${output}/app.js`,
|
||||
dedent`
|
||||
${hooks_file ? `import * as client_hooks from '${relative_path(output, hooks_file)}';` : ''}
|
||||
|
||||
export { matchers } from './matchers.js';
|
||||
|
||||
export const nodes = [
|
||||
${nodes}
|
||||
];
|
||||
|
||||
export const server_loads = [${[...layouts_with_server_load].join(',')}];
|
||||
|
||||
export const dictionary = ${dictionary};
|
||||
|
||||
export const hooks = {
|
||||
handleError: ${
|
||||
hooks_file ? 'client_hooks.handleError || ' : ''
|
||||
}(({ error }) => { console.error(error) }),
|
||||
};
|
||||
|
||||
export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
|
||||
`
|
||||
);
|
||||
|
||||
// write matchers to a separate module so that we don't
|
||||
// need to worry about name conflicts
|
||||
const imports = [];
|
||||
const matchers = [];
|
||||
|
||||
for (const key in manifest_data.matchers) {
|
||||
const src = manifest_data.matchers[key];
|
||||
|
||||
imports.push(`import { match as ${key} } from ${s(relative_path(output, src))};`);
|
||||
matchers.push(key);
|
||||
}
|
||||
|
||||
const module = imports.length
|
||||
? `${imports.join('\n')}\n\nexport const matchers = { ${matchers.join(', ')} };`
|
||||
: 'export const matchers = {};';
|
||||
|
||||
write_if_changed(`${output}/matchers.js`, module);
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { dedent, isSvelte5Plus, write_if_changed } from './utils.js';
|
||||
|
||||
/**
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} output
|
||||
*/
|
||||
export function write_root(manifest_data, output) {
|
||||
// TODO remove default layout altogether
|
||||
|
||||
const max_depth = Math.max(
|
||||
...manifest_data.routes.map((route) =>
|
||||
route.page ? route.page.layouts.filter(Boolean).length + 1 : 0
|
||||
),
|
||||
1
|
||||
);
|
||||
|
||||
const levels = [];
|
||||
for (let i = 0; i <= max_depth; i += 1) {
|
||||
levels.push(i);
|
||||
}
|
||||
|
||||
let l = max_depth;
|
||||
|
||||
let pyramid = `<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} />`;
|
||||
|
||||
while (l--) {
|
||||
pyramid = dedent`
|
||||
{#if constructors[${l + 1}]}
|
||||
<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}}>
|
||||
${pyramid}
|
||||
</svelte:component>
|
||||
{:else}
|
||||
<svelte:component this={constructors[${l}]} bind:this={components[${l}]} data={data_${l}} {form} />
|
||||
{/if}
|
||||
`;
|
||||
}
|
||||
|
||||
write_if_changed(
|
||||
`${output}/root.svelte`,
|
||||
dedent`
|
||||
<!-- This file is generated by @sveltejs/kit — do not edit it! -->
|
||||
${isSvelte5Plus() ? '<svelte:options runes={true} />' : ''}
|
||||
<script>
|
||||
import { setContext, ${isSvelte5Plus() ? '' : 'afterUpdate, '}onMount, tick } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// stores
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
let { stores, page, constructors, components = [], form, ${levels
|
||||
.map((l) => `data_${l} = null`)
|
||||
.join(', ')} } = $props();
|
||||
`
|
||||
: dedent`
|
||||
export let stores;
|
||||
export let page;
|
||||
|
||||
export let constructors;
|
||||
export let components = [];
|
||||
export let form;
|
||||
${levels.map((l) => `export let data_${l} = null;`).join('\n')}
|
||||
`
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
setContext('__svelte__', stores);
|
||||
}
|
||||
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
if (browser) {
|
||||
$effect.pre(() => stores.page.set(page));
|
||||
} else {
|
||||
stores.page.set(page);
|
||||
}
|
||||
`
|
||||
: '$: stores.page.set(page);'
|
||||
}
|
||||
${
|
||||
isSvelte5Plus()
|
||||
? dedent`
|
||||
$effect(() => {
|
||||
stores;page;constructors;components;form;${levels.map((l) => `data_${l}`).join(';')};
|
||||
stores.page.notify();
|
||||
});
|
||||
`
|
||||
: 'afterUpdate(stores.page.notify);'
|
||||
}
|
||||
|
||||
let mounted = ${isSvelte5Plus() ? '$state(false)' : 'false'};
|
||||
let navigated = ${isSvelte5Plus() ? '$state(false)' : 'false'};
|
||||
let title = ${isSvelte5Plus() ? '$state(null)' : 'null'};
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = stores.page.subscribe(() => {
|
||||
if (mounted) {
|
||||
navigated = true;
|
||||
tick().then(() => {
|
||||
title = document.title || 'untitled page';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mounted = true;
|
||||
return unsubscribe;
|
||||
});
|
||||
</script>
|
||||
|
||||
${pyramid}
|
||||
|
||||
{#if mounted}
|
||||
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
|
||||
{#if navigated}
|
||||
{title}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
`
|
||||
);
|
||||
|
||||
if (isSvelte5Plus()) {
|
||||
write_if_changed(
|
||||
`${output}/root.js`,
|
||||
dedent`
|
||||
import { asClassComponent } from 'svelte/legacy';
|
||||
import Root from './root.svelte';
|
||||
export default asClassComponent(Root);
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import path from 'node:path';
|
||||
import { hash } from '../../runtime/hash.js';
|
||||
import { posixify, resolve_entry } from '../../utils/filesystem.js';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { load_error_page, load_template } from '../config/index.js';
|
||||
import { runtime_directory } from '../utils.js';
|
||||
import { isSvelte5Plus, write_if_changed } from './utils.js';
|
||||
import colors from 'kleur';
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* hooks: string | null;
|
||||
* config: import('types').ValidatedConfig;
|
||||
* has_service_worker: boolean;
|
||||
* runtime_directory: string;
|
||||
* template: string;
|
||||
* error_page: string;
|
||||
* }} opts
|
||||
*/
|
||||
const server_template = ({
|
||||
config,
|
||||
hooks,
|
||||
has_service_worker,
|
||||
runtime_directory,
|
||||
template,
|
||||
error_page
|
||||
}) => `
|
||||
import root from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
|
||||
import { set_building } from '__sveltekit/environment';
|
||||
import { set_assets } from '__sveltekit/paths';
|
||||
import { set_private_env, set_public_env } from '${runtime_directory}/shared-server.js';
|
||||
|
||||
export const options = {
|
||||
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
|
||||
csp: ${s(config.kit.csp)},
|
||||
csrf_check_origin: ${s(config.kit.csrf.checkOrigin)},
|
||||
track_server_fetches: ${s(config.kit.dangerZone.trackServerFetches)},
|
||||
embedded: ${config.kit.embedded},
|
||||
env_public_prefix: '${config.kit.env.publicPrefix}',
|
||||
env_private_prefix: '${config.kit.env.privatePrefix}',
|
||||
hooks: null, // added lazily, via \`get_hooks\`
|
||||
preload_strategy: ${s(config.kit.output.preloadStrategy)},
|
||||
root,
|
||||
service_worker: ${has_service_worker},
|
||||
templates: {
|
||||
app: ({ head, body, assets, nonce, env }) => ${s(template)
|
||||
.replace('%sveltekit.head%', '" + head + "')
|
||||
.replace('%sveltekit.body%', '" + body + "')
|
||||
.replace(/%sveltekit\.assets%/g, '" + assets + "')
|
||||
.replace(/%sveltekit\.nonce%/g, '" + nonce + "')
|
||||
.replace(
|
||||
/%sveltekit\.env\.([^%]+)%/g,
|
||||
(_match, capture) => `" + (env[${s(capture)}] ?? "") + "`
|
||||
)},
|
||||
error: ({ status, message }) => ${s(error_page)
|
||||
.replace(/%sveltekit\.status%/g, '" + status + "')
|
||||
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
|
||||
},
|
||||
version_hash: ${s(hash(config.kit.version.name))}
|
||||
};
|
||||
|
||||
export function get_hooks() {
|
||||
return ${hooks ? `import(${s(hooks)})` : '{}'};
|
||||
}
|
||||
|
||||
export { set_assets, set_building, set_private_env, set_public_env };
|
||||
`;
|
||||
|
||||
// TODO need to re-run this whenever src/app.html or src/error.html are
|
||||
// created or changed, or src/service-worker.js is created or deleted.
|
||||
// Also, need to check that updating hooks.server.js works
|
||||
|
||||
/**
|
||||
* Write server configuration to disk
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {string} output
|
||||
*/
|
||||
export function write_server(config, output) {
|
||||
const hooks_file = resolve_entry(config.kit.files.hooks.server);
|
||||
|
||||
const typo = resolve_entry('src/+hooks.server');
|
||||
if (typo) {
|
||||
console.log(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(
|
||||
`Unexpected + prefix. Did you mean ${typo.split('/').at(-1)?.slice(1)}?` +
|
||||
` at ${path.resolve(typo)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {string} file */
|
||||
function relative(file) {
|
||||
return posixify(path.relative(`${output}/server`, file));
|
||||
}
|
||||
|
||||
write_if_changed(
|
||||
`${output}/server/internal.js`,
|
||||
server_template({
|
||||
config,
|
||||
hooks: hooks_file ? relative(hooks_file) : null,
|
||||
has_service_worker:
|
||||
config.kit.serviceWorker.register && !!resolve_entry(config.kit.files.serviceWorker),
|
||||
runtime_directory: relative(runtime_directory),
|
||||
template: load_template(process.cwd(), config),
|
||||
error_page: load_error_page(config)
|
||||
})
|
||||
);
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import colors from 'kleur';
|
||||
import { posixify } from '../../utils/filesystem.js';
|
||||
import { write_if_changed } from './utils.js';
|
||||
import { ts } from './ts.js';
|
||||
|
||||
/**
|
||||
* @param {string} cwd
|
||||
* @param {string} file
|
||||
*/
|
||||
function maybe_file(cwd, file) {
|
||||
const resolved = path.resolve(cwd, file);
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
function project_relative(file) {
|
||||
return posixify(path.relative('.', file));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
function remove_trailing_slashstar(file) {
|
||||
if (file.endsWith('/*')) {
|
||||
return file.slice(0, -2);
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the tsconfig that the user's tsconfig inherits from.
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
*/
|
||||
export function write_tsconfig(kit, cwd = process.cwd()) {
|
||||
const out = path.join(kit.outDir, 'tsconfig.json');
|
||||
|
||||
const user_config = load_user_tsconfig(cwd);
|
||||
if (user_config) validate_user_config(kit, cwd, out, user_config);
|
||||
|
||||
// only specify baseUrl if a) the user doesn't specify their own baseUrl
|
||||
// and b) they have non-relative paths. this causes problems with auto-imports,
|
||||
// so we print a suggestion that they use relative paths instead
|
||||
// TODO(v2): never include base URL, and skip the check below
|
||||
let include_base_url = false;
|
||||
|
||||
if (user_config && !user_config.options.compilerOptions?.baseUrl) {
|
||||
const non_relative_paths = new Set();
|
||||
for (const paths of Object.values(user_config?.options.compilerOptions?.paths || {})) {
|
||||
for (const path of paths) {
|
||||
if (!path.startsWith('.')) non_relative_paths.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (non_relative_paths.size) {
|
||||
include_base_url = true;
|
||||
|
||||
console.log(colors.bold().yellow('Please replace non-relative compilerOptions.paths:\n'));
|
||||
|
||||
for (const path of non_relative_paths) {
|
||||
console.log(` - "${path}" -> "./${path}"`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'\nDoing so allows us to omit "baseUrl" — which causes problems with imports — from the generated tsconfig.json. See https://github.com/sveltejs/kit/pull/8437 for more information.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
write_if_changed(out, JSON.stringify(get_tsconfig(kit, include_base_url), null, '\t'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the tsconfig that the user's tsconfig inherits from.
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {boolean} include_base_url
|
||||
*/
|
||||
export function get_tsconfig(kit, include_base_url) {
|
||||
/** @param {string} file */
|
||||
const config_relative = (file) => posixify(path.relative(kit.outDir, file));
|
||||
|
||||
const include = new Set([
|
||||
'ambient.d.ts',
|
||||
'./types/**/$types.d.ts',
|
||||
config_relative('vite.config.js'),
|
||||
config_relative('vite.config.ts')
|
||||
]);
|
||||
// TODO(v2): find a better way to include all src files. We can't just use routes/lib only because
|
||||
// people might have other folders/files in src that they want included.
|
||||
const src_includes = [kit.files.routes, kit.files.lib, path.resolve('src')].filter((dir) => {
|
||||
const relative = path.relative(path.resolve('src'), dir);
|
||||
return !relative || relative.startsWith('..');
|
||||
});
|
||||
for (const dir of src_includes) {
|
||||
include.add(config_relative(`${dir}/**/*.js`));
|
||||
include.add(config_relative(`${dir}/**/*.ts`));
|
||||
include.add(config_relative(`${dir}/**/*.svelte`));
|
||||
}
|
||||
|
||||
// Test folder is a special case - we advocate putting tests in a top-level test folder
|
||||
// and it's not configurable (should we make it?)
|
||||
const test_folder = project_relative('tests');
|
||||
include.add(config_relative(`${test_folder}/**/*.js`));
|
||||
include.add(config_relative(`${test_folder}/**/*.ts`));
|
||||
include.add(config_relative(`${test_folder}/**/*.svelte`));
|
||||
|
||||
const exclude = [config_relative('node_modules/**'), './[!ambient.d.ts]**'];
|
||||
if (path.extname(kit.files.serviceWorker)) {
|
||||
exclude.push(config_relative(kit.files.serviceWorker));
|
||||
} else {
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}.js`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}.ts`));
|
||||
exclude.push(config_relative(`${kit.files.serviceWorker}.d.ts`));
|
||||
}
|
||||
|
||||
const config = {
|
||||
compilerOptions: {
|
||||
// generated options
|
||||
baseUrl: include_base_url ? config_relative('.') : undefined,
|
||||
paths: get_tsconfig_paths(kit, include_base_url),
|
||||
rootDirs: [config_relative('.'), './types'],
|
||||
|
||||
// essential options
|
||||
// svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
||||
// to enforce using \`import type\` instead of \`import\` for Types.
|
||||
importsNotUsedAsValues: 'error',
|
||||
// Vite compiles modules one at a time
|
||||
isolatedModules: true,
|
||||
// TypeScript doesn't know about import usages in the template because it only sees the
|
||||
// script of a Svelte file. Therefore preserve all value imports. Requires TS 4.5 or higher.
|
||||
preserveValueImports: true,
|
||||
|
||||
// This is required for svelte-package to work as expected
|
||||
// Can be overwritten
|
||||
lib: ['esnext', 'DOM', 'DOM.Iterable'],
|
||||
moduleResolution: 'node', // TODO change to "bundler" in SvelteKit v2
|
||||
module: 'esnext',
|
||||
noEmit: true, // prevent tsconfig error "overwriting input files" - Vite handles the build and ignores this
|
||||
target: 'esnext',
|
||||
|
||||
// TODO(v2): use the new flag verbatimModuleSyntax instead (requires support by Vite/Esbuild)
|
||||
ignoreDeprecations: ts && Number(ts.version.split('.')[0]) >= 5 ? '5.0' : undefined
|
||||
},
|
||||
include: [...include],
|
||||
exclude
|
||||
};
|
||||
|
||||
return kit.typescript.config(config) ?? config;
|
||||
}
|
||||
|
||||
/** @param {string} cwd */
|
||||
function load_user_tsconfig(cwd) {
|
||||
const file = maybe_file(cwd, 'tsconfig.json') || maybe_file(cwd, 'jsconfig.json');
|
||||
|
||||
if (!file) return;
|
||||
|
||||
// we have to eval the file, since it's not parseable as JSON (contains comments)
|
||||
const json = fs.readFileSync(file, 'utf-8');
|
||||
|
||||
return {
|
||||
kind: path.basename(file),
|
||||
options: (0, eval)(`(${json})`)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {string} cwd
|
||||
* @param {string} out
|
||||
* @param {{ kind: string, options: any }} config
|
||||
*/
|
||||
function validate_user_config(kit, cwd, out, config) {
|
||||
// we need to check that the user's tsconfig extends the framework config
|
||||
const extend = config.options.extends;
|
||||
const extends_framework_config =
|
||||
typeof extend === 'string'
|
||||
? path.resolve(cwd, extend) === out
|
||||
: Array.isArray(extend)
|
||||
? extend.some((e) => path.resolve(cwd, e) === out)
|
||||
: false;
|
||||
|
||||
const options = config.options.compilerOptions || {};
|
||||
|
||||
if (extends_framework_config) {
|
||||
const { paths: user_paths } = options;
|
||||
|
||||
if (user_paths && fs.existsSync(kit.files.lib)) {
|
||||
/** @type {string[]} */
|
||||
const lib = user_paths['$lib'] || [];
|
||||
/** @type {string[]} */
|
||||
const lib_ = user_paths['$lib/*'] || [];
|
||||
|
||||
// TODO(v2): check needs to be adjusted when we remove the base path
|
||||
const missing_lib_paths =
|
||||
!lib.some((relative) => path.resolve(cwd, relative) === kit.files.lib) ||
|
||||
!lib_.some((relative) => path.resolve(cwd, relative) === path.join(kit.files.lib, '/*'));
|
||||
|
||||
if (missing_lib_paths) {
|
||||
console.warn(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(`Your compilerOptions.paths in ${config.kind} should include the following:`)
|
||||
);
|
||||
let relative = posixify(path.relative('.', kit.files.lib));
|
||||
if (!relative.startsWith('.')) relative = `./${relative}`;
|
||||
console.warn(`{\n "$lib":["${relative}"],\n "$lib/*":["${relative}/*"]\n}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let relative = posixify(path.relative('.', out));
|
||||
if (!relative.startsWith('./')) relative = './' + relative;
|
||||
|
||||
console.warn(
|
||||
colors
|
||||
.bold()
|
||||
.yellow(`Your ${config.kind} should extend the configuration generated by SvelteKit:`)
|
||||
);
|
||||
console.warn(`{\n "extends": "${relative}"\n}`);
|
||||
}
|
||||
}
|
||||
|
||||
// <something><optional /*>
|
||||
const alias_regex = /^(.+?)(\/\*)?$/;
|
||||
// <path><optional /* or .fileending>
|
||||
const value_regex = /^(.*?)((\/\*)|(\.\w+))?$/;
|
||||
|
||||
/**
|
||||
* Generates tsconfig path aliases from kit's aliases.
|
||||
* Related to vite alias creation.
|
||||
*
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* @param {boolean} include_base_url
|
||||
*/
|
||||
function get_tsconfig_paths(config, include_base_url) {
|
||||
/** @param {string} file */
|
||||
const config_relative = (file) => posixify(path.relative(config.outDir, file));
|
||||
|
||||
const alias = { ...config.alias };
|
||||
if (fs.existsSync(project_relative(config.files.lib))) {
|
||||
alias['$lib'] = project_relative(config.files.lib);
|
||||
}
|
||||
|
||||
/** @type {Record<string, string[]>} */
|
||||
const paths = {};
|
||||
|
||||
for (const [key, value] of Object.entries(alias)) {
|
||||
const key_match = alias_regex.exec(key);
|
||||
if (!key_match) throw new Error(`Invalid alias key: ${key}`);
|
||||
|
||||
const value_match = value_regex.exec(value);
|
||||
if (!value_match) throw new Error(`Invalid alias value: ${value}`);
|
||||
|
||||
const rel_path = (include_base_url ? project_relative : config_relative)(
|
||||
remove_trailing_slashstar(value)
|
||||
);
|
||||
const slashstar = key_match[2];
|
||||
|
||||
if (slashstar) {
|
||||
paths[key] = [rel_path + '/*'];
|
||||
} else {
|
||||
paths[key] = [rel_path];
|
||||
const fileending = value_match[4];
|
||||
|
||||
if (!fileending && !(key + '/*' in alias)) {
|
||||
paths[key + '/*'] = [rel_path + '/*'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import MagicString from 'magic-string';
|
||||
import { posixify, rimraf, walk } from '../../../utils/filesystem.js';
|
||||
import { compact } from '../../../utils/array.js';
|
||||
import { ts } from '../ts.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* file_name: string;
|
||||
* modified: boolean;
|
||||
* code: string;
|
||||
* exports: any[];
|
||||
* } | null} Proxy
|
||||
*
|
||||
* @typedef {{
|
||||
* server: Proxy,
|
||||
* universal: Proxy
|
||||
* }} Proxies
|
||||
*
|
||||
* @typedef {Map<import('types').PageNode, {route: import('types').RouteData, proxies: Proxies}>} RoutesMap
|
||||
*/
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
/**
|
||||
* Creates types for the whole manifest
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
*/
|
||||
export async function write_all_types(config, manifest_data) {
|
||||
if (!ts) return;
|
||||
|
||||
const types_dir = `${config.kit.outDir}/types`;
|
||||
|
||||
// empty out files that no longer need to exist
|
||||
const routes_dir = posixify(path.relative('.', config.kit.files.routes)).replace(/\.\.\//g, '');
|
||||
const expected_directories = new Set(
|
||||
manifest_data.routes.map((route) => path.join(routes_dir, route.id))
|
||||
);
|
||||
|
||||
if (fs.existsSync(types_dir)) {
|
||||
for (const file of walk(types_dir)) {
|
||||
const dir = path.dirname(file);
|
||||
if (!expected_directories.has(dir)) {
|
||||
rimraf(path.join(types_dir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read/write meta data on each invocation, not once per node process,
|
||||
// it could be invoked by another process in the meantime.
|
||||
const meta_data_file = `${types_dir}/route_meta_data.json`;
|
||||
const has_meta_data = fs.existsSync(meta_data_file);
|
||||
const meta_data = has_meta_data
|
||||
? /** @type {Record<string, string[]>} */ (JSON.parse(fs.readFileSync(meta_data_file, 'utf-8')))
|
||||
: {};
|
||||
const routes_map = create_routes_map(manifest_data);
|
||||
// For each directory, write $types.d.ts
|
||||
for (const route of manifest_data.routes) {
|
||||
if (!route.leaf && !route.layout && !route.endpoint) continue; // nothing to do
|
||||
|
||||
const outdir = path.join(config.kit.outDir, 'types', routes_dir, route.id);
|
||||
|
||||
// check if the types are out of date
|
||||
/** @type {string[]} */
|
||||
const input_files = [];
|
||||
|
||||
/** @type {import('types').PageNode | null} */
|
||||
let node = route.leaf;
|
||||
while (node) {
|
||||
if (node.universal) input_files.push(node.universal);
|
||||
if (node.server) input_files.push(node.server);
|
||||
node = node.parent ?? null;
|
||||
}
|
||||
|
||||
/** @type {import('types').PageNode | null} */
|
||||
node = route.layout;
|
||||
while (node) {
|
||||
if (node.universal) input_files.push(node.universal);
|
||||
if (node.server) input_files.push(node.server);
|
||||
node = node.parent ?? null;
|
||||
}
|
||||
|
||||
if (route.endpoint) {
|
||||
input_files.push(route.endpoint.file);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(outdir, { recursive: true });
|
||||
} catch {}
|
||||
|
||||
const output_files = compact(
|
||||
fs.readdirSync(outdir).map((name) => {
|
||||
const stats = fs.statSync(path.join(outdir, name));
|
||||
if (stats.isDirectory()) return;
|
||||
return {
|
||||
name,
|
||||
updated: stats.mtimeMs
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const source_last_updated = Math.max(
|
||||
// ctimeMs includes move operations whereas mtimeMs does not
|
||||
...input_files.map((file) => fs.statSync(file).ctimeMs)
|
||||
);
|
||||
const types_last_updated = Math.max(...output_files.map((file) => file.updated));
|
||||
|
||||
const should_generate =
|
||||
// source files were generated more recently than the types
|
||||
source_last_updated > types_last_updated ||
|
||||
// no meta data file exists yet
|
||||
!has_meta_data ||
|
||||
// some file was deleted
|
||||
!meta_data[route.id]?.every((file) => input_files.includes(file));
|
||||
|
||||
if (should_generate) {
|
||||
// track which old files end up being surplus to requirements
|
||||
const to_delete = new Set(output_files.map((file) => file.name));
|
||||
update_types(config, routes_map, route, to_delete);
|
||||
meta_data[route.id] = input_files;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(meta_data_file, JSON.stringify(meta_data, null, '\t'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates types related to the given file. This should only be called
|
||||
* if the file in question was edited, not if it was created/deleted/moved.
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} file
|
||||
*/
|
||||
export async function write_types(config, manifest_data, file) {
|
||||
if (!ts) return;
|
||||
|
||||
if (!path.basename(file).startsWith('+')) {
|
||||
// Not a route file
|
||||
return;
|
||||
}
|
||||
|
||||
const id = '/' + posixify(path.relative(config.kit.files.routes, path.dirname(file)));
|
||||
|
||||
const route = manifest_data.routes.find((route) => route.id === id);
|
||||
if (!route) return;
|
||||
if (!route.leaf && !route.layout && !route.endpoint) return; // nothing to do
|
||||
|
||||
update_types(config, create_routes_map(manifest_data), route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all leafs into a leaf -> route map
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
*/
|
||||
function create_routes_map(manifest_data) {
|
||||
/** @type {RoutesMap} */
|
||||
const map = new Map();
|
||||
for (const route of manifest_data.routes) {
|
||||
if (route.leaf) {
|
||||
map.set(route.leaf, { route, proxies: { server: null, universal: null } });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update types for a specific route
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
* @param {RoutesMap} routes
|
||||
* @param {import('types').RouteData} route
|
||||
* @param {Set<string>} [to_delete]
|
||||
*/
|
||||
function update_types(config, routes, route, to_delete = new Set()) {
|
||||
const routes_dir = posixify(path.relative('.', config.kit.files.routes)).replace(/\.\.\//g, '');
|
||||
const outdir = path.join(config.kit.outDir, 'types', routes_dir, route.id);
|
||||
|
||||
// now generate new types
|
||||
const imports = ["import type * as Kit from '@sveltejs/kit';"];
|
||||
|
||||
/** @type {string[]} */
|
||||
const declarations = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const exports = [];
|
||||
|
||||
// add 'Expand' helper
|
||||
// Makes sure a type is "repackaged" and therefore more readable
|
||||
declarations.push('type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;');
|
||||
|
||||
// returns the predicate of a matcher's type guard - or string if there is no type guard
|
||||
declarations.push(
|
||||
// TS complains on infer U, which seems weird, therefore ts-ignore it
|
||||
[
|
||||
'// @ts-ignore',
|
||||
'type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;'
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
declarations.push(
|
||||
'type RouteParams = ' + generate_params_type(route.params, outdir, config) + ';'
|
||||
);
|
||||
|
||||
if (route.params.length > 0) {
|
||||
exports.push(
|
||||
'export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;'
|
||||
);
|
||||
}
|
||||
|
||||
declarations.push(`type RouteId = '${route.id}';`);
|
||||
|
||||
// These could also be placed in our public types, but it would bloat them unnecessarily and we may want to change these in the future
|
||||
if (route.layout || route.leaf) {
|
||||
declarations.push(
|
||||
// If T extends the empty object, void is also allowed as a return type
|
||||
'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];',
|
||||
|
||||
// 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>>',
|
||||
|
||||
// null & {} == null, we need to prevent that in some situations
|
||||
'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;',
|
||||
|
||||
// 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>;'
|
||||
);
|
||||
}
|
||||
|
||||
if (route.leaf) {
|
||||
let route_info = routes.get(route.leaf);
|
||||
if (!route_info) {
|
||||
// This should be defined, but belts and braces
|
||||
route_info = { route, proxies: { server: null, universal: null } };
|
||||
routes.set(route.leaf, route_info);
|
||||
}
|
||||
|
||||
const {
|
||||
declarations: d,
|
||||
exports: e,
|
||||
proxies
|
||||
} = process_node(route.leaf, outdir, true, route_info.proxies);
|
||||
|
||||
exports.push(...e);
|
||||
declarations.push(...d);
|
||||
|
||||
if (proxies.server) {
|
||||
route_info.proxies.server = proxies.server;
|
||||
if (proxies.server?.modified) to_delete.delete(proxies.server.file_name);
|
||||
}
|
||||
if (proxies.universal) {
|
||||
route_info.proxies.universal = proxies.universal;
|
||||
if (proxies.universal?.modified) to_delete.delete(proxies.universal.file_name);
|
||||
}
|
||||
|
||||
if (route.leaf.server) {
|
||||
exports.push(
|
||||
'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>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (route.layout) {
|
||||
let all_pages_have_load = true;
|
||||
/** @type {import('types').RouteParam[]} */
|
||||
const layout_params = [];
|
||||
const ids = ['RouteId'];
|
||||
|
||||
route.layout.child_pages?.forEach((page) => {
|
||||
const leaf = routes.get(page);
|
||||
if (leaf) {
|
||||
if (leaf.route.page) ids.push(`"${leaf.route.id}"`);
|
||||
|
||||
for (const param of leaf.route.params) {
|
||||
// skip if already added
|
||||
if (layout_params.some((p) => p.name === param.name)) continue;
|
||||
layout_params.push({ ...param, optional: true });
|
||||
}
|
||||
|
||||
ensureProxies(page, leaf.proxies);
|
||||
|
||||
if (
|
||||
// Be defensive - if a proxy doesn't exist (because it couldn't be created), assume a load function exists.
|
||||
// If we didn't and it's a false negative, the user could wrongfully get a type error on layouts.
|
||||
(leaf.proxies.server && !leaf.proxies.server.exports.includes('load')) ||
|
||||
(leaf.proxies.universal && !leaf.proxies.universal.exports.includes('load'))
|
||||
) {
|
||||
all_pages_have_load = false;
|
||||
}
|
||||
}
|
||||
if (!page.server && !page.universal) {
|
||||
all_pages_have_load = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (route.id === '/') {
|
||||
// root layout is used for fallback error page, where ID can be null
|
||||
ids.push('null');
|
||||
}
|
||||
|
||||
declarations.push(`type LayoutRouteId = ${ids.join(' | ')}`);
|
||||
|
||||
declarations.push(
|
||||
'type LayoutParams = RouteParams & ' + generate_params_type(layout_params, outdir, config)
|
||||
);
|
||||
|
||||
const {
|
||||
exports: e,
|
||||
declarations: d,
|
||||
proxies
|
||||
} = process_node(
|
||||
route.layout,
|
||||
outdir,
|
||||
false,
|
||||
{ server: null, universal: null },
|
||||
all_pages_have_load
|
||||
);
|
||||
|
||||
exports.push(...e);
|
||||
declarations.push(...d);
|
||||
|
||||
if (proxies.server?.modified) to_delete.delete(proxies.server.file_name);
|
||||
if (proxies.universal?.modified) to_delete.delete(proxies.universal.file_name);
|
||||
}
|
||||
|
||||
if (route.endpoint) {
|
||||
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>;');
|
||||
}
|
||||
|
||||
const output = [imports.join('\n'), declarations.join('\n'), exports.join('\n')]
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
fs.writeFileSync(`${outdir}/$types.d.ts`, output);
|
||||
to_delete.delete('$types.d.ts');
|
||||
|
||||
for (const file of to_delete) {
|
||||
fs.unlinkSync(path.join(outdir, file));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').PageNode} node
|
||||
* @param {string} outdir
|
||||
* @param {boolean} is_page
|
||||
* @param {Proxies} proxies
|
||||
* @param {boolean} [all_pages_have_load]
|
||||
*/
|
||||
function process_node(node, outdir, is_page, proxies, all_pages_have_load = true) {
|
||||
const params = `${is_page ? 'Route' : 'Layout'}Params`;
|
||||
const prefix = is_page ? 'Page' : 'Layout';
|
||||
|
||||
const route_id = is_page ? 'RouteId' : 'LayoutRouteId';
|
||||
|
||||
/** @type {string[]} */
|
||||
const declarations = [];
|
||||
/** @type {string[]} */
|
||||
const exports = [];
|
||||
|
||||
/** @type {string} */
|
||||
let server_data;
|
||||
/** @type {string} */
|
||||
let data;
|
||||
|
||||
ensureProxies(node, proxies);
|
||||
|
||||
if (node.server) {
|
||||
const basename = path.basename(node.server);
|
||||
const proxy = proxies.server;
|
||||
if (proxy?.modified) {
|
||||
fs.writeFileSync(`${outdir}/proxy${basename}`, proxy.code);
|
||||
}
|
||||
|
||||
server_data = get_data_type(node.server, 'null', proxy, true);
|
||||
|
||||
const parent_type = `${prefix}ServerParentData`;
|
||||
|
||||
declarations.push(`type ${parent_type} = ${get_parent_type(node, 'LayoutServerData')};`);
|
||||
|
||||
// +page.js load present -> server can return all-optional data
|
||||
const output_data_shape =
|
||||
node.universal || (!is_page && all_pages_have_load)
|
||||
? 'Partial<App.PageData> & Record<string, any> | void'
|
||||
: `OutputDataShape<${parent_type}>`;
|
||||
exports.push(
|
||||
`export type ${prefix}ServerLoad<OutputData extends ${output_data_shape} = ${output_data_shape}> = Kit.ServerLoad<${params}, ${parent_type}, OutputData, ${route_id}>;`
|
||||
);
|
||||
|
||||
exports.push(`export type ${prefix}ServerLoadEvent = Parameters<${prefix}ServerLoad>[0];`);
|
||||
|
||||
if (is_page) {
|
||||
let type = 'unknown';
|
||||
if (proxy && proxy.exports.includes('actions')) {
|
||||
// If the file wasn't tweaked, we can use the return type of the original file.
|
||||
// The advantage is that type updates are reflected without saving.
|
||||
const from = proxy.modified
|
||||
? `./proxy${replace_ext_with_js(basename)}`
|
||||
: path_to_original(outdir, node.server);
|
||||
|
||||
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 ActionsExport = typeof import('${from}').actions`,
|
||||
'export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>'
|
||||
);
|
||||
|
||||
type = 'Expand<Kit.AwaitedActions<ActionsExport>> | null';
|
||||
}
|
||||
exports.push(`export type ActionData = ${type};`);
|
||||
}
|
||||
} else {
|
||||
server_data = 'null';
|
||||
}
|
||||
exports.push(`export type ${prefix}ServerData = ${server_data};`);
|
||||
|
||||
const parent_type = `${prefix}ParentData`;
|
||||
declarations.push(`type ${parent_type} = ${get_parent_type(node, 'LayoutData')};`);
|
||||
|
||||
if (node.universal) {
|
||||
const proxy = proxies.universal;
|
||||
if (proxy?.modified) {
|
||||
fs.writeFileSync(`${outdir}/proxy${path.basename(node.universal)}`, proxy.code);
|
||||
}
|
||||
|
||||
const type = get_data_type(
|
||||
node.universal,
|
||||
`${parent_type} & EnsureDefined<${prefix}ServerData>`,
|
||||
proxy
|
||||
);
|
||||
|
||||
data = `Expand<Omit<${parent_type}, keyof ${type}> & OptionalUnion<EnsureDefined<${type}>>>`;
|
||||
|
||||
const output_data_shape =
|
||||
!is_page && all_pages_have_load
|
||||
? 'Partial<App.PageData> & Record<string, any> | void'
|
||||
: `OutputDataShape<${parent_type}>`;
|
||||
exports.push(
|
||||
`export type ${prefix}Load<OutputData extends ${output_data_shape} = ${output_data_shape}> = Kit.Load<${params}, ${prefix}ServerData, ${parent_type}, OutputData, ${route_id}>;`
|
||||
);
|
||||
|
||||
exports.push(`export type ${prefix}LoadEvent = Parameters<${prefix}Load>[0];`);
|
||||
} else if (server_data === 'null') {
|
||||
data = `Expand<${parent_type}>`;
|
||||
} else {
|
||||
data = `Expand<Omit<${parent_type}, keyof ${prefix}ServerData> & EnsureDefined<${prefix}ServerData>>`;
|
||||
}
|
||||
|
||||
exports.push(`export type ${prefix}Data = ${data};`);
|
||||
|
||||
return { declarations, exports, proxies };
|
||||
|
||||
/**
|
||||
* @param {string} file_path
|
||||
* @param {string} fallback
|
||||
* @param {Proxy} proxy
|
||||
* @param {boolean} expand
|
||||
*/
|
||||
function get_data_type(file_path, fallback, proxy, expand = false) {
|
||||
if (proxy) {
|
||||
if (proxy.exports.includes('load')) {
|
||||
// If the file wasn't tweaked, we can use the return type of the original file.
|
||||
// The advantage is that type updates are reflected without saving.
|
||||
const from = proxy.modified
|
||||
? `./proxy${replace_ext_with_js(path.basename(file_path))}`
|
||||
: path_to_original(outdir, file_path);
|
||||
const type = `Kit.AwaitedProperties<Awaited<ReturnType<typeof import('${from}').load>>>`;
|
||||
return expand ? `Expand<OptionalUnion<EnsureDefined<${type}>>>` : type;
|
||||
} else {
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function populates the proxies object, if necessary and not already done.
|
||||
* Proxies are used to tweak the code of a file before it's typechecked.
|
||||
* They are needed in two places - when generating the types for a page or layout.
|
||||
* To not do the same work twice, we generate the proxies once and pass them around.
|
||||
*
|
||||
* @param {import('types').PageNode} node
|
||||
* @param {Proxies} proxies
|
||||
*/
|
||||
function ensureProxies(node, proxies) {
|
||||
if (node.server && !proxies.server) {
|
||||
proxies.server = createProxy(node.server, true);
|
||||
}
|
||||
|
||||
if (node.universal && !proxies.universal) {
|
||||
proxies.universal = createProxy(node.universal, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file_path
|
||||
* @param {boolean} is_server
|
||||
* @returns {Proxy}
|
||||
*/
|
||||
function createProxy(file_path, is_server) {
|
||||
const proxy = tweak_types(fs.readFileSync(file_path, 'utf8'), is_server);
|
||||
if (proxy) {
|
||||
return {
|
||||
...proxy,
|
||||
file_name: `proxy${path.basename(file_path)}`
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent type string by recursively looking up the parent layout and accumulate them to one type.
|
||||
* @param {import('types').PageNode} node
|
||||
* @param {string} type
|
||||
*/
|
||||
function get_parent_type(node, type) {
|
||||
const parent_imports = [];
|
||||
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent) {
|
||||
const d = node.depth - parent.depth;
|
||||
// unshift because we need it the other way round for the import string
|
||||
parent_imports.unshift(
|
||||
`${d === 0 ? '' : `import('${'../'.repeat(d)}${'$types.js'}').`}${type}`
|
||||
);
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
let parent_str = `EnsureDefined<${parent_imports[0] || '{}'}>`;
|
||||
for (let i = 1; i < parent_imports.length; i++) {
|
||||
// Omit is necessary because a parent could have a property with the same key which would
|
||||
// cause a type conflict. At runtime the child overwrites the parent property in this case,
|
||||
// so reflect that in the type definition.
|
||||
// EnsureDefined is necessary because {something: string} & null becomes null.
|
||||
// Output types of server loads can be null but when passed in through the `parent` parameter they are the empty object instead.
|
||||
parent_str = `Omit<${parent_str}, keyof ${parent_imports[i]}> & EnsureDefined<${parent_imports[i]}>`;
|
||||
}
|
||||
return parent_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} outdir
|
||||
* @param {string} file_path
|
||||
*/
|
||||
function path_to_original(outdir, file_path) {
|
||||
return posixify(path.relative(outdir, path.join(cwd, replace_ext_with_js(file_path))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file_path
|
||||
*/
|
||||
function replace_ext_with_js(file_path) {
|
||||
// Another extension than `.js` (or nothing, but that fails with node16 moduleResolution)
|
||||
// will result in TS failing to lookup the file
|
||||
const ext = path.extname(file_path);
|
||||
return file_path.slice(0, -ext.length) + '.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').RouteParam[]} params
|
||||
* @param {string} outdir
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
function generate_params_type(params, outdir, config) {
|
||||
/** @param {string} matcher */
|
||||
const path_to_matcher = (matcher) =>
|
||||
posixify(path.relative(outdir, path.join(config.kit.files.params, matcher)));
|
||||
|
||||
return `{ ${params
|
||||
.map(
|
||||
(param) =>
|
||||
`${param.name}${param.optional ? '?' : ''}: ${
|
||||
param.matcher
|
||||
? `MatcherParam<typeof import('${path_to_matcher(param.matcher)}').match>`
|
||||
: 'string'
|
||||
}`
|
||||
)
|
||||
.join('; ')} }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {boolean} is_server
|
||||
* @returns {Omit<NonNullable<Proxy>, 'file_name'> | null}
|
||||
*/
|
||||
export function tweak_types(content, is_server) {
|
||||
const names = new Set(is_server ? ['load', 'actions'] : ['load']);
|
||||
|
||||
try {
|
||||
let modified = false;
|
||||
|
||||
const ast = ts.createSourceFile(
|
||||
'filename.ts',
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
|
||||
const code = new MagicString(content);
|
||||
|
||||
const exports = new Map();
|
||||
|
||||
ast.forEachChild((node) => {
|
||||
if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
node.exportClause &&
|
||||
ts.isNamedExports(node.exportClause)
|
||||
) {
|
||||
node.exportClause.elements.forEach((element) => {
|
||||
const exported = element.name;
|
||||
if (names.has(element.name.text)) {
|
||||
const local = element.propertyName || element.name;
|
||||
exports.set(exported.text, local.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
ts.canHaveModifiers(node) &&
|
||||
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
||||
) {
|
||||
if (ts.isFunctionDeclaration(node) && node.name?.text && names.has(node.name?.text)) {
|
||||
exports.set(node.name.text, node.name.text);
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(node)) {
|
||||
node.declarationList.declarations.forEach((declaration) => {
|
||||
if (ts.isIdentifier(declaration.name) && names.has(declaration.name.text)) {
|
||||
exports.set(declaration.name.text, declaration.name.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('typescript').Node} node
|
||||
* @param {import('typescript').Node} value
|
||||
*/
|
||||
function replace_jsdoc_type_tags(node, value) {
|
||||
let _modified = false;
|
||||
// @ts-ignore
|
||||
if (node.jsDoc) {
|
||||
// @ts-ignore
|
||||
for (const comment of node.jsDoc) {
|
||||
for (const tag of comment.tags ?? []) {
|
||||
if (ts.isJSDocTypeTag(tag)) {
|
||||
const is_fn =
|
||||
ts.isFunctionDeclaration(value) ||
|
||||
ts.isFunctionExpression(value) ||
|
||||
ts.isArrowFunction(value);
|
||||
|
||||
if (is_fn && value.parameters?.length > 0) {
|
||||
const name = ts.isIdentifier(value.parameters[0].name)
|
||||
? value.parameters[0].name.text
|
||||
: 'event';
|
||||
code.overwrite(tag.tagName.pos, tag.tagName.end, 'param');
|
||||
code.prependRight(tag.typeExpression.pos + 1, 'Parameters<');
|
||||
code.appendLeft(tag.typeExpression.end - 1, '>[0]');
|
||||
code.appendLeft(tag.typeExpression.end, ` ${name}`);
|
||||
} else {
|
||||
code.overwrite(tag.pos, tag.end, '');
|
||||
}
|
||||
_modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
modified = modified || _modified;
|
||||
return _modified;
|
||||
}
|
||||
|
||||
ast.forEachChild((node) => {
|
||||
if (ts.isFunctionDeclaration(node) && node.name?.text && node.name?.text === 'load') {
|
||||
// remove JSDoc comment above `export function load ...`
|
||||
replace_jsdoc_type_tags(node, node);
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(node)) {
|
||||
// remove JSDoc comment above `export const load = ...`
|
||||
if (
|
||||
ts.isIdentifier(node.declarationList.declarations[0].name) &&
|
||||
names.has(node.declarationList.declarations[0].name.text) &&
|
||||
node.declarationList.declarations[0].initializer
|
||||
) {
|
||||
replace_jsdoc_type_tags(node, node.declarationList.declarations[0].initializer);
|
||||
}
|
||||
|
||||
for (const declaration of node.declarationList.declarations) {
|
||||
if (
|
||||
ts.isIdentifier(declaration.name) &&
|
||||
declaration.name.text === 'load' &&
|
||||
declaration.initializer
|
||||
) {
|
||||
// edge case — remove JSDoc comment above individual export
|
||||
replace_jsdoc_type_tags(declaration, declaration.initializer);
|
||||
|
||||
// remove type from `export const load: Load ...`
|
||||
if (declaration.type) {
|
||||
let a = declaration.type.pos;
|
||||
const b = declaration.type.end;
|
||||
while (/\s/.test(content[a])) a += 1;
|
||||
|
||||
const type = content.slice(a, b);
|
||||
code.remove(declaration.name.end, declaration.type.end);
|
||||
|
||||
const rhs = declaration.initializer;
|
||||
|
||||
if (
|
||||
rhs &&
|
||||
(ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) &&
|
||||
rhs.parameters.length
|
||||
) {
|
||||
const arg = rhs.parameters[0];
|
||||
const add_parens = content[arg.pos - 1] !== '(';
|
||||
|
||||
if (add_parens) code.prependRight(arg.pos, '(');
|
||||
|
||||
if (arg && !arg.type) {
|
||||
code.appendLeft(
|
||||
arg.name.end,
|
||||
`: Parameters<${type}>[0]` + (add_parens ? ')' : '')
|
||||
);
|
||||
} else {
|
||||
// prevent "type X is imported but not used" (isn't silenced by @ts-nocheck) when svelte-check runs
|
||||
code.append(`;null as any as ${type};`);
|
||||
}
|
||||
} else {
|
||||
// prevent "type X is imported but not used" (isn't silenced by @ts-nocheck) when svelte-check runs
|
||||
code.append(`;null as any as ${type};`);
|
||||
}
|
||||
|
||||
modified = true;
|
||||
}
|
||||
} else if (
|
||||
is_server &&
|
||||
ts.isIdentifier(declaration.name) &&
|
||||
declaration.name?.text === 'actions' &&
|
||||
declaration.initializer
|
||||
) {
|
||||
// remove JSDoc comment from `export const actions = ..`
|
||||
const removed = replace_jsdoc_type_tags(node, declaration.initializer);
|
||||
// ... and move type to each individual action
|
||||
if (removed) {
|
||||
const rhs = declaration.initializer;
|
||||
if (ts.isObjectLiteralExpression(rhs)) {
|
||||
for (const prop of rhs.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const rhs = prop.initializer;
|
||||
const replaced = replace_jsdoc_type_tags(prop, rhs);
|
||||
if (
|
||||
!replaced &&
|
||||
rhs &&
|
||||
(ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) &&
|
||||
rhs.parameters?.[0]
|
||||
) {
|
||||
const name = ts.isIdentifier(rhs.parameters[0].name)
|
||||
? rhs.parameters[0].name.text
|
||||
: 'event';
|
||||
code.prependRight(
|
||||
rhs.pos,
|
||||
`/** @param {import('./$types').RequestEvent} ${name} */ `
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove type from `export const actions: Actions ...`
|
||||
if (declaration.type) {
|
||||
let a = declaration.type.pos;
|
||||
const b = declaration.type.end;
|
||||
while (/\s/.test(content[a])) a += 1;
|
||||
|
||||
const type = content.slice(a, b);
|
||||
code.remove(declaration.name.end, declaration.type.end);
|
||||
code.append(`;null as any as ${type};`);
|
||||
modified = true;
|
||||
|
||||
// ... and move type to each individual action
|
||||
const rhs = declaration.initializer;
|
||||
if (ts.isObjectLiteralExpression(rhs)) {
|
||||
for (const prop of rhs.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const rhs = prop.initializer;
|
||||
|
||||
if (
|
||||
rhs &&
|
||||
(ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) &&
|
||||
rhs.parameters.length
|
||||
) {
|
||||
const arg = rhs.parameters[0];
|
||||
const add_parens = content[arg.pos - 1] !== '(';
|
||||
|
||||
if (add_parens) code.prependRight(arg.pos, '(');
|
||||
|
||||
if (arg && !arg.type) {
|
||||
code.appendLeft(
|
||||
arg.name.end,
|
||||
": import('./$types').RequestEvent" + (add_parens ? ')' : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
// Ignore all type errors so they don't show up twice when svelte-check runs
|
||||
// Account for possible @ts-check which would overwrite @ts-nocheck
|
||||
if (code.original.startsWith('// @ts-check')) {
|
||||
code.prependLeft('// @ts-check'.length, '\n// @ts-nocheck\n');
|
||||
} else {
|
||||
code.prepend('// @ts-nocheck\n');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
modified,
|
||||
code: code.toString(),
|
||||
exports: Array.from(exports.keys())
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import colors from 'kleur';
|
||||
import { posixify, to_fs } from '../utils/filesystem.js';
|
||||
|
||||
/**
|
||||
* Resolved path of the `runtime` directory
|
||||
*
|
||||
* TODO Windows issue:
|
||||
* Vite or sth else somehow sets the driver letter inconsistently to lower or upper case depending on the run environment.
|
||||
* In playwright debug mode run through VS Code this a root-to-lowercase conversion is needed in order for the tests to run.
|
||||
* If we do this conversion in other cases it has the opposite effect though and fails.
|
||||
*/
|
||||
export const runtime_directory = posixify(fileURLToPath(new URL('../runtime', import.meta.url)));
|
||||
|
||||
/**
|
||||
* This allows us to import SvelteKit internals that aren't exposed via `pkg.exports` in a
|
||||
* way that works whether `@sveltejs/kit` is installed inside the project's `node_modules`
|
||||
* or in a workspace root
|
||||
*/
|
||||
export const runtime_base = runtime_directory.startsWith(process.cwd())
|
||||
? `/${path.relative('.', runtime_directory)}`
|
||||
: to_fs(runtime_directory);
|
||||
|
||||
function noop() {}
|
||||
|
||||
/** @param {{ verbose: boolean }} opts */
|
||||
export function logger({ verbose }) {
|
||||
/** @type {import('types').Logger} */
|
||||
const log = (msg) => console.log(msg.replace(/^/gm, ' '));
|
||||
|
||||
/** @param {string} msg */
|
||||
const err = (msg) => console.error(msg.replace(/^/gm, ' '));
|
||||
|
||||
log.success = (msg) => log(colors.green(`✔ ${msg}`));
|
||||
log.error = (msg) => err(colors.bold().red(msg));
|
||||
log.warn = (msg) => log(colors.bold().yellow(msg));
|
||||
|
||||
log.minor = verbose ? (msg) => log(colors.grey(msg)) : noop;
|
||||
log.info = verbose ? log : noop;
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
/** @param {import('types').ManifestData} manifest_data */
|
||||
export function get_mime_lookup(manifest_data) {
|
||||
/** @type {Record<string, string>} */
|
||||
const mime = {};
|
||||
|
||||
manifest_data.assets.forEach((asset) => {
|
||||
if (asset.type) {
|
||||
const ext = path.extname(asset.file);
|
||||
mime[ext] = asset.type;
|
||||
}
|
||||
});
|
||||
|
||||
return mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {(file: string) => boolean} [filter]
|
||||
*/
|
||||
export function list_files(dir, filter) {
|
||||
/** @type {string[]} */
|
||||
const files = [];
|
||||
|
||||
/** @param {string} current */
|
||||
function walk(current) {
|
||||
for (const file of fs.readdirSync(path.resolve(dir, current))) {
|
||||
const child = path.posix.join(current, file);
|
||||
if (fs.statSync(path.resolve(dir, child)).isDirectory()) {
|
||||
walk(child);
|
||||
} else {
|
||||
if (!filter || filter(child)) {
|
||||
files.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(dir)) walk('');
|
||||
|
||||
return files;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { sequence } from './sequence.js';
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
export function sequence(...handlers) {
|
||||
const length = handlers.length;
|
||||
if (!length) return ({ event, resolve }) => resolve(event);
|
||||
|
||||
return ({ event, resolve }) => {
|
||||
return apply_handle(0, event, {});
|
||||
|
||||
/**
|
||||
* @param {number} i
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('@sveltejs/kit').ResolveOptions | undefined} parent_options
|
||||
* @returns {import('types').MaybePromise<Response>}
|
||||
*/
|
||||
function apply_handle(i, event, parent_options) {
|
||||
const handle = handlers[i];
|
||||
|
||||
return handle({
|
||||
event,
|
||||
resolve: (event, options) => {
|
||||
/** @type {import('@sveltejs/kit').ResolveOptions['transformPageChunk']} */
|
||||
const transformPageChunk = async ({ html, done }) => {
|
||||
if (options?.transformPageChunk) {
|
||||
html = (await options.transformPageChunk({ html, done })) ?? '';
|
||||
}
|
||||
|
||||
if (parent_options?.transformPageChunk) {
|
||||
html = (await parent_options.transformPageChunk({ html, done })) ?? '';
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
/** @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,
|
||||
filterSerializedResponseHeaders,
|
||||
preload
|
||||
})
|
||||
: resolve(event, { transformPageChunk, filterSerializedResponseHeaders, preload });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import { HttpError, Redirect, ActionFailure } from '../runtime/control.js';
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import { get_route_segments } from '../utils/routing.js';
|
||||
|
||||
export { VERSION } from '../version.js';
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {number} status
|
||||
* @param {App.Error} body
|
||||
* @return {HttpError}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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)) {
|
||||
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
|
||||
}
|
||||
|
||||
return new HttpError(status, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 | URL} location The location to redirect to.
|
||||
*/
|
||||
export function redirect(status, location) {
|
||||
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
|
||||
throw new Error('Invalid status code');
|
||||
}
|
||||
|
||||
return new Redirect(status, location.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// TODO deprecate this in favour of `Response.json` when it's
|
||||
// more widely supported
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
// we can't just do `text(JSON.stringify(data), init)` because
|
||||
// it will set a default `content-type` header. duplicated code
|
||||
// means less duplicated work
|
||||
const headers = new Headers(init?.headers);
|
||||
if (!headers.has('content-length')) {
|
||||
headers.set('content-length', encoder.encode(body).byteLength.toString());
|
||||
}
|
||||
|
||||
if (!headers.has('content-type')) {
|
||||
headers.set('content-type', 'application/json');
|
||||
}
|
||||
|
||||
return new Response(body, {
|
||||
...init,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const headers = new Headers(init?.headers);
|
||||
if (!headers.has('content-length')) {
|
||||
const encoded = encoder.encode(body);
|
||||
headers.set('content-length', encoded.byteLength.toString());
|
||||
return new Response(encoded, {
|
||||
...init,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(body, {
|
||||
...init,
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
export function fail(status, data) {
|
||||
return new ActionFailure(status, data);
|
||||
}
|
||||
|
||||
const basic_param_pattern = /\[(\[)?(\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
|
||||
|
||||
let warned = false;
|
||||
|
||||
/**
|
||||
* @deprecated Use `resolveRoute` from `$app/paths` instead.
|
||||
*
|
||||
* 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) {
|
||||
if (!warned) {
|
||||
console.warn('`resolvePath` is deprecated. Use `resolveRoute` from `$app/paths` instead.');
|
||||
warned = true;
|
||||
}
|
||||
|
||||
const segments = get_route_segments(id);
|
||||
return (
|
||||
'/' +
|
||||
segments
|
||||
.map((segment) =>
|
||||
segment.replace(basic_param_pattern, (_, optional, rest, name) => {
|
||||
const param_value = params[name];
|
||||
|
||||
// This is nested so TS correctly narrows the type
|
||||
if (!param_value) {
|
||||
if (optional) return '';
|
||||
if (rest && param_value !== undefined) 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('/')
|
||||
);
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import * as set_cookie_parser from 'set-cookie-parser';
|
||||
import { error } from '../index.js';
|
||||
|
||||
/**
|
||||
* @param {import('http').IncomingMessage} req
|
||||
* @param {number} [body_size_limit]
|
||||
*/
|
||||
function get_raw_body(req, body_size_limit) {
|
||||
const h = req.headers;
|
||||
|
||||
if (!h['content-type']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content_length = Number(h['content-length']);
|
||||
|
||||
// check if no request body
|
||||
if (
|
||||
(req.httpVersionMajor === 1 && isNaN(content_length) && h['transfer-encoding'] == null) ||
|
||||
content_length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let length = content_length;
|
||||
|
||||
if (body_size_limit) {
|
||||
if (!length) {
|
||||
length = body_size_limit;
|
||||
} else if (length > body_size_limit) {
|
||||
throw error(
|
||||
413,
|
||||
`Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (req.destroyed) {
|
||||
const readable = new ReadableStream();
|
||||
readable.cancel();
|
||||
return readable;
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
let cancelled = false;
|
||||
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
req.on('error', (error) => {
|
||||
cancelled = true;
|
||||
controller.error(error);
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
if (cancelled) return;
|
||||
controller.close();
|
||||
});
|
||||
|
||||
req.on('data', (chunk) => {
|
||||
if (cancelled) return;
|
||||
|
||||
size += chunk.length;
|
||||
if (size > length) {
|
||||
cancelled = true;
|
||||
controller.error(
|
||||
error(
|
||||
413,
|
||||
`request body size exceeded ${
|
||||
content_length ? "'content-length'" : 'BODY_SIZE_LIMIT'
|
||||
} of ${length}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(chunk);
|
||||
|
||||
if (controller.desiredSize === null || controller.desiredSize <= 0) {
|
||||
req.pause();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
pull() {
|
||||
req.resume();
|
||||
},
|
||||
|
||||
cancel(reason) {
|
||||
cancelled = true;
|
||||
req.destroy(reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* request: import('http').IncomingMessage;
|
||||
* base: string;
|
||||
* bodySizeLimit?: number;
|
||||
* }} options
|
||||
* @returns {Promise<Request>}
|
||||
*/
|
||||
export async function getRequest({ request, base, bodySizeLimit }) {
|
||||
return new Request(base + request.url, {
|
||||
// @ts-expect-error
|
||||
duplex: 'half',
|
||||
method: request.method,
|
||||
headers: /** @type {Record<string, string>} */ (request.headers),
|
||||
body: get_raw_body(request, bodySizeLimit)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('http').ServerResponse} res
|
||||
* @param {Response} response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function setResponse(res, response) {
|
||||
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);
|
||||
|
||||
if (!response.body) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body.locked) {
|
||||
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()')."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
||||
if (res.destroyed) {
|
||||
reader.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const cancel = (/** @type {Error|undefined} */ error) => {
|
||||
res.off('close', cancel);
|
||||
res.off('error', cancel);
|
||||
|
||||
// If the reader has already been interrupted with an error earlier,
|
||||
// then it will appear here, it is useless, but it needs to be catch.
|
||||
reader.cancel(error).catch(() => {});
|
||||
if (error) res.destroy(error);
|
||||
};
|
||||
|
||||
res.on('close', cancel);
|
||||
res.on('error', cancel);
|
||||
|
||||
next();
|
||||
async function next() {
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
if (!res.write(value)) {
|
||||
res.once('drain', next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
} catch (error) {
|
||||
cancel(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { ReadableStream, TransformStream, WritableStream } from 'node:stream/web';
|
||||
import buffer from 'node:buffer';
|
||||
import { webcrypto as crypto } from 'node:crypto';
|
||||
import { fetch, Response, Request, Headers, FormData, File as UndiciFile } from 'undici';
|
||||
|
||||
// `buffer.File` was added in Node 18.13.0 while the `File` global was added in Node 20.0.0
|
||||
const File = /** @type {import('node:buffer') & { File?: File}} */ (buffer).File ?? UndiciFile;
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
const globals_post_node_18_11 = {
|
||||
crypto,
|
||||
File
|
||||
};
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
// TODO: remove this once we only support Node 18.11+ (the version multipart/form-data was added)
|
||||
const globals_pre_node_18_11 = {
|
||||
crypto,
|
||||
fetch,
|
||||
Response,
|
||||
Request,
|
||||
Headers,
|
||||
ReadableStream,
|
||||
TransformStream,
|
||||
WritableStream,
|
||||
FormData,
|
||||
File
|
||||
};
|
||||
|
||||
// exported for dev/preview and node environments
|
||||
/**
|
||||
* Make various web APIs available as globals:
|
||||
* - `crypto`
|
||||
* - `fetch` (only in node < 18.11)
|
||||
* - `Headers` (only in node < 18.11)
|
||||
* - `Request` (only in node < 18.11)
|
||||
* - `Response` (only in node < 18.11)
|
||||
*/
|
||||
export function installPolyfills() {
|
||||
// Be defensive (we don't know in which environments this is called) and always apply if something goes wrong
|
||||
let globals = globals_pre_node_18_11;
|
||||
try {
|
||||
const version = process.versions.node.split('.').map((n) => parseInt(n, 10));
|
||||
if ((version[0] === 18 && version[1] >= 11) || version[0] > 18) {
|
||||
globals = globals_post_node_18_11;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
for (const name in globals) {
|
||||
Object.defineProperty(globalThis, name, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: globals[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
+1301
File diff suppressed because it is too large
Load Diff
+111
@@ -0,0 +1,111 @@
|
||||
import fs from 'node:fs';
|
||||
import { mkdirp } from '../../../utils/filesystem.js';
|
||||
import { find_deps, resolve_symlinks } from './utils.js';
|
||||
import { s } from '../../../utils/misc.js';
|
||||
import { normalizePath } from 'vite';
|
||||
|
||||
/**
|
||||
* @param {string} out
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {import('vite').Manifest} server_manifest
|
||||
* @param {import('vite').Manifest | null} client_manifest
|
||||
* @param {import('vite').Rollup.OutputAsset[] | null} css
|
||||
*/
|
||||
export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css) {
|
||||
mkdirp(`${out}/server/nodes`);
|
||||
mkdirp(`${out}/server/stylesheets`);
|
||||
|
||||
const stylesheet_lookup = new Map();
|
||||
|
||||
if (css) {
|
||||
css.forEach((asset) => {
|
||||
if (asset.source.length < kit.inlineStyleThreshold) {
|
||||
const index = stylesheet_lookup.size;
|
||||
const file = `${out}/server/stylesheets/${index}.js`;
|
||||
|
||||
fs.writeFileSync(file, `// ${asset.fileName}\nexport default ${s(asset.source)};`);
|
||||
stylesheet_lookup.set(asset.fileName, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
manifest_data.nodes.forEach((node, i) => {
|
||||
/** @type {string[]} */
|
||||
const imports = [];
|
||||
|
||||
// String representation of
|
||||
/** @type {import('types').SSRNode} */
|
||||
/** @type {string[]} */
|
||||
const exports = [`export const index = ${i};`];
|
||||
|
||||
/** @type {string[]} */
|
||||
const imported = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const stylesheets = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const fonts = [];
|
||||
|
||||
if (node.component && client_manifest) {
|
||||
exports.push(
|
||||
'let component_cache;',
|
||||
`export const component = async () => component_cache ??= (await import('../${
|
||||
resolve_symlinks(server_manifest, node.component).chunk.file
|
||||
}')).default;`
|
||||
);
|
||||
}
|
||||
|
||||
if (node.universal) {
|
||||
imports.push(`import * as universal from '../${server_manifest[node.universal].file}';`);
|
||||
exports.push('export { universal };');
|
||||
exports.push(`export const universal_id = ${s(node.universal)};`);
|
||||
}
|
||||
|
||||
if (node.server) {
|
||||
imports.push(`import * as server from '../${server_manifest[node.server].file}';`);
|
||||
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(
|
||||
`export const imports = ${s(imported)};`,
|
||||
`export const stylesheets = ${s(stylesheets)};`,
|
||||
`export const fonts = ${s(fonts)};`
|
||||
);
|
||||
|
||||
/** @type {string[]} */
|
||||
const styles = [];
|
||||
|
||||
stylesheets.forEach((file) => {
|
||||
if (stylesheet_lookup.has(file)) {
|
||||
const index = stylesheet_lookup.get(file);
|
||||
const name = `stylesheet_${index}`;
|
||||
imports.push(`import ${name} from '../stylesheets/${index}.js';`);
|
||||
styles.push(`\t${s(file)}: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (styles.length > 0) {
|
||||
exports.push(`export const inline_styles = () => ({\n${styles.join(',\n')}\n});`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
`${out}/server/nodes/${i}.js`,
|
||||
`${imports.join('\n')}\n\n${exports.join('\n')}\n`
|
||||
);
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
import fs from 'node:fs';
|
||||
import * as vite from 'vite';
|
||||
import { dedent } from '../../../core/sync/utils.js';
|
||||
import { s } from '../../../utils/misc.js';
|
||||
import { get_config_aliases } from '../utils.js';
|
||||
|
||||
/**
|
||||
* @param {string} out
|
||||
* @param {import('types').ValidatedKitConfig} kit
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
* @param {import('types').ManifestData} manifest_data
|
||||
* @param {string} service_worker_entry_file
|
||||
* @param {import('types').Prerendered} prerendered
|
||||
* @param {import('vite').Manifest} client_manifest
|
||||
*/
|
||||
export async function build_service_worker(
|
||||
out,
|
||||
kit,
|
||||
vite_config,
|
||||
manifest_data,
|
||||
service_worker_entry_file,
|
||||
prerendered,
|
||||
client_manifest
|
||||
) {
|
||||
const build = new Set();
|
||||
for (const key in client_manifest) {
|
||||
const { file, css = [], assets = [] } = client_manifest[key];
|
||||
build.add(file);
|
||||
css.forEach((file) => build.add(file));
|
||||
assets.forEach((file) => build.add(file));
|
||||
}
|
||||
|
||||
const service_worker = `${kit.outDir}/generated/service-worker.js`;
|
||||
|
||||
// in a service worker, `location` is the location of the service worker itself,
|
||||
// which is guaranteed to be `<base>/service-worker.js`
|
||||
const base = "location.pathname.split('/').slice(0, -1).join('/')";
|
||||
|
||||
fs.writeFileSync(
|
||||
service_worker,
|
||||
dedent`
|
||||
export const base = /*@__PURE__*/ ${base};
|
||||
|
||||
export const build = [
|
||||
${Array.from(build)
|
||||
.map((file) => `base + ${s(`/${file}`)}`)
|
||||
.join(',\n')}
|
||||
];
|
||||
|
||||
export const files = [
|
||||
${manifest_data.assets
|
||||
.filter((asset) => kit.serviceWorker.files(asset.file))
|
||||
.map((asset) => `base + ${s(`/${asset.file}`)}`)
|
||||
.join(',\n')}
|
||||
];
|
||||
|
||||
export const prerendered = [
|
||||
${prerendered.paths.map((path) => `base + ${s(path.replace(kit.paths.base, ''))}`).join(',\n')}
|
||||
];
|
||||
|
||||
export const version = ${s(kit.version.name)};
|
||||
`
|
||||
);
|
||||
|
||||
await vite.build({
|
||||
build: {
|
||||
modulePreload: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
'service-worker': service_worker_entry_file
|
||||
},
|
||||
output: {
|
||||
entryFileNames: '[name].js',
|
||||
assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`,
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
},
|
||||
outDir: `${out}/client`,
|
||||
emptyOutDir: false
|
||||
},
|
||||
configFile: false,
|
||||
define: vite_config.define,
|
||||
publicDir: false,
|
||||
resolve: {
|
||||
alias: [...get_config_aliases(kit), { find: '$service-worker', replacement: service_worker }]
|
||||
},
|
||||
experimental: {
|
||||
renderBuiltUrl(filename) {
|
||||
return {
|
||||
runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname`
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { normalizePath } from 'vite';
|
||||
|
||||
/**
|
||||
* Adds transitive JS and CSS dependencies to the js and css inputs.
|
||||
* @param {import('vite').Manifest} manifest
|
||||
* @param {string} entry
|
||||
* @param {boolean} add_dynamic_css
|
||||
* @returns {import('types').AssetDependencies}
|
||||
*/
|
||||
export function find_deps(manifest, entry, add_dynamic_css) {
|
||||
/** @type {Set<string>} */
|
||||
const seen = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const imports = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const stylesheets = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const fonts = new Set();
|
||||
|
||||
/**
|
||||
* @param {string} current
|
||||
* @param {boolean} add_js
|
||||
*/
|
||||
function traverse(current, add_js) {
|
||||
if (seen.has(current)) return;
|
||||
seen.add(current);
|
||||
|
||||
const { chunk } = resolve_symlinks(manifest, current);
|
||||
|
||||
if (add_js) imports.add(chunk.file);
|
||||
|
||||
if (chunk.assets) {
|
||||
for (const asset of chunk.assets) {
|
||||
if (/\.(woff2?|ttf|otf)$/.test(asset)) {
|
||||
fonts.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.css) {
|
||||
chunk.css.forEach((file) => stylesheets.add(file));
|
||||
}
|
||||
|
||||
if (chunk.imports) {
|
||||
chunk.imports.forEach((file) => traverse(file, add_js));
|
||||
}
|
||||
|
||||
if (add_dynamic_css && chunk.dynamicImports) {
|
||||
chunk.dynamicImports.forEach((file) => traverse(file, false));
|
||||
}
|
||||
}
|
||||
|
||||
const { chunk, file } = resolve_symlinks(manifest, entry);
|
||||
|
||||
traverse(file, true);
|
||||
|
||||
return {
|
||||
file: chunk.file,
|
||||
imports: Array.from(imports),
|
||||
stylesheets: Array.from(stylesheets),
|
||||
fonts: Array.from(fonts)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vite').Manifest} manifest
|
||||
* @param {string} file
|
||||
*/
|
||||
export function resolve_symlinks(manifest, file) {
|
||||
while (!manifest[file]) {
|
||||
const next = normalizePath(path.relative('.', fs.realpathSync(file)));
|
||||
if (next === file) throw new Error(`Could not find file "${file}" in Vite manifest`);
|
||||
file = next;
|
||||
}
|
||||
|
||||
const chunk = manifest[file];
|
||||
|
||||
return { chunk, file };
|
||||
}
|
||||
|
||||
const method_names = new Set(['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS']);
|
||||
|
||||
// If we'd written this in TypeScript, it could be easy...
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {str is import('types').HttpMethod}
|
||||
*/
|
||||
export function is_http_method(str) {
|
||||
return method_names.has(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* @returns {string}
|
||||
*/
|
||||
export function assets_base(config) {
|
||||
return (config.paths.assets || config.paths.base || '.') + '/';
|
||||
}
|
||||
+611
@@ -0,0 +1,611 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { URL } from 'node:url';
|
||||
import colors from 'kleur';
|
||||
import sirv from 'sirv';
|
||||
import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite';
|
||||
import { getRequest, setResponse } from '../../../exports/node/index.js';
|
||||
import { installPolyfills } from '../../../exports/node/polyfills.js';
|
||||
import { coalesce_to_error } from '../../../utils/error.js';
|
||||
import { posixify, resolve_entry, to_fs } from '../../../utils/filesystem.js';
|
||||
import { should_polyfill } from '../../../utils/platform.js';
|
||||
import { load_error_page } from '../../../core/config/index.js';
|
||||
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
|
||||
import * as sync from '../../../core/sync/sync.js';
|
||||
import { get_mime_lookup, runtime_base } from '../../../core/utils.js';
|
||||
import { compact } from '../../../utils/array.js';
|
||||
import { not_found } from '../utils.js';
|
||||
import { SCHEME } from '../../../utils/url.js';
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
/**
|
||||
* @param {import('vite').ViteDevServer} vite
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
* @param {import('types').ValidatedConfig} svelte_config
|
||||
* @return {Promise<Promise<() => void>>}
|
||||
*/
|
||||
export async function dev(vite, vite_config, svelte_config) {
|
||||
if (should_polyfill) {
|
||||
installPolyfills();
|
||||
}
|
||||
|
||||
const fetch = globalThis.fetch;
|
||||
globalThis.fetch = (info, init) => {
|
||||
if (typeof info === 'string' && !SCHEME.test(info)) {
|
||||
throw new Error(
|
||||
`Cannot use relative URL (${info}) with global fetch — use \`event.fetch\` instead: https://kit.svelte.dev/docs/web-standards#fetch-apis`
|
||||
);
|
||||
}
|
||||
|
||||
return fetch(info, init);
|
||||
};
|
||||
|
||||
sync.init(svelte_config, vite_config.mode);
|
||||
|
||||
/** @type {import('types').ManifestData} */
|
||||
let manifest_data;
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
let manifest;
|
||||
|
||||
/** @type {Error | null} */
|
||||
let manifest_error = null;
|
||||
|
||||
/** @param {string} url */
|
||||
async function loud_ssr_load_module(url) {
|
||||
try {
|
||||
return await vite.ssrLoadModule(url, { fixStacktrace: true });
|
||||
} catch (/** @type {any} */ err) {
|
||||
const msg = buildErrorMessage(err, [colors.red(`Internal server error: ${err.message}`)]);
|
||||
|
||||
if (!vite.config.logger.hasErrorLogged(err)) {
|
||||
vite.config.logger.error(msg, { error: err });
|
||||
}
|
||||
|
||||
vite.ws.send({
|
||||
type: 'error',
|
||||
err: {
|
||||
...err,
|
||||
// these properties are non-enumerable and will
|
||||
// not be serialized unless we explicitly include them
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
}
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
async function resolve(id) {
|
||||
const url = id.startsWith('..') ? `/@fs${path.posix.resolve(id)}` : `/${id}`;
|
||||
|
||||
const module = await loud_ssr_load_module(url);
|
||||
|
||||
const module_node = await vite.moduleGraph.getModuleByUrl(url);
|
||||
if (!module_node) throw new Error(`Could not find node for ${url}`);
|
||||
|
||||
return { module, module_node, url };
|
||||
}
|
||||
|
||||
async function update_manifest() {
|
||||
try {
|
||||
({ manifest_data } = await sync.create(svelte_config));
|
||||
|
||||
if (manifest_error) {
|
||||
manifest_error = null;
|
||||
vite.ws.send({ type: 'full-reload' });
|
||||
}
|
||||
} catch (error) {
|
||||
manifest_error = /** @type {Error} */ (error);
|
||||
|
||||
console.error(colors.bold().red(manifest_error.message));
|
||||
vite.ws.send({
|
||||
type: 'error',
|
||||
err: {
|
||||
message: manifest_error.message ?? 'Invalid routes',
|
||||
stack: ''
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
manifest = {
|
||||
appDir: svelte_config.kit.appDir,
|
||||
appPath: svelte_config.kit.appDir,
|
||||
assets: new Set(manifest_data.assets.map((asset) => asset.file)),
|
||||
mimeTypes: get_mime_lookup(manifest_data),
|
||||
_: {
|
||||
client: {
|
||||
start: `${runtime_base}/client/start.js`,
|
||||
app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`,
|
||||
imports: [],
|
||||
stylesheets: [],
|
||||
fonts: []
|
||||
},
|
||||
nodes: manifest_data.nodes.map((node, index) => {
|
||||
return async () => {
|
||||
/** @type {import('types').SSRNode} */
|
||||
const result = {};
|
||||
|
||||
/** @type {import('vite').ModuleNode[]} */
|
||||
const module_nodes = [];
|
||||
|
||||
result.index = index;
|
||||
|
||||
// these are unused in dev, it's easier to include them
|
||||
result.imports = [];
|
||||
result.stylesheets = [];
|
||||
result.fonts = [];
|
||||
|
||||
if (node.component) {
|
||||
result.component = async () => {
|
||||
const { module_node, module } = await resolve(
|
||||
/** @type {string} */ (node.component)
|
||||
);
|
||||
|
||||
module_nodes.push(module_node);
|
||||
|
||||
return module.default;
|
||||
};
|
||||
}
|
||||
|
||||
if (node.universal) {
|
||||
const { module, module_node } = await resolve(node.universal);
|
||||
|
||||
module_nodes.push(module_node);
|
||||
|
||||
result.universal = module;
|
||||
result.universal_id = node.universal;
|
||||
}
|
||||
|
||||
if (node.server) {
|
||||
const { module } = await resolve(node.server);
|
||||
result.server = module;
|
||||
result.server_id = node.server;
|
||||
}
|
||||
|
||||
// in dev we inline all styles to avoid FOUC. this gets populated lazily so that
|
||||
// components/stylesheets loaded via import() during `load` are included
|
||||
result.inline_styles = async () => {
|
||||
const deps = new Set();
|
||||
|
||||
for (const module_node of module_nodes) {
|
||||
await find_deps(vite, module_node, deps);
|
||||
}
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const styles = {};
|
||||
|
||||
for (const dep of deps) {
|
||||
const url = new URL(dep.url, 'dummy:/');
|
||||
const query = url.searchParams;
|
||||
|
||||
if (
|
||||
(isCSSRequest(dep.file) ||
|
||||
(query.has('svelte') && query.get('type') === 'style')) &&
|
||||
!(query.has('raw') || query.has('url') || query.has('inline'))
|
||||
) {
|
||||
try {
|
||||
query.set('inline', '');
|
||||
const mod = await vite.ssrLoadModule(
|
||||
`${decodeURI(url.pathname)}${url.search}${url.hash}`
|
||||
);
|
||||
styles[dep.url] = mod.default;
|
||||
} catch {
|
||||
// this can happen with dynamically imported modules, I think
|
||||
// because the Vite module graph doesn't distinguish between
|
||||
// static and dynamic imports? TODO investigate, submit fix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
}),
|
||||
routes: compact(
|
||||
manifest_data.routes.map((route) => {
|
||||
if (!route.page && !route.endpoint) return null;
|
||||
|
||||
const endpoint = route.endpoint;
|
||||
|
||||
return {
|
||||
id: route.id,
|
||||
pattern: route.pattern,
|
||||
params: route.params,
|
||||
page: route.page,
|
||||
endpoint: endpoint
|
||||
? async () => {
|
||||
const url = path.resolve(cwd, endpoint.file);
|
||||
return await loud_ssr_load_module(url);
|
||||
}
|
||||
: null,
|
||||
endpoint_id: endpoint?.file
|
||||
};
|
||||
})
|
||||
),
|
||||
matchers: async () => {
|
||||
/** @type {Record<string, import('@sveltejs/kit').ParamMatcher>} */
|
||||
const matchers = {};
|
||||
|
||||
for (const key in manifest_data.matchers) {
|
||||
const file = manifest_data.matchers[key];
|
||||
const url = path.resolve(cwd, file);
|
||||
const module = await vite.ssrLoadModule(url, { fixStacktrace: true });
|
||||
|
||||
if (module.match) {
|
||||
matchers[key] = module.match;
|
||||
} else {
|
||||
throw new Error(`${file} does not export a \`match\` function`);
|
||||
}
|
||||
}
|
||||
|
||||
return matchers;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {Error} error */
|
||||
function fix_stack_trace(error) {
|
||||
vite.ssrFixStacktrace(error);
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
await update_manifest();
|
||||
|
||||
/**
|
||||
* @param {string} event
|
||||
* @param {(file: string) => void} cb
|
||||
*/
|
||||
const watch = (event, cb) => {
|
||||
vite.watcher.on(event, (file) => {
|
||||
if (
|
||||
file.startsWith(svelte_config.kit.files.routes + path.sep) ||
|
||||
file.startsWith(svelte_config.kit.files.params + path.sep) ||
|
||||
// in contrast to server hooks, client hooks are written to the client manifest
|
||||
// and therefore need rebuilding when they are added/removed
|
||||
file.startsWith(svelte_config.kit.files.hooks.client)
|
||||
) {
|
||||
cb(file);
|
||||
}
|
||||
});
|
||||
};
|
||||
/** @type {NodeJS.Timeout | null } */
|
||||
let timeout = null;
|
||||
/** @param {() => void} to_run */
|
||||
const debounce = (to_run) => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
to_run();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// flag to skip watchers if server is already restarting
|
||||
let restarting = false;
|
||||
|
||||
// Debounce add/unlink events because in case of folder deletion or moves
|
||||
// they fire in rapid succession, causing needless invocations.
|
||||
watch('add', () => debounce(update_manifest));
|
||||
watch('unlink', () => debounce(update_manifest));
|
||||
watch('change', (file) => {
|
||||
// Don't run for a single file if the whole manifest is about to get updated
|
||||
if (timeout || restarting) return;
|
||||
|
||||
sync.update(svelte_config, manifest_data, file);
|
||||
});
|
||||
|
||||
const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files;
|
||||
|
||||
// vite client only executes a full reload if the triggering html file path is index.html
|
||||
// kit defaults to src/app.html, so unless user changed that to index.html
|
||||
// send the vite client a full-reload event without path being set
|
||||
if (appTemplate !== 'index.html') {
|
||||
vite.watcher.on('change', (file) => {
|
||||
if (file === appTemplate && !restarting) {
|
||||
vite.ws.send({ type: 'full-reload' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
vite.watcher.on('all', (_, file) => {
|
||||
if (
|
||||
file === appTemplate ||
|
||||
file === errorTemplate ||
|
||||
file.startsWith(serviceWorker) ||
|
||||
file.startsWith(hooks.server)
|
||||
) {
|
||||
sync.server(svelte_config);
|
||||
}
|
||||
});
|
||||
|
||||
// changing the svelte config requires restarting the dev server
|
||||
// the config is only read on start and passed on to vite-plugin-svelte
|
||||
// which needs up-to-date values to operate correctly
|
||||
vite.watcher.on('change', (file) => {
|
||||
if (path.basename(file) === 'svelte.config.js') {
|
||||
console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`);
|
||||
restarting = true;
|
||||
vite.restart();
|
||||
}
|
||||
});
|
||||
|
||||
const assets = svelte_config.kit.paths.assets ? SVELTE_KIT_ASSETS : svelte_config.kit.paths.base;
|
||||
const asset_server = sirv(svelte_config.kit.files.assets, {
|
||||
dev: true,
|
||||
etag: true,
|
||||
maxAge: 0,
|
||||
extensions: [],
|
||||
setHeaders: (res) => {
|
||||
res.setHeader('access-control-allow-origin', '*');
|
||||
}
|
||||
});
|
||||
|
||||
async function align_exports() {
|
||||
// This shameful hack allows us to load runtime server code via Vite
|
||||
// while apps load `HttpError` and `Redirect` in Node, without
|
||||
// causing `instanceof` checks to fail
|
||||
const control_module_node = await import('../../../runtime/control.js');
|
||||
const control_module_vite = await vite.ssrLoadModule(`${runtime_base}/control.js`);
|
||||
|
||||
control_module_node.replace_implementations({
|
||||
ActionFailure: control_module_vite.ActionFailure,
|
||||
HttpError: control_module_vite.HttpError,
|
||||
Redirect: control_module_vite.Redirect
|
||||
});
|
||||
}
|
||||
align_exports();
|
||||
const ws_send = vite.ws.send;
|
||||
/** @param {any} args */
|
||||
vite.ws.send = function (...args) {
|
||||
// We need to reapply the patch after Vite did dependency optimizations
|
||||
// because that clears the module resolutions
|
||||
if (args[0]?.type === 'full-reload' && args[0].path === '*') {
|
||||
align_exports();
|
||||
}
|
||||
return ws_send.apply(vite.ws, args);
|
||||
};
|
||||
|
||||
vite.middlewares.use(async (req, res, next) => {
|
||||
try {
|
||||
const base = `${vite.config.server.https ? 'https' : 'http'}://${
|
||||
req.headers[':authority'] || req.headers.host
|
||||
}`;
|
||||
|
||||
const decoded = decodeURI(new URL(base + req.url).pathname);
|
||||
|
||||
if (decoded.startsWith(assets)) {
|
||||
const pathname = decoded.slice(assets.length);
|
||||
const file = svelte_config.kit.files.assets + pathname;
|
||||
|
||||
if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) {
|
||||
if (has_correct_case(file, svelte_config.kit.files.assets)) {
|
||||
req.url = encodeURI(pathname); // don't need query/hash
|
||||
asset_server(req, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (e) {
|
||||
const error = coalesce_to_error(e);
|
||||
res.statusCode = 500;
|
||||
res.end(fix_stack_trace(error));
|
||||
}
|
||||
});
|
||||
|
||||
const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, '');
|
||||
|
||||
return () => {
|
||||
const serve_static_middleware = vite.middlewares.stack.find(
|
||||
(middleware) =>
|
||||
/** @type {function} */ (middleware.handle).name === 'viteServeStaticMiddleware'
|
||||
);
|
||||
|
||||
// Vite will give a 403 on URLs like /test, /static, and /package.json preventing us from
|
||||
// serving routes with those names. See https://github.com/vitejs/vite/issues/7363
|
||||
remove_static_middlewares(vite.middlewares);
|
||||
|
||||
vite.middlewares.use(async (req, res) => {
|
||||
// Vite's base middleware strips out the base path. Restore it
|
||||
const original_url = req.url;
|
||||
req.url = req.originalUrl;
|
||||
try {
|
||||
const base = `${vite.config.server.https ? 'https' : 'http'}://${
|
||||
req.headers[':authority'] || req.headers.host
|
||||
}`;
|
||||
|
||||
const decoded = decodeURI(new URL(base + req.url).pathname);
|
||||
const file = posixify(path.resolve(decoded.slice(svelte_config.kit.paths.base.length + 1)));
|
||||
const is_file = fs.existsSync(file) && !fs.statSync(file).isDirectory();
|
||||
const allowed =
|
||||
!vite_config.server.fs.strict ||
|
||||
vite_config.server.fs.allow.some((dir) => file.startsWith(dir));
|
||||
|
||||
if (is_file && allowed) {
|
||||
req.url = original_url;
|
||||
// @ts-expect-error
|
||||
serve_static_middleware.handle(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decoded.startsWith(svelte_config.kit.paths.base)) {
|
||||
return not_found(req, res, svelte_config.kit.paths.base);
|
||||
}
|
||||
|
||||
if (decoded === svelte_config.kit.paths.base + '/service-worker.js') {
|
||||
const resolved = resolve_entry(svelte_config.kit.files.serviceWorker);
|
||||
|
||||
if (resolved) {
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/javascript'
|
||||
});
|
||||
res.end(`import '${to_fs(resolved)}';`);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// we have to import `Server` before calling `set_assets`
|
||||
const { Server } = /** @type {import('types').ServerModule} */ (
|
||||
await vite.ssrLoadModule(`${runtime_base}/server/index.js`, { fixStacktrace: true })
|
||||
);
|
||||
|
||||
const { set_fix_stack_trace } = await vite.ssrLoadModule(
|
||||
`${runtime_base}/shared-server.js`
|
||||
);
|
||||
set_fix_stack_trace(fix_stack_trace);
|
||||
|
||||
const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths');
|
||||
set_assets(assets);
|
||||
|
||||
const server = new Server(manifest);
|
||||
|
||||
await server.init({ env });
|
||||
|
||||
let request;
|
||||
|
||||
try {
|
||||
request = await getRequest({
|
||||
base,
|
||||
request: req
|
||||
});
|
||||
} catch (/** @type {any} */ err) {
|
||||
res.statusCode = err.status || 400;
|
||||
return res.end('Invalid request body');
|
||||
}
|
||||
|
||||
if (manifest_error) {
|
||||
console.error(colors.bold().red(manifest_error.message));
|
||||
|
||||
const error_page = load_error_page(svelte_config);
|
||||
|
||||
/** @param {{ status: number; message: string }} opts */
|
||||
const error_template = ({ status, message }) => {
|
||||
return error_page
|
||||
.replace(/%sveltekit\.status%/g, String(status))
|
||||
.replace(/%sveltekit\.error\.message%/g, message);
|
||||
};
|
||||
|
||||
res.writeHead(500, {
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
});
|
||||
res.end(
|
||||
error_template({ status: 500, message: manifest_error.message ?? 'Invalid routes' })
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const rendered = await server.respond(request, {
|
||||
getClientAddress: () => {
|
||||
const { remoteAddress } = req.socket;
|
||||
if (remoteAddress) return remoteAddress;
|
||||
throw new Error('Could not determine clientAddress');
|
||||
},
|
||||
read: (file) => fs.readFileSync(path.join(svelte_config.kit.files.assets, file))
|
||||
});
|
||||
|
||||
if (rendered.status === 404) {
|
||||
// @ts-expect-error
|
||||
serve_static_middleware.handle(req, res, () => {
|
||||
setResponse(res, rendered);
|
||||
});
|
||||
} else {
|
||||
setResponse(res, rendered);
|
||||
}
|
||||
} catch (e) {
|
||||
const error = coalesce_to_error(e);
|
||||
res.statusCode = 500;
|
||||
res.end(fix_stack_trace(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('connect').Server} server
|
||||
*/
|
||||
function remove_static_middlewares(server) {
|
||||
const static_middlewares = ['viteServeStaticMiddleware'];
|
||||
for (let i = server.stack.length - 1; i > 0; i--) {
|
||||
// @ts-expect-error using internals
|
||||
if (static_middlewares.includes(server.stack[i].handle.name)) {
|
||||
server.stack.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('vite').ViteDevServer} vite
|
||||
* @param {import('vite').ModuleNode} node
|
||||
* @param {Set<import('vite').ModuleNode>} deps
|
||||
*/
|
||||
async function find_deps(vite, node, deps) {
|
||||
// since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous.
|
||||
// instead of using `await`, we resolve all branches in parallel.
|
||||
/** @type {Promise<void>[]} */
|
||||
const branches = [];
|
||||
|
||||
/** @param {import('vite').ModuleNode} node */
|
||||
async function add(node) {
|
||||
if (!deps.has(node)) {
|
||||
deps.add(node);
|
||||
await find_deps(vite, node, deps);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} url */
|
||||
async function add_by_url(url) {
|
||||
const node = await vite.moduleGraph.getModuleByUrl(url);
|
||||
|
||||
if (node) {
|
||||
await add(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.ssrTransformResult) {
|
||||
if (node.ssrTransformResult.deps) {
|
||||
node.ssrTransformResult.deps.forEach((url) => branches.push(add_by_url(url)));
|
||||
}
|
||||
|
||||
if (node.ssrTransformResult.dynamicDeps) {
|
||||
node.ssrTransformResult.dynamicDeps.forEach((url) => branches.push(add_by_url(url)));
|
||||
}
|
||||
} else {
|
||||
node.importedModules.forEach((node) => branches.push(add(node)));
|
||||
}
|
||||
|
||||
await Promise.all(branches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a file is being requested with the correct case,
|
||||
* to ensure consistent behaviour between dev and prod and across
|
||||
* operating systems. Note that we can't use realpath here,
|
||||
* because we don't want to follow symlinks
|
||||
* @param {string} file
|
||||
* @param {string} assets
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function has_correct_case(file, assets) {
|
||||
if (file === assets) return true;
|
||||
|
||||
const parent = path.dirname(file);
|
||||
|
||||
if (fs.readdirSync(parent).includes(path.basename(file))) {
|
||||
return has_correct_case(parent, assets);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import path from 'node:path';
|
||||
import { posixify } from '../../../utils/filesystem.js';
|
||||
import { strip_virtual_prefix } from '../utils.js';
|
||||
|
||||
const ILLEGAL_IMPORTS = new Set([
|
||||
'\0virtual:$env/dynamic/private',
|
||||
'\0virtual:$env/static/private'
|
||||
]);
|
||||
const ILLEGAL_MODULE_NAME_PATTERN = /.*\.server\..+/;
|
||||
|
||||
/**
|
||||
* Checks if given id imports a module that is not allowed to be imported into client-side code.
|
||||
* @param {string} id
|
||||
* @param {{
|
||||
* cwd: string;
|
||||
* node_modules: string;
|
||||
* server: string;
|
||||
* }} dirs
|
||||
*/
|
||||
export function is_illegal(id, dirs) {
|
||||
if (ILLEGAL_IMPORTS.has(id)) return true;
|
||||
if (!id.startsWith(dirs.cwd) || id.startsWith(dirs.node_modules)) return false;
|
||||
return ILLEGAL_MODULE_NAME_PATTERN.test(path.basename(id)) || id.startsWith(dirs.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a guard that checks that no id imports a module that is not allowed to be imported into client-side code.
|
||||
* @param {import('vite').Rollup.PluginContext} context
|
||||
* @param {{ cwd: string; lib: string }} paths
|
||||
*/
|
||||
export function module_guard(context, { cwd, lib }) {
|
||||
/** @type {Set<string>} */
|
||||
const seen = new Set();
|
||||
|
||||
const dirs = {
|
||||
// ids will be posixified, so we need to posixify these, too
|
||||
cwd: posixify(cwd),
|
||||
node_modules: posixify(path.join(cwd, 'node_modules')),
|
||||
server: posixify(path.join(lib, 'server'))
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {Array<{ id: string; dynamic: boolean }>} chain
|
||||
*/
|
||||
function follow(id, chain) {
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
|
||||
if (is_illegal(id, dirs)) {
|
||||
chain.shift(); // discard the entry point
|
||||
id = normalize_id(id, lib, cwd);
|
||||
|
||||
const pyramid =
|
||||
chain.map(({ id, dynamic }, i) => {
|
||||
id = normalize_id(id, lib, cwd);
|
||||
|
||||
return `${' '.repeat(i * 2)}- ${strip_virtual_prefix(id)} ${
|
||||
dynamic ? 'dynamically imports' : 'imports'
|
||||
}\n`;
|
||||
}) + `${' '.repeat(chain.length)}- ${strip_virtual_prefix(id)}`;
|
||||
|
||||
const message = `Cannot import ${strip_virtual_prefix(
|
||||
id
|
||||
)} into client-side code:\n${pyramid}`;
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const module = context.getModuleInfo(id);
|
||||
|
||||
if (module) {
|
||||
for (const child of module.importedIds) {
|
||||
follow(child, [...chain, { id, dynamic: false }]);
|
||||
}
|
||||
|
||||
for (const child of module.dynamicallyImportedIds) {
|
||||
follow(child, [...chain, { id, dynamic: true }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** @param {string} id should be posixified */
|
||||
check: (id) => {
|
||||
follow(id, []);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes cwd/lib path from the start of the id
|
||||
* @param {string} id
|
||||
* @param {string} lib
|
||||
* @param {string} cwd
|
||||
*/
|
||||
export function normalize_id(id, lib, cwd) {
|
||||
if (id.startsWith(lib)) {
|
||||
id = id.replace(lib, '$lib');
|
||||
}
|
||||
|
||||
if (id.startsWith(cwd)) {
|
||||
id = path.relative(cwd, id);
|
||||
}
|
||||
|
||||
return posixify(id);
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export interface ImportGraph {
|
||||
readonly id: string;
|
||||
readonly dynamic: boolean;
|
||||
readonly children: Generator<ImportGraph>;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
const query_pattern = /\?.*$/s;
|
||||
|
||||
/** @param {string} path */
|
||||
export function remove_query_from_id(path) {
|
||||
return path.replace(query_pattern, '');
|
||||
}
|
||||
+908
@@ -0,0 +1,908 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import colors from 'kleur';
|
||||
import * as vite from 'vite';
|
||||
|
||||
import { copy, mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js';
|
||||
import { create_static_module, create_dynamic_module } from '../../core/env.js';
|
||||
import * as sync from '../../core/sync/sync.js';
|
||||
import { create_assets } from '../../core/sync/create_manifest_data/index.js';
|
||||
import { runtime_directory, logger } from '../../core/utils.js';
|
||||
import { load_config } from '../../core/config/index.js';
|
||||
import { generate_manifest } from '../../core/generate_manifest/index.js';
|
||||
import { build_server_nodes } from './build/build_server.js';
|
||||
import { build_service_worker } from './build/build_service_worker.js';
|
||||
import { assets_base, find_deps } from './build/utils.js';
|
||||
import { dev } from './dev/index.js';
|
||||
import { is_illegal, module_guard, normalize_id } from './graph_analysis/index.js';
|
||||
import { preview } from './preview/index.js';
|
||||
import { get_config_aliases, get_env, strip_virtual_prefix } from './utils.js';
|
||||
import { write_client_manifest } from '../../core/sync/write_client_manifest.js';
|
||||
import prerender from '../../core/postbuild/prerender.js';
|
||||
import analyse from '../../core/postbuild/analyse.js';
|
||||
import { s } from '../../utils/misc.js';
|
||||
import { hash } from '../../runtime/hash.js';
|
||||
import { dedent, isSvelte5Plus } from '../../core/sync/utils.js';
|
||||
|
||||
export { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
/** @type {import('./types.js').EnforcedConfig} */
|
||||
const enforced_config = {
|
||||
appType: true,
|
||||
base: true,
|
||||
build: {
|
||||
cssCodeSplit: true,
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: true,
|
||||
name: true,
|
||||
formats: true
|
||||
},
|
||||
manifest: true,
|
||||
outDir: true,
|
||||
rollupOptions: {
|
||||
input: true,
|
||||
output: {
|
||||
format: true,
|
||||
entryFileNames: true,
|
||||
chunkFileNames: true,
|
||||
assetFileNames: true
|
||||
},
|
||||
preserveEntrySignatures: true
|
||||
},
|
||||
ssr: true
|
||||
},
|
||||
publicDir: true,
|
||||
resolve: {
|
||||
alias: {
|
||||
$app: true,
|
||||
$lib: true,
|
||||
'$service-worker': true
|
||||
}
|
||||
},
|
||||
root: true
|
||||
};
|
||||
|
||||
const options_regex = /(export\s+const\s+(prerender|csr|ssr|trailingSlash))\s*=/s;
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const warned = new Set();
|
||||
|
||||
/** @type {import('@sveltejs/vite-plugin-svelte').PreprocessorGroup} */
|
||||
const warning_preprocessor = {
|
||||
script: ({ content, filename }) => {
|
||||
if (!filename) return;
|
||||
|
||||
const basename = path.basename(filename);
|
||||
if (basename.startsWith('+page.') || basename.startsWith('+layout.')) {
|
||||
const match = content.match(options_regex);
|
||||
if (match) {
|
||||
const fixed = basename.replace('.svelte', '(.server).js/ts');
|
||||
|
||||
const message =
|
||||
`\n${colors.bold().red(path.relative('.', filename))}\n` +
|
||||
`\`${match[1]}\` will be ignored — move it to ${fixed} instead. See https://kit.svelte.dev/docs/page-options for more information.`;
|
||||
|
||||
if (!warned.has(message)) {
|
||||
console.log(message);
|
||||
warned.add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
markup: ({ content, filename }) => {
|
||||
if (!filename) return;
|
||||
|
||||
const basename = path.basename(filename);
|
||||
if (basename.startsWith('+layout.') && !content.includes('<slot')) {
|
||||
const message =
|
||||
`\n${colors.bold().red(path.relative('.', filename))}\n` +
|
||||
'`<slot />` missing — inner content will not be rendered';
|
||||
|
||||
if (!warned.has(message)) {
|
||||
console.log(message);
|
||||
warned.add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the SvelteKit Vite plugins.
|
||||
* @returns {Promise<import('vite').Plugin[]>}
|
||||
*/
|
||||
export async function sveltekit() {
|
||||
const svelte_config = await load_config();
|
||||
|
||||
/** @type {import('@sveltejs/vite-plugin-svelte').Options['preprocess']} */
|
||||
let preprocess = svelte_config.preprocess;
|
||||
if (Array.isArray(preprocess)) {
|
||||
preprocess = [...preprocess, warning_preprocessor];
|
||||
} else if (preprocess) {
|
||||
preprocess = [preprocess, warning_preprocessor];
|
||||
} else {
|
||||
preprocess = warning_preprocessor;
|
||||
}
|
||||
|
||||
/** @type {import('@sveltejs/vite-plugin-svelte').Options} */
|
||||
const vite_plugin_svelte_options = {
|
||||
configFile: false,
|
||||
extensions: svelte_config.extensions,
|
||||
preprocess,
|
||||
onwarn: svelte_config.onwarn,
|
||||
compilerOptions: {
|
||||
// @ts-expect-error SvelteKit requires hydratable true by default
|
||||
hydratable: isSvelte5Plus() ? undefined : true,
|
||||
...svelte_config.compilerOptions
|
||||
},
|
||||
...svelte_config.vitePlugin
|
||||
};
|
||||
|
||||
return [...svelte(vite_plugin_svelte_options), ...kit({ svelte_config })];
|
||||
}
|
||||
|
||||
// These variables live outside the `kit()` function because it is re-invoked by each Vite build
|
||||
|
||||
let secondary_build_started = false;
|
||||
|
||||
/** @type {import('types').ManifestData} */
|
||||
let manifest_data;
|
||||
|
||||
/**
|
||||
* Returns the SvelteKit Vite plugin. Vite executes Rollup hooks as well as some of its own.
|
||||
* Background reading is available at:
|
||||
* - https://vitejs.dev/guide/api-plugin.html
|
||||
* - https://rollupjs.org/guide/en/#plugin-development
|
||||
*
|
||||
* You can get an idea of the lifecycle by looking at the flow charts here:
|
||||
* - https://rollupjs.org/guide/en/#build-hooks
|
||||
* - https://rollupjs.org/guide/en/#output-generation-hooks
|
||||
*
|
||||
* @param {{ svelte_config: import('types').ValidatedConfig }} options
|
||||
* @return {import('vite').Plugin[]}
|
||||
*/
|
||||
function kit({ svelte_config }) {
|
||||
const { kit } = svelte_config;
|
||||
const out = `${kit.outDir}/output`;
|
||||
|
||||
const version_hash = hash(kit.version.name);
|
||||
|
||||
/** @type {import('vite').ResolvedConfig} */
|
||||
let vite_config;
|
||||
|
||||
/** @type {import('vite').ConfigEnv} */
|
||||
let vite_config_env;
|
||||
|
||||
/** @type {boolean} */
|
||||
let is_build;
|
||||
|
||||
/** @type {{ public: Record<string, string>; private: Record<string, string> }} */
|
||||
let env;
|
||||
|
||||
/** @type {() => Promise<void>} */
|
||||
let finalise;
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
let initial_config;
|
||||
|
||||
const service_worker_entry_file = resolve_entry(kit.files.serviceWorker);
|
||||
|
||||
const sourcemapIgnoreList = /** @param {string} relative_path */ (relative_path) =>
|
||||
relative_path.includes('node_modules') || relative_path.includes(kit.outDir);
|
||||
|
||||
/** @type {import('vite').Plugin} */
|
||||
const plugin_setup = {
|
||||
name: 'vite-plugin-sveltekit-setup',
|
||||
|
||||
/**
|
||||
* Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file.
|
||||
* @see https://vitejs.dev/guide/api-plugin.html#config
|
||||
*/
|
||||
async config(config, config_env) {
|
||||
initial_config = config;
|
||||
vite_config_env = config_env;
|
||||
is_build = config_env.command === 'build';
|
||||
|
||||
env = get_env(kit.env, vite_config_env.mode);
|
||||
|
||||
const allow = new Set([
|
||||
kit.files.lib,
|
||||
kit.files.routes,
|
||||
kit.outDir,
|
||||
path.resolve('src'), // TODO this isn't correct if user changed all his files to sth else than src (like in test/options)
|
||||
path.resolve('node_modules'),
|
||||
path.resolve(vite.searchForWorkspaceRoot(cwd), 'node_modules')
|
||||
]);
|
||||
|
||||
// We can only add directories to the allow list, so we find out
|
||||
// if there's a client hooks file and pass its directory
|
||||
const client_hooks = resolve_entry(kit.files.hooks.client);
|
||||
if (client_hooks) allow.add(path.dirname(client_hooks));
|
||||
|
||||
const generated = path.posix.join(kit.outDir, 'generated');
|
||||
|
||||
// dev and preview config can be shared
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const new_config = {
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '__SERVER__', replacement: `${generated}/server` },
|
||||
{ find: '$app', replacement: `${runtime_directory}/app` },
|
||||
...get_config_aliases(kit)
|
||||
]
|
||||
},
|
||||
root: cwd,
|
||||
server: {
|
||||
cors: { preflightContinue: true },
|
||||
fs: {
|
||||
allow: [...allow]
|
||||
},
|
||||
sourcemapIgnoreList,
|
||||
watch: {
|
||||
ignored: [
|
||||
// Ignore all siblings of config.kit.outDir/generated
|
||||
`${posixify(kit.outDir)}/!(generated)`
|
||||
]
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
cors: { preflightContinue: true }
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
'@sveltejs/kit',
|
||||
// exclude kit features so that libraries using them work even when they are prebundled
|
||||
// this does not affect app code, just handling of imported libraries that use $app or $env
|
||||
'$app',
|
||||
'$env'
|
||||
]
|
||||
},
|
||||
ssr: {
|
||||
noExternal: [
|
||||
// This ensures that esm-env is inlined into the server output with the
|
||||
// export conditions resolved correctly through Vite. This prevents adapters
|
||||
// that bundle later on from resolving the export conditions incorrectly
|
||||
// and for example include browser-only code in the server output
|
||||
// because they for example use esbuild.build with `platform: 'browser'`
|
||||
'esm-env',
|
||||
// We need this for two reasons:
|
||||
// 1. Without this, `@sveltejs/kit` imports are kept as-is in the server output,
|
||||
// and that causes modules and therefore classes like `Redirect` to be imported twice
|
||||
// under different IDs, which breaks a bunch of stuff because of failing instanceof checks.
|
||||
// 2. Vitest bypasses Vite when loading external modules, so we bundle
|
||||
// when it is detected to keep our virtual modules working.
|
||||
// See https://github.com/sveltejs/kit/pull/9172
|
||||
// and https://vitest.dev/config/#deps-registernodeloader
|
||||
'@sveltejs/kit'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (is_build) {
|
||||
if (!new_config.build) new_config.build = {};
|
||||
new_config.build.ssr = !secondary_build_started;
|
||||
|
||||
new_config.define = {
|
||||
__SVELTEKIT_ADAPTER_NAME__: s(kit.adapter?.name),
|
||||
__SVELTEKIT_APP_VERSION_FILE__: s(`${kit.appDir}/version.json`),
|
||||
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval),
|
||||
__SVELTEKIT_DEV__: 'false',
|
||||
__SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false'
|
||||
};
|
||||
|
||||
if (!secondary_build_started) {
|
||||
manifest_data = (await sync.all(svelte_config, config_env.mode)).manifest_data;
|
||||
}
|
||||
} else {
|
||||
new_config.define = {
|
||||
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0',
|
||||
__SVELTEKIT_DEV__: 'true',
|
||||
__SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false'
|
||||
};
|
||||
|
||||
// These Kit dependencies are packaged as CommonJS, which means they must always be externalized.
|
||||
// Without this, the tests will still pass but `pnpm dev` will fail in projects that link `@sveltejs/kit`.
|
||||
/** @type {NonNullable<import('vite').UserConfig['ssr']>} */ (new_config.ssr).external = [
|
||||
'cookie',
|
||||
'set-cookie-parser'
|
||||
];
|
||||
}
|
||||
|
||||
warn_overridden_config(config, new_config);
|
||||
|
||||
return new_config;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the final config.
|
||||
*/
|
||||
configResolved(config) {
|
||||
vite_config = config;
|
||||
|
||||
// This is a hack to prevent Vite from nuking useful logs,
|
||||
// pending https://github.com/vitejs/vite/issues/9378
|
||||
config.logger.warn('');
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('vite').Plugin} */
|
||||
const plugin_virtual_modules = {
|
||||
name: 'vite-plugin-sveltekit-virtual-modules',
|
||||
|
||||
async resolveId(id) {
|
||||
// treat $env/static/[public|private] as virtual
|
||||
if (id.startsWith('$env/') || id.startsWith('__sveltekit/') || id === '$service-worker') {
|
||||
return `\0virtual:${id}`;
|
||||
}
|
||||
},
|
||||
|
||||
async load(id, options) {
|
||||
const browser = !options?.ssr;
|
||||
|
||||
const global = is_build
|
||||
? `globalThis.__sveltekit_${version_hash}`
|
||||
: 'globalThis.__sveltekit_dev';
|
||||
|
||||
if (options?.ssr === false && process.env.TEST !== 'true') {
|
||||
const normalized_cwd = vite.normalizePath(cwd);
|
||||
const normalized_lib = vite.normalizePath(kit.files.lib);
|
||||
if (
|
||||
is_illegal(id, {
|
||||
cwd: normalized_cwd,
|
||||
node_modules: vite.normalizePath(path.resolve('node_modules')),
|
||||
server: vite.normalizePath(path.join(normalized_lib, 'server'))
|
||||
})
|
||||
) {
|
||||
const relative = normalize_id(id, normalized_lib, normalized_cwd);
|
||||
throw new Error(`Cannot import ${strip_virtual_prefix(relative)} into client-side code`);
|
||||
}
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case '\0virtual:$env/static/private':
|
||||
return create_static_module('$env/static/private', env.private);
|
||||
|
||||
case '\0virtual:$env/static/public':
|
||||
return create_static_module('$env/static/public', env.public);
|
||||
|
||||
case '\0virtual:$env/dynamic/private':
|
||||
return create_dynamic_module(
|
||||
'private',
|
||||
vite_config_env.command === 'serve' ? env.private : undefined
|
||||
);
|
||||
|
||||
case '\0virtual:$env/dynamic/public':
|
||||
// populate `$env/dynamic/public` from `window`
|
||||
if (browser) {
|
||||
return `export const env = ${global}.env;`;
|
||||
}
|
||||
|
||||
return create_dynamic_module(
|
||||
'public',
|
||||
vite_config_env.command === 'serve' ? env.public : undefined
|
||||
);
|
||||
|
||||
case '\0virtual:$service-worker':
|
||||
return create_service_worker_module(svelte_config);
|
||||
|
||||
// for internal use only. it's published as $app/paths externally
|
||||
// we use this alias so that we won't collide with user aliases
|
||||
case '\0virtual:__sveltekit/paths': {
|
||||
const { assets, base } = svelte_config.kit.paths;
|
||||
|
||||
// use the values defined in `global`, but fall back to hard-coded values
|
||||
// for the sake of things like Vitest which may import this module
|
||||
// outside the context of a page
|
||||
if (browser) {
|
||||
return dedent`
|
||||
export const base = ${global}?.base ?? ${s(base)};
|
||||
export const assets = ${global}?.assets ?? ${assets ? s(assets) : 'base'};
|
||||
`;
|
||||
}
|
||||
|
||||
return dedent`
|
||||
export let base = ${s(base)};
|
||||
export let assets = ${assets ? s(assets) : 'base'};
|
||||
|
||||
export const relative = ${svelte_config.kit.paths.relative};
|
||||
|
||||
const initial = { base, assets };
|
||||
|
||||
export function override(paths) {
|
||||
base = paths.base;
|
||||
assets = paths.assets;
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
base = initial.base;
|
||||
assets = initial.assets;
|
||||
}
|
||||
|
||||
/** @param {string} path */
|
||||
export function set_assets(path) {
|
||||
assets = initial.assets = path;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
case '\0virtual:__sveltekit/environment': {
|
||||
const { version } = svelte_config.kit;
|
||||
|
||||
return dedent`
|
||||
export const version = ${s(version.name)};
|
||||
export let building = false;
|
||||
|
||||
export function set_building() {
|
||||
building = true;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensures that client-side code can't accidentally import server-side code,
|
||||
* whether in `*.server.js` files, `$lib/server`, or `$env/[static|dynamic]/private`
|
||||
* @type {import('vite').Plugin}
|
||||
*/
|
||||
const plugin_guard = {
|
||||
name: 'vite-plugin-sveltekit-guard',
|
||||
|
||||
writeBundle: {
|
||||
sequential: true,
|
||||
async handler(_options) {
|
||||
if (vite_config.build.ssr) return;
|
||||
|
||||
const guard = module_guard(this, {
|
||||
cwd: vite.normalizePath(process.cwd()),
|
||||
lib: vite.normalizePath(kit.files.lib)
|
||||
});
|
||||
|
||||
manifest_data.nodes.forEach((_node, i) => {
|
||||
const id = vite.normalizePath(
|
||||
path.resolve(kit.outDir, `generated/client-optimized/nodes/${i}.js`)
|
||||
);
|
||||
|
||||
guard.check(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('vite').Plugin} */
|
||||
const plugin_compile = {
|
||||
name: 'vite-plugin-sveltekit-compile',
|
||||
|
||||
/**
|
||||
* Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file.
|
||||
* @see https://vitejs.dev/guide/api-plugin.html#config
|
||||
*/
|
||||
async config(config) {
|
||||
/** @type {import('vite').UserConfig} */
|
||||
let new_config;
|
||||
|
||||
if (is_build) {
|
||||
const ssr = /** @type {boolean} */ (config.build?.ssr);
|
||||
const prefix = `${kit.appDir}/immutable`;
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const input = {};
|
||||
|
||||
if (ssr) {
|
||||
input.index = `${runtime_directory}/server/index.js`;
|
||||
input.internal = `${kit.outDir}/generated/server/internal.js`;
|
||||
|
||||
// add entry points for every endpoint...
|
||||
manifest_data.routes.forEach((route) => {
|
||||
if (route.endpoint) {
|
||||
const resolved = path.resolve(route.endpoint.file);
|
||||
const relative = decodeURIComponent(path.relative(kit.files.routes, resolved));
|
||||
const name = posixify(path.join('entries/endpoints', relative.replace(/\.js$/, '')));
|
||||
input[name] = resolved;
|
||||
}
|
||||
});
|
||||
|
||||
// ...and every component used by pages...
|
||||
manifest_data.nodes.forEach((node) => {
|
||||
for (const file of [node.component, node.universal, node.server]) {
|
||||
if (file) {
|
||||
const resolved = path.resolve(file);
|
||||
const relative = decodeURIComponent(path.relative(kit.files.routes, resolved));
|
||||
|
||||
const name = relative.startsWith('..')
|
||||
? posixify(path.join('entries/fallbacks', path.basename(file)))
|
||||
: posixify(path.join('entries/pages', relative.replace(/\.js$/, '')));
|
||||
input[name] = resolved;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ...and every matcher
|
||||
Object.entries(manifest_data.matchers).forEach(([key, file]) => {
|
||||
const name = posixify(path.join('entries/matchers', key));
|
||||
input[name] = path.resolve(file);
|
||||
});
|
||||
} else {
|
||||
input['entry/start'] = `${runtime_directory}/client/start.js`;
|
||||
input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`;
|
||||
|
||||
manifest_data.nodes.forEach((node, i) => {
|
||||
if (node.component || node.universal) {
|
||||
input[`nodes/${i}`] = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// see the kit.output.preloadStrategy option for details on why we have multiple options here
|
||||
const ext = kit.output.preloadStrategy === 'preload-mjs' ? 'mjs' : 'js';
|
||||
|
||||
// We could always use a relative asset base path here, but it's better for performance not to.
|
||||
// E.g. Vite generates `new URL('/asset.png', import.meta).href` for a relative path vs just '/asset.png'.
|
||||
// That's larger and takes longer to run and also causes an HTML diff between SSR and client
|
||||
// causing us to do a more expensive hydration check.
|
||||
const client_base =
|
||||
kit.paths.relative !== false || kit.paths.assets ? './' : kit.paths.base || '/';
|
||||
|
||||
new_config = {
|
||||
base: ssr ? assets_base(kit) : client_base,
|
||||
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', // TODO: remove this after bumping peer dep to vite 5
|
||||
outDir: `${out}/${ssr ? 'server' : 'client'}`,
|
||||
rollupOptions: {
|
||||
input,
|
||||
output: {
|
||||
format: 'esm',
|
||||
entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
|
||||
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
|
||||
assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
|
||||
hoistTransitiveImports: false,
|
||||
sourcemapIgnoreList
|
||||
},
|
||||
preserveEntrySignatures: 'strict'
|
||||
},
|
||||
ssrEmitAssets: true,
|
||||
target: ssr ? 'node16.14' : undefined
|
||||
},
|
||||
publicDir: kit.files.assets,
|
||||
worker: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `${prefix}/workers/[name]-[hash].js`,
|
||||
chunkFileNames: `${prefix}/workers/chunks/[name]-[hash].js`,
|
||||
assetFileNames: `${prefix}/workers/assets/[name]-[hash][extname]`,
|
||||
hoistTransitiveImports: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
new_config = {
|
||||
appType: 'custom',
|
||||
base: kit.paths.base,
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Vite dependency crawler needs an explicit JS entry point
|
||||
// eventhough server otherwise works without it
|
||||
input: `${runtime_directory}/client/start.js`
|
||||
}
|
||||
},
|
||||
publicDir: kit.files.assets
|
||||
};
|
||||
}
|
||||
|
||||
warn_overridden_config(config, new_config);
|
||||
|
||||
return new_config;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the SvelteKit middleware to do SSR in dev mode.
|
||||
* @see https://vitejs.dev/guide/api-plugin.html#configureserver
|
||||
*/
|
||||
async configureServer(vite) {
|
||||
return await dev(vite, vite_config, svelte_config);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the SvelteKit middleware to do SSR in preview mode.
|
||||
* @see https://vitejs.dev/guide/api-plugin.html#configurepreviewserver
|
||||
*/
|
||||
configurePreviewServer(vite) {
|
||||
return preview(vite, vite_config, svelte_config);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the output directories.
|
||||
*/
|
||||
buildStart() {
|
||||
if (secondary_build_started) return;
|
||||
|
||||
if (is_build) {
|
||||
if (!vite_config.build.watch) {
|
||||
rimraf(out);
|
||||
}
|
||||
mkdirp(out);
|
||||
}
|
||||
},
|
||||
|
||||
generateBundle() {
|
||||
if (vite_config.build.ssr) return;
|
||||
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: `${kit.appDir}/version.json`,
|
||||
source: s({ version: kit.version.name })
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Vite builds a single bundle. We need three bundles: client, server, and service worker.
|
||||
* The user's package.json scripts will invoke the Vite CLI to execute the server build. We
|
||||
* then use this hook to kick off builds for the client and service worker.
|
||||
*/
|
||||
writeBundle: {
|
||||
sequential: true,
|
||||
async handler(_options) {
|
||||
if (secondary_build_started) return; // only run this once
|
||||
|
||||
const verbose = vite_config.logLevel === 'info';
|
||||
const log = logger({ verbose });
|
||||
|
||||
/** @type {import('vite').Manifest} */
|
||||
const server_manifest = JSON.parse(read(`${out}/server/${vite_config.build.manifest}`));
|
||||
|
||||
/** @type {import('types').BuildData} */
|
||||
const build_data = {
|
||||
app_dir: kit.appDir,
|
||||
app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`,
|
||||
manifest_data,
|
||||
service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable?
|
||||
client: null,
|
||||
server_manifest
|
||||
};
|
||||
|
||||
const manifest_path = `${out}/server/manifest-full.js`;
|
||||
fs.writeFileSync(
|
||||
manifest_path,
|
||||
`export const manifest = ${generate_manifest({
|
||||
build_data,
|
||||
relative_path: '.',
|
||||
routes: manifest_data.routes
|
||||
})};\n`
|
||||
);
|
||||
|
||||
// first, build server nodes without the client manifest so we can analyse it
|
||||
log.info('Analysing routes');
|
||||
|
||||
build_server_nodes(out, kit, manifest_data, server_manifest, null, null);
|
||||
|
||||
const metadata = await analyse({
|
||||
manifest_path,
|
||||
env: { ...env.private, ...env.public }
|
||||
});
|
||||
|
||||
log.info('Building app');
|
||||
|
||||
// create client build
|
||||
write_client_manifest(
|
||||
kit,
|
||||
manifest_data,
|
||||
`${kit.outDir}/generated/client-optimized`,
|
||||
metadata.nodes
|
||||
);
|
||||
|
||||
secondary_build_started = true;
|
||||
|
||||
const { output } = /** @type {import('vite').Rollup.RollupOutput} */ (
|
||||
await vite.build({
|
||||
configFile: vite_config.configFile,
|
||||
// CLI args
|
||||
mode: vite_config_env.mode,
|
||||
logLevel: vite_config.logLevel,
|
||||
clearScreen: vite_config.clearScreen,
|
||||
build: {
|
||||
minify: initial_config.build?.minify,
|
||||
assetsInlineLimit: vite_config.build.assetsInlineLimit,
|
||||
sourcemap: vite_config.build.sourcemap
|
||||
},
|
||||
optimizeDeps: {
|
||||
force: vite_config.optimizeDeps.force
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
copy(
|
||||
`${out}/server/${kit.appDir}/immutable/assets`,
|
||||
`${out}/client/${kit.appDir}/immutable/assets`
|
||||
);
|
||||
|
||||
/** @type {import('vite').Manifest} */
|
||||
const client_manifest = JSON.parse(read(`${out}/client/${vite_config.build.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: start.file,
|
||||
app: app.file,
|
||||
imports: [...start.imports, ...app.imports],
|
||||
stylesheets: [...start.stylesheets, ...app.stylesheets],
|
||||
fonts: [...start.fonts, ...app.fonts]
|
||||
};
|
||||
|
||||
const css = output.filter(
|
||||
/** @type {(value: any) => value is import('vite').Rollup.OutputAsset} */
|
||||
(value) => value.type === 'asset' && value.fileName.endsWith('.css')
|
||||
);
|
||||
|
||||
// regenerate manifest now that we have client entry...
|
||||
fs.writeFileSync(
|
||||
manifest_path,
|
||||
`export const manifest = ${generate_manifest({
|
||||
build_data,
|
||||
relative_path: '.',
|
||||
routes: manifest_data.routes
|
||||
})};\n`
|
||||
);
|
||||
|
||||
// regenerate nodes with the client manifest...
|
||||
build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css);
|
||||
|
||||
// ...and prerender
|
||||
const { prerendered, prerender_map } = await prerender({
|
||||
out,
|
||||
manifest_path,
|
||||
metadata,
|
||||
verbose,
|
||||
env: { ...env.private, ...env.public }
|
||||
});
|
||||
|
||||
// generate a new manifest that doesn't include prerendered pages
|
||||
fs.writeFileSync(
|
||||
`${out}/server/manifest.js`,
|
||||
`export const manifest = ${generate_manifest({
|
||||
build_data,
|
||||
relative_path: '.',
|
||||
routes: manifest_data.routes.filter((route) => prerender_map.get(route.id) !== true)
|
||||
})};\n`
|
||||
);
|
||||
|
||||
if (service_worker_entry_file) {
|
||||
if (kit.paths.assets) {
|
||||
throw new Error('Cannot use service worker alongside config.kit.paths.assets');
|
||||
}
|
||||
|
||||
log.info('Building service worker');
|
||||
|
||||
await build_service_worker(
|
||||
out,
|
||||
kit,
|
||||
vite_config,
|
||||
manifest_data,
|
||||
service_worker_entry_file,
|
||||
prerendered,
|
||||
client_manifest
|
||||
);
|
||||
}
|
||||
|
||||
// we need to defer this to closeBundle, so that adapters copy files
|
||||
// created by other Vite plugins
|
||||
finalise = async () => {
|
||||
console.log(
|
||||
`\nRun ${colors
|
||||
.bold()
|
||||
.cyan('npm run preview')} to preview your production build locally.`
|
||||
);
|
||||
|
||||
if (kit.adapter) {
|
||||
const { adapt } = await import('../../core/adapt/index.js');
|
||||
await adapt(
|
||||
svelte_config,
|
||||
build_data,
|
||||
metadata,
|
||||
prerendered,
|
||||
prerender_map,
|
||||
log,
|
||||
vite_config
|
||||
);
|
||||
} else {
|
||||
console.log(colors.bold().yellow('\nNo adapter specified'));
|
||||
|
||||
const link = colors.bold().cyan('https://kit.svelte.dev/docs/adapters');
|
||||
console.log(
|
||||
`See ${link} to learn how to configure your app to run on the platform of your choosing`
|
||||
);
|
||||
}
|
||||
|
||||
secondary_build_started = false;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs the adapter.
|
||||
*/
|
||||
closeBundle: {
|
||||
sequential: true,
|
||||
async handler() {
|
||||
if (!vite_config.build.ssr) return;
|
||||
await finalise?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [plugin_setup, plugin_virtual_modules, plugin_guard, plugin_compile];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} config
|
||||
* @param {Record<string, any>} resolved_config
|
||||
*/
|
||||
function warn_overridden_config(config, resolved_config) {
|
||||
const overridden = find_overridden_config(config, resolved_config, enforced_config, '', []);
|
||||
|
||||
if (overridden.length > 0) {
|
||||
console.error(
|
||||
colors.bold().red('The following Vite config options will be overridden by SvelteKit:') +
|
||||
overridden.map((key) => `\n - ${key}`).join('')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} config
|
||||
* @param {Record<string, any>} resolved_config
|
||||
* @param {import('./types.js').EnforcedConfig} enforced_config
|
||||
* @param {string} path
|
||||
* @param {string[]} out used locally to compute the return value
|
||||
*/
|
||||
function find_overridden_config(config, resolved_config, enforced_config, path, out) {
|
||||
if (config == null || resolved_config == null) {
|
||||
return out;
|
||||
}
|
||||
|
||||
for (const key in enforced_config) {
|
||||
if (typeof config === 'object' && key in config && key in resolved_config) {
|
||||
const enforced = enforced_config[key];
|
||||
|
||||
if (enforced === true) {
|
||||
if (config[key] !== resolved_config[key]) {
|
||||
out.push(path + key);
|
||||
}
|
||||
} else {
|
||||
find_overridden_config(config[key], resolved_config[key], enforced, path + key + '.', out);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ValidatedConfig} config
|
||||
*/
|
||||
const create_service_worker_module = (config) => dedent`
|
||||
if (typeof self === 'undefined' || self instanceof ServiceWorkerGlobalScope === false) {
|
||||
throw new Error('This module can only be imported inside a service worker');
|
||||
}
|
||||
|
||||
export const build = [];
|
||||
export const files = [
|
||||
${create_assets(config)
|
||||
.filter((asset) => config.kit.serviceWorker.files(asset.file))
|
||||
.map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`)
|
||||
.join(',\n')}
|
||||
];
|
||||
export const prerendered = [];
|
||||
export const version = ${s(config.kit.version.name)};
|
||||
`;
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
import fs from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { lookup } from 'mrmime';
|
||||
import sirv from 'sirv';
|
||||
import { loadEnv, normalizePath } from 'vite';
|
||||
import { getRequest, setResponse } from '../../../exports/node/index.js';
|
||||
import { installPolyfills } from '../../../exports/node/polyfills.js';
|
||||
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
|
||||
import { should_polyfill } from '../../../utils/platform.js';
|
||||
import { not_found } from '../utils.js';
|
||||
|
||||
/** @typedef {import('http').IncomingMessage} Req */
|
||||
/** @typedef {import('http').ServerResponse} Res */
|
||||
/** @typedef {(req: Req, res: Res, next: () => void) => void} Handler */
|
||||
|
||||
/**
|
||||
* @param {{ middlewares: import('connect').Server }} vite
|
||||
* @param {import('vite').ResolvedConfig} vite_config
|
||||
* @param {import('types').ValidatedConfig} svelte_config
|
||||
*/
|
||||
export async function preview(vite, vite_config, svelte_config) {
|
||||
if (should_polyfill) {
|
||||
installPolyfills();
|
||||
}
|
||||
|
||||
const { paths } = svelte_config.kit;
|
||||
const base = paths.base;
|
||||
const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base;
|
||||
|
||||
const protocol = vite_config.preview.https ? 'https' : 'http';
|
||||
|
||||
const etag = `"${Date.now()}"`;
|
||||
|
||||
const dir = join(svelte_config.kit.outDir, 'output/server');
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
throw new Error(`Server files not found at ${dir}, did you run \`build\` first?`);
|
||||
}
|
||||
|
||||
/** @type {import('types').ServerInternalModule} */
|
||||
const { set_assets } = await import(pathToFileURL(join(dir, 'internal.js')).href);
|
||||
|
||||
/** @type {import('types').ServerModule} */
|
||||
const { Server } = await import(pathToFileURL(join(dir, 'index.js')).href);
|
||||
|
||||
const { manifest } = await import(pathToFileURL(join(dir, 'manifest.js')).href);
|
||||
|
||||
set_assets(assets);
|
||||
|
||||
const server = new Server(manifest);
|
||||
await server.init({
|
||||
env: loadEnv(vite_config.mode, svelte_config.kit.env.dir, '')
|
||||
});
|
||||
|
||||
return () => {
|
||||
// generated client assets and the contents of `static`
|
||||
vite.middlewares.use(
|
||||
scoped(
|
||||
assets,
|
||||
sirv(join(svelte_config.kit.outDir, 'output/client'), {
|
||||
setHeaders: (res, pathname) => {
|
||||
// only apply to immutable directory, not e.g. version.json
|
||||
if (pathname.startsWith(`/${svelte_config.kit.appDir}/immutable`)) {
|
||||
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
const original_url = /** @type {string} */ (req.url);
|
||||
const { pathname, search } = new URL(original_url, 'http://dummy');
|
||||
|
||||
// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
|
||||
// regardless of the `trailingSlash` route option
|
||||
if (base.length > 1 && pathname === base) {
|
||||
let location = base + '/';
|
||||
if (search) location += search;
|
||||
res.writeHead(307, {
|
||||
location
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.startsWith(base)) {
|
||||
next();
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
not_found(req, res, base);
|
||||
}
|
||||
});
|
||||
|
||||
// prerendered dependencies
|
||||
vite.middlewares.use(
|
||||
scoped(base, mutable(join(svelte_config.kit.outDir, 'output/prerendered/dependencies')))
|
||||
);
|
||||
|
||||
// prerendered pages (we can't just use sirv because we need to
|
||||
// preserve the correct trailingSlash behaviour)
|
||||
vite.middlewares.use(
|
||||
scoped(base, (req, res, next) => {
|
||||
let if_none_match_value = req.headers['if-none-match'];
|
||||
|
||||
if (if_none_match_value?.startsWith('W/"')) {
|
||||
if_none_match_value = if_none_match_value.substring(2);
|
||||
}
|
||||
|
||||
if (if_none_match_value === etag) {
|
||||
res.statusCode = 304;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy');
|
||||
|
||||
let filename = normalizePath(
|
||||
join(svelte_config.kit.outDir, 'output/prerendered/pages' + pathname)
|
||||
);
|
||||
let prerendered = is_file(filename);
|
||||
|
||||
if (!prerendered) {
|
||||
const has_trailing_slash = pathname.endsWith('/');
|
||||
const html_filename = `${filename}${has_trailing_slash ? 'index.html' : '.html'}`;
|
||||
|
||||
/** @type {string | undefined} */
|
||||
let redirect;
|
||||
|
||||
if (is_file(html_filename)) {
|
||||
filename = html_filename;
|
||||
prerendered = true;
|
||||
} else if (has_trailing_slash) {
|
||||
if (is_file(filename.slice(0, -1) + '.html')) {
|
||||
redirect = pathname.slice(0, -1);
|
||||
}
|
||||
} else if (is_file(filename + '/index.html')) {
|
||||
redirect = pathname + '/';
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
if (search) redirect += search;
|
||||
res.writeHead(307, {
|
||||
location: redirect
|
||||
});
|
||||
|
||||
res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (prerendered) {
|
||||
res.writeHead(200, {
|
||||
'content-type': lookup(pathname) || 'text/html',
|
||||
etag
|
||||
});
|
||||
|
||||
fs.createReadStream(filename).pipe(res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// SSR
|
||||
vite.middlewares.use(async (req, res) => {
|
||||
const host = req.headers['host'];
|
||||
|
||||
let request;
|
||||
|
||||
try {
|
||||
request = await getRequest({
|
||||
base: `${protocol}://${host}`,
|
||||
request: req
|
||||
});
|
||||
} catch (/** @type {any} */ err) {
|
||||
res.statusCode = err.status || 400;
|
||||
return res.end('Invalid request body');
|
||||
}
|
||||
|
||||
setResponse(
|
||||
res,
|
||||
await server.respond(request, {
|
||||
getClientAddress: () => {
|
||||
const { remoteAddress } = req.socket;
|
||||
if (remoteAddress) return remoteAddress;
|
||||
throw new Error('Could not determine clientAddress');
|
||||
},
|
||||
read: (file) => fs.readFileSync(join(svelte_config.kit.files.assets, file))
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @returns {Handler}
|
||||
*/
|
||||
const mutable = (dir) =>
|
||||
fs.existsSync(dir)
|
||||
? sirv(dir, {
|
||||
etag: true,
|
||||
maxAge: 0
|
||||
})
|
||||
: (_req, _res, next) => next();
|
||||
|
||||
/**
|
||||
* @param {string} scope
|
||||
* @param {Handler} handler
|
||||
* @returns {Handler}
|
||||
*/
|
||||
function scoped(scope, handler) {
|
||||
if (scope === '') return handler;
|
||||
|
||||
return (req, res, next) => {
|
||||
if (req.url?.startsWith(scope)) {
|
||||
const original_url = req.url;
|
||||
req.url = req.url.slice(scope.length);
|
||||
handler(req, res, () => {
|
||||
req.url = original_url;
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {string} path */
|
||||
function is_file(path) {
|
||||
return fs.existsSync(path) && !fs.statSync(path).isDirectory();
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export interface EnforcedConfig {
|
||||
[key: string]: EnforcedConfig | true;
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import path from 'node:path';
|
||||
import { loadEnv } from 'vite';
|
||||
import { posixify } from '../../utils/filesystem.js';
|
||||
import { negotiate } from '../../utils/http.js';
|
||||
import { filter_private_env, filter_public_env } from '../../utils/env.js';
|
||||
|
||||
/**
|
||||
* Transforms kit.alias to a valid vite.resolve.alias array.
|
||||
*
|
||||
* Related to tsconfig path alias creation.
|
||||
*
|
||||
* @param {import('types').ValidatedKitConfig} config
|
||||
* */
|
||||
export function get_config_aliases(config) {
|
||||
/** @type {import('vite').Alias[]} */
|
||||
const alias = [
|
||||
// For now, we handle `$lib` specially here rather than make it a default value for
|
||||
// `config.kit.alias` since it has special meaning for packaging, etc.
|
||||
{ find: '$lib', replacement: config.files.lib }
|
||||
];
|
||||
|
||||
for (let [key, value] of Object.entries(config.alias)) {
|
||||
value = posixify(value);
|
||||
if (value.endsWith('/*')) {
|
||||
value = value.slice(0, -2);
|
||||
}
|
||||
if (key.endsWith('/*')) {
|
||||
// Doing just `{ find: key.slice(0, -2) ,..}` would mean `import .. from "key"` would also be matched, which we don't want
|
||||
alias.push({
|
||||
find: new RegExp(`^${escape_for_regexp(key.slice(0, -2))}\\/(.+)$`),
|
||||
replacement: `${path.resolve(value)}/$1`
|
||||
});
|
||||
} else if (key + '/*' in config.alias) {
|
||||
// key and key/* both exist -> the replacement for key needs to happen _only_ on import .. from "key"
|
||||
alias.push({
|
||||
find: new RegExp(`^${escape_for_regexp(key)}$`),
|
||||
replacement: path.resolve(value)
|
||||
});
|
||||
} else {
|
||||
alias.push({ find: key, replacement: path.resolve(value) });
|
||||
}
|
||||
}
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
function escape_for_regexp(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load environment variables from process.env and .env files
|
||||
* @param {import('types').ValidatedKitConfig['env']} env_config
|
||||
* @param {string} mode
|
||||
*/
|
||||
export function get_env(env_config, mode) {
|
||||
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = env_config;
|
||||
const env = loadEnv(mode, env_config.dir, '');
|
||||
|
||||
return {
|
||||
public: filter_public_env(env, { public_prefix, private_prefix }),
|
||||
private: filter_private_env(env, { public_prefix, private_prefix })
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('http').IncomingMessage} req
|
||||
* @param {import('http').ServerResponse} res
|
||||
* @param {string} base
|
||||
*/
|
||||
export function not_found(req, res, base) {
|
||||
const type = negotiate(req.headers.accept ?? '*', ['text/plain', 'text/html']);
|
||||
|
||||
// special case — handle `/` request automatically
|
||||
if (req.url === '/' && type === 'text/html') {
|
||||
res.statusCode = 307;
|
||||
res.setHeader('location', base);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 404;
|
||||
|
||||
const prefixed = base + req.url;
|
||||
|
||||
if (type === 'text/html') {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end(
|
||||
`The server is configured with a public base URL of ${base} - did you mean to visit <a href="${prefixed}">${prefixed}</a> instead?`
|
||||
);
|
||||
} else {
|
||||
res.end(
|
||||
`The server is configured with a public base URL of ${base} - did you mean to visit ${prefixed} instead?`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', '');
|
||||
+1
@@ -0,0 +1 @@
|
||||
throw new Error('$app/env has been renamed to $app/environment');
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
export { building, version } from '__sveltekit/environment';
|
||||
|
||||
/**
|
||||
* `true` if the app is running in the browser.
|
||||
*/
|
||||
export const browser = BROWSER;
|
||||
|
||||
/**
|
||||
* Whether the dev server is running. This is not guaranteed to correspond to `NODE_ENV` or `MODE`.
|
||||
*/
|
||||
export const dev = DEV;
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import * as devalue from 'devalue';
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import { client } from '../client/singletons.js';
|
||||
import { invalidateAll } from './navigation.js';
|
||||
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
export function applyAction(result) {
|
||||
if (BROWSER) {
|
||||
return client.apply_action(result);
|
||||
} else {
|
||||
throw new Error('Cannot call applyAction(...) on the server');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const parsed = JSON.parse(result);
|
||||
if (parsed.data) {
|
||||
parsed.data = devalue.parse(parsed.data);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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"');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* action: URL;
|
||||
* invalidateAll?: boolean;
|
||||
* result: import('@sveltejs/kit').ActionResult;
|
||||
* reset?: boolean
|
||||
* }} opts
|
||||
*/
|
||||
const fallback_callback = async ({
|
||||
action,
|
||||
result,
|
||||
reset = true,
|
||||
invalidateAll: shouldInvalidateAll = true
|
||||
}) => {
|
||||
if (result.type === 'success') {
|
||||
if (reset) {
|
||||
// We call reset from the prototype to avoid DOM clobbering
|
||||
HTMLFormElement.prototype.reset.call(form_element);
|
||||
}
|
||||
if (shouldInvalidateAll) {
|
||||
await invalidateAll();
|
||||
}
|
||||
}
|
||||
|
||||
// For success/failure results, only apply action if it belongs to the
|
||||
// current page, otherwise `form` will be updated erroneously
|
||||
if (
|
||||
location.origin + location.pathname === action.origin + action.pathname ||
|
||||
result.type === 'redirect' ||
|
||||
result.type === 'error'
|
||||
) {
|
||||
applyAction(result);
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {SubmitEvent} event */
|
||||
async function handle_submit(event) {
|
||||
const method = event.submitter?.hasAttribute('formmethod')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
|
||||
: clone(form_element).method;
|
||||
if (method !== 'post') return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const action = new URL(
|
||||
// We can't do submitter.formAction directly because that property is always set
|
||||
event.submitter?.hasAttribute('formaction')
|
||||
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
|
||||
: clone(form_element).action
|
||||
);
|
||||
|
||||
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) {
|
||||
form_data.append(submitter_name, event.submitter?.getAttribute('value') ?? '');
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
let cancelled = false;
|
||||
const cancel = () => (cancelled = true);
|
||||
|
||||
// TODO 2.0: Remove `data` and `form`
|
||||
const callback =
|
||||
(await submit({
|
||||
action,
|
||||
cancel,
|
||||
controller,
|
||||
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
|
||||
})) ?? fallback_callback;
|
||||
if (cancelled) return;
|
||||
|
||||
/** @type {import('@sveltejs/kit').ActionResult} */
|
||||
let result;
|
||||
|
||||
try {
|
||||
const response = await fetch(action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-sveltekit-action': 'true'
|
||||
},
|
||||
cache: 'no-store',
|
||||
body: form_data,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
result = deserialize(await response.text());
|
||||
if (result.type === 'error') result.status = response.status;
|
||||
} catch (error) {
|
||||
if (/** @type {any} */ (error)?.name === 'AbortError') return;
|
||||
result = { type: 'error', error };
|
||||
}
|
||||
|
||||
callback({
|
||||
action,
|
||||
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,
|
||||
invalidateAll: opts?.invalidateAll
|
||||
}),
|
||||
// @ts-expect-error generic constraints stuff we don't care about
|
||||
result
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
HTMLFormElement.prototype.addEventListener.call(form_element, 'submit', handle_submit);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
// @ts-expect-error
|
||||
HTMLFormElement.prototype.removeEventListener.call(form_element, 'submit', handle_submit);
|
||||
}
|
||||
};
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { client_method } from '../client/singletons.js';
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scroll_handling');
|
||||
|
||||
/**
|
||||
* 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 `navigation.type === 'leave'` — meaning the user is navigating away from the app (or closing the tab) — calling `cancel` will trigger the native browser unload confirmation dialog. In this case, the navigation may or may not be cancelled depending on the user's response.
|
||||
*
|
||||
* When a navigation isn't to a SvelteKit-owned route (and therefore controlled by SvelteKit's client-side router), `navigation.to.route.id` will be `null`.
|
||||
*
|
||||
* If the navigation will (if not cancelled) cause the document to unload — in other words `'leave'` navigations and `'link'` navigations where `navigation.to.route === null` — `navigation.willUnload` is `true`.
|
||||
*
|
||||
* `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` immediately before we navigate to a new URL except during full-page navigations.
|
||||
*
|
||||
* If you return a `Promise`, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use `document.startViewTransition`. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.
|
||||
*
|
||||
* If a function (or a `Promise` that resolves to a function) is returned from the callback, it will be called once the DOM has updated.
|
||||
*
|
||||
* `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
|
||||
* @type {(callback: (navigation: import('@sveltejs/kit').OnNavigate) => import('../../types/internal.js').MaybePromise<(() => void) | void>) => void}
|
||||
* @param {(navigation: import('@sveltejs/kit').OnNavigate) => void} callback
|
||||
* @returns {void}
|
||||
*/
|
||||
export const onNavigate = /* @__PURE__ */ client_method('on_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');
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
export { base, assets } from '__sveltekit/paths';
|
||||
import { base } from '__sveltekit/paths';
|
||||
import { get_route_segments } from '../../utils/routing.js';
|
||||
|
||||
const basic_param_pattern = /\[(\[)?(\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
|
||||
|
||||
/**
|
||||
* Populate a route ID with params to resolve a pathname.
|
||||
* @example
|
||||
* ```js
|
||||
* resolveRoute(
|
||||
* `/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 resolveRoute(id, params) {
|
||||
const segments = get_route_segments(id);
|
||||
return (
|
||||
base +
|
||||
'/' +
|
||||
segments
|
||||
.map((segment) =>
|
||||
segment.replace(basic_param_pattern, (_, optional, rest, name) => {
|
||||
const param_value = params[name];
|
||||
|
||||
// This is nested so TS correctly narrows the type
|
||||
if (!param_value) {
|
||||
if (optional) return '';
|
||||
if (rest && param_value !== undefined) 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('/')
|
||||
);
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { getContext } from 'svelte';
|
||||
import { browser } from './environment.js';
|
||||
import { stores as browser_stores } from '../client/singletons.js';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const getStores = () => {
|
||||
const stores = browser ? browser_stores : getContext('__svelte__');
|
||||
|
||||
return {
|
||||
/** @type {typeof page} */
|
||||
page: {
|
||||
subscribe: stores.page.subscribe
|
||||
},
|
||||
/** @type {typeof navigating} */
|
||||
navigating: {
|
||||
subscribe: stores.navigating.subscribe
|
||||
},
|
||||
/** @type {typeof updated} */
|
||||
updated: stores.updated
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
subscribe(fn) {
|
||||
const store = __SVELTEKIT_DEV__ ? get_store('page') : getStores().page;
|
||||
return store.subscribe(fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
subscribe(fn) {
|
||||
const store = __SVELTEKIT_DEV__ ? get_store('navigating') : getStores().navigating;
|
||||
return store.subscribe(fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
subscribe(fn) {
|
||||
const store = __SVELTEKIT_DEV__ ? get_store('updated') : getStores().updated;
|
||||
|
||||
if (browser) {
|
||||
updated.check = store.check;
|
||||
}
|
||||
|
||||
return store.subscribe(fn);
|
||||
},
|
||||
check: () => {
|
||||
throw new Error(
|
||||
browser
|
||||
? 'Cannot check updated store before subscribing'
|
||||
: 'Can only check updated store in browser'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @template {keyof ReturnType<typeof getStores>} Name
|
||||
* @param {Name} name
|
||||
* @returns {ReturnType<typeof getStores>[Name]}
|
||||
*/
|
||||
function get_store(name) {
|
||||
try {
|
||||
return getStores()[name];
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot subscribe to '${name}' store on the server outside of a Svelte component, as it is bound to the current request via component context. This prevents state from leaking between users.` +
|
||||
'For more information, see https://kit.svelte.dev/docs/state-management#avoid-shared-state-on-the-server'
|
||||
);
|
||||
}
|
||||
}
|
||||
+2108
File diff suppressed because it is too large
Load Diff
+12
@@ -0,0 +1,12 @@
|
||||
export const SNAPSHOT_KEY = 'sveltekit:snapshot';
|
||||
export const SCROLL_KEY = 'sveltekit:scroll';
|
||||
export const INDEX_KEY = 'sveltekit:index';
|
||||
|
||||
export const PRELOAD_PRIORITIES = /** @type {const} */ ({
|
||||
tap: 1,
|
||||
hover: 2,
|
||||
viewport: 3,
|
||||
eager: 4,
|
||||
off: -1,
|
||||
false: -1
|
||||
});
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { hash } from '../hash.js';
|
||||
|
||||
let loading = 0;
|
||||
|
||||
export const native_fetch = window.fetch;
|
||||
|
||||
export function lock_fetch() {
|
||||
loading += 1;
|
||||
}
|
||||
|
||||
export function unlock_fetch() {
|
||||
loading -= 1;
|
||||
}
|
||||
|
||||
if (DEV) {
|
||||
let can_inspect_stack_trace = false;
|
||||
|
||||
const check_stack_trace = async () => {
|
||||
const stack = /** @type {string} */ (new Error().stack);
|
||||
can_inspect_stack_trace = stack.includes('check_stack_trace');
|
||||
};
|
||||
|
||||
check_stack_trace();
|
||||
|
||||
/**
|
||||
* @param {RequestInfo | URL} input
|
||||
* @param {RequestInit & Record<string, any> | undefined} init
|
||||
*/
|
||||
window.fetch = (input, init) => {
|
||||
// Check if fetch was called via load_node. the lock method only checks if it was called at the
|
||||
// same time, but not necessarily if it was called from `load`.
|
||||
// We use just the filename as the method name sometimes does not appear on the CI.
|
||||
const url = input instanceof Request ? input.url : input.toString();
|
||||
const stack_array = /** @type {string} */ (new Error().stack).split('\n');
|
||||
// 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 cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load'));
|
||||
const stack = stack_array.slice(0, cutoff + 2).join('\n');
|
||||
|
||||
const in_load_heuristic = can_inspect_stack_trace
|
||||
? stack.includes('src/runtime/client/client.js')
|
||||
: loading;
|
||||
|
||||
// This flag is set in initial_fetch and subsequent_fetch
|
||||
const used_kit_fetch = init?.__sveltekit_fetch__;
|
||||
|
||||
if (in_load_heuristic && !used_kit_fetch) {
|
||||
console.warn(
|
||||
`Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://kit.svelte.dev/docs/load#making-fetch-requests`
|
||||
);
|
||||
}
|
||||
|
||||
const method = input instanceof Request ? input.method : init?.method || 'GET';
|
||||
|
||||
if (method !== 'GET') {
|
||||
cache.delete(build_selector(input));
|
||||
}
|
||||
|
||||
return native_fetch(input, init);
|
||||
};
|
||||
} else {
|
||||
window.fetch = (input, init) => {
|
||||
const method = input instanceof Request ? input.method : init?.method || 'GET';
|
||||
|
||||
if (method !== 'GET') {
|
||||
cache.delete(build_selector(input));
|
||||
}
|
||||
|
||||
return native_fetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {ArrayBufferLike}
|
||||
*/
|
||||
function b64_decode(text) {
|
||||
const d = atob(text);
|
||||
|
||||
const u8 = new Uint8Array(d.length);
|
||||
|
||||
for (let i = 0; i < d.length; i++) {
|
||||
u8[i] = d.charCodeAt(i);
|
||||
}
|
||||
|
||||
return u8.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on the initial run of load functions that hydrate the page.
|
||||
* Saves any requests with cache-control max-age to the cache.
|
||||
* @param {URL | string} resource
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
export function initial_fetch(resource, opts) {
|
||||
const selector = build_selector(resource, opts);
|
||||
|
||||
const script = document.querySelector(selector);
|
||||
if (script?.textContent) {
|
||||
let { body, ...init } = JSON.parse(script.textContent);
|
||||
|
||||
const ttl = script.getAttribute('data-ttl');
|
||||
if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
|
||||
const b64 = script.getAttribute('data-b64');
|
||||
if (b64 !== null) {
|
||||
// Can't use native_fetch('data:...;base64,${body}')
|
||||
// csp can block the request
|
||||
body = b64_decode(body);
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(body, init));
|
||||
}
|
||||
|
||||
return DEV ? dev_fetch(resource, opts) : window.fetch(resource, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get the response from the cache, if max-age allows it, else does a fetch.
|
||||
* @param {URL | string} resource
|
||||
* @param {string} resolved
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
export function subsequent_fetch(resource, resolved, opts) {
|
||||
if (cache.size > 0) {
|
||||
const selector = build_selector(resource, opts);
|
||||
const cached = cache.get(selector);
|
||||
if (cached) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/cache#value
|
||||
if (
|
||||
performance.now() < cached.ttl &&
|
||||
['default', 'force-cache', 'only-if-cached', undefined].includes(opts?.cache)
|
||||
) {
|
||||
return new Response(cached.body, cached.init);
|
||||
}
|
||||
|
||||
cache.delete(selector);
|
||||
}
|
||||
}
|
||||
|
||||
return DEV ? dev_fetch(resolved, opts) : window.fetch(resolved, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RequestInfo | URL} resource
|
||||
* @param {RequestInit & Record<string, any> | undefined} opts
|
||||
*/
|
||||
function dev_fetch(resource, opts) {
|
||||
const patched_opts = { ...opts };
|
||||
// This assigns the __sveltekit_fetch__ flag and makes it non-enumerable
|
||||
Object.defineProperty(patched_opts, '__sveltekit_fetch__', {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
return window.fetch(resource, patched_opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the cache key for a given request
|
||||
* @param {URL | RequestInfo} resource
|
||||
* @param {RequestInit} [opts]
|
||||
*/
|
||||
function build_selector(resource, opts) {
|
||||
const url = JSON.stringify(resource instanceof Request ? resource.url : resource);
|
||||
|
||||
let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
|
||||
|
||||
if (opts?.headers || opts?.body) {
|
||||
/** @type {import('types').StrictBody[]} */
|
||||
const values = [];
|
||||
|
||||
if (opts.headers) {
|
||||
values.push([...new Headers(opts.headers)].join(','));
|
||||
}
|
||||
|
||||
if (opts.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
|
||||
values.push(opts.body);
|
||||
}
|
||||
|
||||
selector += `[data-hash="${hash(...values)}"]`;
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { exec, parse_route_id } from '../../utils/routing.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types.js').SvelteKitApp} app
|
||||
* @returns {import('types').CSRRoute[]}
|
||||
*/
|
||||
export function parse({ nodes, server_loads, dictionary, matchers }) {
|
||||
const layouts_with_server_load = new Set(server_loads);
|
||||
|
||||
return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
|
||||
const { pattern, params } = parse_route_id(id);
|
||||
|
||||
const route = {
|
||||
id,
|
||||
/** @param {string} path */
|
||||
exec: (path) => {
|
||||
const match = pattern.exec(path);
|
||||
if (match) return exec(match, params, matchers);
|
||||
},
|
||||
errors: [1, ...(errors || [])].map((n) => nodes[n]),
|
||||
layouts: [0, ...(layouts || [])].map(create_layout_loader),
|
||||
leaf: create_leaf_loader(leaf)
|
||||
};
|
||||
|
||||
// bit of a hack, but ensures that layout/error node lists are the same
|
||||
// length, without which the wrong data will be applied if the route
|
||||
// manifest looks like `[[a, b], [c,], d]`
|
||||
route.errors.length = route.layouts.length = Math.max(
|
||||
route.errors.length,
|
||||
route.layouts.length
|
||||
);
|
||||
|
||||
return route;
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {[boolean, import('types').CSRPageNodeLoader]}
|
||||
*/
|
||||
function create_leaf_loader(id) {
|
||||
// whether or not the route uses the server data is
|
||||
// encoded using the ones' complement, to save space
|
||||
const uses_server_data = id < 0;
|
||||
if (uses_server_data) id = ~id;
|
||||
return [uses_server_data, nodes[id]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number | undefined} id
|
||||
* @returns {[boolean, import('types').CSRPageNodeLoader] | undefined}
|
||||
*/
|
||||
function create_layout_loader(id) {
|
||||
// whether or not the layout uses the server data is
|
||||
// encoded in the layouts array, to save space
|
||||
return id === undefined ? id : [layouts_with_server_load.has(id), nodes[id]];
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Read a value from `sessionStorage`
|
||||
* @param {string} key
|
||||
*/
|
||||
export function get(key) {
|
||||
try {
|
||||
return JSON.parse(sessionStorage[key]);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to `sessionStorage`
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
*/
|
||||
export function set(key, value) {
|
||||
const json = JSON.stringify(value);
|
||||
try {
|
||||
sessionStorage[key] = json;
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { create_updated_store, notifiable_store } from './utils.js';
|
||||
import { BROWSER } from 'esm-env';
|
||||
|
||||
/** @type {import('./types.js').Client} */
|
||||
export let client;
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* client: import('./types.js').Client;
|
||||
* }} opts
|
||||
*/
|
||||
export function init(opts) {
|
||||
client = opts.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' || key === 'on_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: /* @__PURE__ */ notifiable_store({}),
|
||||
page: /* @__PURE__ */ notifiable_store({}),
|
||||
navigating: /* @__PURE__ */ writable(
|
||||
/** @type {import('@sveltejs/kit').Navigation | null} */ (null)
|
||||
),
|
||||
updated: /* @__PURE__ */ create_updated_store()
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { create_client } from './client.js';
|
||||
import { init } from './singletons.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types.js').SvelteKitApp} app
|
||||
* @param {HTMLElement} target
|
||||
* @param {Parameters<import('./types.js').Client['_hydrate']>[0]} [hydrate]
|
||||
*/
|
||||
export async function start(app, target, hydrate) {
|
||||
if (DEV && target === document.body) {
|
||||
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>'
|
||||
);
|
||||
}
|
||||
|
||||
const client = create_client(app, target);
|
||||
|
||||
init({ client });
|
||||
|
||||
if (hydrate) {
|
||||
await client._hydrate(hydrate);
|
||||
} else {
|
||||
client.goto(location.href, { replaceState: true });
|
||||
}
|
||||
|
||||
client._start_router();
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { applyAction } from '../app/forms.js';
|
||||
import {
|
||||
afterNavigate,
|
||||
beforeNavigate,
|
||||
onNavigate,
|
||||
goto,
|
||||
invalidate,
|
||||
invalidateAll,
|
||||
preloadCode,
|
||||
preloadData
|
||||
} from '../app/navigation.js';
|
||||
import { SvelteComponent } from 'svelte';
|
||||
import { ClientHooks, CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types';
|
||||
import { Page, ParamMatcher } from '@sveltejs/kit';
|
||||
|
||||
export interface SvelteKitApp {
|
||||
/**
|
||||
* A list of all the error/layout/page nodes used in the app
|
||||
*/
|
||||
nodes: CSRPageNodeLoader[];
|
||||
|
||||
/**
|
||||
* A list of all layout node ids that have a server load function.
|
||||
* Pages are not present because it's shorter to encode it on the leaf itself.
|
||||
*/
|
||||
server_loads: number[];
|
||||
|
||||
/**
|
||||
* A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which
|
||||
* is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`.
|
||||
* If the leaf number is negative, it means it does use a server load function and the complement is the node index.
|
||||
* The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply.
|
||||
*/
|
||||
dictionary: Record<string, [leaf: number, layouts: number[], errors?: number[]]>;
|
||||
|
||||
matchers: Record<string, ParamMatcher>;
|
||||
|
||||
hooks: ClientHooks;
|
||||
|
||||
root: typeof SvelteComponent;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
// public API, exposed via $app/navigation
|
||||
after_navigate: typeof afterNavigate;
|
||||
before_navigate: typeof beforeNavigate;
|
||||
on_navigate: typeof onNavigate;
|
||||
disable_scroll_handling(): void;
|
||||
goto: typeof goto;
|
||||
invalidate: typeof invalidate;
|
||||
invalidate_all: typeof invalidateAll;
|
||||
preload_code: typeof preloadCode;
|
||||
preload_data: typeof preloadData;
|
||||
apply_action: typeof applyAction;
|
||||
|
||||
// private API
|
||||
_hydrate(opts: {
|
||||
status: number;
|
||||
error: App.Error | null;
|
||||
node_ids: number[];
|
||||
params: Record<string, string>;
|
||||
route: { id: string | null };
|
||||
data: Array<import('types').ServerDataNode | null>;
|
||||
form: Record<string, any> | null;
|
||||
}): Promise<void>;
|
||||
_start_router(): void;
|
||||
}
|
||||
|
||||
export type NavigationIntent = {
|
||||
/** `url.pathname + url.search` */
|
||||
id: string;
|
||||
/** Whether we are invalidating or navigating */
|
||||
invalidating: boolean;
|
||||
/** The route parameters */
|
||||
params: Record<string, string>;
|
||||
/** The route that matches `path` */
|
||||
route: CSRRoute;
|
||||
/** The destination URL */
|
||||
url: URL;
|
||||
};
|
||||
|
||||
export type NavigationResult = NavigationRedirect | NavigationFinished;
|
||||
|
||||
export type NavigationRedirect = {
|
||||
type: 'redirect';
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type NavigationFinished = {
|
||||
type: 'loaded';
|
||||
state: NavigationState;
|
||||
props: {
|
||||
constructors: Array<typeof SvelteComponent>;
|
||||
components?: Array<SvelteComponent>;
|
||||
page?: Page;
|
||||
form?: Record<string, any> | null;
|
||||
[key: `data_${number}`]: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
export type BranchNode = {
|
||||
node: CSRPageNode;
|
||||
loader: CSRPageNodeLoader;
|
||||
server: DataNode | null;
|
||||
universal: DataNode | null;
|
||||
data: Record<string, any> | null;
|
||||
slash?: TrailingSlash;
|
||||
};
|
||||
|
||||
export interface DataNode {
|
||||
type: 'data';
|
||||
data: Record<string, any> | null;
|
||||
uses: Uses;
|
||||
slash?: TrailingSlash;
|
||||
}
|
||||
|
||||
export interface NavigationState {
|
||||
branch: Array<BranchNode | undefined>;
|
||||
error: App.Error | null;
|
||||
params: Record<string, string>;
|
||||
route: CSRRoute | null;
|
||||
url: URL;
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
import { BROWSER, DEV } from 'esm-env';
|
||||
import { writable } from 'svelte/store';
|
||||
import { assets } from '__sveltekit/paths';
|
||||
import { version } from '__sveltekit/environment';
|
||||
import { PRELOAD_PRIORITIES } from './constants.js';
|
||||
|
||||
/* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */
|
||||
|
||||
export const origin = BROWSER ? location.origin : '';
|
||||
|
||||
/** @param {HTMLDocument} doc */
|
||||
export function get_base_uri(doc) {
|
||||
let baseURI = doc.baseURI;
|
||||
|
||||
if (!baseURI) {
|
||||
const baseTags = doc.getElementsByTagName('base');
|
||||
baseURI = baseTags.length ? baseTags[0].href : doc.URL;
|
||||
}
|
||||
|
||||
return baseURI;
|
||||
}
|
||||
|
||||
export function scroll_state() {
|
||||
return {
|
||||
x: pageXOffset,
|
||||
y: pageYOffset
|
||||
};
|
||||
}
|
||||
|
||||
const warned = new WeakSet();
|
||||
|
||||
/** @typedef {keyof typeof valid_link_options} LinkOptionName */
|
||||
|
||||
const valid_link_options = /** @type {const} */ ({
|
||||
'preload-code': ['', 'off', 'false', 'tap', 'hover', 'viewport', 'eager'],
|
||||
'preload-data': ['', 'off', 'false', 'tap', 'hover'],
|
||||
keepfocus: ['', 'true', 'off', 'false'],
|
||||
noscroll: ['', 'true', 'off', 'false'],
|
||||
reload: ['', 'true', 'off', 'false'],
|
||||
replacestate: ['', 'true', 'off', 'false']
|
||||
});
|
||||
|
||||
/**
|
||||
* @template {LinkOptionName} T
|
||||
* @typedef {typeof valid_link_options[T][number]} ValidLinkOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {LinkOptionName} T
|
||||
* @param {Element} element
|
||||
* @param {T} name
|
||||
*/
|
||||
function link_option(element, name) {
|
||||
const value = /** @type {ValidLinkOptions<T> | null} */ (
|
||||
element.getAttribute(`data-sveltekit-${name}`)
|
||||
);
|
||||
|
||||
if (DEV) {
|
||||
validate_link_option(element, name, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {LinkOptionName} T
|
||||
* @template {ValidLinkOptions<T> | null} U
|
||||
* @param {Element} element
|
||||
* @param {T} name
|
||||
* @param {U} value
|
||||
*/
|
||||
function validate_link_option(element, name, value) {
|
||||
if (value === null) return;
|
||||
|
||||
// @ts-expect-error - includes is dumb
|
||||
if (!warned.has(element) && !valid_link_options[name].includes(value)) {
|
||||
console.error(
|
||||
`Unexpected value for ${name} — should be one of ${valid_link_options[name]
|
||||
.map((option) => JSON.stringify(option))
|
||||
.join(', ')}`,
|
||||
element
|
||||
);
|
||||
|
||||
warned.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
const levels = {
|
||||
...PRELOAD_PRIORITIES,
|
||||
'': PRELOAD_PRIORITIES.hover
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @returns {Element | null}
|
||||
*/
|
||||
function parent_element(element) {
|
||||
let parent = element.assignedSlot ?? element.parentNode;
|
||||
|
||||
// @ts-expect-error handle shadow roots
|
||||
if (parent?.nodeType === 11) parent = parent.host;
|
||||
|
||||
return /** @type {Element} */ (parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @param {Element} target
|
||||
*/
|
||||
export function find_anchor(element, target) {
|
||||
while (element && element !== target) {
|
||||
if (element.nodeName.toUpperCase() === 'A' && element.hasAttribute('href')) {
|
||||
return /** @type {HTMLAnchorElement | SVGAElement} */ (element);
|
||||
}
|
||||
|
||||
element = /** @type {Element} */ (parent_element(element));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLAnchorElement | SVGAElement} a
|
||||
* @param {string} base
|
||||
*/
|
||||
export function get_link_info(a, base) {
|
||||
/** @type {URL | undefined} */
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
|
||||
} catch {}
|
||||
|
||||
const target = a instanceof SVGAElement ? a.target.baseVal : a.target;
|
||||
|
||||
const external =
|
||||
!url ||
|
||||
!!target ||
|
||||
is_external_url(url, base) ||
|
||||
(a.getAttribute('rel') || '').split(/\s+/).includes('external');
|
||||
|
||||
const download = url?.origin === origin && a.hasAttribute('download');
|
||||
|
||||
return { url, external, target, download };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLFormElement | HTMLAnchorElement | SVGAElement} element
|
||||
*/
|
||||
export function get_router_options(element) {
|
||||
/** @type {ValidLinkOptions<'keepfocus'> | null} */
|
||||
let keep_focus = null;
|
||||
|
||||
/** @type {ValidLinkOptions<'noscroll'> | null} */
|
||||
let noscroll = null;
|
||||
|
||||
/** @type {ValidLinkOptions<'preload-code'> | null} */
|
||||
let preload_code = null;
|
||||
|
||||
/** @type {ValidLinkOptions<'preload-data'> | null} */
|
||||
let preload_data = null;
|
||||
|
||||
/** @type {ValidLinkOptions<'reload'> | null} */
|
||||
let reload = null;
|
||||
|
||||
/** @type {ValidLinkOptions<'replacestate'> | null} */
|
||||
let replace_state = null;
|
||||
|
||||
/** @type {Element} */
|
||||
let el = element;
|
||||
|
||||
while (el && el !== document.documentElement) {
|
||||
if (preload_code === null) preload_code = link_option(el, 'preload-code');
|
||||
if (preload_data === null) preload_data = link_option(el, 'preload-data');
|
||||
if (keep_focus === null) keep_focus = link_option(el, 'keepfocus');
|
||||
if (noscroll === null) noscroll = link_option(el, 'noscroll');
|
||||
if (reload === null) reload = link_option(el, 'reload');
|
||||
if (replace_state === null) replace_state = link_option(el, 'replacestate');
|
||||
|
||||
el = /** @type {Element} */ (parent_element(el));
|
||||
}
|
||||
|
||||
/** @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: get_option_state(keep_focus),
|
||||
noscroll: get_option_state(noscroll),
|
||||
reload: get_option_state(reload),
|
||||
replace_state: get_option_state(replace_state)
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {any} value */
|
||||
export function notifiable_store(value) {
|
||||
const store = writable(value);
|
||||
let ready = true;
|
||||
|
||||
function notify() {
|
||||
ready = true;
|
||||
store.update((val) => val);
|
||||
}
|
||||
|
||||
/** @param {any} new_value */
|
||||
function set(new_value) {
|
||||
ready = false;
|
||||
store.set(new_value);
|
||||
}
|
||||
|
||||
/** @param {(value: any) => void} run */
|
||||
function subscribe(run) {
|
||||
/** @type {any} */
|
||||
let old_value;
|
||||
return store.subscribe((new_value) => {
|
||||
if (old_value === undefined || (ready && new_value !== old_value)) {
|
||||
run((old_value = new_value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { notify, set, subscribe };
|
||||
}
|
||||
|
||||
export function create_updated_store() {
|
||||
const { set, subscribe } = writable(false);
|
||||
|
||||
if (DEV || !BROWSER) {
|
||||
return {
|
||||
subscribe,
|
||||
check: async () => false
|
||||
};
|
||||
}
|
||||
|
||||
const interval = __SVELTEKIT_APP_VERSION_POLL_INTERVAL__;
|
||||
|
||||
/** @type {NodeJS.Timeout} */
|
||||
let timeout;
|
||||
|
||||
/** @type {() => Promise<boolean>} */
|
||||
async function check() {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (interval) timeout = setTimeout(check, interval);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${assets}/${__SVELTEKIT_APP_VERSION_FILE__}`, {
|
||||
headers: {
|
||||
pragma: 'no-cache',
|
||||
'cache-control': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const updated = data.version !== version;
|
||||
|
||||
if (updated) {
|
||||
set(true);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
return updated;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (interval) timeout = setTimeout(check, interval);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
check
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {string} base
|
||||
*/
|
||||
export function is_external_url(url, base) {
|
||||
return url.origin !== origin || !url.pathname.startsWith(base);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<h1>{$page.status}</h1>
|
||||
<p>{$page.error?.message}</p>
|
||||
+1
@@ -0,0 +1 @@
|
||||
<slot />
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
export class HttpError {
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body
|
||||
*/
|
||||
constructor(status, body) {
|
||||
this.status = status;
|
||||
if (typeof body === 'string') {
|
||||
this.body = { message: body };
|
||||
} else if (body) {
|
||||
this.body = body;
|
||||
} else {
|
||||
this.body = { message: `Error: ${status}` };
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return JSON.stringify(this.body);
|
||||
}
|
||||
}
|
||||
|
||||
export class Redirect {
|
||||
/**
|
||||
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status
|
||||
* @param {string} location
|
||||
*/
|
||||
constructor(status, location) {
|
||||
this.status = status;
|
||||
this.location = location;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFound extends Error {
|
||||
/**
|
||||
* @param {string} pathname
|
||||
*/
|
||||
constructor(pathname) {
|
||||
super();
|
||||
|
||||
this.status = 404;
|
||||
this.message = `Not found: ${pathname}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Record<string, unknown> | undefined} [T=undefined]
|
||||
*/
|
||||
export class ActionFailure {
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {T} [data]
|
||||
*/
|
||||
constructor(status, data) {
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a grotesque hack that, in dev, allows us to replace the implementations
|
||||
* of these classes that you'd get by importing them from `@sveltejs/kit` with the
|
||||
* ones that are imported via Vite and loaded internally, so that instanceof
|
||||
* checks work even though SvelteKit imports this module via Vite and consumers
|
||||
* import it via Node
|
||||
* @param {{
|
||||
* ActionFailure: typeof ActionFailure;
|
||||
* HttpError: typeof HttpError;
|
||||
* Redirect: typeof Redirect;
|
||||
* }} implementations
|
||||
*/
|
||||
export function replace_implementations(implementations) {
|
||||
// @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
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { private_env as env } from '../../shared-server.js';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { public_env as env } from '../../shared-server.js';
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Hash using djb2
|
||||
* @param {import('types').StrictBody[]} values
|
||||
*/
|
||||
export function hash(...values) {
|
||||
let hash = 5381;
|
||||
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string') {
|
||||
let i = value.length;
|
||||
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
|
||||
} else if (ArrayBuffer.isView(value)) {
|
||||
const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
||||
let i = buffer.length;
|
||||
while (i) hash = (hash * 33) ^ buffer[--i];
|
||||
} else {
|
||||
throw new TypeError('value must be a string or TypedArray');
|
||||
}
|
||||
}
|
||||
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
declare module '__SERVER__/internal.js' {
|
||||
export const options: import('types').SSROptions;
|
||||
export const get_hooks: () => Promise<{
|
||||
handle?: import('@sveltejs/kit').Handle;
|
||||
handleError?: import('@sveltejs/kit').HandleServerError;
|
||||
handleFetch?: import('@sveltejs/kit').HandleFetch;
|
||||
}>;
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import { parse, serialize } from 'cookie';
|
||||
import { normalize_path, resolve } from '../../utils/url.js';
|
||||
import { warn_with_callsite } from './utils.js';
|
||||
|
||||
/**
|
||||
* Tracks all cookies set during dev mode so we can emit warnings
|
||||
* when we detect that there's likely cookie misusage due to wrong paths
|
||||
*
|
||||
* @type {Record<string, Set<string>>} */
|
||||
const cookie_paths = {};
|
||||
|
||||
/**
|
||||
* Cookies that are larger than this size (including the name and other
|
||||
* attributes) are discarded by browsers
|
||||
*/
|
||||
const MAX_COOKIE_SIZE = 4129;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('cookie').CookieSerializeOptions} opts
|
||||
* @param {'set' | 'delete' | 'serialize'} method
|
||||
*/
|
||||
function deprecate_missing_path(opts, method) {
|
||||
if (opts.path === undefined) {
|
||||
warn_with_callsite(
|
||||
`Calling \`cookies.${method}(...)\` without specifying a \`path\` is deprecated, and will be disallowed in SvelteKit 2.0. Relative paths can be used`,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.path === '') {
|
||||
warn_with_callsite(
|
||||
`Calling \`cookies.${method}(...)\` with \`path: ''\` will behave differently in SvelteKit 2.0. Instead of using the browser default behaviour, it will set the cookie path to the current pathname`,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {URL} url
|
||||
* @param {import('types').TrailingSlash} trailing_slash
|
||||
*/
|
||||
export function get_cookies(request, url, trailing_slash) {
|
||||
const header = request.headers.get('cookie') ?? '';
|
||||
const initial_cookies = parse(header, { decode: (value) => value });
|
||||
|
||||
const normalized_url = normalize_path(url.pathname, trailing_slash);
|
||||
// Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo'
|
||||
const default_path = normalized_url.split('/').slice(0, -1).join('/') || '/';
|
||||
|
||||
/** @type {Record<string, import('./page/types.js').Cookie>} */
|
||||
const new_cookies = {};
|
||||
|
||||
/** @type {import('cookie').CookieSerializeOptions} */
|
||||
const defaults = {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: url.hostname === 'localhost' && url.protocol === 'http:' ? false : true
|
||||
};
|
||||
|
||||
/** @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('@sveltejs/kit').Cookies}` above is not
|
||||
// sufficient to do so.
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {import('cookie').CookieParseOptions} opts
|
||||
*/
|
||||
get(name, opts) {
|
||||
const c = new_cookies[name];
|
||||
if (
|
||||
c &&
|
||||
domain_matches(url.hostname, c.options.domain) &&
|
||||
path_matches(url.pathname, c.options.path)
|
||||
) {
|
||||
return c.value;
|
||||
}
|
||||
|
||||
const decoder = opts?.decode || decodeURIComponent;
|
||||
const req_cookies = parse(header, { decode: decoder });
|
||||
const cookie = req_cookies[name]; // the decoded string or undefined
|
||||
|
||||
// in development, if the cookie was set during this session with `cookies.set`,
|
||||
// but at a different path, warn the user. (ignore cookies from request headers,
|
||||
// since we don't know which path they were set at)
|
||||
if (__SVELTEKIT_DEV__ && !cookie) {
|
||||
const paths = Array.from(cookie_paths[name] ?? []).filter((path) => {
|
||||
// we only care about paths that are _more_ specific than the current path
|
||||
return path_matches(path, url.pathname) && path !== url.pathname;
|
||||
});
|
||||
|
||||
if (paths.length > 0) {
|
||||
console.warn(
|
||||
// prettier-ignore
|
||||
`'${name}' cookie does not exist for ${url.pathname}, but was previously set at ${conjoin([...paths])}. Did you mean to set its 'path' to '/' instead?`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return cookie;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {import('cookie').CookieParseOptions} opts
|
||||
*/
|
||||
getAll(opts) {
|
||||
const decoder = opts?.decode || decodeURIComponent;
|
||||
const cookies = parse(header, { decode: decoder });
|
||||
|
||||
for (const c of Object.values(new_cookies)) {
|
||||
if (
|
||||
domain_matches(url.hostname, c.options.domain) &&
|
||||
path_matches(url.pathname, c.options.path)
|
||||
) {
|
||||
cookies[c.name] = c.value;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(cookies).map(([name, value]) => ({ name, value }));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {import('cookie').CookieSerializeOptions} opts
|
||||
*/
|
||||
set(name, value, opts = {}) {
|
||||
deprecate_missing_path(opts, 'set');
|
||||
set_internal(name, value, { ...defaults, ...opts });
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {import('cookie').CookieSerializeOptions} opts
|
||||
*/
|
||||
delete(name, opts = {}) {
|
||||
deprecate_missing_path(opts, 'delete');
|
||||
|
||||
cookies.set(name, '', {
|
||||
path: default_path, // TODO 2.0 remove this
|
||||
...opts,
|
||||
maxAge: 0
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {import('cookie').CookieSerializeOptions} opts
|
||||
*/
|
||||
serialize(name, value, opts = {}) {
|
||||
deprecate_missing_path(opts, 'serialize');
|
||||
|
||||
return serialize(name, value, {
|
||||
...defaults,
|
||||
...opts
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {URL} destination
|
||||
* @param {string | null} header
|
||||
*/
|
||||
function get_cookie_header(destination, header) {
|
||||
/** @type {Record<string, string>} */
|
||||
const combined_cookies = {
|
||||
// cookies sent by the user agent have lowest precedence
|
||||
...initial_cookies
|
||||
};
|
||||
|
||||
// cookies previous set during this event with cookies.set have higher precedence
|
||||
for (const key in new_cookies) {
|
||||
const cookie = new_cookies[key];
|
||||
if (!domain_matches(destination.hostname, cookie.options.domain)) continue;
|
||||
if (!path_matches(destination.pathname, cookie.options.path)) continue;
|
||||
|
||||
const encoder = cookie.options.encode || encodeURIComponent;
|
||||
combined_cookies[cookie.name] = encoder(cookie.value);
|
||||
}
|
||||
|
||||
// explicit header has highest precedence
|
||||
if (header) {
|
||||
const parsed = parse(header, { decode: (value) => value });
|
||||
for (const name in parsed) {
|
||||
combined_cookies[name] = parsed[name];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(combined_cookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} value
|
||||
* @param {import('cookie').CookieSerializeOptions} opts
|
||||
*/
|
||||
function set_internal(name, value, opts) {
|
||||
let path = opts.path;
|
||||
|
||||
if (!opts.domain || opts.domain === url.hostname) {
|
||||
if (path) {
|
||||
if (path[0] === '.') path = resolve(url.pathname, path);
|
||||
} else {
|
||||
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) {
|
||||
// @ts-expect-error temporary
|
||||
cookie_paths[name].delete(path);
|
||||
} else {
|
||||
// @ts-expect-error temporary
|
||||
cookie_paths[name].add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { cookies, new_cookies, get_cookie_header, set_internal };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hostname
|
||||
* @param {string} [constraint]
|
||||
*/
|
||||
export function domain_matches(hostname, constraint) {
|
||||
if (!constraint) return true;
|
||||
|
||||
const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint;
|
||||
|
||||
if (hostname === normalized) return true;
|
||||
return hostname.endsWith('.' + normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {string} [constraint]
|
||||
*/
|
||||
export function path_matches(path, constraint) {
|
||||
if (!constraint) return true;
|
||||
|
||||
const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint;
|
||||
|
||||
if (path === normalized) return true;
|
||||
return path.startsWith(normalized + '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Headers} headers
|
||||
* @param {import('./page/types.js').Cookie[]} cookies
|
||||
*/
|
||||
export function add_cookies_to_headers(headers, cookies) {
|
||||
for (const new_cookie of cookies) {
|
||||
const { name, value, options } = new_cookie;
|
||||
headers.append('set-cookie', serialize(name, value, options));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} array
|
||||
*/
|
||||
function conjoin(array) {
|
||||
if (array.length <= 2) return array.join(' and ');
|
||||
return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`;
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
import { HttpError, Redirect } from '../../control.js';
|
||||
import { normalize_error } from '../../../utils/error.js';
|
||||
import { once } from '../../../utils/functions.js';
|
||||
import { load_server_data } from '../page/load_data.js';
|
||||
import { clarify_devalue_error, handle_error_and_jsonify, stringify_uses } from '../utils.js';
|
||||
import { normalize_path } from '../../../utils/url.js';
|
||||
import { text } from '../../../exports/index.js';
|
||||
import * as devalue from 'devalue';
|
||||
import { create_async_iterator } from '../../../utils/streaming.js';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSRRoute} route
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {import('@sveltejs/kit').SSRManifest} manifest
|
||||
* @param {import('types').SSRState} state
|
||||
* @param {boolean[] | undefined} invalidated_data_nodes
|
||||
* @param {import('types').TrailingSlash} trailing_slash
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function render_data(
|
||||
event,
|
||||
route,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
invalidated_data_nodes,
|
||||
trailing_slash
|
||||
) {
|
||||
if (!route.page) {
|
||||
// requesting /__data.json should fail for a +server.js
|
||||
return new Response(undefined, {
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const node_ids = [...route.page.layouts, route.page.leaf];
|
||||
const invalidated = invalidated_data_nodes ?? node_ids.map(() => true);
|
||||
|
||||
let aborted = false;
|
||||
|
||||
const url = new URL(event.url);
|
||||
url.pathname = normalize_path(url.pathname, trailing_slash);
|
||||
|
||||
const new_event = { ...event, url };
|
||||
|
||||
const functions = node_ids.map((n, i) => {
|
||||
return once(async () => {
|
||||
try {
|
||||
if (aborted) {
|
||||
return /** @type {import('types').ServerDataSkippedNode} */ ({
|
||||
type: 'skip'
|
||||
});
|
||||
}
|
||||
|
||||
// == because it could be undefined (in dev) or null (in build, because of JSON.stringify)
|
||||
const node = n == undefined ? n : await manifest._.nodes[n]();
|
||||
// load this. for the child, return as is. for the final result, stream things
|
||||
return load_server_data({
|
||||
event: new_event,
|
||||
state,
|
||||
node,
|
||||
parent: async () => {
|
||||
/** @type {Record<string, any>} */
|
||||
const data = {};
|
||||
for (let j = 0; j < i; j += 1) {
|
||||
const parent = /** @type {import('types').ServerDataNode | null} */ (
|
||||
await functions[j]()
|
||||
);
|
||||
|
||||
if (parent) {
|
||||
Object.assign(data, parent.data);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
track_server_fetches: options.track_server_fetches
|
||||
});
|
||||
} catch (e) {
|
||||
aborted = true;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const promises = functions.map(async (fn, i) => {
|
||||
if (!invalidated[i]) {
|
||||
return /** @type {import('types').ServerDataSkippedNode} */ ({
|
||||
type: 'skip'
|
||||
});
|
||||
}
|
||||
|
||||
return fn();
|
||||
});
|
||||
|
||||
let length = promises.length;
|
||||
const nodes = await Promise.all(
|
||||
promises.map((p, i) =>
|
||||
p.catch(async (error) => {
|
||||
if (error instanceof Redirect) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Math.min because array isn't guaranteed to resolve in order
|
||||
length = Math.min(length, i + 1);
|
||||
|
||||
return /** @type {import('types').ServerErrorNode} */ ({
|
||||
type: 'error',
|
||||
error: await handle_error_and_jsonify(event, options, error),
|
||||
status: error instanceof HttpError ? error.status : undefined
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const { data, chunks } = get_data_json(event, options, nodes);
|
||||
|
||||
if (!chunks) {
|
||||
// use a normal JSON response where possible, so we get `content-length`
|
||||
// and can use browser JSON devtools for easier inspecting
|
||||
return json_response(data);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
controller.enqueue(encoder.encode(data));
|
||||
for await (const chunk of chunks) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
|
||||
type: 'bytes'
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
// we use a proprietary content type to prevent buffering.
|
||||
// the `text` prefix makes it inspectable
|
||||
'content-type': 'text/sveltekit-data',
|
||||
'cache-control': 'private, no-store'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
const error = normalize_error(e);
|
||||
|
||||
if (error instanceof Redirect) {
|
||||
return redirect_json_response(error);
|
||||
} else {
|
||||
return json_response(await handle_error_and_jsonify(event, options, error), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any> | string} json
|
||||
* @param {number} [status]
|
||||
*/
|
||||
function json_response(json, status = 200) {
|
||||
return text(typeof json === 'string' ? json : JSON.stringify(json), {
|
||||
status,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'cache-control': 'private, no-store'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Redirect} redirect
|
||||
*/
|
||||
export function redirect_json_response(redirect) {
|
||||
return json_response({
|
||||
type: 'redirect',
|
||||
location: redirect.location
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the serialized data contains promises, `chunks` will be an
|
||||
* async iterable containing their resolutions
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {Array<import('types').ServerDataSkippedNode | import('types').ServerDataNode | import('types').ServerErrorNode | null | undefined>} nodes
|
||||
* @returns {{ data: string, chunks: AsyncIterable<string> | null }}
|
||||
*/
|
||||
export function get_data_json(event, options, nodes) {
|
||||
let promise_id = 1;
|
||||
let count = 0;
|
||||
|
||||
const { iterator, push, done } = create_async_iterator();
|
||||
|
||||
const reducers = {
|
||||
/** @param {any} thing */
|
||||
Promise: (thing) => {
|
||||
if (typeof thing?.then === 'function') {
|
||||
const id = promise_id++;
|
||||
count += 1;
|
||||
|
||||
/** @type {'data' | 'error'} */
|
||||
let key = 'data';
|
||||
|
||||
thing
|
||||
.catch(
|
||||
/** @param {any} e */ async (e) => {
|
||||
key = 'error';
|
||||
return handle_error_and_jsonify(event, options, /** @type {any} */ (e));
|
||||
}
|
||||
)
|
||||
.then(
|
||||
/** @param {any} value */
|
||||
async (value) => {
|
||||
let str;
|
||||
try {
|
||||
str = devalue.stringify(value, reducers);
|
||||
} catch (e) {
|
||||
const error = await handle_error_and_jsonify(
|
||||
event,
|
||||
options,
|
||||
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
|
||||
);
|
||||
|
||||
key = 'error';
|
||||
str = devalue.stringify(error, reducers);
|
||||
}
|
||||
|
||||
count -= 1;
|
||||
|
||||
push(`{"type":"chunk","id":${id},"${key}":${str}}\n`);
|
||||
if (count === 0) done();
|
||||
}
|
||||
);
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const strings = nodes.map((node) => {
|
||||
if (!node) return 'null';
|
||||
|
||||
if (node.type === 'error' || node.type === 'skip') {
|
||||
return JSON.stringify(node);
|
||||
}
|
||||
|
||||
return `{"type":"data","data":${devalue.stringify(node.data, reducers)},${stringify_uses(
|
||||
node
|
||||
)}${node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''}}`;
|
||||
});
|
||||
|
||||
return {
|
||||
data: `{"type":"data","nodes":[${strings.join(',')}]}\n`,
|
||||
chunks: count > 0 ? iterator : null
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { ENDPOINT_METHODS, PAGE_METHODS } from '../../constants.js';
|
||||
import { negotiate } from '../../utils/http.js';
|
||||
import { Redirect } from '../control.js';
|
||||
import { method_not_allowed } from './utils.js';
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSREndpoint} mod
|
||||
* @param {import('types').SSRState} state
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function render_endpoint(event, mod, state) {
|
||||
const method = /** @type {import('types').HttpMethod} */ (event.request.method);
|
||||
|
||||
let handler = mod[method] || mod.fallback;
|
||||
|
||||
if (method === 'HEAD' && mod.GET && !mod.HEAD) {
|
||||
handler = mod.GET;
|
||||
}
|
||||
|
||||
if (!handler) {
|
||||
return method_not_allowed(mod, method);
|
||||
}
|
||||
|
||||
const prerender = mod.prerender ?? state.prerender_default;
|
||||
|
||||
if (prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
|
||||
throw new Error('Cannot prerender endpoints that have mutative methods');
|
||||
}
|
||||
|
||||
if (state.prerendering && !prerender) {
|
||||
if (state.depth > 0) {
|
||||
// if request came from a prerendered page, bail
|
||||
throw new Error(`${event.route.id} is not prerenderable`);
|
||||
} else {
|
||||
// if request came direct from the crawler, signal that
|
||||
// this route cannot be prerendered, but don't bail
|
||||
return new Response(undefined, { status: 204 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await handler(
|
||||
/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event)
|
||||
);
|
||||
|
||||
if (!(response instanceof Response)) {
|
||||
throw new Error(
|
||||
`Invalid response from route ${event.url.pathname}: handler should return a Response object`
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
if (e instanceof Redirect) {
|
||||
return new Response(undefined, {
|
||||
status: e.status,
|
||||
headers: { location: e.location }
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
*/
|
||||
export function is_endpoint_request(event) {
|
||||
const { method, headers } = event.request;
|
||||
|
||||
// These methods exist exclusively for endpoints
|
||||
if (ENDPOINT_METHODS.includes(method) && !PAGE_METHODS.includes(method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// use:enhance uses a custom header to disambiguate
|
||||
if (method === 'POST' && headers.get('x-sveltekit-action') === 'true') return false;
|
||||
|
||||
// GET/POST requests may be for endpoints or pages. We prefer endpoints if this isn't a text/html request
|
||||
const accept = event.request.headers.get('accept') ?? '*/*';
|
||||
return negotiate(accept, ['*', 'text/html']) !== 'text/html';
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import * as set_cookie_parser from 'set-cookie-parser';
|
||||
import { respond } from './respond.js';
|
||||
import * as paths from '__sveltekit/paths';
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* event: import('@sveltejs/kit').RequestEvent;
|
||||
* options: import('types').SSROptions;
|
||||
* 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, set_internal }) {
|
||||
/**
|
||||
* @type {typeof fetch}
|
||||
*/
|
||||
const server_fetch = async (info, init) => {
|
||||
const original_request = normalize_fetch_input(info, init, event.url);
|
||||
|
||||
// some runtimes (e.g. Cloudflare) error if you access `request.mode`,
|
||||
// annoyingly, so we need to read the value from the `init` object instead
|
||||
let mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
|
||||
let credentials =
|
||||
(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';
|
||||
|
||||
return options.hooks.handleFetch({
|
||||
event,
|
||||
request: original_request,
|
||||
fetch: async (info, init) => {
|
||||
const request = normalize_fetch_input(info, init, event.url);
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (!request.headers.has('origin')) {
|
||||
request.headers.set('origin', event.url.origin);
|
||||
}
|
||||
|
||||
if (info !== original_request) {
|
||||
mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
|
||||
credentials =
|
||||
(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';
|
||||
}
|
||||
|
||||
// Remove Origin, according to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin#description
|
||||
if (
|
||||
(request.method === 'GET' || request.method === 'HEAD') &&
|
||||
((mode === 'no-cors' && url.origin !== event.url.origin) ||
|
||||
url.origin === event.url.origin)
|
||||
) {
|
||||
request.headers.delete('origin');
|
||||
}
|
||||
|
||||
if (url.origin !== event.url.origin) {
|
||||
// Allow cookie passthrough for "credentials: same-origin" and "credentials: include"
|
||||
// if SvelteKit is serving my.domain.com:
|
||||
// - domain.com WILL NOT receive cookies
|
||||
// - my.domain.com WILL receive cookies
|
||||
// - api.domain.dom WILL NOT receive cookies
|
||||
// - sub.my.domain.com WILL receive cookies
|
||||
// ports do not affect the resolution
|
||||
// leading dot prevents mydomain.com matching domain.com
|
||||
// Do not forward other cookies for "credentials: include" because we don't know
|
||||
// which cookie belongs to which domain (browser does not pass this info)
|
||||
if (`.${url.hostname}`.endsWith(`.${event.url.hostname}`) && credentials !== 'omit') {
|
||||
const cookie = get_cookie_header(url, request.headers.get('cookie'));
|
||||
if (cookie) request.headers.set('cookie', cookie);
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
// handle fetch requests for static assets. e.g. prebaked data, etc.
|
||||
// we need to support everything the browser's fetch supports
|
||||
const prefix = paths.assets || paths.base;
|
||||
const decoded = decodeURIComponent(url.pathname);
|
||||
const filename = (
|
||||
decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded
|
||||
).slice(1);
|
||||
const filename_html = `${filename}/index.html`; // path may also match path/index.html
|
||||
|
||||
const is_asset = manifest.assets.has(filename);
|
||||
const is_asset_html = manifest.assets.has(filename_html);
|
||||
|
||||
if (is_asset || is_asset_html) {
|
||||
const file = is_asset ? filename : filename_html;
|
||||
|
||||
if (state.read) {
|
||||
const type = is_asset
|
||||
? manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))]
|
||||
: 'text/html';
|
||||
|
||||
return new Response(state.read(file), {
|
||||
headers: type ? { 'content-type': type } : {}
|
||||
});
|
||||
}
|
||||
|
||||
return await fetch(request);
|
||||
}
|
||||
|
||||
if (credentials !== 'omit') {
|
||||
const cookie = get_cookie_header(url, request.headers.get('cookie'));
|
||||
if (cookie) {
|
||||
request.headers.set('cookie', cookie);
|
||||
}
|
||||
|
||||
const authorization = event.request.headers.get('authorization');
|
||||
if (authorization && !request.headers.has('authorization')) {
|
||||
request.headers.set('authorization', authorization);
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.headers.has('accept')) {
|
||||
request.headers.set('accept', '*/*');
|
||||
}
|
||||
|
||||
if (!request.headers.has('accept-language')) {
|
||||
request.headers.set(
|
||||
'accept-language',
|
||||
/** @type {string} */ (event.request.headers.get('accept-language'))
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {Response} */
|
||||
const response = await respond(request, options, manifest, {
|
||||
...state,
|
||||
depth: state.depth + 1
|
||||
});
|
||||
|
||||
const set_cookie = response.headers.get('set-cookie');
|
||||
if (set_cookie) {
|
||||
for (const str of set_cookie_parser.splitCookiesString(set_cookie)) {
|
||||
const { name, value, ...options } = set_cookie_parser.parseString(str);
|
||||
|
||||
// options.sameSite is string, something more specific is required - type cast is safe
|
||||
set_internal(
|
||||
name,
|
||||
value,
|
||||
/** @type {import('cookie').CookieSerializeOptions} */ (options)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Don't make this function `async`! Otherwise, the user has to `catch` promises they use for streaming responses or else
|
||||
// it will be an unhandled rejection. Instead, we add a `.catch(() => {})` ourselves below to this from happening.
|
||||
return (input, init) => {
|
||||
// See docs in fetch.js for why we need to do this
|
||||
const response = server_fetch(input, init);
|
||||
response.catch(() => {});
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RequestInfo | URL} info
|
||||
* @param {RequestInit | undefined} init
|
||||
* @param {URL} url
|
||||
*/
|
||||
function normalize_fetch_input(info, init, url) {
|
||||
if (info instanceof Request) {
|
||||
return info;
|
||||
}
|
||||
|
||||
return new Request(typeof info === 'string' ? new URL(info, url) : info, init);
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
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';
|
||||
import { filter_private_env, filter_public_env } from '../../utils/env.js';
|
||||
|
||||
export class Server {
|
||||
/** @type {import('types').SSROptions} */
|
||||
#options;
|
||||
|
||||
/** @type {import('@sveltejs/kit').SSRManifest} */
|
||||
#manifest;
|
||||
|
||||
/** @param {import('@sveltejs/kit').SSRManifest} manifest */
|
||||
constructor(manifest) {
|
||||
/** @type {import('types').SSROptions} */
|
||||
this.#options = options;
|
||||
this.#manifest = manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* env: Record<string, string>
|
||||
* }} opts
|
||||
*/
|
||||
async init({ env }) {
|
||||
// Take care: Some adapters may have to call `Server.init` per-request to set env vars,
|
||||
// so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't
|
||||
// been done already.
|
||||
// set env, in case it's used in initialisation
|
||||
set_private_env(
|
||||
filter_private_env(env, {
|
||||
public_prefix: this.#options.env_public_prefix,
|
||||
private_prefix: this.#options.env_private_prefix
|
||||
})
|
||||
);
|
||||
set_public_env(
|
||||
filter_public_env(env, {
|
||||
public_prefix: this.#options.env_public_prefix,
|
||||
private_prefix: this.#options.env_private_prefix
|
||||
})
|
||||
);
|
||||
|
||||
if (!this.#options.hooks) {
|
||||
try {
|
||||
const module = await get_hooks();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {import('types').RequestOptions} options
|
||||
*/
|
||||
async respond(request, options) {
|
||||
// TODO this should probably have been removed for 1.0 — i think we can get rid of it?
|
||||
if (!(request instanceof Request)) {
|
||||
throw new Error(
|
||||
'The first argument to server.respond must be a Request object. See https://github.com/sveltejs/kit/pull/3384 for details'
|
||||
);
|
||||
}
|
||||
|
||||
return respond(request, this.#options, this.#manifest, {
|
||||
...options,
|
||||
error: false,
|
||||
depth: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
import * as devalue from 'devalue';
|
||||
import { error, json } from '../../../exports/index.js';
|
||||
import { normalize_error } from '../../../utils/error.js';
|
||||
import { is_form_content_type, negotiate } from '../../../utils/http.js';
|
||||
import { HttpError, Redirect, ActionFailure } from '../../control.js';
|
||||
import { handle_error_and_jsonify } from '../utils.js';
|
||||
|
||||
/** @param {import('@sveltejs/kit').RequestEvent} event */
|
||||
export function is_action_json_request(event) {
|
||||
const accept = negotiate(event.request.headers.get('accept') ?? '*/*', [
|
||||
'application/json',
|
||||
'text/html'
|
||||
]);
|
||||
|
||||
return accept === 'application/json' && event.request.method === 'POST';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {import('types').SSRNode['server'] | undefined} server
|
||||
*/
|
||||
export async function handle_action_json_request(event, options, server) {
|
||||
const actions = server?.actions;
|
||||
|
||||
if (!actions) {
|
||||
// TODO should this be a different error altogether?
|
||||
const no_actions_error = error(405, 'POST method not allowed. No actions exist for this page');
|
||||
return action_json(
|
||||
{
|
||||
type: 'error',
|
||||
error: await handle_error_and_jsonify(event, options, no_actions_error)
|
||||
},
|
||||
{
|
||||
status: no_actions_error.status,
|
||||
headers: {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
|
||||
// "The server must generate an Allow header field in a 405 status code response"
|
||||
allow: 'GET'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
check_named_default_separate(actions);
|
||||
|
||||
try {
|
||||
const data = await call_action(event, actions);
|
||||
|
||||
if (__SVELTEKIT_DEV__) {
|
||||
validate_action_return(data);
|
||||
}
|
||||
|
||||
if (data instanceof ActionFailure) {
|
||||
return action_json({
|
||||
type: 'failure',
|
||||
status: data.status,
|
||||
// @ts-expect-error we assign a string to what is supposed to be an object. That's ok
|
||||
// because we don't use the object outside, and this way we have better code navigation
|
||||
// through knowing where the related interface is used.
|
||||
data: stringify_action_response(data.data, /** @type {string} */ (event.route.id))
|
||||
});
|
||||
} else {
|
||||
return action_json({
|
||||
type: 'success',
|
||||
status: data ? 200 : 204,
|
||||
// @ts-expect-error see comment above
|
||||
data: stringify_action_response(data, /** @type {string} */ (event.route.id))
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const err = normalize_error(e);
|
||||
|
||||
if (err instanceof Redirect) {
|
||||
return action_json_redirect(err);
|
||||
}
|
||||
|
||||
return action_json(
|
||||
{
|
||||
type: 'error',
|
||||
error: await handle_error_and_jsonify(event, options, check_incorrect_fail_use(err))
|
||||
},
|
||||
{
|
||||
status: err instanceof HttpError ? err.status : 500
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HttpError | Error} error
|
||||
*/
|
||||
function check_incorrect_fail_use(error) {
|
||||
return error instanceof ActionFailure
|
||||
? new Error('Cannot "throw fail()". Use "return fail()"')
|
||||
: error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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]
|
||||
*/
|
||||
function action_json(data, init) {
|
||||
return json(data, init);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
*/
|
||||
export function is_action_request(event) {
|
||||
return event.request.method === 'POST';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSRNode['server'] | undefined} server
|
||||
* @returns {Promise<import('@sveltejs/kit').ActionResult>}
|
||||
*/
|
||||
export async function handle_action_request(event, server) {
|
||||
const actions = server?.actions;
|
||||
|
||||
if (!actions) {
|
||||
// TODO should this be a different error altogether?
|
||||
event.setHeaders({
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
|
||||
// "The server must generate an Allow header field in a 405 status code response"
|
||||
allow: 'GET'
|
||||
});
|
||||
return {
|
||||
type: 'error',
|
||||
error: error(405, 'POST method not allowed. No actions exist for this page')
|
||||
};
|
||||
}
|
||||
|
||||
check_named_default_separate(actions);
|
||||
|
||||
try {
|
||||
const data = await call_action(event, actions);
|
||||
|
||||
if (__SVELTEKIT_DEV__) {
|
||||
validate_action_return(data);
|
||||
}
|
||||
|
||||
if (data instanceof ActionFailure) {
|
||||
return {
|
||||
type: 'failure',
|
||||
status: data.status,
|
||||
data: data.data
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'success',
|
||||
status: 200,
|
||||
// @ts-expect-error this will be removed upon serialization, so `undefined` is the same as omission
|
||||
data
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
const err = normalize_error(e);
|
||||
|
||||
if (err instanceof Redirect) {
|
||||
return {
|
||||
type: 'redirect',
|
||||
status: err.status,
|
||||
location: err.location
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'error',
|
||||
error: check_incorrect_fail_use(err)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').Actions} actions
|
||||
*/
|
||||
function check_named_default_separate(actions) {
|
||||
if (actions.default && Object.keys(actions).length > 1) {
|
||||
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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {NonNullable<import('types').SSRNode['server']['actions']>} actions
|
||||
* @throws {Redirect | ActionFailure | HttpError | Error}
|
||||
*/
|
||||
async function call_action(event, actions) {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
let name = 'default';
|
||||
for (const param of url.searchParams) {
|
||||
if (param[0].startsWith('/')) {
|
||||
name = param[0].slice(1);
|
||||
if (name === 'default') {
|
||||
throw new Error('Cannot use reserved action name "default"');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const action = actions[name];
|
||||
if (!action) {
|
||||
throw new Error(`No action with name '${name}' found`);
|
||||
}
|
||||
|
||||
if (!is_form_content_type(event.request)) {
|
||||
throw new Error(
|
||||
`Actions expect form-encoded data (received ${event.request.headers.get('content-type')})`
|
||||
);
|
||||
}
|
||||
|
||||
return action(event);
|
||||
}
|
||||
|
||||
/** @param {any} data */
|
||||
function validate_action_return(data) {
|
||||
if (data instanceof Redirect) {
|
||||
throw new Error('Cannot `return redirect(...)` — use `throw redirect(...)` instead');
|
||||
}
|
||||
|
||||
if (data instanceof HttpError) {
|
||||
throw new Error(
|
||||
'Cannot `return error(...)` — use `throw error(...)` or `return fail(...)` instead'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to `devalue.uneval` the data object, and if it fails, return a proper Error with context
|
||||
* @param {any} data
|
||||
* @param {string} route_id
|
||||
*/
|
||||
export function uneval_action_response(data, route_id) {
|
||||
return try_deserialize(data, devalue.uneval, route_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to `devalue.stringify` the data object, and if it fails, return a proper Error with context
|
||||
* @param {any} data
|
||||
* @param {string} route_id
|
||||
*/
|
||||
function stringify_action_response(data, route_id) {
|
||||
return try_deserialize(data, devalue.stringify, route_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} data
|
||||
* @param {(data: any) => string} fn
|
||||
* @param {string} route_id
|
||||
*/
|
||||
function try_deserialize(data, fn, route_id) {
|
||||
try {
|
||||
return fn(data);
|
||||
} catch (e) {
|
||||
// If we're here, the data could not be serialized with devalue
|
||||
const error = /** @type {any} */ (e);
|
||||
|
||||
if ('path' in error) {
|
||||
let message = `Data returned from action inside ${route_id} is not serializable: ${error.message}`;
|
||||
if (error.path !== '') message += ` (data.${error.path})`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* SHA-256 hashing function adapted from https://bitwiseshiftleft.github.io/sjcl
|
||||
* modified and redistributed under BSD license
|
||||
* @param {string} data
|
||||
*/
|
||||
export function sha256(data) {
|
||||
if (!key[0]) precompute();
|
||||
|
||||
const out = init.slice(0);
|
||||
const array = encode(data);
|
||||
|
||||
for (let i = 0; i < array.length; i += 16) {
|
||||
const w = array.subarray(i, i + 16);
|
||||
|
||||
let tmp;
|
||||
let a;
|
||||
let b;
|
||||
|
||||
let out0 = out[0];
|
||||
let out1 = out[1];
|
||||
let out2 = out[2];
|
||||
let out3 = out[3];
|
||||
let out4 = out[4];
|
||||
let out5 = out[5];
|
||||
let out6 = out[6];
|
||||
let out7 = out[7];
|
||||
|
||||
/* Rationale for placement of |0 :
|
||||
* If a value can overflow is original 32 bits by a factor of more than a few
|
||||
* million (2^23 ish), there is a possibility that it might overflow the
|
||||
* 53-bit mantissa and lose precision.
|
||||
*
|
||||
* To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that
|
||||
* propagates around the loop, and on the hash state out[]. I don't believe
|
||||
* that the clamps on out4 and on out0 are strictly necessary, but it's close
|
||||
* (for out4 anyway), and better safe than sorry.
|
||||
*
|
||||
* The clamps on out[] are necessary for the output to be correct even in the
|
||||
* common case and for short inputs.
|
||||
*/
|
||||
|
||||
for (let i = 0; i < 64; i++) {
|
||||
// load up the input word for this round
|
||||
|
||||
if (i < 16) {
|
||||
tmp = w[i];
|
||||
} else {
|
||||
a = w[(i + 1) & 15];
|
||||
|
||||
b = w[(i + 14) & 15];
|
||||
|
||||
tmp = w[i & 15] =
|
||||
(((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) +
|
||||
((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) +
|
||||
w[i & 15] +
|
||||
w[(i + 9) & 15]) |
|
||||
0;
|
||||
}
|
||||
|
||||
tmp =
|
||||
tmp +
|
||||
out7 +
|
||||
((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) +
|
||||
(out6 ^ (out4 & (out5 ^ out6))) +
|
||||
key[i]; // | 0;
|
||||
|
||||
// shift register
|
||||
out7 = out6;
|
||||
out6 = out5;
|
||||
out5 = out4;
|
||||
|
||||
out4 = (out3 + tmp) | 0;
|
||||
|
||||
out3 = out2;
|
||||
out2 = out1;
|
||||
out1 = out0;
|
||||
|
||||
out0 =
|
||||
(tmp +
|
||||
((out1 & out2) ^ (out3 & (out1 ^ out2))) +
|
||||
((out1 >>> 2) ^
|
||||
(out1 >>> 13) ^
|
||||
(out1 >>> 22) ^
|
||||
(out1 << 30) ^
|
||||
(out1 << 19) ^
|
||||
(out1 << 10))) |
|
||||
0;
|
||||
}
|
||||
|
||||
out[0] = (out[0] + out0) | 0;
|
||||
out[1] = (out[1] + out1) | 0;
|
||||
out[2] = (out[2] + out2) | 0;
|
||||
out[3] = (out[3] + out3) | 0;
|
||||
out[4] = (out[4] + out4) | 0;
|
||||
out[5] = (out[5] + out5) | 0;
|
||||
out[6] = (out[6] + out6) | 0;
|
||||
out[7] = (out[7] + out7) | 0;
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(out.buffer);
|
||||
reverse_endianness(bytes);
|
||||
|
||||
return base64(bytes);
|
||||
}
|
||||
|
||||
/** The SHA-256 initialization vector */
|
||||
const init = new Uint32Array(8);
|
||||
|
||||
/** The SHA-256 hash key */
|
||||
const key = new Uint32Array(64);
|
||||
|
||||
/** Function to precompute init and key. */
|
||||
function precompute() {
|
||||
/** @param {number} x */
|
||||
function frac(x) {
|
||||
return (x - Math.floor(x)) * 0x100000000;
|
||||
}
|
||||
|
||||
let prime = 2;
|
||||
|
||||
for (let i = 0; i < 64; prime++) {
|
||||
let is_prime = true;
|
||||
|
||||
for (let factor = 2; factor * factor <= prime; factor++) {
|
||||
if (prime % factor === 0) {
|
||||
is_prime = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_prime) {
|
||||
if (i < 8) {
|
||||
init[i] = frac(prime ** (1 / 2));
|
||||
}
|
||||
|
||||
key[i] = frac(prime ** (1 / 3));
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Uint8Array} bytes */
|
||||
function reverse_endianness(bytes) {
|
||||
for (let i = 0; i < bytes.length; i += 4) {
|
||||
const a = bytes[i + 0];
|
||||
const b = bytes[i + 1];
|
||||
const c = bytes[i + 2];
|
||||
const d = bytes[i + 3];
|
||||
|
||||
bytes[i + 0] = d;
|
||||
bytes[i + 1] = c;
|
||||
bytes[i + 2] = b;
|
||||
bytes[i + 3] = a;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
function encode(str) {
|
||||
const encoded = encoder.encode(str);
|
||||
const length = encoded.length * 8;
|
||||
|
||||
// result should be a multiple of 512 bits in length,
|
||||
// with room for a 1 (after the data) and two 32-bit
|
||||
// words containing the original input bit length
|
||||
const size = 512 * Math.ceil((length + 65) / 512);
|
||||
const bytes = new Uint8Array(size / 8);
|
||||
bytes.set(encoded);
|
||||
|
||||
// append a 1
|
||||
bytes[encoded.length] = 0b10000000;
|
||||
|
||||
reverse_endianness(bytes);
|
||||
|
||||
// add the input bit length
|
||||
const words = new Uint32Array(bytes.buffer);
|
||||
words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us
|
||||
words[words.length - 1] = length;
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/*
|
||||
Based on https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
|
||||
|
||||
MIT License
|
||||
Copyright (c) 2020 Egor Nepomnyaschih
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
|
||||
|
||||
/** @param {Uint8Array} bytes */
|
||||
export function base64(bytes) {
|
||||
const l = bytes.length;
|
||||
|
||||
let result = '';
|
||||
let i;
|
||||
|
||||
for (i = 2; i < l; i += 3) {
|
||||
result += chars[bytes[i - 2] >> 2];
|
||||
result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
|
||||
result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)];
|
||||
result += chars[bytes[i] & 0x3f];
|
||||
}
|
||||
|
||||
if (i === l + 1) {
|
||||
// 1 octet yet to write
|
||||
result += chars[bytes[i - 2] >> 2];
|
||||
result += chars[(bytes[i - 2] & 0x03) << 4];
|
||||
result += '==';
|
||||
}
|
||||
|
||||
if (i === l) {
|
||||
// 2 octets yet to write
|
||||
result += chars[bytes[i - 2] >> 2];
|
||||
result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
|
||||
result += chars[(bytes[i - 1] & 0x0f) << 2];
|
||||
result += '=';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { escape_html_attr } from '../../../utils/escape.js';
|
||||
import { base64, sha256 } from './crypto.js';
|
||||
|
||||
const array = new Uint8Array(16);
|
||||
|
||||
function generate_nonce() {
|
||||
crypto.getRandomValues(array);
|
||||
return base64(array);
|
||||
}
|
||||
|
||||
const quoted = new Set([
|
||||
'self',
|
||||
'unsafe-eval',
|
||||
'unsafe-hashes',
|
||||
'unsafe-inline',
|
||||
'none',
|
||||
'strict-dynamic',
|
||||
'report-sample',
|
||||
'wasm-unsafe-eval',
|
||||
'script'
|
||||
]);
|
||||
|
||||
const crypto_pattern = /^(nonce|sha\d\d\d)-/;
|
||||
|
||||
// CSP and CSP Report Only are extremely similar with a few caveats
|
||||
// the easiest/DRYest way to express this is with some private encapsulation
|
||||
class BaseProvider {
|
||||
/** @type {boolean} */
|
||||
#use_hashes;
|
||||
|
||||
/** @type {boolean} */
|
||||
#script_needs_csp;
|
||||
|
||||
/** @type {boolean} */
|
||||
#style_needs_csp;
|
||||
|
||||
/** @type {import('types').CspDirectives} */
|
||||
#directives;
|
||||
|
||||
/** @type {import('types').Csp.Source[]} */
|
||||
#script_src;
|
||||
|
||||
/** @type {import('types').Csp.Source[]} */
|
||||
#style_src;
|
||||
|
||||
/** @type {string} */
|
||||
#nonce;
|
||||
|
||||
/**
|
||||
* @param {boolean} use_hashes
|
||||
* @param {import('types').CspDirectives} directives
|
||||
* @param {string} nonce
|
||||
*/
|
||||
constructor(use_hashes, directives, nonce) {
|
||||
this.#use_hashes = use_hashes;
|
||||
this.#directives = __SVELTEKIT_DEV__ ? { ...directives } : directives; // clone in dev so we can safely mutate
|
||||
|
||||
const d = this.#directives;
|
||||
|
||||
if (__SVELTEKIT_DEV__) {
|
||||
// remove strict-dynamic in dev...
|
||||
// TODO reinstate this if we can figure out how to make strict-dynamic work
|
||||
// if (d['default-src']) {
|
||||
// d['default-src'] = d['default-src'].filter((name) => name !== 'strict-dynamic');
|
||||
// if (d['default-src'].length === 0) delete d['default-src'];
|
||||
// }
|
||||
|
||||
// if (d['script-src']) {
|
||||
// d['script-src'] = d['script-src'].filter((name) => name !== 'strict-dynamic');
|
||||
// if (d['script-src'].length === 0) delete d['script-src'];
|
||||
// }
|
||||
|
||||
const effective_style_src = d['style-src'] || d['default-src'];
|
||||
|
||||
// ...and add unsafe-inline so we can inject <style> elements
|
||||
if (effective_style_src && !effective_style_src.includes('unsafe-inline')) {
|
||||
d['style-src'] = [...effective_style_src, 'unsafe-inline'];
|
||||
}
|
||||
}
|
||||
|
||||
this.#script_src = [];
|
||||
this.#style_src = [];
|
||||
|
||||
const effective_script_src = d['script-src'] || d['default-src'];
|
||||
const effective_style_src = d['style-src'] || d['default-src'];
|
||||
|
||||
this.#script_needs_csp =
|
||||
!!effective_script_src &&
|
||||
effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0;
|
||||
|
||||
this.#style_needs_csp =
|
||||
!__SVELTEKIT_DEV__ &&
|
||||
!!effective_style_src &&
|
||||
effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0;
|
||||
|
||||
this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes;
|
||||
this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes;
|
||||
this.#nonce = nonce;
|
||||
}
|
||||
|
||||
/** @param {string} content */
|
||||
add_script(content) {
|
||||
if (this.#script_needs_csp) {
|
||||
if (this.#use_hashes) {
|
||||
this.#script_src.push(`sha256-${sha256(content)}`);
|
||||
} else if (this.#script_src.length === 0) {
|
||||
this.#script_src.push(`nonce-${this.#nonce}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} content */
|
||||
add_style(content) {
|
||||
if (this.#style_needs_csp) {
|
||||
if (this.#use_hashes) {
|
||||
this.#style_src.push(`sha256-${sha256(content)}`);
|
||||
} else if (this.#style_src.length === 0) {
|
||||
this.#style_src.push(`nonce-${this.#nonce}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [is_meta]
|
||||
*/
|
||||
get_header(is_meta = false) {
|
||||
const header = [];
|
||||
|
||||
// due to browser inconsistencies, we can't append sources to default-src
|
||||
// (specifically, Firefox appears to not ignore nonce-{nonce} directives
|
||||
// on default-src), so we ensure that script-src and style-src exist
|
||||
|
||||
const directives = { ...this.#directives };
|
||||
|
||||
if (this.#style_src.length > 0) {
|
||||
directives['style-src'] = [
|
||||
...(directives['style-src'] || directives['default-src'] || []),
|
||||
...this.#style_src
|
||||
];
|
||||
}
|
||||
|
||||
if (this.#script_src.length > 0) {
|
||||
directives['script-src'] = [
|
||||
...(directives['script-src'] || directives['default-src'] || []),
|
||||
...this.#script_src
|
||||
];
|
||||
}
|
||||
|
||||
for (const key in directives) {
|
||||
if (is_meta && (key === 'frame-ancestors' || key === 'report-uri' || key === 'sandbox')) {
|
||||
// these values cannot be used with a <meta> tag
|
||||
// TODO warn?
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-expect-error gimme a break typescript, `key` is obviously a member of internal_directives
|
||||
const value = /** @type {string[] | true} */ (directives[key]);
|
||||
|
||||
if (!value) continue;
|
||||
|
||||
const directive = [key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((value) => {
|
||||
if (quoted.has(value) || crypto_pattern.test(value)) {
|
||||
directive.push(`'${value}'`);
|
||||
} else {
|
||||
directive.push(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
header.push(directive.join(' '));
|
||||
}
|
||||
|
||||
return header.join('; ');
|
||||
}
|
||||
}
|
||||
|
||||
class CspProvider extends BaseProvider {
|
||||
get_meta() {
|
||||
const content = this.get_header(true);
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
return `<meta http-equiv="content-security-policy" content=${escape_html_attr(content)}>`;
|
||||
}
|
||||
}
|
||||
|
||||
class CspReportOnlyProvider extends BaseProvider {
|
||||
/**
|
||||
* @param {boolean} use_hashes
|
||||
* @param {import('types').CspDirectives} directives
|
||||
* @param {string} nonce
|
||||
*/
|
||||
constructor(use_hashes, directives, nonce) {
|
||||
super(use_hashes, directives, nonce);
|
||||
|
||||
if (Object.values(directives).filter((v) => !!v).length > 0) {
|
||||
// If we're generating content-security-policy-report-only,
|
||||
// if there are any directives, we need a report-uri or report-to (or both)
|
||||
// else it's just an expensive noop.
|
||||
const has_report_to = directives['report-to']?.length ?? 0 > 0;
|
||||
const has_report_uri = directives['report-uri']?.length ?? 0 > 0;
|
||||
if (!has_report_to && !has_report_uri) {
|
||||
throw Error(
|
||||
'`content-security-policy-report-only` must be specified with either the `report-to` or `report-uri` directives, or both'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Csp {
|
||||
/** @readonly */
|
||||
nonce = generate_nonce();
|
||||
|
||||
/** @type {CspProvider} */
|
||||
csp_provider;
|
||||
|
||||
/** @type {CspReportOnlyProvider} */
|
||||
report_only_provider;
|
||||
|
||||
/**
|
||||
* @param {import('./types.js').CspConfig} config
|
||||
* @param {import('./types.js').CspOpts} opts
|
||||
*/
|
||||
constructor({ mode, directives, reportOnly }, { prerender }) {
|
||||
const use_hashes = mode === 'hash' || (mode === 'auto' && prerender);
|
||||
this.csp_provider = new CspProvider(use_hashes, directives, this.nonce);
|
||||
this.report_only_provider = new CspReportOnlyProvider(use_hashes, reportOnly, this.nonce);
|
||||
}
|
||||
|
||||
get script_needs_nonce() {
|
||||
return this.csp_provider.script_needs_nonce || this.report_only_provider.script_needs_nonce;
|
||||
}
|
||||
|
||||
get style_needs_nonce() {
|
||||
return this.csp_provider.style_needs_nonce || this.report_only_provider.style_needs_nonce;
|
||||
}
|
||||
|
||||
/** @param {string} content */
|
||||
add_script(content) {
|
||||
this.csp_provider.add_script(content);
|
||||
this.report_only_provider.add_script(content);
|
||||
}
|
||||
|
||||
/** @param {string} content */
|
||||
add_style(content) {
|
||||
this.csp_provider.add_style(content);
|
||||
this.report_only_provider.add_style(content);
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
import { text } from '../../../exports/index.js';
|
||||
import { compact } from '../../../utils/array.js';
|
||||
import { normalize_error } from '../../../utils/error.js';
|
||||
import { add_data_suffix } from '../../../utils/url.js';
|
||||
import { HttpError, Redirect } from '../../control.js';
|
||||
import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js';
|
||||
import {
|
||||
handle_action_json_request,
|
||||
handle_action_request,
|
||||
is_action_json_request,
|
||||
is_action_request
|
||||
} from './actions.js';
|
||||
import { load_data, load_server_data } from './load_data.js';
|
||||
import { render_response } from './render.js';
|
||||
import { respond_with_error } from './respond_with_error.js';
|
||||
import { get_option } from '../../../utils/options.js';
|
||||
import { get_data_json } from '../data/index.js';
|
||||
|
||||
/**
|
||||
* The maximum request depth permitted before assuming we're stuck in an infinite loop
|
||||
*/
|
||||
const MAX_DEPTH = 10;
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').PageNodeIndexes} page
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {import('@sveltejs/kit').SSRManifest} manifest
|
||||
* @param {import('types').SSRState} state
|
||||
* @param {import('types').RequiredResolveOptions} resolve_opts
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function render_page(event, page, options, manifest, state, resolve_opts) {
|
||||
if (state.depth > MAX_DEPTH) {
|
||||
// infinite request cycle detected
|
||||
return text(`Not found: ${event.url.pathname}`, {
|
||||
status: 404 // TODO in some cases this should be 500. not sure how to differentiate
|
||||
});
|
||||
}
|
||||
|
||||
if (is_action_json_request(event)) {
|
||||
const node = await manifest._.nodes[page.leaf]();
|
||||
return handle_action_json_request(event, options, node?.server);
|
||||
}
|
||||
|
||||
try {
|
||||
const nodes = await Promise.all([
|
||||
// we use == here rather than === because [undefined] serializes as "[null]"
|
||||
...page.layouts.map((n) => (n == undefined ? n : manifest._.nodes[n]())),
|
||||
manifest._.nodes[page.leaf]()
|
||||
]);
|
||||
|
||||
const leaf_node = /** @type {import('types').SSRNode} */ (nodes.at(-1));
|
||||
|
||||
let status = 200;
|
||||
|
||||
/** @type {import('@sveltejs/kit').ActionResult | undefined} */
|
||||
let action_result = undefined;
|
||||
|
||||
if (is_action_request(event)) {
|
||||
// for action requests, first call handler in +page.server.js
|
||||
// (this also determines status code)
|
||||
action_result = await handle_action_request(event, leaf_node.server);
|
||||
if (action_result?.type === 'redirect') {
|
||||
return redirect_response(action_result.status, action_result.location);
|
||||
}
|
||||
if (action_result?.type === 'error') {
|
||||
const error = action_result.error;
|
||||
status = error instanceof HttpError ? error.status : 500;
|
||||
}
|
||||
if (action_result?.type === 'failure') {
|
||||
status = action_result.status;
|
||||
}
|
||||
}
|
||||
|
||||
const should_prerender_data = nodes.some((node) => node?.server);
|
||||
const data_pathname = add_data_suffix(event.url.pathname);
|
||||
|
||||
// it's crucial that we do this before returning the non-SSR response, otherwise
|
||||
// SvelteKit will erroneously believe that the path has been prerendered,
|
||||
// causing functions to be omitted from the manifesst generated later
|
||||
const should_prerender = get_option(nodes, 'prerender') ?? false;
|
||||
if (should_prerender) {
|
||||
const mod = leaf_node.server;
|
||||
if (mod?.actions) {
|
||||
throw new Error('Cannot prerender pages with actions');
|
||||
}
|
||||
} else if (state.prerendering) {
|
||||
// if the page isn't marked as prerenderable, then bail out at this point
|
||||
return new Response(undefined, {
|
||||
status: 204
|
||||
});
|
||||
}
|
||||
|
||||
// if we fetch any endpoints while loading data for this page, they should
|
||||
// inherit the prerender option of the page
|
||||
state.prerender_default = should_prerender;
|
||||
|
||||
/** @type {import('./types.js').Fetched[]} */
|
||||
const fetched = [];
|
||||
|
||||
if (get_option(nodes, 'ssr') === false && !state.prerendering) {
|
||||
return await render_response({
|
||||
branch: [],
|
||||
fetched,
|
||||
page_config: {
|
||||
ssr: false,
|
||||
csr: get_option(nodes, 'csr') ?? true
|
||||
},
|
||||
status,
|
||||
error: null,
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
resolve_opts
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {Array<import('./types.js').Loaded | null>} */
|
||||
const branch = [];
|
||||
|
||||
/** @type {Error | null} */
|
||||
let load_error = null;
|
||||
|
||||
/** @type {Array<Promise<import('types').ServerDataNode | null>>} */
|
||||
const server_promises = nodes.map((node, i) => {
|
||||
if (load_error) {
|
||||
// if an error happens immediately, don't bother with the rest of the nodes
|
||||
throw load_error;
|
||||
}
|
||||
|
||||
return Promise.resolve().then(async () => {
|
||||
try {
|
||||
if (node === leaf_node && action_result?.type === 'error') {
|
||||
// we wait until here to throw the error so that we can use
|
||||
// any nested +error.svelte components that were defined
|
||||
throw action_result.error;
|
||||
}
|
||||
|
||||
return await load_server_data({
|
||||
event,
|
||||
state,
|
||||
node,
|
||||
parent: async () => {
|
||||
/** @type {Record<string, any>} */
|
||||
const data = {};
|
||||
for (let j = 0; j < i; j += 1) {
|
||||
const parent = await server_promises[j];
|
||||
if (parent) Object.assign(data, await parent.data);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
track_server_fetches: options.track_server_fetches
|
||||
});
|
||||
} catch (e) {
|
||||
load_error = /** @type {Error} */ (e);
|
||||
throw load_error;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const csr = get_option(nodes, 'csr') ?? true;
|
||||
|
||||
/** @type {Array<Promise<Record<string, any> | null>>} */
|
||||
const load_promises = nodes.map((node, i) => {
|
||||
if (load_error) throw load_error;
|
||||
return Promise.resolve().then(async () => {
|
||||
try {
|
||||
return await load_data({
|
||||
event,
|
||||
fetched,
|
||||
node,
|
||||
parent: async () => {
|
||||
const data = {};
|
||||
for (let j = 0; j < i; j += 1) {
|
||||
Object.assign(data, await load_promises[j]);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
resolve_opts,
|
||||
server_data_promise: server_promises[i],
|
||||
state,
|
||||
csr
|
||||
});
|
||||
} catch (e) {
|
||||
load_error = /** @type {Error} */ (e);
|
||||
throw load_error;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// if we don't do this, rejections will be unhandled
|
||||
for (const p of server_promises) p.catch(() => {});
|
||||
for (const p of load_promises) p.catch(() => {});
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (node) {
|
||||
try {
|
||||
const server_data = await server_promises[i];
|
||||
const data = await load_promises[i];
|
||||
|
||||
branch.push({ node, server_data, data });
|
||||
} catch (e) {
|
||||
const err = normalize_error(e);
|
||||
|
||||
if (err instanceof Redirect) {
|
||||
if (state.prerendering && should_prerender_data) {
|
||||
const body = JSON.stringify({
|
||||
type: 'redirect',
|
||||
location: err.location
|
||||
});
|
||||
|
||||
state.prerendering.dependencies.set(data_pathname, {
|
||||
response: text(body),
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
return redirect_response(err.status, err.location);
|
||||
}
|
||||
|
||||
const status = err instanceof HttpError ? err.status : 500;
|
||||
const error = await handle_error_and_jsonify(event, options, err);
|
||||
|
||||
while (i--) {
|
||||
if (page.errors[i]) {
|
||||
const index = /** @type {number} */ (page.errors[i]);
|
||||
const node = await manifest._.nodes[index]();
|
||||
|
||||
let j = i;
|
||||
while (!branch[j]) j -= 1;
|
||||
|
||||
return await render_response({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
resolve_opts,
|
||||
page_config: { ssr: true, csr: true },
|
||||
status,
|
||||
error,
|
||||
branch: compact(branch.slice(0, j + 1)).concat({
|
||||
node,
|
||||
data: null,
|
||||
server_data: null
|
||||
}),
|
||||
fetched
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if we're still here, it means the error happened in the root layout,
|
||||
// which means we have to fall back to error.html
|
||||
return static_error_page(options, status, error.message);
|
||||
}
|
||||
} else {
|
||||
// push an empty slot so we can rewind past gaps to the
|
||||
// layout that corresponds with an +error.svelte page
|
||||
branch.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.prerendering && should_prerender_data) {
|
||||
// ndjson format
|
||||
let { data, chunks } = get_data_json(
|
||||
event,
|
||||
options,
|
||||
branch.map((node) => node?.server_data)
|
||||
);
|
||||
|
||||
if (chunks) {
|
||||
for await (const chunk of chunks) {
|
||||
data += chunk;
|
||||
}
|
||||
}
|
||||
|
||||
state.prerendering.dependencies.set(data_pathname, {
|
||||
response: text(data),
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
return await render_response({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
resolve_opts,
|
||||
page_config: {
|
||||
csr: get_option(nodes, 'csr') ?? true,
|
||||
ssr: get_option(nodes, 'ssr') ?? true
|
||||
},
|
||||
status,
|
||||
error: null,
|
||||
branch: compact(branch),
|
||||
action_result,
|
||||
fetched
|
||||
});
|
||||
} catch (e) {
|
||||
// if we end up here, it means the data loaded successfully
|
||||
// but the page failed to render, or that a prerendering error occurred
|
||||
return await respond_with_error({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
status: 500,
|
||||
error: e,
|
||||
resolve_opts
|
||||
});
|
||||
}
|
||||
}
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
import { disable_search, make_trackable } from '../../../utils/url.js';
|
||||
import { unwrap_promises } from '../../../utils/promises.js';
|
||||
import { DEV } from 'esm-env';
|
||||
import { validate_depends } from '../../shared.js';
|
||||
|
||||
/**
|
||||
* Calls the user's server `load` function.
|
||||
* @param {{
|
||||
* 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,
|
||||
// TODO 2.0: Remove this
|
||||
track_server_fetches
|
||||
}) {
|
||||
if (!node?.server) return null;
|
||||
|
||||
let done = false;
|
||||
|
||||
const uses = {
|
||||
dependencies: new Set(),
|
||||
params: new Set(),
|
||||
parent: false,
|
||||
route: false,
|
||||
url: false
|
||||
};
|
||||
|
||||
const url = make_trackable(event.url, () => {
|
||||
if (DEV && done && !uses.url) {
|
||||
console.warn(
|
||||
`${node.server_id}: Accessing URL properties in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the URL changes`
|
||||
);
|
||||
}
|
||||
|
||||
uses.url = true;
|
||||
});
|
||||
|
||||
if (state.prerendering) {
|
||||
disable_search(url);
|
||||
}
|
||||
|
||||
const result = await node.server.load?.call(null, {
|
||||
...event,
|
||||
fetch: (info, init) => {
|
||||
const url = new URL(info instanceof Request ? info.url : info, event.url);
|
||||
|
||||
if (DEV && done && !uses.dependencies.has(url.href)) {
|
||||
console.warn(
|
||||
`${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated`
|
||||
);
|
||||
}
|
||||
|
||||
// TODO 2.0: Remove this
|
||||
if (track_server_fetches) {
|
||||
uses.dependencies.add(url.href);
|
||||
}
|
||||
|
||||
return event.fetch(info, init);
|
||||
},
|
||||
/** @param {string[]} deps */
|
||||
depends: (...deps) => {
|
||||
for (const dep of deps) {
|
||||
const { href } = new URL(dep, event.url);
|
||||
|
||||
if (DEV) {
|
||||
validate_depends(node.server_id, dep);
|
||||
|
||||
if (done && !uses.dependencies.has(href)) {
|
||||
console.warn(
|
||||
`${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
uses.dependencies.add(href);
|
||||
}
|
||||
},
|
||||
params: new Proxy(event.params, {
|
||||
get: (target, key) => {
|
||||
if (DEV && done && typeof key === 'string' && !uses.params.has(key)) {
|
||||
console.warn(
|
||||
`${node.server_id}: Accessing \`params.${String(
|
||||
key
|
||||
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes`
|
||||
);
|
||||
}
|
||||
|
||||
uses.params.add(key);
|
||||
return target[/** @type {string} */ (key)];
|
||||
}
|
||||
}),
|
||||
parent: async () => {
|
||||
if (DEV && done && !uses.parent) {
|
||||
console.warn(
|
||||
`${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes`
|
||||
);
|
||||
}
|
||||
|
||||
uses.parent = true;
|
||||
return parent();
|
||||
},
|
||||
route: new Proxy(event.route, {
|
||||
get: (target, key) => {
|
||||
if (DEV && done && typeof key === 'string' && !uses.route) {
|
||||
console.warn(
|
||||
`${node.server_id}: Accessing \`route.${String(
|
||||
key
|
||||
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes`
|
||||
);
|
||||
}
|
||||
|
||||
uses.route = true;
|
||||
return target[/** @type {'id'} */ (key)];
|
||||
}
|
||||
}),
|
||||
url
|
||||
});
|
||||
|
||||
const data = result ? await unwrap_promises(result, node.server_id) : null;
|
||||
if (__SVELTEKIT_DEV__) {
|
||||
validate_load_response(data, node.server_id);
|
||||
}
|
||||
|
||||
done = true;
|
||||
|
||||
return {
|
||||
type: 'data',
|
||||
data,
|
||||
uses,
|
||||
slash: node.server.trailingSlash
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the user's `load` function.
|
||||
* @param {{
|
||||
* event: import('@sveltejs/kit').RequestEvent;
|
||||
* fetched: import('./types.js').Fetched[];
|
||||
* node: import('types').SSRNode | undefined;
|
||||
* parent: () => Promise<Record<string, any>>;
|
||||
* resolve_opts: import('types').RequiredResolveOptions;
|
||||
* server_data_promise: Promise<import('types').ServerDataNode | null>;
|
||||
* state: import('types').SSRState;
|
||||
* csr: boolean;
|
||||
* }} opts
|
||||
* @returns {Promise<Record<string, any | Promise<any>> | null>}
|
||||
*/
|
||||
export async function load_data({
|
||||
event,
|
||||
fetched,
|
||||
node,
|
||||
parent,
|
||||
server_data_promise,
|
||||
state,
|
||||
resolve_opts,
|
||||
csr
|
||||
}) {
|
||||
const server_data_node = await server_data_promise;
|
||||
|
||||
if (!node?.universal?.load) {
|
||||
return server_data_node?.data ?? null;
|
||||
}
|
||||
|
||||
const result = await node.universal.load.call(null, {
|
||||
url: event.url,
|
||||
params: event.params,
|
||||
data: server_data_node?.data ?? null,
|
||||
route: event.route,
|
||||
fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts),
|
||||
setHeaders: event.setHeaders,
|
||||
depends: () => {},
|
||||
parent
|
||||
});
|
||||
|
||||
const data = result ? await unwrap_promises(result, node.universal_id) : null;
|
||||
if (__SVELTEKIT_DEV__) {
|
||||
validate_load_response(data, node.universal_id);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
function b64_encode(buffer) {
|
||||
if (globalThis.Buffer) {
|
||||
return Buffer.from(buffer).toString('base64');
|
||||
}
|
||||
|
||||
const little_endian = new Uint8Array(new Uint16Array([1]).buffer)[0] > 0;
|
||||
|
||||
// The Uint16Array(Uint8Array(...)) ensures the code points are padded with 0's
|
||||
return btoa(
|
||||
new TextDecoder(little_endian ? 'utf-16le' : 'utf-16be').decode(
|
||||
new Uint16Array(new Uint8Array(buffer))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Pick<import('@sveltejs/kit').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>} event
|
||||
* @param {import('types').SSRState} state
|
||||
* @param {import('./types.js').Fetched[]} fetched
|
||||
* @param {boolean} csr
|
||||
* @param {Pick<Required<import('@sveltejs/kit').ResolveOptions>, 'filterSerializedResponseHeaders'>} resolve_opts
|
||||
* @returns {typeof fetch}
|
||||
*/
|
||||
export function create_universal_fetch(event, state, fetched, csr, resolve_opts) {
|
||||
/**
|
||||
* @param {URL | RequestInfo} input
|
||||
* @param {RequestInit} [init]
|
||||
*/
|
||||
const universal_fetch = async (input, init) => {
|
||||
const cloned_body = input instanceof Request && input.body ? input.clone().body : null;
|
||||
|
||||
const cloned_headers =
|
||||
input instanceof Request && [...input.headers].length
|
||||
? new Headers(input.headers)
|
||||
: init?.headers;
|
||||
|
||||
let response = await event.fetch(input, init);
|
||||
|
||||
const url = new URL(input instanceof Request ? input.url : input, event.url);
|
||||
const same_origin = url.origin === event.url.origin;
|
||||
|
||||
/** @type {import('types').PrerenderDependency} */
|
||||
let dependency;
|
||||
|
||||
if (same_origin) {
|
||||
if (state.prerendering) {
|
||||
dependency = { response, body: null };
|
||||
state.prerendering.dependencies.set(url.pathname, dependency);
|
||||
}
|
||||
} else {
|
||||
// simulate CORS errors and "no access to body in no-cors mode" server-side for consistency with client-side behaviour
|
||||
const mode = input instanceof Request ? input.mode : init?.mode ?? 'cors';
|
||||
if (mode === 'no-cors') {
|
||||
response = new Response('', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers
|
||||
});
|
||||
} else {
|
||||
const acao = response.headers.get('access-control-allow-origin');
|
||||
if (!acao || (acao !== event.url.origin && acao !== '*')) {
|
||||
throw new Error(
|
||||
`CORS error: ${
|
||||
acao ? 'Incorrect' : 'No'
|
||||
} 'Access-Control-Allow-Origin' header is present on the requested resource`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const proxy = new Proxy(response, {
|
||||
get(response, key, _receiver) {
|
||||
/**
|
||||
* @param {string} body
|
||||
* @param {boolean} is_b64
|
||||
*/
|
||||
async function push_fetched(body, is_b64) {
|
||||
const status_number = Number(response.status);
|
||||
if (isNaN(status_number)) {
|
||||
throw new Error(
|
||||
`response.status is not a number. value: "${
|
||||
response.status
|
||||
}" type: ${typeof response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
fetched.push({
|
||||
url: same_origin ? url.href.slice(event.url.origin.length) : url.href,
|
||||
method: event.request.method,
|
||||
request_body: /** @type {string | ArrayBufferView | undefined} */ (
|
||||
input instanceof Request && cloned_body
|
||||
? await stream_to_string(cloned_body)
|
||||
: init?.body
|
||||
),
|
||||
request_headers: cloned_headers,
|
||||
response_body: body,
|
||||
response,
|
||||
is_b64
|
||||
});
|
||||
}
|
||||
|
||||
if (key === 'arrayBuffer') {
|
||||
return async () => {
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
if (dependency) {
|
||||
dependency.body = new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
await push_fetched(b64_encode(buffer), true);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
}
|
||||
|
||||
async function text() {
|
||||
const body = await response.text();
|
||||
|
||||
if (!body || typeof body === 'string') {
|
||||
await push_fetched(body, false);
|
||||
}
|
||||
|
||||
if (dependency) {
|
||||
dependency.body = body;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
if (key === 'text') {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (key === 'json') {
|
||||
return async () => {
|
||||
return JSON.parse(await text());
|
||||
};
|
||||
}
|
||||
|
||||
return Reflect.get(response, key, response);
|
||||
}
|
||||
});
|
||||
|
||||
if (csr) {
|
||||
// ensure that excluded headers can't be read
|
||||
const get = response.headers.get;
|
||||
response.headers.get = (key) => {
|
||||
const lower = key.toLowerCase();
|
||||
const value = get.call(response.headers, lower);
|
||||
if (value && !lower.startsWith('x-sveltekit-')) {
|
||||
const included = resolve_opts.filterSerializedResponseHeaders(lower, value);
|
||||
if (!included) {
|
||||
throw new Error(
|
||||
`Failed to get response header "${lower}" — it must be included by the \`filterSerializedResponseHeaders\` option: https://kit.svelte.dev/docs/hooks#server-hooks-handle (at ${event.route.id})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
return proxy;
|
||||
};
|
||||
|
||||
// Don't make this function `async`! Otherwise, the user has to `catch` promises they use for streaming responses or else
|
||||
// it will be an unhandled rejection. Instead, we add a `.catch(() => {})` ourselves below to this from happening.
|
||||
return (input, init) => {
|
||||
// See docs in fetch.js for why we need to do this
|
||||
const response = universal_fetch(input, init);
|
||||
response.catch(() => {});
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ReadableStream<Uint8Array>} stream
|
||||
*/
|
||||
async function stream_to_string(stream) {
|
||||
let result = '';
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
result += decoder.decode(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} data
|
||||
* @param {string} [id]
|
||||
*/
|
||||
function validate_load_response(data, id) {
|
||||
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
|
||||
throw new Error(
|
||||
`a load function in ${id} returned ${
|
||||
typeof data !== 'object'
|
||||
? `a ${typeof data}`
|
||||
: data instanceof Response
|
||||
? 'a Response object'
|
||||
: Array.isArray(data)
|
||||
? 'an array'
|
||||
: 'a non-plain object'
|
||||
}, but must return a plain object at the top level (i.e. \`return {...}\`)`
|
||||
);
|
||||
}
|
||||
}
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
import * as devalue from 'devalue';
|
||||
import { readable, writable } from 'svelte/store';
|
||||
import { DEV } from 'esm-env';
|
||||
import * as paths from '__sveltekit/paths';
|
||||
import { hash } from '../../hash.js';
|
||||
import { serialize_data } from './serialize_data.js';
|
||||
import { s } from '../../../utils/misc.js';
|
||||
import { Csp } from './csp.js';
|
||||
import { uneval_action_response } from './actions.js';
|
||||
import { clarify_devalue_error, stringify_uses, handle_error_and_jsonify } from '../utils.js';
|
||||
import { public_env } from '../../shared-server.js';
|
||||
import { text } from '../../../exports/index.js';
|
||||
import { create_async_iterator } from '../../../utils/streaming.js';
|
||||
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
|
||||
import { SCHEME } from '../../../utils/url.js';
|
||||
|
||||
// TODO rename this function/module
|
||||
|
||||
const updated = {
|
||||
...readable(false),
|
||||
check: () => false
|
||||
};
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* Creates the HTML response.
|
||||
* @param {{
|
||||
* branch: Array<import('./types.js').Loaded>;
|
||||
* fetched: Array<import('./types.js').Fetched>;
|
||||
* options: import('types').SSROptions;
|
||||
* manifest: import('@sveltejs/kit').SSRManifest;
|
||||
* state: import('types').SSRState;
|
||||
* page_config: { ssr: boolean; csr: boolean };
|
||||
* status: number;
|
||||
* error: App.Error | null;
|
||||
* event: import('@sveltejs/kit').RequestEvent;
|
||||
* resolve_opts: import('types').RequiredResolveOptions;
|
||||
* action_result?: import('@sveltejs/kit').ActionResult;
|
||||
* }} opts
|
||||
*/
|
||||
export async function render_response({
|
||||
branch,
|
||||
fetched,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
page_config,
|
||||
status,
|
||||
error = null,
|
||||
event,
|
||||
resolve_opts,
|
||||
action_result
|
||||
}) {
|
||||
if (state.prerendering) {
|
||||
if (options.csp.mode === 'nonce') {
|
||||
throw new Error('Cannot use prerendering if config.kit.csp.mode === "nonce"');
|
||||
}
|
||||
|
||||
if (options.app_template_contains_nonce) {
|
||||
throw new Error('Cannot use prerendering if page template contains %sveltekit.nonce%');
|
||||
}
|
||||
}
|
||||
|
||||
const { client } = manifest._;
|
||||
|
||||
const modulepreloads = new Set(client.imports);
|
||||
const stylesheets = new Set(client.stylesheets);
|
||||
const fonts = new Set(client.fonts);
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const link_header_preloads = new Set();
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
// TODO if we add a client entry point one day, we will need to include inline_styles with the entry, otherwise stylesheets will be linked even if they are below inlineStyleThreshold
|
||||
const inline_styles = new Map();
|
||||
|
||||
let rendered;
|
||||
|
||||
const form_value =
|
||||
action_result?.type === 'success' || action_result?.type === 'failure'
|
||||
? action_result.data ?? null
|
||||
: null;
|
||||
|
||||
/** @type {string} */
|
||||
let base = paths.base;
|
||||
|
||||
/** @type {string} */
|
||||
let assets = paths.assets;
|
||||
|
||||
/**
|
||||
* An expression that will evaluate in the client to determine the resolved base path.
|
||||
* We use a relative path when possible to support IPFS, the internet archive, etc.
|
||||
*/
|
||||
let base_expression = s(paths.base);
|
||||
|
||||
// if appropriate, use relative paths for greater portability
|
||||
if (paths.relative !== false && !state.prerendering?.fallback) {
|
||||
const segments = event.url.pathname.slice(paths.base.length).split('/').slice(2);
|
||||
|
||||
base = segments.map(() => '..').join('/') || '.';
|
||||
|
||||
// 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)) {
|
||||
assets = base;
|
||||
}
|
||||
}
|
||||
|
||||
if (page_config.ssr) {
|
||||
if (__SVELTEKIT_DEV__ && !branch.at(-1)?.node.component) {
|
||||
// Can only be the leaf, layouts have a fallback component generated
|
||||
throw new Error(`Missing +page.svelte component for route ${event.route.id}`);
|
||||
}
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
const props = {
|
||||
stores: {
|
||||
page: writable(null),
|
||||
navigating: writable(null),
|
||||
updated
|
||||
},
|
||||
constructors: await Promise.all(branch.map(({ node }) => node.component())),
|
||||
form: form_value
|
||||
};
|
||||
|
||||
let data = {};
|
||||
|
||||
// props_n (instead of props[n]) makes it easy to avoid
|
||||
// unnecessary updates for layout components
|
||||
for (let i = 0; i < branch.length; i += 1) {
|
||||
data = { ...data, ...branch[i].data };
|
||||
props[`data_${i}`] = data;
|
||||
}
|
||||
|
||||
props.page = {
|
||||
error,
|
||||
params: /** @type {Record<string, any>} */ (event.params),
|
||||
route: event.route,
|
||||
status,
|
||||
url: event.url,
|
||||
data,
|
||||
form: form_value
|
||||
};
|
||||
|
||||
// use relative paths during rendering, so that the resulting HTML is as
|
||||
// portable as possible, but reset afterwards
|
||||
if (paths.relative) paths.override({ base, assets });
|
||||
|
||||
if (__SVELTEKIT_DEV__) {
|
||||
const fetch = globalThis.fetch;
|
||||
let warned = false;
|
||||
globalThis.fetch = (info, init) => {
|
||||
if (typeof info === 'string' && !SCHEME.test(info)) {
|
||||
throw new Error(
|
||||
`Cannot call \`fetch\` eagerly during server side rendering with relative URL (${info}) — put your \`fetch\` calls inside \`onMount\` or a \`load\` function instead`
|
||||
);
|
||||
} else if (!warned) {
|
||||
console.warn(
|
||||
'Avoid calling `fetch` eagerly during server side rendering — put your `fetch` calls inside `onMount` or a `load` function instead'
|
||||
);
|
||||
warned = true;
|
||||
}
|
||||
|
||||
return fetch(info, init);
|
||||
};
|
||||
|
||||
try {
|
||||
rendered = options.root.render(props);
|
||||
} finally {
|
||||
globalThis.fetch = fetch;
|
||||
paths.reset();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
rendered = options.root.render(props);
|
||||
} finally {
|
||||
paths.reset();
|
||||
}
|
||||
}
|
||||
|
||||
for (const { node } of branch) {
|
||||
for (const url of node.imports) modulepreloads.add(url);
|
||||
for (const url of node.stylesheets) stylesheets.add(url);
|
||||
for (const url of node.fonts) fonts.add(url);
|
||||
|
||||
if (node.inline_styles) {
|
||||
Object.entries(await node.inline_styles()).forEach(([k, v]) => inline_styles.set(k, v));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rendered = { head: '', html: '', css: { code: '', map: null } };
|
||||
}
|
||||
|
||||
let head = '';
|
||||
let body = rendered.html;
|
||||
|
||||
const csp = new Csp(options.csp, {
|
||||
prerender: !!state.prerendering
|
||||
});
|
||||
|
||||
/** @param {string} path */
|
||||
const prefixed = (path) => {
|
||||
if (path.startsWith('/')) {
|
||||
// Vite makes the start script available through the base path and without it.
|
||||
// We load it via the base path in order to support remote IDE environments which proxy
|
||||
// all URLs under the base path during development.
|
||||
return paths.base + path;
|
||||
}
|
||||
return `${assets}/${path}`;
|
||||
};
|
||||
|
||||
if (inline_styles.size > 0) {
|
||||
const content = Array.from(inline_styles.values()).join('\n');
|
||||
|
||||
const attributes = __SVELTEKIT_DEV__ ? [' data-sveltekit'] : [];
|
||||
if (csp.style_needs_nonce) attributes.push(` nonce="${csp.nonce}"`);
|
||||
|
||||
csp.add_style(content);
|
||||
|
||||
head += `\n\t<style${attributes.join('')}>${content}</style>`;
|
||||
}
|
||||
|
||||
for (const dep of stylesheets) {
|
||||
const path = prefixed(dep);
|
||||
|
||||
const attributes = ['rel="stylesheet"'];
|
||||
|
||||
if (inline_styles.has(dep)) {
|
||||
// don't load stylesheets that are already inlined
|
||||
// include them in disabled state so that Vite can detect them and doesn't try to add them
|
||||
attributes.push('disabled', 'media="(max-width: 0)"');
|
||||
} else {
|
||||
if (resolve_opts.preload({ type: 'css', path })) {
|
||||
const preload_atts = ['rel="preload"', 'as="style"'];
|
||||
link_header_preloads.add(`<${encodeURI(path)}>; ${preload_atts.join(';')}; nopush`);
|
||||
}
|
||||
}
|
||||
|
||||
head += `\n\t\t<link href="${path}" ${attributes.join(' ')}>`;
|
||||
}
|
||||
|
||||
for (const dep of fonts) {
|
||||
const path = prefixed(dep);
|
||||
|
||||
if (resolve_opts.preload({ type: 'font', path })) {
|
||||
const ext = dep.slice(dep.lastIndexOf('.') + 1);
|
||||
const attributes = [
|
||||
'rel="preload"',
|
||||
'as="font"',
|
||||
`type="font/${ext}"`,
|
||||
`href="${path}"`,
|
||||
'crossorigin'
|
||||
];
|
||||
|
||||
head += `\n\t\t<link ${attributes.join(' ')}>`;
|
||||
}
|
||||
}
|
||||
|
||||
const global = __SVELTEKIT_DEV__ ? '__sveltekit_dev' : `__sveltekit_${options.version_hash}`;
|
||||
|
||||
const { data, chunks } = get_data(
|
||||
event,
|
||||
options,
|
||||
branch.map((b) => b.server_data),
|
||||
global
|
||||
);
|
||||
|
||||
if (page_config.ssr && page_config.csr) {
|
||||
body += `\n\t\t\t${fetched
|
||||
.map((item) =>
|
||||
serialize_data(item, resolve_opts.filterSerializedResponseHeaders, !!state.prerendering)
|
||||
)
|
||||
.join('\n\t\t\t')}`;
|
||||
}
|
||||
|
||||
if (page_config.csr) {
|
||||
const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter(
|
||||
(path) => resolve_opts.preload({ type: 'js', path })
|
||||
);
|
||||
|
||||
for (const path of included_modulepreloads) {
|
||||
// see the kit.output.preloadStrategy option for details on why we have multiple options here
|
||||
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
|
||||
if (options.preload_strategy !== 'modulepreload') {
|
||||
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
|
||||
} else if (state.prerendering) {
|
||||
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
|
||||
}
|
||||
}
|
||||
|
||||
const blocks = [];
|
||||
|
||||
const properties = [
|
||||
paths.assets && `assets: ${s(paths.assets)}`,
|
||||
`base: ${base_expression}`,
|
||||
`env: ${s(public_env)}`
|
||||
].filter(Boolean);
|
||||
|
||||
if (chunks) {
|
||||
blocks.push('const deferred = new Map();');
|
||||
|
||||
properties.push(`defer: (id) => new Promise((fulfil, reject) => {
|
||||
deferred.set(id, { fulfil, reject });
|
||||
})`);
|
||||
|
||||
properties.push(`resolve: ({ id, data, error }) => {
|
||||
const { fulfil, reject } = deferred.get(id);
|
||||
deferred.delete(id);
|
||||
|
||||
if (error) reject(error);
|
||||
else fulfil(data);
|
||||
}`);
|
||||
}
|
||||
|
||||
blocks.push(`${global} = {
|
||||
${properties.join(',\n\t\t\t\t\t\t')}
|
||||
};`);
|
||||
|
||||
const args = ['app', 'element'];
|
||||
|
||||
blocks.push('const element = document.currentScript.parentElement;');
|
||||
|
||||
if (page_config.ssr) {
|
||||
const serialized = { form: 'null', error: 'null' };
|
||||
|
||||
blocks.push(`const data = ${data};`);
|
||||
|
||||
if (form_value) {
|
||||
serialized.form = uneval_action_response(
|
||||
form_value,
|
||||
/** @type {string} */ (event.route.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
serialized.error = devalue.uneval(error);
|
||||
}
|
||||
|
||||
const hydrate = [
|
||||
`node_ids: [${branch.map(({ node }) => node.index).join(', ')}]`,
|
||||
'data',
|
||||
`form: ${serialized.form}`,
|
||||
`error: ${serialized.error}`
|
||||
];
|
||||
|
||||
if (status !== 200) {
|
||||
hydrate.push(`status: ${status}`);
|
||||
}
|
||||
|
||||
if (options.embedded) {
|
||||
hydrate.push(`params: ${devalue.uneval(event.params)}`, `route: ${s(event.route)}`);
|
||||
}
|
||||
|
||||
args.push(`{\n\t\t\t\t\t\t\t${hydrate.join(',\n\t\t\t\t\t\t\t')}\n\t\t\t\t\t\t}`);
|
||||
}
|
||||
|
||||
blocks.push(`Promise.all([
|
||||
import(${s(prefixed(client.start))}),
|
||||
import(${s(prefixed(client.app))})
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(${args.join(', ')});
|
||||
});`);
|
||||
|
||||
if (options.service_worker) {
|
||||
const opts = __SVELTEKIT_DEV__ ? ", { type: 'module' }" : '';
|
||||
|
||||
// we use an anonymous function instead of an arrow function to support
|
||||
// older browsers (https://github.com/sveltejs/kit/pull/5417)
|
||||
blocks.push(`if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('${prefixed('service-worker.js')}'${opts});
|
||||
});
|
||||
}`);
|
||||
}
|
||||
|
||||
const init_app = `
|
||||
{
|
||||
${blocks.join('\n\n\t\t\t\t\t')}
|
||||
}
|
||||
`;
|
||||
csp.add_script(init_app);
|
||||
|
||||
body += `\n\t\t\t<script${
|
||||
csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''
|
||||
}>${init_app}</script>\n\t\t`;
|
||||
}
|
||||
|
||||
const headers = new Headers({
|
||||
'x-sveltekit-page': 'true',
|
||||
'content-type': 'text/html'
|
||||
});
|
||||
|
||||
if (state.prerendering) {
|
||||
// TODO read headers set with setHeaders and convert into http-equiv where possible
|
||||
const http_equiv = [];
|
||||
|
||||
const csp_headers = csp.csp_provider.get_meta();
|
||||
if (csp_headers) {
|
||||
http_equiv.push(csp_headers);
|
||||
}
|
||||
|
||||
if (state.prerendering.cache) {
|
||||
http_equiv.push(`<meta http-equiv="cache-control" content="${state.prerendering.cache}">`);
|
||||
}
|
||||
|
||||
if (http_equiv.length > 0) {
|
||||
head = http_equiv.join('\n') + head;
|
||||
}
|
||||
} else {
|
||||
const csp_header = csp.csp_provider.get_header();
|
||||
if (csp_header) {
|
||||
headers.set('content-security-policy', csp_header);
|
||||
}
|
||||
const report_only_header = csp.report_only_provider.get_header();
|
||||
if (report_only_header) {
|
||||
headers.set('content-security-policy-report-only', report_only_header);
|
||||
}
|
||||
|
||||
if (link_header_preloads.size) {
|
||||
headers.set('link', Array.from(link_header_preloads).join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
// add the content after the script/css links so the link elements are parsed first
|
||||
head += rendered.head;
|
||||
|
||||
const html = options.templates.app({
|
||||
head,
|
||||
body,
|
||||
assets,
|
||||
nonce: /** @type {string} */ (csp.nonce),
|
||||
env: public_env
|
||||
});
|
||||
|
||||
// TODO flush chunks as early as we can
|
||||
const transformed =
|
||||
(await resolve_opts.transformPageChunk({
|
||||
html,
|
||||
done: true
|
||||
})) || '';
|
||||
|
||||
if (!chunks) {
|
||||
headers.set('etag', `"${hash(transformed)}"`);
|
||||
}
|
||||
|
||||
if (DEV) {
|
||||
if (page_config.csr) {
|
||||
if (transformed.split('<!--').length < html.split('<!--').length) {
|
||||
// the \u001B stuff is ANSI codes, so that we don't need to add a library to the runtime
|
||||
// https://svelte.dev/repl/1b3f49696f0c44c881c34587f2537aa2
|
||||
console.warn(
|
||||
"\u001B[1m\u001B[31mRemoving comments in transformPageChunk can break Svelte's hydration\u001B[39m\u001B[22m"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (chunks) {
|
||||
console.warn(
|
||||
'\u001B[1m\u001B[31mReturning promises from server `load` functions will only work if `csr === true`\u001B[39m\u001B[22m'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !chunks
|
||||
? text(transformed, {
|
||||
status,
|
||||
headers
|
||||
})
|
||||
: new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
controller.enqueue(encoder.encode(transformed + '\n'));
|
||||
for await (const chunk of chunks) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
|
||||
type: 'bytes'
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'text/html'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the serialized data contains promises, `chunks` will be an
|
||||
* async iterable containing their resolutions
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {Array<import('types').ServerDataNode | null>} nodes
|
||||
* @param {string} global
|
||||
* @returns {{ data: string, chunks: AsyncIterable<string> | null }}
|
||||
*/
|
||||
function get_data(event, options, nodes, global) {
|
||||
let promise_id = 1;
|
||||
let count = 0;
|
||||
|
||||
const { iterator, push, done } = create_async_iterator();
|
||||
|
||||
/** @param {any} thing */
|
||||
function replacer(thing) {
|
||||
if (typeof thing?.then === 'function') {
|
||||
const id = promise_id++;
|
||||
count += 1;
|
||||
|
||||
thing
|
||||
.then(/** @param {any} data */ (data) => ({ data }))
|
||||
.catch(
|
||||
/** @param {any} error */ async (error) => ({
|
||||
error: await handle_error_and_jsonify(event, options, error)
|
||||
})
|
||||
)
|
||||
.then(
|
||||
/**
|
||||
* @param {{data: any; error: any}} result
|
||||
*/
|
||||
async ({ data, error }) => {
|
||||
count -= 1;
|
||||
|
||||
let str;
|
||||
try {
|
||||
str = devalue.uneval({ id, data, error }, replacer);
|
||||
} catch (e) {
|
||||
error = await handle_error_and_jsonify(
|
||||
event,
|
||||
options,
|
||||
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
|
||||
);
|
||||
data = undefined;
|
||||
str = devalue.uneval({ id, data, error }, replacer);
|
||||
}
|
||||
|
||||
push(`<script>${global}.resolve(${str})</script>\n`);
|
||||
if (count === 0) done();
|
||||
}
|
||||
);
|
||||
|
||||
return `${global}.defer(${id})`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const strings = nodes.map((node) => {
|
||||
if (!node) return 'null';
|
||||
|
||||
return `{"type":"data","data":${devalue.uneval(node.data, replacer)},${stringify_uses(node)}${
|
||||
node.slash ? `,"slash":${JSON.stringify(node.slash)}` : ''
|
||||
}}`;
|
||||
});
|
||||
|
||||
return {
|
||||
data: `[${strings.join(',')}]`,
|
||||
chunks: count > 0 ? iterator : null
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(clarify_devalue_error(event, /** @type {any} */ (e)));
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+111
@@ -0,0 +1,111 @@
|
||||
import { render_response } from './render.js';
|
||||
import { load_data, load_server_data } from './load_data.js';
|
||||
import { handle_error_and_jsonify, static_error_page, redirect_response } from '../utils.js';
|
||||
import { get_option } from '../../../utils/options.js';
|
||||
import { HttpError, Redirect } from '../../control.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('./types.js').Loaded} Loaded
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* event: import('@sveltejs/kit').RequestEvent;
|
||||
* options: import('types').SSROptions;
|
||||
* manifest: import('@sveltejs/kit').SSRManifest;
|
||||
* state: import('types').SSRState;
|
||||
* status: number;
|
||||
* error: unknown;
|
||||
* resolve_opts: import('types').RequiredResolveOptions;
|
||||
* }} opts
|
||||
*/
|
||||
export async function respond_with_error({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
status,
|
||||
error,
|
||||
resolve_opts
|
||||
}) {
|
||||
// reroute to the fallback page to prevent an infinite chain of requests.
|
||||
if (event.request.headers.get('x-sveltekit-error')) {
|
||||
return static_error_page(options, status, /** @type {Error} */ (error).message);
|
||||
}
|
||||
|
||||
/** @type {import('./types.js').Fetched[]} */
|
||||
const fetched = [];
|
||||
|
||||
try {
|
||||
const branch = [];
|
||||
const default_layout = await manifest._.nodes[0](); // 0 is always the root layout
|
||||
const ssr = get_option([default_layout], 'ssr') ?? true;
|
||||
const csr = get_option([default_layout], 'csr') ?? true;
|
||||
|
||||
if (ssr) {
|
||||
state.error = true;
|
||||
|
||||
const server_data_promise = load_server_data({
|
||||
event,
|
||||
state,
|
||||
node: default_layout,
|
||||
parent: async () => ({}),
|
||||
track_server_fetches: options.track_server_fetches
|
||||
});
|
||||
|
||||
const server_data = await server_data_promise;
|
||||
|
||||
const data = await load_data({
|
||||
event,
|
||||
fetched,
|
||||
node: default_layout,
|
||||
parent: async () => ({}),
|
||||
resolve_opts,
|
||||
server_data_promise,
|
||||
state,
|
||||
csr
|
||||
});
|
||||
|
||||
branch.push(
|
||||
{
|
||||
node: default_layout,
|
||||
server_data,
|
||||
data
|
||||
},
|
||||
{
|
||||
node: await manifest._.nodes[1](), // 1 is always the root error
|
||||
data: null,
|
||||
server_data: null
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return await render_response({
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
page_config: {
|
||||
ssr,
|
||||
csr: get_option([default_layout], 'csr') ?? true
|
||||
},
|
||||
status,
|
||||
error: await handle_error_and_jsonify(event, options, error),
|
||||
branch,
|
||||
fetched,
|
||||
event,
|
||||
resolve_opts
|
||||
});
|
||||
} catch (e) {
|
||||
// Edge case: If route is a 404 and the user redirects to somewhere from the root layout,
|
||||
// we end up here.
|
||||
if (e instanceof Redirect) {
|
||||
return redirect_response(e.status, e.location);
|
||||
}
|
||||
|
||||
return static_error_page(
|
||||
options,
|
||||
e instanceof HttpError ? e.status : 500,
|
||||
(await handle_error_and_jsonify(event, options, e)).message
|
||||
);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { escape_html_attr } from '../../../utils/escape.js';
|
||||
import { hash } from '../../hash.js';
|
||||
|
||||
/**
|
||||
* Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
|
||||
*
|
||||
* The first closes the script element, so everything after is treated as raw HTML.
|
||||
* The second disables further parsing until `-->`, so the script element might be unexpectedly
|
||||
* kept open until until an unrelated HTML comment in the page.
|
||||
*
|
||||
* U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
|
||||
* browsers.
|
||||
*
|
||||
* @see tests for unsafe parsing examples.
|
||||
* @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
|
||||
* @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
|
||||
* @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
|
||||
* @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
|
||||
* @see https://github.com/tc39/proposal-json-superset
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
const replacements = {
|
||||
'<': '\\u003C',
|
||||
'\u2028': '\\u2028',
|
||||
'\u2029': '\\u2029'
|
||||
};
|
||||
|
||||
const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
|
||||
|
||||
/**
|
||||
* Generates a raw HTML string containing a safe script element carrying data and associated attributes.
|
||||
*
|
||||
* It escapes all the special characters needed to guarantee the element is unbroken, but care must
|
||||
* be taken to ensure it is inserted in the document at an acceptable position for a script element,
|
||||
* and that the resulting string isn't further modified.
|
||||
*
|
||||
* @param {import('./types.js').Fetched} fetched
|
||||
* @param {(name: string, value: string) => boolean} filter
|
||||
* @param {boolean} [prerendering]
|
||||
* @returns {string} The raw HTML of a script element carrying the JSON payload.
|
||||
* @example const html = serialize_data('/data.json', null, { foo: 'bar' });
|
||||
*/
|
||||
export function serialize_data(fetched, filter, prerendering = false) {
|
||||
/** @type {Record<string, string>} */
|
||||
const headers = {};
|
||||
|
||||
let cache_control = null;
|
||||
let age = null;
|
||||
let varyAny = false;
|
||||
|
||||
for (const [key, value] of fetched.response.headers) {
|
||||
if (filter(key, value)) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
if (key === 'cache-control') cache_control = value;
|
||||
else if (key === 'age') age = value;
|
||||
else if (key === 'vary' && value.trim() === '*') varyAny = true;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
status: fetched.response.status,
|
||||
statusText: fetched.response.statusText,
|
||||
headers,
|
||||
body: fetched.response_body
|
||||
};
|
||||
|
||||
const safe_payload = JSON.stringify(payload).replace(pattern, (match) => replacements[match]);
|
||||
|
||||
const attrs = [
|
||||
'type="application/json"',
|
||||
'data-sveltekit-fetched',
|
||||
`data-url=${escape_html_attr(fetched.url)}`
|
||||
];
|
||||
|
||||
if (fetched.is_b64) {
|
||||
attrs.push('data-b64');
|
||||
}
|
||||
|
||||
if (fetched.request_headers || fetched.request_body) {
|
||||
/** @type {import('types').StrictBody[]} */
|
||||
const values = [];
|
||||
|
||||
if (fetched.request_headers) {
|
||||
values.push([...new Headers(fetched.request_headers)].join(','));
|
||||
}
|
||||
|
||||
if (fetched.request_body) {
|
||||
values.push(fetched.request_body);
|
||||
}
|
||||
|
||||
attrs.push(`data-hash="${hash(...values)}"`);
|
||||
}
|
||||
|
||||
// Compute the time the response should be cached, taking into account max-age and age.
|
||||
// Do not cache at all if a `Vary: *` header is present, as this indicates that the
|
||||
// cache is likely to get busted.
|
||||
if (!prerendering && fetched.method === 'GET' && cache_control && !varyAny) {
|
||||
const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control);
|
||||
if (match) {
|
||||
const ttl = +match[1] - +(age ?? '0');
|
||||
attrs.push(`data-ttl="${ttl}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return `<script ${attrs.join(' ')}>${safe_payload}</script>`;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { CookieSerializeOptions } from 'cookie';
|
||||
import { SSRNode, CspDirectives, ServerDataNode } from 'types';
|
||||
|
||||
export interface Fetched {
|
||||
url: string;
|
||||
method: string;
|
||||
request_body?: string | ArrayBufferView | null;
|
||||
request_headers?: HeadersInit | undefined;
|
||||
response_body: string;
|
||||
response: Response;
|
||||
is_b64?: boolean;
|
||||
}
|
||||
|
||||
export type Loaded = {
|
||||
node: SSRNode;
|
||||
data: Record<string, any> | null;
|
||||
server_data: ServerDataNode | null;
|
||||
};
|
||||
|
||||
type CspMode = 'hash' | 'nonce' | 'auto';
|
||||
|
||||
export interface CspConfig {
|
||||
mode: CspMode;
|
||||
directives: CspDirectives;
|
||||
reportOnly: CspDirectives;
|
||||
}
|
||||
|
||||
export interface CspOpts {
|
||||
prerender: boolean;
|
||||
}
|
||||
|
||||
export interface Cookie {
|
||||
name: string;
|
||||
value: string;
|
||||
options: CookieSerializeOptions;
|
||||
}
|
||||
+511
@@ -0,0 +1,511 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { base } from '__sveltekit/paths';
|
||||
import { is_endpoint_request, render_endpoint } from './endpoint.js';
|
||||
import { render_page } from './page/index.js';
|
||||
import { render_response } from './page/render.js';
|
||||
import { respond_with_error } from './page/respond_with_error.js';
|
||||
import { is_form_content_type } from '../../utils/http.js';
|
||||
import { handle_fatal_error, method_not_allowed, redirect_response } from './utils.js';
|
||||
import {
|
||||
decode_pathname,
|
||||
decode_params,
|
||||
disable_search,
|
||||
has_data_suffix,
|
||||
normalize_path,
|
||||
strip_data_suffix
|
||||
} from '../../utils/url.js';
|
||||
import { exec } from '../../utils/routing.js';
|
||||
import { redirect_json_response, render_data } from './data/index.js';
|
||||
import { add_cookies_to_headers, get_cookies } from './cookie.js';
|
||||
import { create_fetch } from './fetch.js';
|
||||
import { Redirect, NotFound } from '../control.js';
|
||||
import {
|
||||
validate_layout_exports,
|
||||
validate_layout_server_exports,
|
||||
validate_page_exports,
|
||||
validate_page_server_exports,
|
||||
validate_server_exports
|
||||
} from '../../utils/exports.js';
|
||||
import { get_option } from '../../utils/options.js';
|
||||
import { error, json, text } from '../../exports/index.js';
|
||||
import { action_json_redirect, is_action_json_request } from './page/actions.js';
|
||||
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js';
|
||||
|
||||
/* global __SVELTEKIT_ADAPTER_NAME__ */
|
||||
|
||||
/** @type {import('types').RequiredResolveOptions['transformPageChunk']} */
|
||||
const default_transform = ({ html }) => html;
|
||||
|
||||
/** @type {import('types').RequiredResolveOptions['filterSerializedResponseHeaders']} */
|
||||
const default_filter = () => false;
|
||||
|
||||
/** @type {import('types').RequiredResolveOptions['preload']} */
|
||||
const default_preload = ({ type }) => type === 'js' || type === 'css';
|
||||
|
||||
const page_methods = new Set(['GET', 'HEAD', 'POST']);
|
||||
|
||||
const allowed_page_methods = new Set(['GET', 'HEAD', 'OPTIONS']);
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {import('@sveltejs/kit').SSRManifest} manifest
|
||||
* @param {import('types').SSRState} state
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function respond(request, options, manifest, state) {
|
||||
/** URL but stripped from the potential `/__data.json` suffix and its search param */
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (options.csrf_check_origin) {
|
||||
const forbidden =
|
||||
is_form_content_type(request) &&
|
||||
(request.method === 'POST' ||
|
||||
request.method === 'PUT' ||
|
||||
request.method === 'PATCH' ||
|
||||
request.method === 'DELETE') &&
|
||||
request.headers.get('origin') !== url.origin;
|
||||
|
||||
if (forbidden) {
|
||||
const csrf_error = error(403, `Cross-site ${request.method} form submissions are forbidden`);
|
||||
if (request.headers.get('accept') === 'application/json') {
|
||||
return json(csrf_error.body, { status: csrf_error.status });
|
||||
}
|
||||
return text(csrf_error.body.message, { status: csrf_error.status });
|
||||
}
|
||||
}
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = decode_pathname(url.pathname);
|
||||
} catch {
|
||||
return text('Malformed URI', { status: 400 });
|
||||
}
|
||||
|
||||
/** @type {import('types').SSRRoute | null} */
|
||||
let route = null;
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
let params = {};
|
||||
|
||||
if (base && !state.prerendering?.fallback) {
|
||||
if (!decoded.startsWith(base)) {
|
||||
return text('Not found', { status: 404 });
|
||||
}
|
||||
decoded = decoded.slice(base.length) || '/';
|
||||
}
|
||||
|
||||
const is_data_request = has_data_suffix(decoded);
|
||||
/** @type {boolean[] | undefined} */
|
||||
let invalidated_data_nodes;
|
||||
if (is_data_request) {
|
||||
decoded = strip_data_suffix(decoded) || '/';
|
||||
url.pathname =
|
||||
strip_data_suffix(url.pathname) +
|
||||
(url.searchParams.get(TRAILING_SLASH_PARAM) === '1' ? '/' : '') || '/';
|
||||
url.searchParams.delete(TRAILING_SLASH_PARAM);
|
||||
invalidated_data_nodes = url.searchParams
|
||||
.get(INVALIDATED_PARAM)
|
||||
?.split('')
|
||||
.map((node) => node === '1');
|
||||
url.searchParams.delete(INVALIDATED_PARAM);
|
||||
}
|
||||
|
||||
if (!state.prerendering?.fallback) {
|
||||
// TODO this could theoretically break — should probably be inside a try-catch
|
||||
const matchers = await manifest._.matchers();
|
||||
|
||||
for (const candidate of manifest._.routes) {
|
||||
const match = candidate.pattern.exec(decoded);
|
||||
if (!match) continue;
|
||||
|
||||
const matched = exec(match, candidate.params, matchers);
|
||||
if (matched) {
|
||||
route = candidate;
|
||||
params = decode_params(matched);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('types').TrailingSlash | void} */
|
||||
let trailing_slash = undefined;
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const headers = {};
|
||||
|
||||
/** @type {Record<string, import('./page/types.js').Cookie>} */
|
||||
let cookies_to_add = {};
|
||||
|
||||
/** @type {import('@sveltejs/kit').RequestEvent} */
|
||||
const event = {
|
||||
// @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself
|
||||
cookies: null,
|
||||
// @ts-expect-error
|
||||
fetch: null,
|
||||
getClientAddress:
|
||||
state.getClientAddress ||
|
||||
(() => {
|
||||
throw new Error(
|
||||
`${__SVELTEKIT_ADAPTER_NAME__} does not specify getClientAddress. Please raise an issue`
|
||||
);
|
||||
}),
|
||||
locals: {},
|
||||
params,
|
||||
platform: state.platform,
|
||||
request,
|
||||
route: { id: route?.id ?? null },
|
||||
setHeaders: (new_headers) => {
|
||||
for (const key in new_headers) {
|
||||
const lower = key.toLowerCase();
|
||||
const value = new_headers[key];
|
||||
|
||||
if (lower === 'set-cookie') {
|
||||
throw new Error(
|
||||
'Use `event.cookies.set(name, value, options)` instead of `event.setHeaders` to set cookies'
|
||||
);
|
||||
} else if (lower in headers) {
|
||||
throw new Error(`"${key}" header is already set`);
|
||||
} else {
|
||||
headers[lower] = value;
|
||||
|
||||
if (state.prerendering && lower === 'cache-control') {
|
||||
state.prerendering.cache = /** @type {string} */ (value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
url,
|
||||
isDataRequest: is_data_request,
|
||||
isSubRequest: state.depth > 0
|
||||
};
|
||||
|
||||
/** @type {import('types').RequiredResolveOptions} */
|
||||
let resolve_opts = {
|
||||
transformPageChunk: default_transform,
|
||||
filterSerializedResponseHeaders: default_filter,
|
||||
preload: default_preload
|
||||
};
|
||||
|
||||
try {
|
||||
// determine whether we need to redirect to add/remove a trailing slash
|
||||
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([
|
||||
// we use == here rather than === because [undefined] serializes as "[null]"
|
||||
...route.page.layouts.map((n) => (n == undefined ? n : manifest._.nodes[n]())),
|
||||
manifest._.nodes[route.page.leaf]()
|
||||
]);
|
||||
|
||||
if (DEV) {
|
||||
const layouts = nodes.slice(0, -1);
|
||||
const page = nodes.at(-1);
|
||||
|
||||
for (const layout of layouts) {
|
||||
if (layout) {
|
||||
validate_layout_server_exports(
|
||||
layout.server,
|
||||
/** @type {string} */ (layout.server_id)
|
||||
);
|
||||
validate_layout_exports(
|
||||
layout.universal,
|
||||
/** @type {string} */ (layout.universal_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (page) {
|
||||
validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
|
||||
validate_page_exports(page.universal, /** @type {string} */ (page.universal_id));
|
||||
}
|
||||
}
|
||||
|
||||
trailing_slash = get_option(nodes, 'trailingSlash');
|
||||
} else if (route.endpoint) {
|
||||
const node = await route.endpoint();
|
||||
trailing_slash = node.trailingSlash;
|
||||
|
||||
if (DEV) {
|
||||
validate_server_exports(node, /** @type {string} */ (route.endpoint_id));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { cookies, new_cookies, get_cookie_header, set_internal } = get_cookies(
|
||||
request,
|
||||
url,
|
||||
trailing_slash ?? 'never'
|
||||
);
|
||||
|
||||
cookies_to_add = new_cookies;
|
||||
event.cookies = cookies;
|
||||
event.fetch = create_fetch({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
get_cookie_header,
|
||||
set_internal
|
||||
});
|
||||
|
||||
if (state.prerendering && !state.prerendering.fallback) disable_search(url);
|
||||
|
||||
const response = await options.hooks.handle({
|
||||
event,
|
||||
resolve: (event, opts) =>
|
||||
resolve(event, opts).then((response) => {
|
||||
// add headers/cookies here, rather than inside `resolve`, so that we
|
||||
// can do it once for all responses instead of once per `return`
|
||||
for (const key in headers) {
|
||||
const value = headers[key];
|
||||
response.headers.set(key, /** @type {string} */ (value));
|
||||
}
|
||||
|
||||
add_cookies_to_headers(response.headers, Object.values(cookies_to_add));
|
||||
|
||||
if (state.prerendering && event.route.id !== null) {
|
||||
response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id));
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
});
|
||||
|
||||
// respond with 304 if etag matches
|
||||
if (response.status === 200 && response.headers.has('etag')) {
|
||||
let if_none_match_value = request.headers.get('if-none-match');
|
||||
|
||||
// ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
|
||||
if (if_none_match_value?.startsWith('W/"')) {
|
||||
if_none_match_value = if_none_match_value.substring(2);
|
||||
}
|
||||
|
||||
const etag = /** @type {string} */ (response.headers.get('etag'));
|
||||
|
||||
if (if_none_match_value === etag) {
|
||||
const headers = new Headers({ etag });
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie
|
||||
for (const key of [
|
||||
'cache-control',
|
||||
'content-location',
|
||||
'date',
|
||||
'expires',
|
||||
'vary',
|
||||
'set-cookie'
|
||||
]) {
|
||||
const value = response.headers.get(key);
|
||||
if (value) headers.set(key, value);
|
||||
}
|
||||
|
||||
return new Response(undefined, {
|
||||
status: 304,
|
||||
headers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Edge case: If user does `return Response(30x)` in handle hook while processing a data request,
|
||||
// we need to transform the redirect response to a corresponding JSON response.
|
||||
if (is_data_request && response.status >= 300 && response.status <= 308) {
|
||||
const location = response.headers.get('location');
|
||||
if (location) {
|
||||
return redirect_json_response(new Redirect(/** @type {any} */ (response.status), location));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
if (e instanceof Redirect) {
|
||||
const response = is_data_request
|
||||
? redirect_json_response(e)
|
||||
: route?.page && is_action_json_request(event)
|
||||
? action_json_redirect(e)
|
||||
: redirect_response(e.status, e.location);
|
||||
add_cookies_to_headers(response.headers, Object.values(cookies_to_add));
|
||||
return response;
|
||||
}
|
||||
return await handle_fatal_error(event, options, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('@sveltejs/kit').ResolveOptions} [opts]
|
||||
*/
|
||||
async function resolve(event, opts) {
|
||||
try {
|
||||
if (opts) {
|
||||
if ('ssr' in opts) {
|
||||
throw new Error(
|
||||
'ssr has been removed, set it in the appropriate +layout.js instead. See the PR for more information: https://github.com/sveltejs/kit/pull/6197'
|
||||
);
|
||||
}
|
||||
|
||||
resolve_opts = {
|
||||
transformPageChunk: opts.transformPageChunk || default_transform,
|
||||
filterSerializedResponseHeaders: opts.filterSerializedResponseHeaders || default_filter,
|
||||
preload: opts.preload || default_preload
|
||||
};
|
||||
}
|
||||
|
||||
if (state.prerendering?.fallback) {
|
||||
return await render_response({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
page_config: { ssr: false, csr: true },
|
||||
status: 200,
|
||||
error: null,
|
||||
branch: [],
|
||||
fetched: [],
|
||||
resolve_opts
|
||||
});
|
||||
}
|
||||
|
||||
if (route) {
|
||||
const method = /** @type {import('types').HttpMethod} */ (event.request.method);
|
||||
|
||||
/** @type {Response} */
|
||||
let response;
|
||||
|
||||
if (is_data_request) {
|
||||
response = await render_data(
|
||||
event,
|
||||
route,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
invalidated_data_nodes,
|
||||
trailing_slash ?? 'never'
|
||||
);
|
||||
} else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
|
||||
response = await render_endpoint(event, await route.endpoint(), state);
|
||||
} else if (route.page) {
|
||||
if (page_methods.has(method)) {
|
||||
response = await render_page(event, route.page, options, manifest, state, resolve_opts);
|
||||
} else {
|
||||
const allowed_methods = new Set(allowed_page_methods);
|
||||
const node = await manifest._.nodes[route.page.leaf]();
|
||||
if (node?.server?.actions) {
|
||||
allowed_methods.add('POST');
|
||||
}
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
// This will deny CORS preflight requests implicitly because we don't
|
||||
// add the required CORS headers to the response.
|
||||
response = new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
allow: Array.from(allowed_methods.values()).join(', ')
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const mod = [...allowed_methods].reduce((acc, curr) => {
|
||||
acc[curr] = true;
|
||||
return acc;
|
||||
}, /** @type {Record<string, any>} */ ({}));
|
||||
response = method_not_allowed(mod, method);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// a route will always have a page or an endpoint, but TypeScript
|
||||
// doesn't know that
|
||||
throw new Error('This should never happen');
|
||||
}
|
||||
|
||||
// If the route contains a page and an endpoint, we need to add a
|
||||
// `Vary: Accept` header to the response because of browser caching
|
||||
if (request.method === 'GET' && route.page && route.endpoint) {
|
||||
const vary = response.headers
|
||||
.get('vary')
|
||||
?.split(',')
|
||||
?.map((v) => v.trim().toLowerCase());
|
||||
if (!(vary?.includes('accept') || vary?.includes('*'))) {
|
||||
// the returned response might have immutable headers,
|
||||
// so we have to 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.append('Vary', 'Accept');
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
if (state.error && event.isSubRequest) {
|
||||
return await fetch(request, {
|
||||
headers: {
|
||||
'x-sveltekit-error': 'true'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
return text('Internal Server Error', {
|
||||
status: 500
|
||||
});
|
||||
}
|
||||
|
||||
// if this request came direct from the user, rather than
|
||||
// via our own `fetch`, render a 404 page
|
||||
if (state.depth === 0) {
|
||||
return await respond_with_error({
|
||||
event,
|
||||
options,
|
||||
manifest,
|
||||
state,
|
||||
status: 404,
|
||||
error: new NotFound(event.url.pathname),
|
||||
resolve_opts
|
||||
});
|
||||
}
|
||||
|
||||
if (state.prerendering) {
|
||||
return text('not found', { status: 404 });
|
||||
}
|
||||
|
||||
// we can't load the endpoint from our own manifest,
|
||||
// so we need to make an actual HTTP request
|
||||
return await fetch(request);
|
||||
} catch (e) {
|
||||
// TODO if `e` is instead named `error`, some fucked up Vite transformation happens
|
||||
// and I don't even know how to describe it. need to investigate at some point
|
||||
|
||||
// HttpError from endpoint can end up here - TODO should it be handled there instead?
|
||||
return await handle_fatal_error(event, options, e);
|
||||
} finally {
|
||||
event.cookies.set = () => {
|
||||
throw new Error('Cannot use `cookies.set(...)` after the response has been generated');
|
||||
};
|
||||
|
||||
event.setHeaders = () => {
|
||||
throw new Error('Cannot use `setHeaders(...)` after the response has been generated');
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { json, text } from '../../exports/index.js';
|
||||
import { coalesce_to_error } from '../../utils/error.js';
|
||||
import { negotiate } from '../../utils/http.js';
|
||||
import { HttpError, NotFound } from '../control.js';
|
||||
import { fix_stack_trace } from '../shared-server.js';
|
||||
import { ENDPOINT_METHODS } from '../../constants.js';
|
||||
|
||||
/** @param {any} body */
|
||||
export function is_pojo(body) {
|
||||
if (typeof body !== 'object') return false;
|
||||
|
||||
if (body) {
|
||||
if (body instanceof Uint8Array) return false;
|
||||
if (body instanceof ReadableStream) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Partial<Record<import('types').HttpMethod, any>>} mod
|
||||
* @param {import('types').HttpMethod} method
|
||||
*/
|
||||
export function method_not_allowed(mod, method) {
|
||||
return text(`${method} method not allowed`, {
|
||||
status: 405,
|
||||
headers: {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
|
||||
// "The server must generate an Allow header field in a 405 status code response"
|
||||
allow: allowed_methods(mod).join(', ')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {Partial<Record<import('types').HttpMethod, any>>} mod */
|
||||
export function allowed_methods(mod) {
|
||||
const allowed = ENDPOINT_METHODS.filter((method) => method in mod);
|
||||
|
||||
if ('GET' in mod || 'HEAD' in mod) allowed.push('HEAD');
|
||||
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return as a response that renders the error.html
|
||||
*
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {number} status
|
||||
* @param {string} message
|
||||
*/
|
||||
export function static_error_page(options, status, message) {
|
||||
let page = options.templates.error({ status, message });
|
||||
|
||||
if (DEV) {
|
||||
// inject Vite HMR client, for easier debugging
|
||||
page = page.replace('</head>', '<script type="module" src="/@vite/client"></script></head>');
|
||||
}
|
||||
|
||||
return text(page, {
|
||||
headers: { 'content-type': 'text/html; charset=utf-8' },
|
||||
status
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {unknown} error
|
||||
*/
|
||||
export async function handle_fatal_error(event, options, error) {
|
||||
error = error instanceof HttpError ? error : coalesce_to_error(error);
|
||||
const status = error instanceof HttpError ? error.status : 500;
|
||||
const body = await handle_error_and_jsonify(event, options, error);
|
||||
|
||||
// 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', [
|
||||
'application/json',
|
||||
'text/html'
|
||||
]);
|
||||
|
||||
if (event.isDataRequest || type === 'application/json') {
|
||||
return json(body, {
|
||||
status
|
||||
});
|
||||
}
|
||||
|
||||
return static_error_page(options, status, body.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {import('types').SSROptions} options
|
||||
* @param {any} error
|
||||
* @returns {Promise<App.Error>}
|
||||
*/
|
||||
export async function handle_error_and_jsonify(event, options, error) {
|
||||
if (error instanceof HttpError) {
|
||||
return error.body;
|
||||
}
|
||||
|
||||
if (__SVELTEKIT_DEV__ && typeof error == 'object') {
|
||||
fix_stack_trace(error);
|
||||
}
|
||||
|
||||
return (
|
||||
(await options.hooks.handleError({ error, event })) ?? {
|
||||
message: event.route.id === null && error instanceof NotFound ? 'Not Found' : 'Internal Error'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} status
|
||||
* @param {string} location
|
||||
*/
|
||||
export function redirect_response(status, location) {
|
||||
const response = new Response(undefined, {
|
||||
status,
|
||||
headers: { location }
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@sveltejs/kit').RequestEvent} event
|
||||
* @param {Error & { path: string }} error
|
||||
*/
|
||||
export function clarify_devalue_error(event, error) {
|
||||
if (error.path) {
|
||||
return `Data returned from \`load\` while rendering ${event.route.id} is not serializable: ${error.message} (data${error.path})`;
|
||||
}
|
||||
|
||||
if (error.path === '') {
|
||||
return `Data returned from \`load\` while rendering ${event.route.id} is not a plain object`;
|
||||
}
|
||||
|
||||
// belt and braces — this should never happen
|
||||
return error.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').ServerDataNode} node
|
||||
*/
|
||||
export function stringify_uses(node) {
|
||||
const uses = [];
|
||||
|
||||
if (node.uses && node.uses.dependencies.size > 0) {
|
||||
uses.push(`"dependencies":${JSON.stringify(Array.from(node.uses.dependencies))}`);
|
||||
}
|
||||
|
||||
if (node.uses && node.uses.params.size > 0) {
|
||||
uses.push(`"params":${JSON.stringify(Array.from(node.uses.params))}`);
|
||||
}
|
||||
|
||||
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(',')}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {number} offset
|
||||
*/
|
||||
export function warn_with_callsite(message, offset = 0) {
|
||||
if (DEV) {
|
||||
const stack = fix_stack_trace(new Error()).split('\n');
|
||||
const line = stack.at(3 + offset);
|
||||
message += `\n${line}`;
|
||||
}
|
||||
|
||||
console.warn(message);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/** @type {Record<string, string>} */
|
||||
export let private_env = {};
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
export let public_env = {};
|
||||
|
||||
/** @param {any} error */
|
||||
export let fix_stack_trace = (error) => error?.stack;
|
||||
|
||||
/** @type {(environment: Record<string, string>) => void} */
|
||||
export function set_private_env(environment) {
|
||||
private_env = environment;
|
||||
}
|
||||
|
||||
/** @type {(environment: Record<string, string>) => void} */
|
||||
export function set_public_env(environment) {
|
||||
public_env = environment;
|
||||
}
|
||||
|
||||
/** @param {(error: Error) => string} value */
|
||||
export function set_fix_stack_trace(value) {
|
||||
fix_stack_trace = value;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @param {string} route_id
|
||||
* @param {string} dep
|
||||
*/
|
||||
export function validate_depends(route_id, dep) {
|
||||
const match = /^(moz-icon|view-source|jar):/.exec(dep);
|
||||
if (match) {
|
||||
console.warn(
|
||||
`${route_id}: Calling \`depends('${dep}')\` will throw an error in Firefox because \`${match[1]}\` is a special URI scheme`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
|
||||
|
||||
export const TRAILING_SLASH_PARAM = 'x-sveltekit-trailing-slash';
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
declare global {
|
||||
const __SVELTEKIT_ADAPTER_NAME__: string;
|
||||
const __SVELTEKIT_APP_VERSION_FILE__: string;
|
||||
const __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: number;
|
||||
const __SVELTEKIT_DEV__: boolean;
|
||||
const __SVELTEKIT_EMBEDDED__: boolean;
|
||||
var Bun: object;
|
||||
var Deno: object;
|
||||
}
|
||||
|
||||
export {};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following:
|
||||
*
|
||||
* ```ts
|
||||
* declare global {
|
||||
* namespace App {
|
||||
* // interface Error {}
|
||||
* // interface Locals {}
|
||||
* // interface PageData {}
|
||||
* // interface Platform {}
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* export {};
|
||||
* ```
|
||||
*
|
||||
* The `export {}` line exists because without it, the file would be treated as an _ambient module_ which prevents you from adding `import` declarations.
|
||||
* If you need to add ambient `declare module` declarations, do so in a separate file like `src/ambient.d.ts`.
|
||||
*
|
||||
* By populating these interfaces, you will gain type safety when using `event.locals`, `event.platform`, and `data` from `load` functions.
|
||||
*/
|
||||
declare namespace App {
|
||||
/**
|
||||
* Defines the common shape of expected and unexpected errors. Expected errors are thrown using the `error` function. Unexpected errors are handled by the `handleError` hooks which should return this shape.
|
||||
*/
|
||||
export interface Error {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface that defines `event.locals`, which can be accessed in [hooks](https://kit.svelte.dev/docs/hooks) (`handle`, and `handleError`), server-only `load` functions, and `+server.js` files.
|
||||
*/
|
||||
export interface Locals {}
|
||||
|
||||
/**
|
||||
* Defines the common shape of the [$page.data store](https://kit.svelte.dev/docs/modules#$app-stores-page) - that is, the data that is shared between all pages.
|
||||
* The `Load` and `ServerLoad` functions in `./$types` will be narrowed accordingly.
|
||||
* Use optional properties for data that is only present on specific pages. Do not add an index signature (`[key: string]: any`).
|
||||
*/
|
||||
export interface PageData {}
|
||||
|
||||
/**
|
||||
* If your adapter provides [platform-specific context](https://kit.svelte.dev/docs/adapters#platform-specific-context) via `event.platform`, you can specify it here.
|
||||
*/
|
||||
export interface Platform {}
|
||||
}
|
||||
|
||||
/**
|
||||
* This module is only available to [service workers](https://kit.svelte.dev/docs/service-workers).
|
||||
*/
|
||||
declare module '$service-worker' {
|
||||
/**
|
||||
* The `base` path of the deployment. Typically this is equivalent to `config.kit.paths.base`, but it is calculated from `location.pathname` meaning that it will continue to work correctly if the site is deployed to a subdirectory.
|
||||
* Note that there is a `base` but no `assets`, since service workers cannot be used if `config.kit.paths.assets` is specified.
|
||||
*/
|
||||
export const base: string;
|
||||
/**
|
||||
* An array of URL strings representing the files generated by Vite, suitable for caching with `cache.addAll(build)`.
|
||||
* During development, this is an empty array.
|
||||
*/
|
||||
export const build: string[];
|
||||
/**
|
||||
* An array of URL strings representing the files in your static directory, or whatever directory is specified by `config.kit.files.assets`. You can customize which files are included from `static` directory using [`config.kit.serviceWorker.files`](https://kit.svelte.dev/docs/configuration)
|
||||
*/
|
||||
export const files: string[];
|
||||
/**
|
||||
* An array of pathnames corresponding to prerendered pages and endpoints.
|
||||
* During development, this is an empty array.
|
||||
*/
|
||||
export const prerendered: string[];
|
||||
/**
|
||||
* See [`config.kit.version`](https://kit.svelte.dev/docs/configuration#version). It's useful for generating unique cache names inside your service worker, so that a later deployment of your app can invalidate old caches.
|
||||
*/
|
||||
export const version: string;
|
||||
}
|
||||
|
||||
/** Internal version of $app/environment */
|
||||
declare module '__sveltekit/environment' {
|
||||
/**
|
||||
* SvelteKit analyses your app during the `build` step by running it. During this process, `building` is `true`. This also applies during prerendering.
|
||||
*/
|
||||
export const building: boolean;
|
||||
/**
|
||||
* The value of `config.kit.version.name`.
|
||||
*/
|
||||
export const version: string;
|
||||
export function set_building(): void;
|
||||
}
|
||||
|
||||
/** Internal version of $app/paths */
|
||||
declare module '__sveltekit/paths' {
|
||||
/**
|
||||
* A string that matches [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths).
|
||||
*
|
||||
* Example usage: `<a href="{base}/your-page">Link</a>`
|
||||
*/
|
||||
export let base: '' | `/${string}`;
|
||||
/**
|
||||
* An absolute path that matches [`config.kit.paths.assets`](https://kit.svelte.dev/docs/configuration#paths).
|
||||
*
|
||||
* > If a value for `config.kit.paths.assets` is specified, it will be replaced with `'/_svelte_kit_assets'` during `vite dev` or `vite preview`, since the assets don't yet live at their eventual URL.
|
||||
*/
|
||||
export let assets: '' | `https://${string}` | `http://${string}` | '/_svelte_kit_assets';
|
||||
export let relative: boolean | undefined; // TODO in 2.0, make this a `boolean` that defaults to `true`
|
||||
export function reset(): void;
|
||||
export function override(paths: { base: string; assets: string }): void;
|
||||
export function set_assets(path: string): void;
|
||||
}
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
import { SvelteComponent } from 'svelte';
|
||||
import {
|
||||
Config,
|
||||
ServerLoad,
|
||||
Handle,
|
||||
HandleServerError,
|
||||
KitConfig,
|
||||
Load,
|
||||
RequestHandler,
|
||||
ResolveOptions,
|
||||
Server,
|
||||
ServerInitOptions,
|
||||
HandleFetch,
|
||||
Actions,
|
||||
HandleClientError
|
||||
} from '@sveltejs/kit';
|
||||
import {
|
||||
HttpMethod,
|
||||
MaybePromise,
|
||||
PrerenderOption,
|
||||
RequestOptions,
|
||||
TrailingSlash
|
||||
} from './private.js';
|
||||
|
||||
export interface ServerModule {
|
||||
Server: typeof InternalServer;
|
||||
}
|
||||
|
||||
export interface ServerInternalModule {
|
||||
set_building(building: boolean): void;
|
||||
set_assets(path: string): void;
|
||||
set_private_env(environment: Record<string, string>): void;
|
||||
set_public_env(environment: Record<string, string>): void;
|
||||
set_version(version: string): void;
|
||||
set_fix_stack_trace(fix_stack_trace: (error: unknown) => string): void;
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
file: string;
|
||||
size: number;
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
export interface AssetDependencies {
|
||||
file: string;
|
||||
imports: string[];
|
||||
stylesheets: string[];
|
||||
fonts: string[];
|
||||
}
|
||||
|
||||
export interface BuildData {
|
||||
app_dir: string;
|
||||
app_path: string;
|
||||
manifest_data: ManifestData;
|
||||
service_worker: string | null;
|
||||
client: {
|
||||
start: string;
|
||||
app: string;
|
||||
imports: string[];
|
||||
stylesheets: string[];
|
||||
fonts: string[];
|
||||
} | null;
|
||||
server_manifest: import('vite').Manifest;
|
||||
}
|
||||
|
||||
export interface CSRPageNode {
|
||||
component: typeof SvelteComponent;
|
||||
universal: {
|
||||
load?: Load;
|
||||
trailingSlash?: TrailingSlash;
|
||||
};
|
||||
}
|
||||
|
||||
export type CSRPageNodeLoader = () => Promise<CSRPageNode>;
|
||||
|
||||
/**
|
||||
* Definition of a client side route.
|
||||
* The boolean in the tuples indicates whether the route has a server load.
|
||||
*/
|
||||
export type CSRRoute = {
|
||||
id: string;
|
||||
exec(path: string): undefined | Record<string, string>;
|
||||
errors: Array<CSRPageNodeLoader | undefined>;
|
||||
layouts: Array<[has_server_load: boolean, node_loader: CSRPageNodeLoader] | undefined>;
|
||||
leaf: [has_server_load: boolean, node_loader: CSRPageNodeLoader];
|
||||
};
|
||||
|
||||
export interface Deferred {
|
||||
fulfil: (value: any) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
export type GetParams = (match: RegExpExecArray) => Record<string, string>;
|
||||
|
||||
export interface ServerHooks {
|
||||
handleFetch: HandleFetch;
|
||||
handle: Handle;
|
||||
handleError: HandleServerError;
|
||||
}
|
||||
|
||||
export interface ClientHooks {
|
||||
handleError: HandleClientError;
|
||||
}
|
||||
|
||||
export interface Env {
|
||||
private: Record<string, string>;
|
||||
public: Record<string, string>;
|
||||
}
|
||||
|
||||
export class InternalServer extends Server {
|
||||
init(options: ServerInitOptions): Promise<void>;
|
||||
respond(
|
||||
request: Request,
|
||||
options: RequestOptions & {
|
||||
prerendering?: PrerenderOptions;
|
||||
read: (file: string) => Buffer;
|
||||
}
|
||||
): Promise<Response>;
|
||||
}
|
||||
|
||||
export interface ManifestData {
|
||||
assets: Asset[];
|
||||
nodes: PageNode[];
|
||||
routes: RouteData[];
|
||||
matchers: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PageNode {
|
||||
depth: number;
|
||||
component?: string; // TODO supply default component if it's missing (bit of an edge case)
|
||||
universal?: string;
|
||||
server?: string;
|
||||
parent_id?: string;
|
||||
parent?: PageNode;
|
||||
/**
|
||||
* Filled with the pages that reference this layout (if this is a layout)
|
||||
*/
|
||||
child_pages?: PageNode[];
|
||||
}
|
||||
|
||||
export interface PrerenderDependency {
|
||||
response: Response;
|
||||
body: null | string | Uint8Array;
|
||||
}
|
||||
|
||||
export interface PrerenderOptions {
|
||||
cache?: string; // including this here is a bit of a hack, but it makes it easy to add <meta http-equiv>
|
||||
fallback?: boolean;
|
||||
dependencies: Map<string, PrerenderDependency>;
|
||||
}
|
||||
|
||||
export type RecursiveRequired<T> = {
|
||||
// Recursive implementation of TypeScript's Required utility type.
|
||||
// Will recursively continue until it reaches a primitive or Function
|
||||
[K in keyof T]-?: Extract<T[K], Function> extends never // If it does not have a Function type
|
||||
? RecursiveRequired<T[K]> // recursively continue through.
|
||||
: T[K]; // Use the exact type for everything else
|
||||
};
|
||||
|
||||
export type RequiredResolveOptions = Required<ResolveOptions>;
|
||||
|
||||
export interface RouteParam {
|
||||
name: string;
|
||||
matcher: string;
|
||||
optional: boolean;
|
||||
rest: boolean;
|
||||
chained: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a route segment in the app. It can either be an intermediate node
|
||||
* with only layout/error pages, or a leaf, at which point either `page` and `leaf`
|
||||
* or `endpoint` is set.
|
||||
*/
|
||||
export interface RouteData {
|
||||
id: string;
|
||||
parent: RouteData | null;
|
||||
|
||||
segment: string;
|
||||
pattern: RegExp;
|
||||
params: RouteParam[];
|
||||
|
||||
layout: PageNode | null;
|
||||
error: PageNode | null;
|
||||
leaf: PageNode | null;
|
||||
|
||||
page: {
|
||||
layouts: Array<number | undefined>;
|
||||
errors: Array<number | undefined>;
|
||||
leaf: number;
|
||||
} | null;
|
||||
|
||||
endpoint: {
|
||||
file: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type ServerRedirectNode = {
|
||||
type: 'redirect';
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type ServerNodesResponse = {
|
||||
type: 'data';
|
||||
/**
|
||||
* If `null`, then there was no load function <- TODO is this outdated now with the recent changes?
|
||||
*/
|
||||
nodes: Array<ServerDataNode | ServerDataSkippedNode | ServerErrorNode | null>;
|
||||
};
|
||||
|
||||
export type ServerDataResponse = ServerRedirectNode | ServerNodesResponse;
|
||||
|
||||
/**
|
||||
* Signals a successful response of the server `load` function.
|
||||
* The `uses` property tells the client when it's possible to reuse this data
|
||||
* in a subsequent request.
|
||||
*/
|
||||
export interface ServerDataNode {
|
||||
type: 'data';
|
||||
/**
|
||||
* The serialized version of this contains a serialized representation of any deferred promises,
|
||||
* which will be resolved later through chunk nodes.
|
||||
*/
|
||||
data: Record<string, any> | null;
|
||||
uses: Uses;
|
||||
slash?: TrailingSlash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved data/error of a deferred promise.
|
||||
*/
|
||||
export interface ServerDataChunkNode {
|
||||
type: 'chunk';
|
||||
id: number;
|
||||
data?: Record<string, any>;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the server `load` function was not run, and the
|
||||
* client should use what it has in memory
|
||||
*/
|
||||
export interface ServerDataSkippedNode {
|
||||
type: 'skip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the server `load` function failed
|
||||
*/
|
||||
export interface ServerErrorNode {
|
||||
type: 'error';
|
||||
error: App.Error;
|
||||
/**
|
||||
* Only set for HttpErrors
|
||||
*/
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface ServerMetadataRoute {
|
||||
config: any;
|
||||
api: {
|
||||
methods: Array<HttpMethod | '*'>;
|
||||
};
|
||||
page: {
|
||||
methods: Array<'GET' | 'POST'>;
|
||||
};
|
||||
methods: Array<HttpMethod | '*'>;
|
||||
prerender: PrerenderOption | undefined;
|
||||
entries: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface ServerMetadata {
|
||||
nodes: Array<{ has_server_load: boolean }>;
|
||||
routes: Map<string, ServerMetadataRoute>;
|
||||
}
|
||||
|
||||
export interface SSRComponent {
|
||||
default: {
|
||||
render(props: Record<string, any>): {
|
||||
html: string;
|
||||
head: string;
|
||||
css: {
|
||||
code: string;
|
||||
map: any; // TODO
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type SSRComponentLoader = () => Promise<SSRComponent>;
|
||||
|
||||
export interface SSRNode {
|
||||
component: SSRComponentLoader;
|
||||
/** index into the `components` array in client/manifest.js */
|
||||
index: number;
|
||||
/** external JS files */
|
||||
imports: string[];
|
||||
/** external CSS files */
|
||||
stylesheets: string[];
|
||||
/** external font files */
|
||||
fonts: string[];
|
||||
/** inlined styles */
|
||||
inline_styles?(): MaybePromise<Record<string, string>>;
|
||||
|
||||
universal: {
|
||||
load?: Load;
|
||||
prerender?: PrerenderOption;
|
||||
ssr?: boolean;
|
||||
csr?: boolean;
|
||||
trailingSlash?: TrailingSlash;
|
||||
config?: any;
|
||||
entries?: PrerenderEntryGenerator;
|
||||
};
|
||||
|
||||
server: {
|
||||
load?: ServerLoad;
|
||||
prerender?: PrerenderOption;
|
||||
ssr?: boolean;
|
||||
csr?: boolean;
|
||||
trailingSlash?: TrailingSlash;
|
||||
actions?: Actions;
|
||||
config?: any;
|
||||
entries?: PrerenderEntryGenerator;
|
||||
};
|
||||
|
||||
universal_id: string;
|
||||
server_id: string;
|
||||
}
|
||||
|
||||
export type SSRNodeLoader = () => Promise<SSRNode>;
|
||||
|
||||
export interface SSROptions {
|
||||
app_template_contains_nonce: boolean;
|
||||
csp: ValidatedConfig['kit']['csp'];
|
||||
csrf_check_origin: boolean;
|
||||
track_server_fetches: boolean;
|
||||
embedded: boolean;
|
||||
env_public_prefix: string;
|
||||
env_private_prefix: string;
|
||||
hooks: ServerHooks;
|
||||
preload_strategy: ValidatedConfig['kit']['output']['preloadStrategy'];
|
||||
root: SSRComponent['default'];
|
||||
service_worker: boolean;
|
||||
templates: {
|
||||
app(values: {
|
||||
head: string;
|
||||
body: string;
|
||||
assets: string;
|
||||
nonce: string;
|
||||
env: Record<string, string>;
|
||||
}): string;
|
||||
error(values: { message: string; status: number }): string;
|
||||
};
|
||||
version_hash: string;
|
||||
}
|
||||
|
||||
export interface PageNodeIndexes {
|
||||
errors: Array<number | undefined>;
|
||||
layouts: Array<number | undefined>;
|
||||
leaf: number;
|
||||
}
|
||||
|
||||
export type PrerenderEntryGenerator = () => MaybePromise<Array<Record<string, string>>>;
|
||||
|
||||
export type SSREndpoint = Partial<Record<HttpMethod, RequestHandler>> & {
|
||||
prerender?: PrerenderOption;
|
||||
trailingSlash?: TrailingSlash;
|
||||
config?: any;
|
||||
entries?: PrerenderEntryGenerator;
|
||||
fallback?: RequestHandler;
|
||||
};
|
||||
|
||||
export interface SSRRoute {
|
||||
id: string;
|
||||
pattern: RegExp;
|
||||
params: RouteParam[];
|
||||
page: PageNodeIndexes | null;
|
||||
endpoint: (() => Promise<SSREndpoint>) | null;
|
||||
endpoint_id?: string;
|
||||
}
|
||||
|
||||
export interface SSRState {
|
||||
fallback?: string;
|
||||
getClientAddress(): string;
|
||||
/**
|
||||
* True if we're currently attempting to render an error page
|
||||
*/
|
||||
error: boolean;
|
||||
/**
|
||||
* Allows us to prevent `event.fetch` from making infinitely looping internal requests
|
||||
*/
|
||||
depth: number;
|
||||
platform?: any;
|
||||
prerendering?: PrerenderOptions;
|
||||
/**
|
||||
* When fetching data from a +server.js endpoint in `load`, the page's
|
||||
* prerender option is inherited by the endpoint, unless overridden
|
||||
*/
|
||||
prerender_default?: PrerenderOption;
|
||||
read?: (file: string) => Buffer;
|
||||
}
|
||||
|
||||
export type StrictBody = string | ArrayBufferView;
|
||||
|
||||
export interface Uses {
|
||||
dependencies: Set<string>;
|
||||
params: Set<string>;
|
||||
parent: boolean;
|
||||
route: boolean;
|
||||
url: boolean;
|
||||
}
|
||||
|
||||
export type ValidatedConfig = RecursiveRequired<Config>;
|
||||
|
||||
export type ValidatedKitConfig = RecursiveRequired<KitConfig>;
|
||||
|
||||
export * from '../exports/index.js';
|
||||
export * from './private.js';
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
// This module contains types that are visible in the documentation,
|
||||
// but which cannot be imported from `@sveltejs/kit`. Care should
|
||||
// be taken to avoid breaking changes when editing this file
|
||||
|
||||
import { RouteDefinition } from '@sveltejs/kit';
|
||||
|
||||
export interface AdapterEntry {
|
||||
/**
|
||||
* A string that uniquely identifies an HTTP service (e.g. serverless function) and is used for deduplication.
|
||||
* For example, `/foo/a-[b]` and `/foo/[c]` are different routes, but would both
|
||||
* be represented in a Netlify _redirects file as `/foo/:param`, so they share an ID
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* A function that compares the candidate route with the current route to determine
|
||||
* if it should be grouped with the current route.
|
||||
*
|
||||
* Use cases:
|
||||
* - Fallback pages: `/foo/[c]` is a fallback for `/foo/a-[b]`, and `/[...catchall]` is a fallback for all routes
|
||||
* - Grouping routes that share a common `config`: `/foo` should be deployed to the edge, `/bar` and `/baz` should be deployed to a serverless function
|
||||
*/
|
||||
filter(route: RouteDefinition): boolean;
|
||||
|
||||
/**
|
||||
* A function that is invoked once the entry has been created. This is where you
|
||||
* should write the function to the filesystem and generate redirect manifests.
|
||||
*/
|
||||
complete(entry: { generateManifest(opts: { relativePath: string }): string }): MaybePromise<void>;
|
||||
}
|
||||
|
||||
// Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2021-present, Joshua Hemphill
|
||||
// Copyright (c) 2021, Tecnico Corporation
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
export namespace Csp {
|
||||
type ActionSource = 'strict-dynamic' | 'report-sample';
|
||||
type BaseSource =
|
||||
| 'self'
|
||||
| 'unsafe-eval'
|
||||
| 'unsafe-hashes'
|
||||
| 'unsafe-inline'
|
||||
| 'wasm-unsafe-eval'
|
||||
| 'none';
|
||||
type CryptoSource = `${'nonce' | 'sha256' | 'sha384' | 'sha512'}-${string}`;
|
||||
type FrameSource = HostSource | SchemeSource | 'self' | 'none';
|
||||
type HostNameScheme = `${string}.${string}` | 'localhost';
|
||||
type HostSource = `${HostProtocolSchemes}${HostNameScheme}${PortScheme}`;
|
||||
type HostProtocolSchemes = `${string}://` | '';
|
||||
type HttpDelineator = '/' | '?' | '#' | '\\';
|
||||
type PortScheme = `:${number}` | '' | ':*';
|
||||
type SchemeSource = 'http:' | 'https:' | 'data:' | 'mediastream:' | 'blob:' | 'filesystem:';
|
||||
type Source = HostSource | SchemeSource | CryptoSource | BaseSource;
|
||||
type Sources = Source[];
|
||||
type UriPath = `${HttpDelineator}${string}`;
|
||||
}
|
||||
|
||||
export interface CspDirectives {
|
||||
'child-src'?: Csp.Sources;
|
||||
'default-src'?: Array<Csp.Source | Csp.ActionSource>;
|
||||
'frame-src'?: Csp.Sources;
|
||||
'worker-src'?: Csp.Sources;
|
||||
'connect-src'?: Csp.Sources;
|
||||
'font-src'?: Csp.Sources;
|
||||
'img-src'?: Csp.Sources;
|
||||
'manifest-src'?: Csp.Sources;
|
||||
'media-src'?: Csp.Sources;
|
||||
'object-src'?: Csp.Sources;
|
||||
'prefetch-src'?: Csp.Sources;
|
||||
'script-src'?: Array<Csp.Source | Csp.ActionSource>;
|
||||
'script-src-elem'?: Csp.Sources;
|
||||
'script-src-attr'?: Csp.Sources;
|
||||
'style-src'?: Array<Csp.Source | Csp.ActionSource>;
|
||||
'style-src-elem'?: Csp.Sources;
|
||||
'style-src-attr'?: Csp.Sources;
|
||||
'base-uri'?: Array<Csp.Source | Csp.ActionSource>;
|
||||
sandbox?: Array<
|
||||
| 'allow-downloads-without-user-activation'
|
||||
| 'allow-forms'
|
||||
| 'allow-modals'
|
||||
| 'allow-orientation-lock'
|
||||
| 'allow-pointer-lock'
|
||||
| 'allow-popups'
|
||||
| 'allow-popups-to-escape-sandbox'
|
||||
| 'allow-presentation'
|
||||
| 'allow-same-origin'
|
||||
| 'allow-scripts'
|
||||
| 'allow-storage-access-by-user-activation'
|
||||
| 'allow-top-navigation'
|
||||
| 'allow-top-navigation-by-user-activation'
|
||||
>;
|
||||
'form-action'?: Array<Csp.Source | Csp.ActionSource>;
|
||||
'frame-ancestors'?: Array<Csp.HostSource | Csp.SchemeSource | Csp.FrameSource>;
|
||||
'navigate-to'?: Array<Csp.Source | Csp.ActionSource>;
|
||||
'report-uri'?: Csp.UriPath[];
|
||||
'report-to'?: string[];
|
||||
|
||||
'require-trusted-types-for'?: Array<'script'>;
|
||||
'trusted-types'?: Array<'none' | 'allow-duplicates' | '*' | string>;
|
||||
'upgrade-insecure-requests'?: boolean;
|
||||
|
||||
/** @deprecated */
|
||||
'require-sri-for'?: Array<'script' | 'style' | 'script style'>;
|
||||
|
||||
/** @deprecated */
|
||||
'block-all-mixed-content'?: boolean;
|
||||
|
||||
/** @deprecated */
|
||||
'plugin-types'?: Array<`${string}/${string}` | 'none'>;
|
||||
|
||||
/** @deprecated */
|
||||
referrer?: Array<
|
||||
| 'no-referrer'
|
||||
| 'no-referrer-when-downgrade'
|
||||
| 'origin'
|
||||
| 'origin-when-cross-origin'
|
||||
| 'same-origin'
|
||||
| 'strict-origin'
|
||||
| 'strict-origin-when-cross-origin'
|
||||
| 'unsafe-url'
|
||||
| 'none'
|
||||
>;
|
||||
}
|
||||
|
||||
export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';
|
||||
|
||||
export interface Logger {
|
||||
(msg: string): void;
|
||||
success(msg: string): void;
|
||||
error(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
minor(msg: string): void;
|
||||
info(msg: string): void;
|
||||
}
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export interface Prerendered {
|
||||
/**
|
||||
* A map of `path` to `{ file }` objects, where a path like `/foo` corresponds to `foo.html` and a path like `/bar/` corresponds to `bar/index.html`.
|
||||
*/
|
||||
pages: Map<
|
||||
string,
|
||||
{
|
||||
/** The location of the .html file relative to the output directory */
|
||||
file: string;
|
||||
}
|
||||
>;
|
||||
/**
|
||||
* A map of `path` to `{ type }` objects.
|
||||
*/
|
||||
assets: Map<
|
||||
string,
|
||||
{
|
||||
/** The MIME type of the asset */
|
||||
type: string;
|
||||
}
|
||||
>;
|
||||
/**
|
||||
* A map of redirects encountered during prerendering.
|
||||
*/
|
||||
redirects: Map<
|
||||
string,
|
||||
{
|
||||
status: number;
|
||||
location: string;
|
||||
}
|
||||
>;
|
||||
/** An array of prerendered paths (without trailing slashes, regardless of the trailingSlash config) */
|
||||
paths: string[];
|
||||
}
|
||||
|
||||
export interface PrerenderHttpErrorHandler {
|
||||
(details: {
|
||||
status: number;
|
||||
path: string;
|
||||
referrer: string | null;
|
||||
referenceType: 'linked' | 'fetched';
|
||||
message: string;
|
||||
}): void;
|
||||
}
|
||||
|
||||
export interface PrerenderMissingIdHandler {
|
||||
(details: { path: string; id: string; referrers: string[]; message: string }): void;
|
||||
}
|
||||
|
||||
export interface PrerenderEntryGeneratorMismatchHandler {
|
||||
(details: { generatedFromId: string; entry: string; matchedId: string; message: string }): void;
|
||||
}
|
||||
|
||||
export type PrerenderHttpErrorHandlerValue = 'fail' | 'warn' | 'ignore' | PrerenderHttpErrorHandler;
|
||||
export type PrerenderMissingIdHandlerValue = 'fail' | 'warn' | 'ignore' | PrerenderMissingIdHandler;
|
||||
export type PrerenderEntryGeneratorMismatchHandlerValue =
|
||||
| 'fail'
|
||||
| 'warn'
|
||||
| 'ignore'
|
||||
| PrerenderEntryGeneratorMismatchHandler;
|
||||
|
||||
export type PrerenderOption = boolean | 'auto';
|
||||
|
||||
export type PrerenderMap = Map<string, PrerenderOption>;
|
||||
|
||||
export interface RequestOptions {
|
||||
getClientAddress(): string;
|
||||
platform?: App.Platform;
|
||||
}
|
||||
|
||||
export interface RouteSegment {
|
||||
content: string;
|
||||
dynamic: boolean;
|
||||
rest: boolean;
|
||||
}
|
||||
|
||||
export type TrailingSlash = 'never' | 'always' | 'ignore';
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).
|
||||
|
||||
This module cannot be imported into client-side code.
|
||||
|
||||
```ts
|
||||
import { env } from '$env/dynamic/private';
|
||||
console.log(env.DEPLOYMENT_SPECIFIC_VARIABLE);
|
||||
```
|
||||
|
||||
> In `dev`, `$env/dynamic` always includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
Similar to [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
|
||||
|
||||
Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead.
|
||||
|
||||
```ts
|
||||
import { env } from '$env/dynamic/public';
|
||||
console.log(env.PUBLIC_DEPLOYMENT_SPECIFIC_VARIABLE);
|
||||
```
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).
|
||||
|
||||
_Unlike_ [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.
|
||||
|
||||
```ts
|
||||
import { API_KEY } from '$env/static/private';
|
||||
```
|
||||
|
||||
Note that all environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
|
||||
|
||||
```
|
||||
MY_FEATURE_FLAG=""
|
||||
```
|
||||
|
||||
You can override `.env` values from the command line like so:
|
||||
|
||||
```bash
|
||||
MY_FEATURE_FLAG="enabled" npm run dev
|
||||
```
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
Similar to [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
|
||||
|
||||
Values are replaced statically at build time.
|
||||
|
||||
```ts
|
||||
import { PUBLIC_BASE_URL } from '$env/static/public';
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
This is a simple alias to `src/lib`, or whatever directory is specified as [`config.kit.files.lib`](https://kit.svelte.dev/docs/configuration#files). It allows you to access common components and utility modules without `../../../../` nonsense.
|
||||
|
||||
### `$lib/server`
|
||||
|
||||
A subdirectory of `$lib`. SvelteKit will prevent you from importing any modules in `$lib/server` into client-side code. See [server-only modules](/docs/server-only-modules).
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Removes nullish values from an array.
|
||||
*
|
||||
* @template T
|
||||
* @param {Array<T>} arr
|
||||
*/
|
||||
export function compact(arr) {
|
||||
return arr.filter(/** @returns {val is NonNullable<T>} */ (val) => val != null);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @param {Record<string, string>} env
|
||||
* @param {{
|
||||
* public_prefix: string;
|
||||
* private_prefix: string;
|
||||
* }} prefixes
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function filter_private_env(env, { public_prefix, private_prefix }) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter(
|
||||
([k]) =>
|
||||
k.startsWith(private_prefix) && (public_prefix === '' || !k.startsWith(public_prefix))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} env
|
||||
* @param {{
|
||||
* public_prefix: string;
|
||||
* private_prefix: string;
|
||||
* }} prefixes
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function filter_public_env(env, { public_prefix, private_prefix }) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter(
|
||||
([k]) =>
|
||||
k.startsWith(public_prefix) && (private_prefix === '' || !k.startsWith(private_prefix))
|
||||
)
|
||||
);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @param {unknown} err
|
||||
* @return {Error}
|
||||
*/
|
||||
export function coalesce_to_error(err) {
|
||||
return err instanceof Error ||
|
||||
(err && /** @type {any} */ (err).name && /** @type {any} */ (err).message)
|
||||
? /** @type {Error} */ (err)
|
||||
: new Error(JSON.stringify(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an identity function that exists to make TypeScript less
|
||||
* paranoid about people throwing things that aren't errors, which
|
||||
* frankly is not something we should care about
|
||||
* @param {unknown} error
|
||||
*/
|
||||
export function normalize_error(error) {
|
||||
return /** @type {import('../runtime/control.js').Redirect | import('../runtime/control.js').HttpError | Error} */ (
|
||||
error
|
||||
);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* When inside a double-quoted attribute value, only `&` and `"` hold special meaning.
|
||||
* @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
const escape_html_attr_dict = {
|
||||
'&': '&',
|
||||
'"': '"'
|
||||
};
|
||||
|
||||
const escape_html_attr_regex = new RegExp(
|
||||
// special characters
|
||||
`[${Object.keys(escape_html_attr_dict).join('')}]|` +
|
||||
// high surrogate without paired low surrogate
|
||||
'[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
|
||||
// a valid surrogate pair, the only match with 2 code units
|
||||
// we match it so that we can match unpaired low surrogates in the same pass
|
||||
// TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
|
||||
'[\\ud800-\\udbff][\\udc00-\\udfff]|' +
|
||||
// unpaired low surrogate (see previous match)
|
||||
'[\\udc00-\\udfff]',
|
||||
'g'
|
||||
);
|
||||
|
||||
/**
|
||||
* Formats a string to be used as an attribute's value in raw HTML.
|
||||
*
|
||||
* It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes
|
||||
* characters that are special in attributes, and surrounds the whole string in double-quotes.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string} Escaped string surrounded by double-quotes.
|
||||
* @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`;
|
||||
*/
|
||||
export function escape_html_attr(str) {
|
||||
const escaped_str = str.replace(escape_html_attr_regex, (match) => {
|
||||
if (match.length === 2) {
|
||||
// valid surrogate pair
|
||||
return match;
|
||||
}
|
||||
|
||||
return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`;
|
||||
});
|
||||
|
||||
return `"${escaped_str}"`;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user