This commit is contained in:
2026-01-22 17:13:23 -06:00
parent 02b0f053d6
commit f15f7b8e38
1728 changed files with 925278 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
export { sequence } from './sequence.js';
+145
View File
@@ -0,0 +1,145 @@
/** @import { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit' */
/** @import { MaybePromise } from 'types' */
import {
merge_tracing,
get_request_store,
with_request_store
} from '@sveltejs/kit/internal/server';
/**
* 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');
* return true;
* }
* });
* 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');
* return true;
* },
* filterSerializedResponseHeaders: () => {
* // this one wins as it's the first defined in the chain
* console.log('second filterSerializedResponseHeaders');
* return true;
* }
* });
* 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 {...Handle} handlers The chain of `handle` functions
* @returns {Handle}
*/
export function sequence(...handlers) {
const length = handlers.length;
if (!length) return ({ event, resolve }) => resolve(event);
return ({ event, resolve }) => {
const { state } = get_request_store();
return apply_handle(0, event, {});
/**
* @param {number} i
* @param {RequestEvent} event
* @param {ResolveOptions | undefined} parent_options
* @returns {MaybePromise<Response>}
*/
function apply_handle(i, event, parent_options) {
const handle = handlers[i];
return state.tracing.record_span({
name: `sveltekit.handle.sequenced.${handle.name ? handle.name : i}`,
attributes: {},
fn: async (current) => {
const traced_event = merge_tracing(event, current);
return await with_request_store({ event: traced_event, state }, () =>
handle({
event: traced_event,
resolve: (event, options) => {
/** @type {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 {ResolveOptions['filterSerializedResponseHeaders']} */
const filterSerializedResponseHeaders =
parent_options?.filterSerializedResponseHeaders ??
options?.filterSerializedResponseHeaders;
/** @type {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
});
}
})
);
}
});
}
};
}
+308
View File
@@ -0,0 +1,308 @@
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
import { HttpError, Redirect, ActionFailure, ValidationError } from './internal/index.js';
import { BROWSER, DEV } from 'esm-env';
import {
add_data_suffix,
add_resolution_suffix,
has_data_suffix,
has_resolution_suffix,
strip_data_suffix,
strip_resolution_suffix
} from '../runtime/pathname.js';
import { text_encoder } from '../runtime/utils.js';
export { VERSION } from '../version.js';
// TODO 3.0: remove these types as they are not used anymore (we can't remove them yet because that would be a breaking change)
/**
* @template {number} TNumber
* @template {any[]} [TArray=[]]
* @typedef {TNumber extends TArray['length'] ? TArray[number] : LessThan<TNumber, [...TArray, TArray['length']]>} LessThan
*/
/**
* @template {number} TStart
* @template {number} TEnd
* @typedef {Exclude<TEnd | LessThan<TEnd>, LessThan<TStart>>} NumericRange
*/
// Keep the status codes as `number` because restricting to certain numbers makes it unnecessarily hard to use compared to the benefits
// (we have runtime errors already to check for invalid codes). Also see https://github.com/sveltejs/kit/issues/11780
// we have to repeat the JSDoc because the display for function overloads is broken
// see https://github.com/microsoft/TypeScript/issues/55056
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this 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 {App.Error} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
* @overload
* @param {number} status
* @param {App.Error} body
* @return {never}
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this 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.
* @overload
* @param {number} status
* @param {{ message: string } extends App.Error ? App.Error | string | undefined : never} [body]
* @return {never}
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/
/**
* Throws an error with a HTTP status code and an optional message.
* When called during request handling, this 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.
* @return {never}
* @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling.
* @throws {Error} If the provided status is invalid (not between 400 and 599).
*/
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`);
}
throw new HttpError(status, body);
}
/**
* Checks whether this is an error thrown by {@link error}.
* @template {number} T
* @param {unknown} e
* @param {T} [status] The status to filter for.
* @return {e is (HttpError & { status: T extends undefined ? never : T })}
*/
export function isHttpError(e, status) {
if (!(e instanceof HttpError)) return false;
return !status || e.status === status;
}
/**
* Redirect a request. When called 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.
*
* Most common status codes:
* * `303 See Other`: redirect as a GET request (often used after a form POST request)
* * `307 Temporary Redirect`: redirect will keep the request method
* * `308 Permanent Redirect`: redirect will keep the request method, SEO will be transferred to the new page
*
* [See all redirect status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages)
*
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number)} 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.
* @throws {Redirect} This error instructs SvelteKit to redirect to the specified location.
* @throws {Error} If the provided status is invalid.
* @return {never}
*/
export function redirect(status, location) {
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
throw new Error('Invalid status code');
}
throw new Redirect(
// @ts-ignore
status,
location.toString()
);
}
/**
* Checks whether this is a redirect thrown by {@link redirect}.
* @param {unknown} e The object to check.
* @return {e is Redirect}
*/
export function isRedirect(e) {
return e instanceof Redirect;
}
/**
* 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', text_encoder.encode(body).byteLength.toString());
}
if (!headers.has('content-type')) {
headers.set('content-type', 'application/json');
}
return new Response(body, {
...init,
headers
});
}
/**
* 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 = text_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. Call when form submission fails.
* @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.
* @overload
* @param {number} status
* @returns {import('./public.js').ActionFailure<undefined>}
*/
/**
* Create an `ActionFailure` object. Call when form submission fails.
* @template [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)
* @overload
* @param {number} status
* @param {T} data
* @returns {import('./public.js').ActionFailure<T>}
*/
/**
* Create an `ActionFailure` object. Call when form submission fails.
* @param {number} status
* @param {any} [data]
* @returns {import('./public.js').ActionFailure<any>}
*/
export function fail(status, data) {
// @ts-expect-error unique symbol missing
return new ActionFailure(status, data);
}
/**
* Checks whether this is an action failure thrown by {@link fail}.
* @param {unknown} e The object to check.
* @return {e is import('./public.js').ActionFailure}
*/
export function isActionFailure(e) {
return e instanceof ActionFailure;
}
/**
* Use this to throw a validation error to imperatively fail form validation.
* Can be used in combination with `issue` passed to form actions to create field-specific issues.
*
* @example
* ```ts
* import { invalid } from '@sveltejs/kit';
* import { form } from '$app/server';
* import { tryLogin } from '$lib/server/auth';
* import * as v from 'valibot';
*
* export const login = form(
* v.object({ name: v.string(), _password: v.string() }),
* async ({ name, _password }) => {
* const success = tryLogin(name, _password);
* if (!success) {
* invalid('Incorrect username or password');
* }
*
* // ...
* }
* );
* ```
* @param {...(StandardSchemaV1.Issue | string)} issues
* @returns {never}
* @since 2.47.3
*/
export function invalid(...issues) {
throw new ValidationError(
issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue))
);
}
/**
* Checks whether this is an validation error thrown by {@link invalid}.
* @param {unknown} e The object to check.
* @return {e is import('./public.js').ActionFailure}
* @since 2.47.3
*/
export function isValidationError(e) {
return e instanceof ValidationError;
}
/**
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
* Returns the normalized URL as well as a method for adding the potential suffix back
* based on a new pathname (possibly including search) or URL.
* ```js
* import { normalizeUrl } from '@sveltejs/kit';
*
* const { url, denormalize } = normalizeUrl('/blog/post/__data.json');
* console.log(url.pathname); // /blog/post
* console.log(denormalize('/blog/post/a')); // /blog/post/a/__data.json
* ```
* @param {URL | string} url
* @returns {{ url: URL, wasNormalized: boolean, denormalize: (url?: string | URL) => URL }}
* @since 2.18.0
*/
export function normalizeUrl(url) {
url = new URL(url, 'http://internal');
const is_route_resolution = has_resolution_suffix(url.pathname);
const is_data_request = has_data_suffix(url.pathname);
const has_trailing_slash = url.pathname !== '/' && url.pathname.endsWith('/');
if (is_route_resolution) {
url.pathname = strip_resolution_suffix(url.pathname);
} else if (is_data_request) {
url.pathname = strip_data_suffix(url.pathname);
} else if (has_trailing_slash) {
url.pathname = url.pathname.slice(0, -1);
}
return {
url,
wasNormalized: is_data_request || is_route_resolution || has_trailing_slash,
denormalize: (new_url = url) => {
new_url = new URL(new_url, url);
if (is_route_resolution) {
new_url.pathname = add_resolution_suffix(new_url.pathname);
} else if (is_data_request) {
new_url.pathname = add_data_suffix(new_url.pathname);
} else if (has_trailing_slash && !new_url.pathname.endsWith('/')) {
new_url.pathname += '/';
}
return new_url;
}
};
}
+85
View File
@@ -0,0 +1,85 @@
/** @import { RequestEvent } from '@sveltejs/kit' */
/** @import { RequestStore } from 'types' */
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
import { IN_WEBCONTAINER } from '../../runtime/server/constants.js';
/** @type {RequestStore | null} */
let sync_store = null;
/** @type {AsyncLocalStorage<RequestStore | null> | null} */
let als;
import('node:async_hooks')
.then((hooks) => (als = new hooks.AsyncLocalStorage()))
.catch(() => {
// can't use AsyncLocalStorage, but can still call getRequestEvent synchronously.
// this isn't behind `supports` because it's basically just StackBlitz (i.e.
// in-browser usage) that doesn't support it AFAICT
});
/**
* Returns the current `RequestEvent`. Can be used inside server hooks, server `load` functions, actions, and endpoints (and functions called by them).
*
* In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`).
* @since 2.20.0
*
* @returns {RequestEvent}
*/
export function getRequestEvent() {
const event = try_get_request_store()?.event;
if (!event) {
let message =
'Can only read the current request event inside functions invoked during `handle`, such as server `load` functions, actions, endpoints, and other server hooks.';
if (!als) {
message +=
' In environments without `AsyncLocalStorage`, the event must be read synchronously, not after an `await`.';
}
throw new Error(message);
}
return event;
}
export function get_request_store() {
const result = try_get_request_store();
if (!result) {
let message = 'Could not get the request store.';
if (als) {
message += ' This is an internal error.';
} else {
message +=
' In environments without `AsyncLocalStorage`, the request store (used by e.g. remote functions) must be accessed synchronously, not after an `await`.' +
' If it was accessed synchronously then this is an internal error.';
}
throw new Error(message);
}
return result;
}
export function try_get_request_store() {
return sync_store ?? als?.getStore() ?? null;
}
/**
* @template T
* @param {RequestStore | null} store
* @param {() => T} fn
*/
export function with_request_store(store, fn) {
try {
sync_store = store;
return als ? als.run(store, fn) : fn();
} finally {
// Since AsyncLocalStorage is not working in webcontainers, we don't reset `sync_store`
// and handle only one request at a time in `src/runtime/server/index.js`.
if (!IN_WEBCONTAINER) {
sync_store = null;
}
}
}
+81
View File
@@ -0,0 +1,81 @@
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
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;
}
}
/**
* An error that was thrown from within the SvelteKit runtime that is not fatal and doesn't result in a 500, such as a 404.
* `SvelteKitError` goes through `handleError`.
* @extends Error
*/
export class SvelteKitError extends Error {
/**
* @param {number} status
* @param {string} text
* @param {string} message
*/
constructor(status, text, message) {
super(message);
this.status = status;
this.text = text;
}
}
/**
* @template [T=undefined]
*/
export class ActionFailure {
/**
* @param {number} status
* @param {T} data
*/
constructor(status, data) {
this.status = status;
this.data = data;
}
}
/**
* Error thrown when form validation fails imperatively
*/
export class ValidationError extends Error {
/**
* @param {StandardSchemaV1.Issue[]} issues
*/
constructor(issues) {
super('Validation failed');
this.name = 'ValidationError';
this.issues = issues;
}
}
export { init_remote_functions } from './remote-functions.js';
+28
View File
@@ -0,0 +1,28 @@
/** @import { RemoteInfo } from 'types' */
/** @type {RemoteInfo['type'][]} */
const types = ['command', 'form', 'prerender', 'query', 'query_batch'];
/**
* @param {Record<string, any>} module
* @param {string} file
* @param {string} hash
*/
export function init_remote_functions(module, file, hash) {
if (module.default) {
throw new Error(
`Cannot export \`default\` from a remote module (${file}) — please use named exports instead`
);
}
for (const [name, fn] of Object.entries(module)) {
if (!types.includes(fn?.__?.type)) {
throw new Error(
`\`${name}\` exported from ${file} is invalid — all exports from this file must be remote functions`
);
}
fn.__.id = `${hash}/${name}`;
fn.__.name = name;
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* @template {{ tracing: { enabled: boolean, root: import('@opentelemetry/api').Span, current: import('@opentelemetry/api').Span } }} T
* @param {T} event_like
* @param {import('@opentelemetry/api').Span} current
* @returns {T}
*/
export function merge_tracing(event_like, current) {
return {
...event_like,
tracing: {
...event_like.tracing,
current
}
};
}
export {
with_request_store,
getRequestEvent,
get_request_store,
try_get_request_store
} from './event.js';
+238
View File
@@ -0,0 +1,238 @@
import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
import * as set_cookie_parser from 'set-cookie-parser';
import { SvelteKitError } from '../internal/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;
}
if (req.destroyed) {
const readable = new ReadableStream();
void readable.cancel();
return readable;
}
let size = 0;
let cancelled = false;
return new ReadableStream({
start(controller) {
if (body_size_limit !== undefined && content_length > body_size_limit) {
let message = `Content-length of ${content_length} exceeds limit of ${body_size_limit} bytes.`;
if (body_size_limit === 0) {
// https://github.com/sveltejs/kit/pull/11589
// TODO this exists to aid migration — remove in a future version
message += ' To disable body size limits, specify Infinity rather than 0.';
}
const error = new SvelteKitError(413, 'Payload Too Large', message);
controller.error(error);
return;
}
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 > content_length) {
cancelled = true;
const constraint = content_length ? 'content-length' : 'BODY_SIZE_LIMIT';
const message = `request body size exceeded ${constraint} of ${content_length}`;
const error = new SvelteKitError(413, 'Payload Too Large', message);
controller.error(error);
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>}
*/
// TODO 3.0 make the signature synchronous?
// eslint-disable-next-line @typescript-eslint/require-await
export async function getRequest({ request, base, bodySizeLimit }) {
let headers = /** @type {Record<string, string>} */ (request.headers);
if (request.httpVersionMajor >= 2) {
// the Request constructor rejects headers with ':' in the name
headers = Object.assign({}, headers);
// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.5
if (headers[':authority']) {
headers.host = headers[':authority'];
}
delete headers[':authority'];
delete headers[':method'];
delete headers[':path'];
delete headers[':scheme'];
}
// TODO: Whenever Node >=22 is minimum supported version, we can use `request.readableAborted`
// @see https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/lib/internal/streams/readable.js#L1443-L1453
const controller = new AbortController();
let errored = false;
let end_emitted = false;
request.once('error', () => (errored = true));
request.once('end', () => (end_emitted = true));
request.once('close', () => {
if ((errored || request.destroyed) && !end_emitted) {
controller.abort();
}
});
return new Request(base + request.url, {
// @ts-expect-error
duplex: 'half',
method: request.method,
headers: Object.entries(headers),
signal: controller.signal,
body:
request.method === 'GET' || request.method === 'HEAD'
? undefined
: get_raw_body(request, bodySizeLimit)
});
}
/**
* @param {import('http').ServerResponse} res
* @param {Response} response
* @returns {Promise<void>}
*/
// TODO 3.0 make the signature synchronous?
// eslint-disable-next-line @typescript-eslint/require-await
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) {
void 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);
void 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)));
}
}
}
/**
* Converts a file on disk to a readable stream
* @param {string} file
* @returns {ReadableStream}
* @since 2.4.0
*/
export function createReadableStream(file) {
return /** @type {ReadableStream} */ (Readable.toWeb(createReadStream(file)));
}
+30
View File
@@ -0,0 +1,30 @@
import buffer from 'node:buffer';
import { webcrypto as crypto } from 'node:crypto';
// `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;
/** @type {Record<string, any>} */
const globals = {
crypto,
File
};
// exported for dev/preview and node environments
/**
* Make various web APIs available as globals:
* - `crypto`
* - `File`
*/
export function installPolyfills() {
for (const name in globals) {
if (name in globalThis) continue;
Object.defineProperty(globalThis, name, {
enumerable: true,
configurable: true,
writable: true,
value: globals[name]
});
}
}
File diff suppressed because it is too large Load Diff
+245
View File
@@ -0,0 +1,245 @@
import fs from 'node:fs';
import { mkdirp } from '../../../utils/filesystem.js';
import { filter_fonts, find_deps, resolve_symlinks } from './utils.js';
import { s } from '../../../utils/misc.js';
import { normalizePath } from 'vite';
import { basename, join } from 'node:path';
import { create_node_analyser } from '../static_analysis/index.js';
/**
* @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.OutputBundle | null} server_bundle
* @param {import('vite').Rollup.RollupOutput['output'] | null} client_chunks
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports
*/
export async function build_server_nodes(
out,
kit,
manifest_data,
server_manifest,
client_manifest,
server_bundle,
client_chunks,
output_config,
static_exports
) {
mkdirp(`${out}/server/nodes`);
mkdirp(`${out}/server/stylesheets`);
/** @type {Map<string, string>} */
const stylesheets_to_inline = new Map();
if (server_bundle && client_chunks && kit.inlineStyleThreshold > 0) {
const client = get_stylesheets(client_chunks);
const server = get_stylesheets(Object.values(server_bundle));
// map server stylesheet name to the client stylesheet name
for (const [id, client_stylesheet] of client.stylesheets_used) {
const server_stylesheet = server.stylesheets_used.get(id);
if (!server_stylesheet) {
continue;
}
client_stylesheet.forEach((file, i) => {
stylesheets_to_inline.set(file, server_stylesheet[i]);
});
}
// filter out stylesheets that should not be inlined
for (const [fileName, content] of client.stylesheet_content) {
if (content.length >= kit.inlineStyleThreshold) {
stylesheets_to_inline.delete(fileName);
}
}
// map server stylesheet source to the client stylesheet name
for (const [client_file, server_file] of stylesheets_to_inline) {
const source = server.stylesheet_content.get(server_file);
if (!source) {
throw new Error(`Server stylesheet source not found for client stylesheet ${client_file}`);
}
stylesheets_to_inline.set(client_file, source);
}
}
const { get_page_options } = create_node_analyser({
resolve: (server_node) => {
// Windows needs the file:// protocol for absolute path dynamic imports
return import(
`file://${join(out, 'server', resolve_symlinks(server_manifest, server_node).chunk.file)}`
);
},
static_exports
});
for (let i = 0; i < manifest_data.nodes.length; i++) {
const node = manifest_data.nodes[i];
/** @type {string[]} */
const imports = [];
// String representation of
/** @type {import('types').SSRNode} */
/** @type {string[]} */
const exports = [`export const index = ${i};`];
/** @type {string[]} */
let imported = [];
/** @type {string[]} */
let stylesheets = [];
/** @type {string[]} */
let 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) {
const page_options = await get_page_options(node);
if (!!page_options && page_options.ssr === false) {
exports.push(`export const universal = ${s(page_options, null, 2)};`);
} else {
imports.push(
`import * as universal from '../${resolve_symlinks(server_manifest, node.universal).chunk.file}';`
);
// TODO: when building for analysis, explain why the file was loaded on the server if we fail to load it
exports.push('export { universal };');
}
exports.push(`export const universal_id = ${s(node.universal)};`);
}
if (node.server) {
imports.push(
`import * as server from '../${resolve_symlinks(server_manifest, node.server).chunk.file}';`
);
exports.push('export { server };');
exports.push(`export const server_id = ${s(node.server)};`);
}
if (
client_manifest &&
(node.universal || node.component) &&
output_config.bundleStrategy === 'split'
) {
const entry_path = `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`;
const entry = find_deps(client_manifest, entry_path, true);
// eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC.
// However, if it is not used during SSR (not present in the server manifest),
// then it can be lazily loaded in the browser.
/** @type {import('types').AssetDependencies | undefined} */
let component;
if (node.component) {
component = find_deps(server_manifest, node.component, true);
}
/** @type {import('types').AssetDependencies | undefined} */
let universal;
if (node.universal) {
universal = find_deps(server_manifest, node.universal, true);
}
/** @type {Set<string>} */
const eager_css = new Set();
/** @type {Set<string>} */
const eager_assets = new Set();
entry.stylesheet_map.forEach((value, filepath) => {
// pages and layouts are renamed to node indexes when optimised for the client
// so we use the original filename instead to check against the server manifest
if (filepath === entry_path) {
filepath = node.component ?? filepath;
}
if (component?.stylesheet_map.has(filepath) || universal?.stylesheet_map.has(filepath)) {
value.css.forEach((file) => eager_css.add(file));
value.assets.forEach((file) => eager_assets.add(file));
}
});
imported = entry.imports;
stylesheets = Array.from(eager_css);
fonts = filter_fonts(Array.from(eager_assets));
}
exports.push(
`export const imports = ${s(imported)};`,
`export const stylesheets = ${s(stylesheets)};`,
`export const fonts = ${s(fonts)};`
);
/** @type {string[]} */
const inline_styles = [];
stylesheets.forEach((file, i) => {
if (stylesheets_to_inline.has(file)) {
const filename = basename(file);
const dest = `${out}/server/stylesheets/${filename}.js`;
const source = stylesheets_to_inline.get(file);
if (!source) {
throw new Error(`Server stylesheet source not found for client stylesheet ${file}`);
}
fs.writeFileSync(dest, `// ${filename}\nexport default ${s(source)};`);
const name = `stylesheet_${i}`;
imports.push(`import ${name} from '../stylesheets/${filename}.js';`);
inline_styles.push(`\t${s(file)}: ${name}`);
}
});
if (inline_styles.length > 0) {
exports.push(`export const inline_styles = () => ({\n${inline_styles.join(',\n')}\n});`);
}
fs.writeFileSync(
`${out}/server/nodes/${i}.js`,
`${imports.join('\n')}\n\n${exports.join('\n')}\n`
);
}
}
/**
* @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[]} chunks
*/
function get_stylesheets(chunks) {
/**
* A map of module IDs and the stylesheets they use.
* @type {Map<string, string[]>}
*/
const stylesheets_used = new Map();
/**
* A map of stylesheet names and their content.
* @type {Map<string, string>}
*/
const stylesheet_content = new Map();
for (const chunk of chunks) {
if (chunk.type === 'asset') {
if (chunk.fileName.endsWith('.css')) {
stylesheet_content.set(chunk.fileName, chunk.source.toString());
}
continue;
}
if (chunk.viteMetadata?.importedCss.size) {
const css = Array.from(chunk.viteMetadata.importedCss);
for (const id of chunk.moduleIds) {
stylesheets_used.set(id, css);
}
}
}
return { stylesheets_used, stylesheet_content };
}
@@ -0,0 +1,138 @@
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, strip_virtual_prefix, get_env, normalize_id } from '../utils.js';
import { create_static_module } from '../../../core/env.js';
import { env_static_public, service_worker } from '../module_ids.js';
// @ts-ignore `vite.rolldownVersion` only exists in `rolldown-vite`
const isRolldown = !!vite.rolldownVersion;
/**
* @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));
}
// 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('/')";
const service_worker_code = 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)};
`;
const env = get_env(kit.env, vite_config.mode);
/**
* @type {import('vite').Plugin}
*/
const sw_virtual_modules = {
name: 'service-worker-build-virtual-modules',
resolveId(id) {
if (id.startsWith('$env/') || id.startsWith('$app/') || id === '$service-worker') {
// ids with :$ don't work with reverse proxies like nginx
return `\0virtual:${id.substring(1)}`;
}
},
load(id) {
if (!id.startsWith('\0virtual:')) return;
if (id === service_worker) {
return service_worker_code;
}
if (id === env_static_public) {
return create_static_module('$env/static/public', env.public);
}
const normalized_cwd = vite.normalizePath(process.cwd());
const normalized_lib = vite.normalizePath(kit.files.lib);
const relative = normalize_id(id, normalized_lib, normalized_cwd);
const stripped = strip_virtual_prefix(relative);
throw new Error(
`Cannot import ${stripped} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.`
);
}
};
await vite.build({
build: {
modulePreload: false,
rollupOptions: {
input: {
'service-worker': service_worker_entry_file
},
output: {
// .mjs so that esbuild doesn't incorrectly inject `export` https://github.com/vitejs/vite/issues/15379
entryFileNames: `service-worker.${isRolldown ? 'js' : 'mjs'}`,
assetFileNames: `${kit.appDir}/immutable/assets/[name].[hash][extname]`,
inlineDynamicImports: true
}
},
outDir: `${out}/client`,
emptyOutDir: false,
minify: vite_config.build.minify
},
configFile: false,
define: vite_config.define,
publicDir: false,
plugins: [sw_virtual_modules],
resolve: {
alias: [...get_config_aliases(kit)]
},
experimental: {
renderBuiltUrl(filename) {
return {
runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname`
};
}
}
});
// rename .mjs to .js to avoid incorrect MIME types with ancient webservers
if (!isRolldown) {
fs.renameSync(`${out}/client/service-worker.mjs`, `${out}/client/service-worker.js`);
}
}
+131
View File
@@ -0,0 +1,131 @@
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 imported_assets = new Set();
/** @type {Map<string, { css: Set<string>; assets: Set<string> }>} */
const stylesheet_map = new Map();
/**
* @param {string} current
* @param {boolean} add_js
* @param {string} initial_importer
* @param {number} dynamic_import_depth
*/
function traverse(current, add_js, initial_importer, dynamic_import_depth) {
if (seen.has(current)) return;
seen.add(current);
const { chunk } = resolve_symlinks(manifest, current);
if (add_js) imports.add(chunk.file);
if (chunk.assets) {
chunk.assets.forEach((asset) => imported_assets.add(asset));
}
if (chunk.css) {
chunk.css.forEach((file) => stylesheets.add(file));
}
if (chunk.imports) {
chunk.imports.forEach((file) =>
traverse(file, add_js, initial_importer, dynamic_import_depth)
);
}
if (!add_dynamic_css) return;
if ((chunk.css || chunk.assets) && dynamic_import_depth <= 1) {
// group files based on the initial importer because if a file is only ever
// a transitive dependency, it doesn't have a suitable name we can map back to
// the server manifest
if (stylesheet_map.has(initial_importer)) {
const { css, assets } = /** @type {{ css: Set<string>; assets: Set<string> }} */ (
stylesheet_map.get(initial_importer)
);
if (chunk.css) chunk.css.forEach((file) => css.add(file));
if (chunk.assets) chunk.assets.forEach((file) => assets.add(file));
} else {
stylesheet_map.set(initial_importer, {
css: new Set(chunk.css),
assets: new Set(chunk.assets)
});
}
}
if (chunk.dynamicImports) {
dynamic_import_depth++;
chunk.dynamicImports.forEach((file) => {
traverse(file, false, file, dynamic_import_depth);
});
}
}
const { chunk, file } = resolve_symlinks(manifest, entry);
traverse(file, true, entry, 0);
const assets = Array.from(imported_assets);
return {
assets,
file: chunk.file,
imports: Array.from(imports),
stylesheets: Array.from(stylesheets),
// TODO do we need this separately?
fonts: filter_fonts(assets),
stylesheet_map
};
}
/**
* @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 };
}
/**
* @param {string[]} assets
* @returns {string[]}
*/
export function filter_fonts(assets) {
return assets.filter((asset) => /\.(woff2?|ttf|otf)$/.test(asset));
}
/**
* @param {import('types').ValidatedKitConfig} config
* @returns {string}
*/
export function assets_base(config) {
return (config.paths.assets || config.paths.base || '.') + '/';
}
+686
View File
@@ -0,0 +1,686 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { URL } from 'node:url';
import { AsyncLocalStorage } from 'node:async_hooks';
import colors from 'kleur';
import sirv from 'sirv';
import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite';
import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
import { installPolyfills } from '../../../exports/node/polyfills.js';
import { coalesce_to_error } from '../../../utils/error.js';
import { from_fs, posixify, resolve_entry, to_fs } from '../../../utils/filesystem.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';
import { check_feature } from '../../../utils/features.js';
import { escape_html } from '../../../utils/escape.js';
import { create_node_analyser } from '../static_analysis/index.js';
const cwd = process.cwd();
// vite-specifc queries that we should skip handling for css urls
const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/;
/**
* @param {import('vite').ViteDevServer} vite
* @param {import('vite').ResolvedConfig} vite_config
* @param {import('types').ValidatedConfig} svelte_config
* @param {() => Array<{ hash: string, file: string }>} get_remotes
* @return {Promise<Promise<() => void>>}
*/
export async function dev(vite, vite_config, svelte_config, get_remotes) {
installPolyfills();
const async_local_storage = new AsyncLocalStorage();
globalThis.__SVELTEKIT_TRACK__ = (label) => {
const context = async_local_storage.getStore();
if (!context || context.prerender === true) return;
check_feature(context.event.route.id, context.config, label, svelte_config.kit.adapter);
};
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://svelte.dev/docs/kit/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('..') ? to_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 };
}
/** @type {(file: string) => void} */
let invalidate_page_options;
function update_manifest() {
try {
({ manifest_data } = 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;
}
const node_analyser = create_node_analyser({
resolve: async (server_node) => {
const { module } = await resolve(server_node);
return module;
}
});
invalidate_page_options = node_analyser.invalidate_page_options;
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/entry.js`,
app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`,
imports: [],
stylesheets: [],
fonts: [],
uses_env_dynamic_public: true,
nodes:
svelte_config.kit.router.resolution === 'client'
? undefined
: manifest_data.nodes.map((node, i) => {
if (node.component || node.universal) {
return `${svelte_config.kit.paths.base}${to_fs(svelte_config.kit.outDir)}/generated/client/nodes/${i}.js`;
}
}),
// `css` is not necessary in dev, as the JS file from `nodes` will reference the CSS file
routes:
svelte_config.kit.router.resolution === 'client'
? undefined
: compact(
manifest_data.routes.map((route) => {
if (!route.page) return;
return {
id: route.id,
pattern: route.pattern,
params: route.params,
layouts: route.page.layouts.map((l) =>
l !== undefined ? [!!manifest_data.nodes[l].server, l] : undefined
),
errors: route.page.errors,
leaf: [!!manifest_data.nodes[route.page.leaf].server, route.page.leaf]
};
})
)
},
server_assets: new Proxy(
{},
{
has: (_, /** @type {string} */ file) => fs.existsSync(from_fs(file)),
get: (_, /** @type {string} */ file) => fs.statSync(from_fs(file)).size
}
),
nodes: manifest_data.nodes.map((node, index) => {
return async () => {
/** @type {import('types').SSRNode} */
const result = {};
result.index = index;
result.universal_id = node.universal;
result.server_id = node.server;
// these are unused in dev, but it's easier to include them
result.imports = [];
result.stylesheets = [];
result.fonts = [];
/** @type {import('vite').ModuleNode[]} */
const module_nodes = [];
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 page_options = await node_analyser.get_page_options(node);
if (page_options?.ssr === false) {
result.universal = page_options;
} else {
// TODO: explain why the file was loaded on the server if we fail to load it
const { module, module_node } = await resolve(node.universal);
module_nodes.push(module_node);
result.universal = module;
}
}
if (node.server) {
const { module } = await resolve(node.server);
result.server = module;
}
// 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 () => {
/** @type {Set<import('vite').ModuleNode | import('vite').EnvironmentModuleNode>} */
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) {
if (isCSSRequest(dep.url) && !vite_css_query_regex.test(dep.url)) {
const inlineCssUrl = dep.url.includes('?')
? dep.url.replace('?', '?inline&')
: dep.url + '?inline';
try {
const mod = await vite.ssrLoadModule(inlineCssUrl);
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;
};
}),
prerendered_routes: new Set(),
get remotes() {
return Object.fromEntries(
get_remotes().map((remote) => [
remote.hash,
() => vite.ssrLoadModule(remote.file).then((module) => ({ default: module }))
])
);
},
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) {
try {
vite.ssrFixStacktrace(error);
} catch {
// ssrFixStacktrace can fail on StackBlitz web containers and we don't know why
// by ignoring it the line numbers are wrong, but at least we can show the error
}
return error.stack;
}
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) ||
svelte_config.kit.moduleExtensions.some((ext) => file.endsWith(`.remote${ext}`)) ||
// 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.
// These watchers only run for routes, param matchers, and client hooks.
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;
if (/\+(page|layout).*$/.test(file)) {
invalidate_page_options(path.relative(cwd, file));
}
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);
}
});
vite.watcher.on('change', async (file) => {
// 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
if (file.match(/[/\\]svelte\.config\.[jt]s$/)) {
console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`);
restarting = true;
await 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', '*');
}
});
vite.middlewares.use((req, res, next) => {
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();
});
const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, '');
const emulator = await svelte_config.kit.adapter?.emulate?.();
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 '${svelte_config.kit.paths.base}${to_fs(resolved)}';`);
} else {
res.writeHead(404);
res.end('not found');
}
return;
}
const resolved_instrumentation = resolve_entry(
path.join(svelte_config.kit.files.src, 'instrumentation.server')
);
if (resolved_instrumentation) {
await vite.ssrLoadModule(resolved_instrumentation);
}
// 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('$app/paths/internal/server');
set_assets(assets);
const server = new Server(manifest);
await server.init({
env,
read: (file) => createReadableStream(from_fs(file))
});
const request = await getRequest({
base,
request: req
});
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, escape_html(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) => {
if (file in manifest._.server_assets) {
return fs.readFileSync(from_fs(file));
}
return fs.readFileSync(path.join(svelte_config.kit.files.assets, file));
},
before_handle: (event, config, prerender) => {
async_local_storage.enterWith({ event, config, prerender });
},
emulator
});
if (rendered.status === 404) {
// @ts-expect-error
serve_static_middleware.handle(req, res, () => {
void setResponse(res, rendered);
});
} else {
void 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', 'viteServePublicMiddleware'];
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 | import('vite').EnvironmentModuleNode} node
* @param {Set<import('vite').ModuleNode | import('vite').EnvironmentModuleNode>} 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 | import('vite').EnvironmentModuleNode} 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 get_server_module_by_url(vite, url);
if (node) {
await add(node);
}
}
const transform_result =
/** @type {import('vite').ModuleNode} */ (node).ssrTransformResult || node.transformResult;
if (transform_result) {
if (transform_result.deps) {
transform_result.deps.forEach((url) => branches.push(add_by_url(url)));
}
if (transform_result.dynamicDeps) {
transform_result.dynamicDeps.forEach((url) => branches.push(add_by_url(url)));
}
} else {
node.importedModules.forEach((node) => branches.push(add(node)));
}
await Promise.all(branches);
}
/**
* @param {import('vite').ViteDevServer} vite
* @param {string} url
*/
function get_server_module_by_url(vite, url) {
return vite.environments
? vite.environments.ssr.moduleGraph.getModuleByUrl(url)
: vite.moduleGraph.getModuleByUrl(url, true);
}
/**
* 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;
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
import { fileURLToPath } from 'node:url';
import { posixify } from '../../utils/filesystem.js';
export const env_static_private = '\0virtual:env/static/private';
export const env_static_public = '\0virtual:env/static/public';
export const env_dynamic_private = '\0virtual:env/dynamic/private';
export const env_dynamic_public = '\0virtual:env/dynamic/public';
export const service_worker = '\0virtual:service-worker';
export const sveltekit_environment = '\0virtual:__sveltekit/environment';
export const sveltekit_server = '\0virtual:__sveltekit/server';
export const app_server = posixify(
fileURLToPath(new URL('../../runtime/app/server/index.js', import.meta.url))
);
+261
View File
@@ -0,0 +1,261 @@
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 { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
import { installPolyfills } from '../../../exports/node/polyfills.js';
import { SVELTE_KIT_ASSETS } from '../../../constants.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 {import('vite').PreviewServer} vite
* @param {import('vite').ResolvedConfig} vite_config
* @param {import('types').ValidatedConfig} svelte_config
*/
export async function preview(vite, vite_config, svelte_config) {
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?`);
}
const instrumentation = join(dir, 'instrumentation.server.js');
if (fs.existsSync(instrumentation)) {
await import(pathToFileURL(instrumentation).href);
}
/** @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, ''),
read: (file) => createReadableStream(`${dir}/${file}`)
});
const emulator = await svelte_config.kit.adapter?.emulate?.();
return () => {
// Remove the base middleware. It screws with the URL.
// It also only lets through requests beginning with the base path, so that requests beginning
// with the assets URL never reach us. We could serve assets separately before the base
// middleware, but we'd need that to occur after the compression and cors middlewares, so would
// need to insert it manually into the stack, which would be at least as bad as doing this.
for (let i = vite.middlewares.stack.length - 1; i > 0; i--) {
// @ts-expect-error using internals
if (vite.middlewares.stack[i].handle.name === 'viteBaseMiddleware') {
vite.middlewares.stack.splice(i, 1);
}
}
// 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');
const dir = pathname.startsWith(`/${svelte_config.kit.appDir}/remote/`) ? 'data' : 'pages';
let filename = normalizePath(
join(svelte_config.kit.outDir, `output/prerendered/${dir}` + pathname)
);
try {
filename = decodeURI(filename);
} catch {
// malformed URI
}
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[':authority'] || req.headers.host;
const request = await getRequest({
base: `${protocol}://${host}`,
request: req
});
await setResponse(
res,
await server.respond(request, {
getClientAddress: () => {
const { remoteAddress } = req.socket;
if (remoteAddress) return remoteAddress;
throw new Error('Could not determine clientAddress');
},
read: (file) => {
if (file in manifest._.server_assets) {
return fs.readFileSync(join(dir, file));
}
return fs.readFileSync(join(svelte_config.kit.files.assets, file));
},
emulator
})
);
});
};
}
/**
* @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();
}
+282
View File
@@ -0,0 +1,282 @@
import { tsPlugin } from '@sveltejs/acorn-typescript';
import { Parser } from 'acorn';
import { read } from '../../../utils/filesystem.js';
const inheritable_page_options = new Set(['ssr', 'prerender', 'csr', 'trailingSlash', 'config']);
const valid_page_options = new Set([...inheritable_page_options, 'entries', 'load']);
const skip_parsing_regex = new RegExp(
`${Array.from(valid_page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)`
);
const parser = Parser.extend(tsPlugin());
/**
* Collects page options from a +page.js/+layout.js file, ignoring reassignments
* and using the declared value (except for load functions, for which the value is `true`).
* Returns `null` if any export is too difficult to analyse.
* @param {string} filename The name of the file to report when an error occurs
* @param {string} input
* @returns {Record<string, any> | null}
*/
export function statically_analyse_page_options(filename, input) {
// if there's a chance there are no page exports or export all declaration,
// then we can skip the AST parsing which is expensive
if (!skip_parsing_regex.test(input)) {
return {};
}
try {
const source = parser.parse(input, {
sourceType: 'module',
ecmaVersion: 'latest'
});
/** @type {Map<string, import('acorn').Literal['value']>} */
const page_options = new Map();
for (const statement of source.body) {
// ignore export all declarations with aliases that are not page options
if (
statement.type === 'ExportAllDeclaration' &&
statement.exported &&
!valid_page_options.has(get_name(statement.exported))
) {
continue;
}
if (
statement.type === 'ExportDefaultDeclaration' ||
statement.type === 'ExportAllDeclaration'
) {
return null;
} else if (statement.type !== 'ExportNamedDeclaration') {
continue;
}
if (statement.specifiers.length) {
/** @type {Map<string, string>} */
const export_specifiers = new Map();
for (const specifier of statement.specifiers) {
const exported_name = get_name(specifier.exported);
if (!valid_page_options.has(exported_name)) {
continue;
}
if (statement.source) {
return null;
}
export_specifiers.set(get_name(specifier.local), exported_name);
}
for (const statement of source.body) {
switch (statement.type) {
case 'ImportDeclaration': {
for (const import_specifier of statement.specifiers) {
if (export_specifiers.has(import_specifier.local.name)) {
return null;
}
}
break;
}
case 'ExportNamedDeclaration':
case 'VariableDeclaration':
case 'FunctionDeclaration':
case 'ClassDeclaration': {
const declaration =
statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
if (!declaration) {
break;
}
// class and function declarations
if (declaration.type !== 'VariableDeclaration') {
if (export_specifiers.has(declaration.id.name)) {
return null;
}
break;
}
for (const variable_declarator of declaration.declarations) {
if (
variable_declarator.id.type !== 'Identifier' ||
!export_specifiers.has(variable_declarator.id.name)
) {
continue;
}
if (variable_declarator.init?.type === 'Literal') {
page_options.set(
/** @type {string} */ (export_specifiers.get(variable_declarator.id.name)),
variable_declarator.init.value
);
export_specifiers.delete(variable_declarator.id.name);
continue;
}
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
if (variable_declarator.id.name === 'load') {
page_options.set('load', null);
export_specifiers.delete('load');
continue;
}
// references a declaration we can't easily evaluate statically
return null;
}
break;
}
}
}
// there were some export specifiers that we couldn't resolve
if (export_specifiers.size) {
return null;
}
continue;
}
if (!statement.declaration) {
continue;
}
// class and function declarations
if (statement.declaration.type !== 'VariableDeclaration') {
if (valid_page_options.has(statement.declaration.id.name)) {
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
if (statement.declaration.id.name === 'load') {
page_options.set('load', null);
} else {
return null;
}
}
continue;
}
for (const declaration of statement.declaration.declarations) {
if (declaration.id.type !== 'Identifier') {
return null;
}
if (!valid_page_options.has(declaration.id.name)) {
continue;
}
if (declaration.init?.type === 'Literal') {
page_options.set(declaration.id.name, declaration.init.value);
continue;
}
// Special case: We only want to know that 'load' is exported (in a way that doesn't cause truthy checks in other places to trigger)
if (declaration.id.name === 'load') {
page_options.set('load', null);
continue;
}
// references a declaration we can't easily evaluate statically
return null;
}
}
return Object.fromEntries(page_options);
} catch (error) {
if (error instanceof Error) {
error.message = `Failed to statically analyse page options for ${filename}. ${error.message}`;
}
throw error;
}
}
/**
* @param {import('acorn').Identifier | import('acorn').Literal} node
* @returns {string}
*/
export function get_name(node) {
return node.type === 'Identifier' ? node.name : /** @type {string} */ (node.value);
}
/**
* @param {{
* resolve: (file: string) => Promise<Record<string, any>>;
* static_exports?: Map<string, { page_options: Record<string, any> | null, children: string[] }>;
* }} opts
*/
export function create_node_analyser({ resolve, static_exports = new Map() }) {
/**
* Computes the final page options (may include load function as `load: null`; special case) for a node (if possible). Otherwise, returns `null`.
* @param {import('types').PageNode} node
* @returns {Promise<Record<string, any> | null>}
*/
const get_page_options = async (node) => {
const key = node.universal || node.server;
if (key && static_exports.has(key)) {
return { ...static_exports.get(key)?.page_options };
}
/** @type {Record<string, any>} */
let page_options = {};
if (node.parent) {
const parent_options = await get_page_options(node.parent);
const parent_key = node.parent.universal || node.parent.server;
if (key && parent_key) {
static_exports.get(parent_key)?.children.push(key);
}
if (parent_options === null) {
// if the parent cannot be analysed, we can't know what page options
// the child node inherits, so we also mark it as unanalysable
if (key) {
static_exports.set(key, { page_options: null, children: [] });
}
return null;
}
page_options = { ...parent_options };
}
if (node.server) {
const module = await resolve(node.server);
for (const page_option in inheritable_page_options) {
if (page_option in module) {
page_options[page_option] = module[page_option];
}
}
}
if (node.universal) {
const input = read(node.universal);
const universal_page_options = statically_analyse_page_options(node.universal, input);
if (universal_page_options === null) {
static_exports.set(node.universal, { page_options: null, children: [] });
return null;
}
page_options = { ...page_options, ...universal_page_options };
}
if (key) {
static_exports.set(key, { page_options, children: [] });
}
return page_options;
};
/**
* @param {string} file
*/
const invalidate_page_options = (file) => {
static_exports.get(file)?.children.forEach((child) => static_exports.delete(child));
static_exports.delete(file);
};
return {
get_page_options,
invalidate_page_options
};
}
+101
View File
@@ -0,0 +1,101 @@
/**
* Check if a match position is within a comment or a string
* @param {string} content - The full content
* @param {number} match_index - The index where the match starts
* @returns {boolean} - True if the match is within a comment
*/
export function should_ignore(content, match_index) {
// Track if we're inside different types of quotes and comments
let in_single_quote = false;
let in_double_quote = false;
let in_template_literal = false;
let in_single_line_comment = false;
let in_multi_line_comment = false;
let in_html_comment = false;
for (let i = 0; i < match_index; i++) {
const char = content[i];
const next_two = content.slice(i, i + 2);
const next_four = content.slice(i, i + 4);
// Handle end of single line comment
if (in_single_line_comment && char === '\n') {
in_single_line_comment = false;
continue;
}
// Handle end of multi-line comment
if (in_multi_line_comment && next_two === '*/') {
in_multi_line_comment = false;
i++; // Skip the '/' part
continue;
}
// Handle end of HTML comment
if (in_html_comment && content.slice(i, i + 3) === '-->') {
in_html_comment = false;
i += 2; // Skip the '-->' part
continue;
}
// If we're in any comment, skip processing
if (in_single_line_comment || in_multi_line_comment || in_html_comment) {
continue;
}
// Handle escape sequences in strings
if ((in_single_quote || in_double_quote || in_template_literal) && char === '\\') {
i++; // Skip the escaped character
continue;
}
// Handle string boundaries
if (!in_double_quote && !in_template_literal && char === "'") {
in_single_quote = !in_single_quote;
continue;
}
if (!in_single_quote && !in_template_literal && char === '"') {
in_double_quote = !in_double_quote;
continue;
}
if (!in_single_quote && !in_double_quote && char === '`') {
in_template_literal = !in_template_literal;
continue;
}
// If we're inside any string, don't process comment delimiters
if (in_single_quote || in_double_quote || in_template_literal) {
continue;
}
// Check for comment starts
if (next_two === '//') {
in_single_line_comment = true;
i++; // Skip the second '/'
continue;
}
if (next_two === '/*') {
in_multi_line_comment = true;
i++; // Skip the '*'
continue;
}
if (next_four === '<!--') {
in_html_comment = true;
i += 3; // Skip the '<!--'
continue;
}
}
return (
in_single_line_comment ||
in_multi_line_comment ||
in_html_comment ||
in_single_quote ||
in_double_quote ||
in_template_literal
);
}
+3
View File
@@ -0,0 +1,3 @@
export interface EnforcedConfig {
[key: string]: EnforcedConfig | true;
}
+216
View File
@@ -0,0 +1,216 @@
import path from 'node:path';
import { loadEnv } from 'vite';
import { posixify } from '../../utils/filesystem.js';
import { negotiate } from '../../utils/http.js';
import { filter_env } from '../../utils/env.js';
import { escape_html } from '../../utils/escape.js';
import { dedent } from '../../core/sync/utils.js';
import {
app_server,
env_dynamic_private,
env_dynamic_public,
env_static_private,
env_static_public,
service_worker
} from './module_ids.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_env(env, public_prefix, private_prefix),
private: filter_env(env, private_prefix, public_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 ${escape_html(
base
)} - did you mean to visit <a href="${escape_html(prefixed, true)}">${escape_html(
prefixed
)}</a> instead?`
);
} else {
res.end(
`The server is configured with a public base URL of ${escape_html(
base
)} - did you mean to visit ${escape_html(prefixed)} instead?`
);
}
}
const query_pattern = /\?.*$/s;
/**
* 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) {
id = id.replace(query_pattern, '');
if (id.startsWith(lib)) {
id = id.replace(lib, '$lib');
}
if (id.startsWith(cwd)) {
id = path.relative(cwd, id);
}
if (id === app_server) {
return '$app/server';
}
if (id === env_static_private) {
return '$env/static/private';
}
if (id === env_static_public) {
return '$env/static/public';
}
if (id === env_dynamic_private) {
return '$env/dynamic/private';
}
if (id === env_dynamic_public) {
return '$env/dynamic/public';
}
if (id === service_worker) {
return '$service-worker';
}
return posixify(id);
}
/**
* For times when you need to throw an error, but without
* displaying a useless stack trace (since the developer
* can't do anything useful with it)
* @param {string} message
*/
export function stackless(message) {
const error = new Error(message);
error.stack = '';
return error;
}
export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', '');
/**
* For `error_for_missing_config('instrumentation.server.js', 'kit.experimental.instrumentation.server', true)`,
* returns:
*
* ```
* To enable `instrumentation.server.js`, add the following to your `svelte.config.js`:
*
*\`\`\`js
* kit:
* experimental:
* instrumentation:
* server: true
* }
* }
* }
*\`\`\`
*```
* @param {string} feature_name
* @param {string} path
* @param {string} value
* @returns {never}
*/
export function error_for_missing_config(feature_name, path, value) {
const hole = '__HOLE__';
const result = path.split('.').reduce((acc, part, i, parts) => {
const indent = ' '.repeat(i);
const rhs = i === parts.length - 1 ? value : `{\n${hole}\n${indent}}`;
return acc.replace(hole, `${indent}${part}: ${rhs}`);
}, hole);
throw stackless(
dedent`\
To enable ${feature_name}, add the following to your \`svelte.config.js\`:
${result}
`
);
}