init
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
export { sequence } from './sequence.js';
|
||||
+145
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
});
|
||||
}
|
||||
}
|
||||
+2164
File diff suppressed because it is too large
Load Diff
+245
@@ -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 };
|
||||
}
|
||||
+138
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
+1434
File diff suppressed because it is too large
Load Diff
+16
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
export interface EnforcedConfig {
|
||||
[key: string]: EnforcedConfig | true;
|
||||
}
|
||||
+216
@@ -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}
|
||||
`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user