From 0371d43c2f938853e0868d002f4228e011dc28cc Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 13:05:27 +0100 Subject: [PATCH 01/21] trailing_slash --- packages/kit/src/core/postbuild/analyse.js | 15 +- .../core/sync/create_manifest_data/index.js | 86 ++++- .../kit/src/core/sync/write_non_ambient.js | 36 +- .../trailing-slash/always/+page.server.js | 1 + .../sync/write_types/test/app-types/+page.ts | 4 + packages/kit/src/exports/vite/index.js | 4 + packages/kit/src/types/internal.d.ts | 2 + packages/kit/types/index.d.ts | 326 ++++++++++++------ 8 files changed, 366 insertions(+), 108 deletions(-) create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/+page.server.js diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 997c8fc957fd..b56a6cc02872 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -153,6 +153,15 @@ async function analyse({ const api_methods = endpoint?.methods ?? []; const entries = page?.entries ?? endpoint?.entries; + // Get trailingSlash: from page nodes, endpoint, or default to 'never' + const trailing_slash = page?.trailingSlash ?? endpoint?.trailingSlash ?? 'never'; + + // Set trailingSlash on the route in manifest_data + const route_data = manifest_data.routes.find((r) => r.id === route.id); + if (route_data) { + route_data.trailingSlash = trailing_slash; + } + metadata.routes.set(route.id, { config: route_config, methods: Array.from(new Set([...page_methods, ...api_methods])), @@ -219,7 +228,8 @@ function analyse_endpoint(route, mod) { config: mod.config, entries: mod.entries, methods, - prerender: mod.prerender ?? false + prerender: mod.prerender ?? false, + trailingSlash: mod.trailingSlash }; } @@ -239,7 +249,8 @@ function analyse_page(layouts, leaf) { config: nodes.get_config(), entries: leaf.universal?.entries ?? leaf.server?.entries, methods, - prerender: nodes.prerender() + prerender: nodes.prerender(), + trailingSlash: nodes.trailing_slash() }; } diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index b7c5e93d658d..8bc283611758 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -4,10 +4,11 @@ import process from 'node:process'; import colors from 'kleur'; import { lookup } from 'mrmime'; import { list_files, runtime_directory } from '../../utils.js'; -import { posixify, resolve_entry } from '../../../utils/filesystem.js'; +import { posixify, read, resolve_entry } from '../../../utils/filesystem.js'; import { parse_route_id } from '../../../utils/routing.js'; import { sort_routes } from './sort.js'; import { isSvelte5Plus } from '../utils.js'; +import { statically_analyse_page_options } from '../../../exports/vite/static_analysis/index.js'; /** * Generates the manifest data used for the client-side manifest and types generation. @@ -126,6 +127,12 @@ function create_routes_and_nodes(cwd, config, fallback) { /** @type {import('types').PageNode[]} */ const nodes = []; + /** @type {Map} */ + const node_trailing_slash = new Map(); + + /** @type {Map} */ + const endpoint_trailing_slash = new Map(); + if (fs.existsSync(config.kit.files.routes)) { /** * @param {number} depth @@ -325,6 +332,22 @@ function create_routes_and_nodes(cwd, config, fallback) { } route.layout[item.kind] = project_relative; + + // Extract trailingSlash from server files + if (item.kind === 'server') { + const file_path = path.join(cwd, project_relative); + if (fs.existsSync(file_path)) { + try { + const input = read(file_path); + const page_options = statically_analyse_page_options(project_relative, input); + if (page_options?.trailingSlash !== undefined) { + node_trailing_slash.set(route.layout, page_options.trailingSlash); + } + } catch { + // If static analysis fails, we'll default to 'never' later + } + } + } } else if (item.is_page) { if (!route.leaf) { route.leaf = { depth }; @@ -336,6 +359,23 @@ function create_routes_and_nodes(cwd, config, fallback) { } route.leaf[item.kind] = project_relative; + + // Extract trailingSlash from server files + if (item.kind === 'server') { + const file_path = path.join(cwd, project_relative); + if (fs.existsSync(file_path)) { + try { + const input = read(file_path); + const page_options = statically_analyse_page_options(project_relative, input); + if (page_options?.trailingSlash !== undefined) { + node_trailing_slash.set(route.leaf, page_options.trailingSlash); + } + } catch (error) { + // If static analysis fails, we'll default to 'never' later + // This can happen if the file has complex exports that can't be statically analyzed + } + } + } } else { if (route.endpoint) { throw duplicate_files_error('endpoint', route.endpoint.file); @@ -344,6 +384,20 @@ function create_routes_and_nodes(cwd, config, fallback) { route.endpoint = { file: project_relative }; + + // Extract trailingSlash from endpoint files + const file_path = path.join(cwd, project_relative); + if (fs.existsSync(file_path)) { + try { + const input = read(file_path); + const page_options = statically_analyse_page_options(project_relative, input); + if (page_options?.trailingSlash !== undefined) { + endpoint_trailing_slash.set(route.id, page_options.trailingSlash); + } + } catch { + // If static analysis fails, we'll default to 'never' later + } + } } } @@ -459,6 +513,36 @@ function create_routes_and_nodes(cwd, config, fallback) { } } + // Extract and propagate trailingSlash from routes + for (const route of routes) { + /** @type {import('types').TrailingSlash | undefined} */ + let trailing_slash; + + if (route.endpoint) { + // For endpoints, use the endpoint's trailingSlash directly + trailing_slash = endpoint_trailing_slash.get(route.id); + } else if (route.leaf) { + // For pages, check leaf first, then walk up parent layouts + trailing_slash = node_trailing_slash.get(route.leaf); + + // Walk up the parent chain to find trailingSlash from layouts + if (trailing_slash === undefined) { + let current_route = route.parent; + while (current_route && trailing_slash === undefined) { + if (current_route.layout) { + trailing_slash = node_trailing_slash.get(current_route.layout); + } + if (trailing_slash === undefined) { + current_route = current_route.parent; + } + } + } + } + + // Set trailingSlash on route (default to 'never' if not found) + route.trailingSlash = trailing_slash ?? 'never'; + } + return { nodes, routes: sort_routes(routes) diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index c4161720879a..ee63e4a81688 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -13,6 +13,27 @@ const remove_group_segments = (/** @type {string} */ id) => { return '/' + get_route_segments(id).join('/'); }; +/** + * Get pathnames to add based on trailingSlash setting + * @param {string} pathname + * @param {import('types').TrailingSlash} trailing_slash + * @returns {string[]} + */ +function get_pathnames_for_trailing_slash(pathname, trailing_slash) { + if (pathname === '/') { + return [pathname]; + } + + if (trailing_slash === 'never') { + return [pathname]; + } else if (trailing_slash === 'always') { + return [pathname + '/']; + } else { + // 'ignore' → both versions + return [pathname, pathname + '/']; + } +} + // `declare module "svelte/elements"` needs to happen in a non-ambient module, and dts-buddy generates one big ambient module, // so we can't add it there - therefore generate the typings ourselves here. // We're not using the `declare namespace svelteHTML` variant because that one doesn't augment the HTMLAttributes interface @@ -59,6 +80,8 @@ function generate_app_types(manifest_data) { const layouts = []; for (const route of manifest_data.routes) { + const trailing_slash = route.trailingSlash ?? 'never'; + if (route.params.length > 0) { const params = route.params.map((p) => `${p.name}${p.optional ? '?:' : ':'} string`); const route_type = `${s(route.id)}: { ${params.join('; ')} }`; @@ -67,19 +90,14 @@ function generate_app_types(manifest_data) { const pathname = remove_group_segments(route.id); const replaced_pathname = replace_required_params(replace_optional_params(pathname)); - pathnames.add(`\`${replaced_pathname}\` & {}`); - if (pathname !== '/') { - // Support trailing slash - pathnames.add(`\`${replaced_pathname + '/'}\` & {}`); + for (const p of get_pathnames_for_trailing_slash(replaced_pathname, trailing_slash)) { + pathnames.add(`\`${p}\` & {}`); } } else { const pathname = remove_group_segments(route.id); - pathnames.add(s(pathname)); - - if (pathname !== '/') { - // Support trailing slash - pathnames.add(s(pathname + '/')); + for (const p of get_pathnames_for_trailing_slash(pathname, trailing_slash)) { + pathnames.add(s(p)); } } diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/+page.server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/+page.server.js new file mode 100644 index 000000000000..d3c325085ed2 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/+page.server.js @@ -0,0 +1 @@ +export const trailingSlash = 'always'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/+page.ts index c47c696e7203..133a1c33afb3 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.ts @@ -31,9 +31,13 @@ pathname = '/foo/1/2/'; // Test layout groups pathname = '/path-a'; +// @ts-expect-error trailing slash is not part of the pathname type pathname = '/path-a/'; // @ts-expect-error layout group names are NOT part of the pathname type pathname = '/(group)/path-a'; +// Test trailing-slash +pathname = '/path-a/trailing-slash/always/'; + // read `pathname` otherwise it is treated as unused pathname; diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index c1b12dbd06e6..b918b9768c22 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -24,6 +24,7 @@ import { stackless } from './utils.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; +import { write_non_ambient } from '../../core/sync/write_non_ambient.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; @@ -1068,6 +1069,9 @@ async function kit({ svelte_config }) { build_metadata = metadata; + // Regenerate types with accurate trailingSlash information from analysis + write_non_ambient(kit, manifest_data); + log.info('Building app'); // create client build diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6384201af551..2048d6658edd 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -279,6 +279,8 @@ export interface RouteData { endpoint: { file: string; } | null; + + trailingSlash?: TrailingSlash; } export type ServerRedirectNode = { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b3611645dc44..417f54b13f67 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -4,7 +4,11 @@ declare module '@sveltejs/kit' { import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; import type { StandardSchemaV1 } from '@standard-schema/spec'; - import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams, ResolvedPathname } from '$app/types'; + import type { + RouteId as AppRouteId, + LayoutParams as AppLayoutParams, + ResolvedPathname + } from '$app/types'; // @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore type Span = import('@opentelemetry/api').Span; @@ -271,7 +275,10 @@ declare module '@sveltejs/kit' { * @param name the name of the cookie * @param opts the options, passed directly to `cookie.serialize`. The `path` must match the path of the cookie you want to delete. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) */ - delete: (name: string, opts: import('cookie').CookieSerializeOptions & { path: string }) => void; + delete: ( + name: string, + opts: import('cookie').CookieSerializeOptions & { path: string } + ) => void; /** * Serialize a cookie name-value pair into a `Set-Cookie` header string, but don't apply it to the response. @@ -1541,7 +1548,10 @@ declare module '@sveltejs/kit' { * but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. * @param input the html chunk and the info if this is the last chunk */ - transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise; + transformPageChunk?: (input: { + html: string; + done: boolean; + }) => MaybePromise; /** * Determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. * By default, none will be included. @@ -1789,7 +1799,11 @@ declare module '@sveltejs/kit' { } // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle - type WillRecurseIndefinitely = unknown extends T ? true : string extends keyof T ? true : false; + type WillRecurseIndefinitely = unknown extends T + ? true + : string extends keyof T + ? true + : false; // Input type mappings for form fields type InputTypeMap = { @@ -1888,19 +1902,20 @@ declare module '@sveltejs/kit' { /** * Form field accessor type that provides name(), value(), and issues() methods */ - export type RemoteFormField = RemoteFormFieldMethods & { - /** - * Returns an object that can be spread onto an input element with the correct type attribute, - * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters. - * @example - * ```svelte - * - * - * - * ``` - */ - as>(...args: AsArgs): InputElementProps; - }; + export type RemoteFormField = + RemoteFormFieldMethods & { + /** + * Returns an object that can be spread onto an input element with the correct type attribute, + * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters. + * @example + * ```svelte + * + * + * + * ``` + */ + as>(...args: AsArgs): InputElementProps; + }; type RemoteFormFieldContainer = RemoteFormFieldMethods & { /** Validation issues belonging to this or any of the fields that belong to it, if any */ @@ -2178,7 +2193,9 @@ declare module '@sveltejs/kit' { * A function that is invoked once the entry has been created. This is where you * should write the function to the filesystem and generate redirect manifests. */ - complete(entry: { generateManifest(opts: { relativePath: string }): string }): MaybePromise; + complete(entry: { + generateManifest(opts: { relativePath: string }): string; + }): MaybePromise; } // Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts @@ -2510,6 +2527,8 @@ declare module '@sveltejs/kit' { endpoint: { file: string; } | null; + + trailingSlash?: TrailingSlash; } interface SSRComponent { @@ -2640,16 +2659,24 @@ declare module '@sveltejs/kit' { * @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: number, body?: { - message: string; - } extends App.Error ? App.Error | string | undefined : never): never; + export function error( + status: number, + body?: { + message: string; + } extends App.Error + ? App.Error | string | undefined + : never + ): never; /** * Checks whether this is an error thrown by {@link error}. * @param status The status to filter for. * */ - export function isHttpError(e: unknown, status?: T): e is (HttpError_1 & { + export function isHttpError( + e: unknown, + status?: T + ): e is HttpError_1 & { status: T extends undefined ? never : T; - }); + }; /** * 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. @@ -2666,7 +2693,10 @@ declare module '@sveltejs/kit' { * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. * @throws {Error} If the provided status is invalid. * */ - export function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never; + export function redirect( + status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), + location: string | URL + ): never; /** * Checks whether this is a redirect thrown by {@link redirect}. * @param e The object to check. @@ -2750,20 +2780,31 @@ declare module '@sveltejs/kit' { wasNormalized: boolean; denormalize: (url?: string | URL) => URL; }; - export type LessThan = TNumber extends TArray["length"] ? TArray[number] : LessThan; - export type NumericRange = Exclude, LessThan>; + export type LessThan< + TNumber extends number, + TArray extends any[] = [] + > = TNumber extends TArray['length'] + ? TArray[number] + : LessThan; + export type NumericRange = Exclude< + TEnd | LessThan, + LessThan + >; export const VERSION: string; class HttpError_1 { - - constructor(status: number, body: { - message: string; - } extends App.Error ? (App.Error | string | undefined) : App.Error); + constructor( + status: number, + body: { + message: string; + } extends App.Error + ? App.Error | string | undefined + : App.Error + ); status: number; body: App.Error; toString(): string; } class Redirect_1 { - constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; location: string; @@ -2850,13 +2891,20 @@ declare module '@sveltejs/kit/hooks' { } declare module '@sveltejs/kit/node' { - export function getRequest({ request, base, bodySizeLimit }: { - request: import("http").IncomingMessage; + export function getRequest({ + request, + base, + bodySizeLimit + }: { + request: import('http').IncomingMessage; base: string; bodySizeLimit?: number; }): Promise; - export function setResponse(res: import("http").ServerResponse, response: Response): Promise; + export function setResponse( + res: import('http').ServerResponse, + response: Response + ): Promise; /** * Converts a file on disk to a readable stream * @since 2.4.0 @@ -2881,7 +2929,7 @@ declare module '@sveltejs/kit/vite' { /** * Returns the SvelteKit Vite plugins. * */ - export function sveltekit(): Promise; + export function sveltekit(): Promise; export {}; } @@ -2929,7 +2977,10 @@ declare module '$app/forms' { * } * ``` * */ - export function deserialize | undefined, Failure extends Record | undefined>(result: string): import("@sveltejs/kit").ActionResult; + export function deserialize< + Success extends Record | undefined, + Failure extends Record | undefined + >(result: string): import('@sveltejs/kit').ActionResult; /** * This action enhances a `
` element that otherwise would work without JavaScript. * @@ -2953,14 +3004,23 @@ declare module '$app/forms' { * @param form_element The form element * @param submit Submit callback */ - export function enhance | undefined, Failure extends Record | undefined>(form_element: HTMLFormElement, submit?: import("@sveltejs/kit").SubmitFunction): { + export function enhance< + Success extends Record | undefined, + Failure extends Record | undefined + >( + form_element: HTMLFormElement, + submit?: import('@sveltejs/kit').SubmitFunction + ): { destroy(): void; }; /** * This action updates the `form` property of the current page with the given data and updates `page.status`. * In case of an error, it redirects to the nearest error page. * */ - export function applyAction | undefined, Failure extends Record | undefined>(result: import("@sveltejs/kit").ActionResult): Promise; + export function applyAction< + Success extends Record | undefined, + Failure extends Record | undefined + >(result: import('@sveltejs/kit').ActionResult): Promise; export {}; } @@ -2971,7 +3031,9 @@ declare module '$app/navigation' { * * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function afterNavigate(callback: (navigation: import("@sveltejs/kit").AfterNavigate) => void): void; + export function afterNavigate( + callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void + ): void; /** * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. * @@ -2983,7 +3045,9 @@ declare module '$app/navigation' { * * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function beforeNavigate(callback: (navigation: import("@sveltejs/kit").BeforeNavigate) => void): void; + export function beforeNavigate( + callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void + ): void; /** * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. * @@ -2993,7 +3057,9 @@ declare module '$app/navigation' { * * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<(() => void) | void>): void; + export function onNavigate( + callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void> + ): void; /** * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling. * This is generally discouraged, since it breaks user expectations. @@ -3008,14 +3074,17 @@ declare module '$app/navigation' { * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} opts Options related to the navigation * */ - export function goto(url: string | URL, opts?: { - replaceState?: boolean | undefined; - noScroll?: boolean | undefined; - keepFocus?: boolean | undefined; - invalidateAll?: boolean | undefined; - invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; - state?: App.PageState | undefined; - }): Promise; + export function goto( + url: string | URL, + opts?: { + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; + state?: App.PageState | undefined; + } + ): Promise; /** * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated. * @@ -3042,7 +3111,9 @@ declare module '$app/navigation' { * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run (unless disabled via the option argument). * Returns a `Promise` that resolves when the page is subsequently updated. * */ - export function refreshAll({ includeLoadFunctions }?: { + export function refreshAll({ + includeLoadFunctions + }?: { includeLoadFunctions?: boolean; }): Promise; /** @@ -3056,14 +3127,17 @@ declare module '$app/navigation' { * * @param href Page to preload * */ - export function preloadData(href: string): Promise<{ - type: "loaded"; - status: number; - data: Record; - } | { - type: "redirect"; - location: string; - }>; + export function preloadData(href: string): Promise< + | { + type: 'loaded'; + status: number; + data: Record; + } + | { + type: 'redirect'; + location: string; + } + >; /** * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. @@ -3164,7 +3238,15 @@ declare module '$app/paths' { } declare module '$app/server' { - import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, InvalidField, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit'; + import type { + RequestEvent, + RemoteCommand, + RemoteForm, + RemoteFormInput, + InvalidField, + RemotePrerenderFunction, + RemoteQueryFunction + } from '@sveltejs/kit'; import type { StandardSchemaV1 } from '@standard-schema/spec'; /** * Read the contents of an imported asset from the filesystem @@ -3202,7 +3284,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function command(validate: "unchecked", fn: (arg: Input) => Output): RemoteCommand; + export function command( + validate: 'unchecked', + fn: (arg: Input) => Output + ): RemoteCommand; /** * Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3210,7 +3295,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function command(validate: Schema, fn: (arg: StandardSchemaV1.InferOutput) => Output): RemoteCommand, Output>; + export function command( + validate: Schema, + fn: (arg: StandardSchemaV1.InferOutput) => Output + ): RemoteCommand, Output>; /** * Creates a form object that can be spread onto a `` element. * @@ -3226,7 +3314,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function form(validate: "unchecked", fn: (data: Input, issue: InvalidField) => MaybePromise): RemoteForm; + export function form( + validate: 'unchecked', + fn: (data: Input, issue: InvalidField) => MaybePromise + ): RemoteForm; /** * Creates a form object that can be spread onto a `` element. * @@ -3234,7 +3325,16 @@ declare module '$app/server' { * * @since 2.27 */ - export function form>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput, issue: InvalidField>) => MaybePromise): RemoteForm, Output>; + export function form< + Schema extends StandardSchemaV1>, + Output + >( + validate: Schema, + fn: ( + data: StandardSchemaV1.InferOutput, + issue: InvalidField> + ) => MaybePromise + ): RemoteForm, Output>; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3242,10 +3342,15 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender(fn: () => MaybePromise, options?: { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } | undefined): RemotePrerenderFunction; + export function prerender( + fn: () => MaybePromise, + options?: + | { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } + | undefined + ): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3253,10 +3358,16 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender(validate: "unchecked", fn: (arg: Input) => MaybePromise, options?: { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } | undefined): RemotePrerenderFunction; + export function prerender( + validate: 'unchecked', + fn: (arg: Input) => MaybePromise, + options?: + | { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } + | undefined + ): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3264,10 +3375,16 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, options?: { - inputs?: RemotePrerenderInputsGenerator>; - dynamic?: boolean; - } | undefined): RemotePrerenderFunction, Output>; + export function prerender( + schema: Schema, + fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, + options?: + | { + inputs?: RemotePrerenderInputsGenerator>; + dynamic?: boolean; + } + | undefined + ): RemotePrerenderFunction, Output>; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3283,7 +3400,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function query(validate: "unchecked", fn: (arg: Input) => MaybePromise): RemoteQueryFunction; + export function query( + validate: 'unchecked', + fn: (arg: Input) => MaybePromise + ): RemoteQueryFunction; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3291,7 +3411,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function query(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise): RemoteQueryFunction, Output>; + export function query( + schema: Schema, + fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise + ): RemoteQueryFunction, Output>; export namespace query { /** * Creates a batch query function that collects multiple calls and executes them in a single request @@ -3300,7 +3423,10 @@ declare module '$app/server' { * * @since 2.35 */ - function batch(validate: "unchecked", fn: (args: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>): RemoteQueryFunction; + function batch( + validate: 'unchecked', + fn: (args: Input[]) => MaybePromise<(arg: Input, idx: number) => Output> + ): RemoteQueryFunction; /** * Creates a batch query function that collects multiple calls and executes them in a single request * @@ -3308,7 +3434,12 @@ declare module '$app/server' { * * @since 2.35 */ - function batch(schema: Schema, fn: (args: StandardSchemaV1.InferOutput[]) => MaybePromise<(arg: StandardSchemaV1.InferOutput, idx: number) => Output>): RemoteQueryFunction, Output>; + function batch( + schema: Schema, + fn: ( + args: StandardSchemaV1.InferOutput[] + ) => MaybePromise<(arg: StandardSchemaV1.InferOutput, idx: number) => Output> + ): RemoteQueryFunction, Output>; } type RemotePrerenderInputsGenerator = () => MaybePromise; type MaybePromise = T | Promise; @@ -3353,19 +3484,21 @@ declare module '$app/state' { * On the server, values can only be read during rendering (in other words _not_ in e.g. `load` functions). In the browser, the values can be read at any time. * * */ - export const page: import("@sveltejs/kit").Page; + export const page: import('@sveltejs/kit').Page; /** * A read-only object representing an in-progress navigation, with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. * Values are `null` when no navigation is occurring, or during server rendering. * */ - export const navigating: import("@sveltejs/kit").Navigation | { - from: null; - to: null; - type: null; - willUnload: null; - delta: null; - complete: null; - }; + export const navigating: + | import('@sveltejs/kit').Navigation + | { + from: null; + to: null; + type: null; + willUnload: null; + delta: null; + complete: null; + }; /** * A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * */ @@ -3379,11 +3512,10 @@ declare module '$app/state' { declare module '$app/stores' { export function getStores(): { - page: typeof page; - + navigating: typeof navigating; - + updated: typeof updated; }; /** @@ -3393,7 +3525,7 @@ declare module '$app/stores' { * * @deprecated Use `page` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const page: import("svelte/store").Readable; + export const page: import('svelte/store').Readable; /** * A readable store. * When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. @@ -3403,7 +3535,9 @@ declare module '$app/stores' { * * @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const navigating: import("svelte/store").Readable; + export const navigating: import('svelte/store').Readable< + import('@sveltejs/kit').Navigation | null + >; /** * A readable store whose initial value is `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * @@ -3411,12 +3545,12 @@ declare module '$app/stores' { * * @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const updated: import("svelte/store").Readable & { + export const updated: import('svelte/store').Readable & { check(): Promise; }; export {}; -}/** +} /** * It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following: * * ```ts @@ -3552,4 +3686,4 @@ declare module '$app/types' { export type Asset = ReturnType; } -//# sourceMappingURL=index.d.ts.map \ No newline at end of file +//# sourceMappingURL=index.d.ts.map From 78025b4588e75421f946a31476c6b5a9cce4ad93 Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 13:11:21 +0100 Subject: [PATCH 02/21] cleanup! --- packages/kit/src/core/postbuild/analyse.js | 15 ++------------- packages/kit/src/exports/vite/index.js | 4 ---- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index b56a6cc02872..997c8fc957fd 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -153,15 +153,6 @@ async function analyse({ const api_methods = endpoint?.methods ?? []; const entries = page?.entries ?? endpoint?.entries; - // Get trailingSlash: from page nodes, endpoint, or default to 'never' - const trailing_slash = page?.trailingSlash ?? endpoint?.trailingSlash ?? 'never'; - - // Set trailingSlash on the route in manifest_data - const route_data = manifest_data.routes.find((r) => r.id === route.id); - if (route_data) { - route_data.trailingSlash = trailing_slash; - } - metadata.routes.set(route.id, { config: route_config, methods: Array.from(new Set([...page_methods, ...api_methods])), @@ -228,8 +219,7 @@ function analyse_endpoint(route, mod) { config: mod.config, entries: mod.entries, methods, - prerender: mod.prerender ?? false, - trailingSlash: mod.trailingSlash + prerender: mod.prerender ?? false }; } @@ -249,8 +239,7 @@ function analyse_page(layouts, leaf) { config: nodes.get_config(), entries: leaf.universal?.entries ?? leaf.server?.entries, methods, - prerender: nodes.prerender(), - trailingSlash: nodes.trailing_slash() + prerender: nodes.prerender() }; } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index b918b9768c22..c1b12dbd06e6 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -24,7 +24,6 @@ import { stackless } from './utils.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; -import { write_non_ambient } from '../../core/sync/write_non_ambient.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; @@ -1069,9 +1068,6 @@ async function kit({ svelte_config }) { build_metadata = metadata; - // Regenerate types with accurate trailingSlash information from analysis - write_non_ambient(kit, manifest_data); - log.info('Building app'); // create client build From c5353c711268c0be5983e6bd5b7d95ec29de1c64 Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 15:43:43 +0100 Subject: [PATCH 03/21] adding some unit tests (layouts) --- .../src/core/sync/create_manifest_data/index.js | 8 ++++---- .../trailing-slash/always/layout/+layout.js | 1 + .../always/layout/inside/+page.svelte | 0 .../path-a/trailing-slash/ignore/+page.server.js | 1 + .../trailing-slash/ignore/layout/+layout.js | 1 + .../ignore/layout/inside/+page.svelte | 0 .../path-a/trailing-slash/never/+page.server.js | 1 + .../path-a/trailing-slash/never/layout/+layout.js | 1 + .../never/layout/inside/+page.svelte | 0 .../core/sync/write_types/test/app-types/+page.ts | 15 +++++++++++++-- 10 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/+layout.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/inside/+page.svelte create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/+page.server.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/+layout.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/inside/+page.svelte create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/+page.server.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/+layout.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/inside/+page.svelte diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 8bc283611758..a0d3b725f69c 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -333,8 +333,8 @@ function create_routes_and_nodes(cwd, config, fallback) { route.layout[item.kind] = project_relative; - // Extract trailingSlash from server files - if (item.kind === 'server') { + // Extract trailingSlash from server and universal files + if (item.kind === 'server' || item.kind === 'universal') { const file_path = path.join(cwd, project_relative); if (fs.existsSync(file_path)) { try { @@ -360,8 +360,8 @@ function create_routes_and_nodes(cwd, config, fallback) { route.leaf[item.kind] = project_relative; - // Extract trailingSlash from server files - if (item.kind === 'server') { + // Extract trailingSlash from server and universal files + if (item.kind === 'server' || item.kind === 'universal') { const file_path = path.join(cwd, project_relative); if (fs.existsSync(file_path)) { try { diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/+layout.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/+layout.js new file mode 100644 index 000000000000..d3c325085ed2 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/+layout.js @@ -0,0 +1 @@ +export const trailingSlash = 'always'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/inside/+page.svelte b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/inside/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/+page.server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/+page.server.js new file mode 100644 index 000000000000..42a828c116a3 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/+page.server.js @@ -0,0 +1 @@ +export const trailingSlash = 'ignore'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/+layout.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/+layout.js new file mode 100644 index 000000000000..42a828c116a3 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/+layout.js @@ -0,0 +1 @@ +export const trailingSlash = 'ignore'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/inside/+page.svelte b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/layout/inside/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/+page.server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/+page.server.js new file mode 100644 index 000000000000..844f51956c3e --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/+page.server.js @@ -0,0 +1 @@ +export const trailingSlash = 'never'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/+layout.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/+layout.js new file mode 100644 index 000000000000..844f51956c3e --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/+layout.js @@ -0,0 +1 @@ +export const trailingSlash = 'never'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/inside/+page.svelte b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/layout/inside/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/+page.ts index 133a1c33afb3..a136087be5a6 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.ts @@ -31,13 +31,24 @@ pathname = '/foo/1/2/'; // Test layout groups pathname = '/path-a'; -// @ts-expect-error trailing slash is not part of the pathname type +// @ts-expect-error default trailing slash is never, so we should not have it here pathname = '/path-a/'; // @ts-expect-error layout group names are NOT part of the pathname type pathname = '/(group)/path-a'; -// Test trailing-slash +// Test trailing-slash - always pathname = '/path-a/trailing-slash/always/'; +pathname = '/path-a/trailing-slash/always/layout/inside/'; + +// Test trailing-slash - ignore +pathname = '/path-a/trailing-slash/ignore'; +pathname = '/path-a/trailing-slash/ignore/'; +pathname = '/path-a/trailing-slash/ignore/layout/inside'; +pathname = '/path-a/trailing-slash/ignore/layout/inside/'; + +// Test trailing-slash - never (default) +pathname = '/path-a/trailing-slash/never'; +pathname = '/path-a/trailing-slash/never/layout/inside'; // read `pathname` otherwise it is treated as unused pathname; From 81182d87ffb6923bf1a677b6ccdf4ff9c6be20d6 Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 16:22:22 +0100 Subject: [PATCH 04/21] add changeset --- .changeset/cold-streets-refuse.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-streets-refuse.md diff --git a/.changeset/cold-streets-refuse.md b/.changeset/cold-streets-refuse.md new file mode 100644 index 000000000000..76c0efd66df8 --- /dev/null +++ b/.changeset/cold-streets-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: `resolve` will narrow types to follow trailing slash page settings From 7acee7aef9ac80614f9a78635cdf4a2a23785961 Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 17:10:47 +0100 Subject: [PATCH 05/21] up --- .../kit/src/core/sync/create_manifest_data/index.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index a0d3b725f69c..d1451fe98494 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -343,9 +343,7 @@ function create_routes_and_nodes(cwd, config, fallback) { if (page_options?.trailingSlash !== undefined) { node_trailing_slash.set(route.layout, page_options.trailingSlash); } - } catch { - // If static analysis fails, we'll default to 'never' later - } + } catch {} } } } else if (item.is_page) { @@ -370,10 +368,7 @@ function create_routes_and_nodes(cwd, config, fallback) { if (page_options?.trailingSlash !== undefined) { node_trailing_slash.set(route.leaf, page_options.trailingSlash); } - } catch (error) { - // If static analysis fails, we'll default to 'never' later - // This can happen if the file has complex exports that can't be statically analyzed - } + } catch {} } } } else { @@ -394,9 +389,7 @@ function create_routes_and_nodes(cwd, config, fallback) { if (page_options?.trailingSlash !== undefined) { endpoint_trailing_slash.set(route.id, page_options.trailingSlash); } - } catch { - // If static analysis fails, we'll default to 'never' later - } + } catch {} } } } From 5760861451a54e6be67c902877918058d0ccd8db Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 17:24:07 +0100 Subject: [PATCH 06/21] prepub --- packages/kit/types/index.d.ts | 324 ++++++++++------------------------ 1 file changed, 96 insertions(+), 228 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 417f54b13f67..220add5587a1 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -4,11 +4,7 @@ declare module '@sveltejs/kit' { import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; import type { StandardSchemaV1 } from '@standard-schema/spec'; - import type { - RouteId as AppRouteId, - LayoutParams as AppLayoutParams, - ResolvedPathname - } from '$app/types'; + import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams, ResolvedPathname } from '$app/types'; // @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore type Span = import('@opentelemetry/api').Span; @@ -275,10 +271,7 @@ declare module '@sveltejs/kit' { * @param name the name of the cookie * @param opts the options, passed directly to `cookie.serialize`. The `path` must match the path of the cookie you want to delete. See documentation [here](https://github.com/jshttp/cookie#cookieserializename-value-options) */ - delete: ( - name: string, - opts: import('cookie').CookieSerializeOptions & { path: string } - ) => void; + delete: (name: string, opts: import('cookie').CookieSerializeOptions & { path: string }) => void; /** * Serialize a cookie name-value pair into a `Set-Cookie` header string, but don't apply it to the response. @@ -1548,10 +1541,7 @@ declare module '@sveltejs/kit' { * but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. * @param input the html chunk and the info if this is the last chunk */ - transformPageChunk?: (input: { - html: string; - done: boolean; - }) => MaybePromise; + transformPageChunk?: (input: { html: string; done: boolean }) => MaybePromise; /** * Determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. * By default, none will be included. @@ -1799,11 +1789,7 @@ declare module '@sveltejs/kit' { } // If T is unknown or has an index signature, the types below will recurse indefinitely and create giant unions that TS can't handle - type WillRecurseIndefinitely = unknown extends T - ? true - : string extends keyof T - ? true - : false; + type WillRecurseIndefinitely = unknown extends T ? true : string extends keyof T ? true : false; // Input type mappings for form fields type InputTypeMap = { @@ -1902,20 +1888,19 @@ declare module '@sveltejs/kit' { /** * Form field accessor type that provides name(), value(), and issues() methods */ - export type RemoteFormField = - RemoteFormFieldMethods & { - /** - * Returns an object that can be spread onto an input element with the correct type attribute, - * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters. - * @example - * ```svelte - * - * - * - * ``` - */ - as>(...args: AsArgs): InputElementProps; - }; + export type RemoteFormField = RemoteFormFieldMethods & { + /** + * Returns an object that can be spread onto an input element with the correct type attribute, + * aria-invalid attribute if the field is invalid, and appropriate value/checked property getters/setters. + * @example + * ```svelte + * + * + * + * ``` + */ + as>(...args: AsArgs): InputElementProps; + }; type RemoteFormFieldContainer = RemoteFormFieldMethods & { /** Validation issues belonging to this or any of the fields that belong to it, if any */ @@ -2193,9 +2178,7 @@ declare module '@sveltejs/kit' { * A function that is invoked once the entry has been created. This is where you * should write the function to the filesystem and generate redirect manifests. */ - complete(entry: { - generateManifest(opts: { relativePath: string }): string; - }): MaybePromise; + complete(entry: { generateManifest(opts: { relativePath: string }): string }): MaybePromise; } // Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts @@ -2659,24 +2642,16 @@ declare module '@sveltejs/kit' { * @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: number, - body?: { - message: string; - } extends App.Error - ? App.Error | string | undefined - : never - ): never; + export function error(status: number, body?: { + message: string; + } extends App.Error ? App.Error | string | undefined : never): never; /** * Checks whether this is an error thrown by {@link error}. * @param status The status to filter for. * */ - export function isHttpError( - e: unknown, - status?: T - ): e is HttpError_1 & { + export function isHttpError(e: unknown, status?: T): e is (HttpError_1 & { status: T extends undefined ? never : T; - }; + }); /** * 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. @@ -2693,10 +2668,7 @@ declare module '@sveltejs/kit' { * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. * @throws {Error} If the provided status is invalid. * */ - export function redirect( - status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), - location: string | URL - ): never; + export function redirect(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number), location: string | URL): never; /** * Checks whether this is a redirect thrown by {@link redirect}. * @param e The object to check. @@ -2780,31 +2752,20 @@ declare module '@sveltejs/kit' { wasNormalized: boolean; denormalize: (url?: string | URL) => URL; }; - export type LessThan< - TNumber extends number, - TArray extends any[] = [] - > = TNumber extends TArray['length'] - ? TArray[number] - : LessThan; - export type NumericRange = Exclude< - TEnd | LessThan, - LessThan - >; + export type LessThan = TNumber extends TArray["length"] ? TArray[number] : LessThan; + export type NumericRange = Exclude, LessThan>; export const VERSION: string; class HttpError_1 { - constructor( - status: number, - body: { - message: string; - } extends App.Error - ? App.Error | string | undefined - : App.Error - ); + + constructor(status: number, body: { + message: string; + } extends App.Error ? (App.Error | string | undefined) : App.Error); status: number; body: App.Error; toString(): string; } class Redirect_1 { + constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; location: string; @@ -2891,20 +2852,13 @@ declare module '@sveltejs/kit/hooks' { } declare module '@sveltejs/kit/node' { - export function getRequest({ - request, - base, - bodySizeLimit - }: { - request: import('http').IncomingMessage; + export function getRequest({ request, base, bodySizeLimit }: { + request: import("http").IncomingMessage; base: string; bodySizeLimit?: number; }): Promise; - export function setResponse( - res: import('http').ServerResponse, - response: Response - ): Promise; + export function setResponse(res: import("http").ServerResponse, response: Response): Promise; /** * Converts a file on disk to a readable stream * @since 2.4.0 @@ -2929,7 +2883,7 @@ declare module '@sveltejs/kit/vite' { /** * Returns the SvelteKit Vite plugins. * */ - export function sveltekit(): Promise; + export function sveltekit(): Promise; export {}; } @@ -2977,10 +2931,7 @@ declare module '$app/forms' { * } * ``` * */ - export function deserialize< - Success extends Record | undefined, - Failure extends Record | undefined - >(result: string): import('@sveltejs/kit').ActionResult; + export function deserialize | undefined, Failure extends Record | undefined>(result: string): import("@sveltejs/kit").ActionResult; /** * This action enhances a `` element that otherwise would work without JavaScript. * @@ -3004,23 +2955,14 @@ declare module '$app/forms' { * @param form_element The form element * @param submit Submit callback */ - export function enhance< - Success extends Record | undefined, - Failure extends Record | undefined - >( - form_element: HTMLFormElement, - submit?: import('@sveltejs/kit').SubmitFunction - ): { + export function enhance | undefined, Failure extends Record | undefined>(form_element: HTMLFormElement, submit?: import("@sveltejs/kit").SubmitFunction): { destroy(): void; }; /** * This action updates the `form` property of the current page with the given data and updates `page.status`. * In case of an error, it redirects to the nearest error page. * */ - export function applyAction< - Success extends Record | undefined, - Failure extends Record | undefined - >(result: import('@sveltejs/kit').ActionResult): Promise; + export function applyAction | undefined, Failure extends Record | undefined>(result: import("@sveltejs/kit").ActionResult): Promise; export {}; } @@ -3031,9 +2973,7 @@ declare module '$app/navigation' { * * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function afterNavigate( - callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void - ): void; + export function afterNavigate(callback: (navigation: import("@sveltejs/kit").AfterNavigate) => void): void; /** * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. * @@ -3045,9 +2985,7 @@ declare module '$app/navigation' { * * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function beforeNavigate( - callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void - ): void; + export function beforeNavigate(callback: (navigation: import("@sveltejs/kit").BeforeNavigate) => void): void; /** * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. * @@ -3057,9 +2995,7 @@ declare module '$app/navigation' { * * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export function onNavigate( - callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void> - ): void; + export function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<(() => void) | void>): void; /** * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling. * This is generally discouraged, since it breaks user expectations. @@ -3074,17 +3010,14 @@ declare module '$app/navigation' { * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} opts Options related to the navigation * */ - export function goto( - url: string | URL, - opts?: { - replaceState?: boolean | undefined; - noScroll?: boolean | undefined; - keepFocus?: boolean | undefined; - invalidateAll?: boolean | undefined; - invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; - state?: App.PageState | undefined; - } - ): Promise; + export function goto(url: string | URL, opts?: { + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined; + state?: App.PageState | undefined; + }): Promise; /** * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated. * @@ -3111,9 +3044,7 @@ declare module '$app/navigation' { * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run (unless disabled via the option argument). * Returns a `Promise` that resolves when the page is subsequently updated. * */ - export function refreshAll({ - includeLoadFunctions - }?: { + export function refreshAll({ includeLoadFunctions }?: { includeLoadFunctions?: boolean; }): Promise; /** @@ -3127,17 +3058,14 @@ declare module '$app/navigation' { * * @param href Page to preload * */ - export function preloadData(href: string): Promise< - | { - type: 'loaded'; - status: number; - data: Record; - } - | { - type: 'redirect'; - location: string; - } - >; + export function preloadData(href: string): Promise<{ + type: "loaded"; + status: number; + data: Record; + } | { + type: "redirect"; + location: string; + }>; /** * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. @@ -3238,15 +3166,7 @@ declare module '$app/paths' { } declare module '$app/server' { - import type { - RequestEvent, - RemoteCommand, - RemoteForm, - RemoteFormInput, - InvalidField, - RemotePrerenderFunction, - RemoteQueryFunction - } from '@sveltejs/kit'; + import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, InvalidField, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit'; import type { StandardSchemaV1 } from '@standard-schema/spec'; /** * Read the contents of an imported asset from the filesystem @@ -3284,10 +3204,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function command( - validate: 'unchecked', - fn: (arg: Input) => Output - ): RemoteCommand; + export function command(validate: "unchecked", fn: (arg: Input) => Output): RemoteCommand; /** * Creates a remote command. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3295,10 +3212,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function command( - validate: Schema, - fn: (arg: StandardSchemaV1.InferOutput) => Output - ): RemoteCommand, Output>; + export function command(validate: Schema, fn: (arg: StandardSchemaV1.InferOutput) => Output): RemoteCommand, Output>; /** * Creates a form object that can be spread onto a `` element. * @@ -3314,10 +3228,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function form( - validate: 'unchecked', - fn: (data: Input, issue: InvalidField) => MaybePromise - ): RemoteForm; + export function form(validate: "unchecked", fn: (data: Input, issue: InvalidField) => MaybePromise): RemoteForm; /** * Creates a form object that can be spread onto a `` element. * @@ -3325,16 +3236,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function form< - Schema extends StandardSchemaV1>, - Output - >( - validate: Schema, - fn: ( - data: StandardSchemaV1.InferOutput, - issue: InvalidField> - ) => MaybePromise - ): RemoteForm, Output>; + export function form>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput, issue: InvalidField>) => MaybePromise): RemoteForm, Output>; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3342,15 +3244,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender( - fn: () => MaybePromise, - options?: - | { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } - | undefined - ): RemotePrerenderFunction; + export function prerender(fn: () => MaybePromise, options?: { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } | undefined): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3358,16 +3255,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender( - validate: 'unchecked', - fn: (arg: Input) => MaybePromise, - options?: - | { - inputs?: RemotePrerenderInputsGenerator; - dynamic?: boolean; - } - | undefined - ): RemotePrerenderFunction; + export function prerender(validate: "unchecked", fn: (arg: Input) => MaybePromise, options?: { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } | undefined): RemotePrerenderFunction; /** * Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3375,16 +3266,10 @@ declare module '$app/server' { * * @since 2.27 */ - export function prerender( - schema: Schema, - fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, - options?: - | { - inputs?: RemotePrerenderInputsGenerator>; - dynamic?: boolean; - } - | undefined - ): RemotePrerenderFunction, Output>; + export function prerender(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise, options?: { + inputs?: RemotePrerenderInputsGenerator>; + dynamic?: boolean; + } | undefined): RemotePrerenderFunction, Output>; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3400,10 +3285,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function query( - validate: 'unchecked', - fn: (arg: Input) => MaybePromise - ): RemoteQueryFunction; + export function query(validate: "unchecked", fn: (arg: Input) => MaybePromise): RemoteQueryFunction; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. * @@ -3411,10 +3293,7 @@ declare module '$app/server' { * * @since 2.27 */ - export function query( - schema: Schema, - fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise - ): RemoteQueryFunction, Output>; + export function query(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => MaybePromise): RemoteQueryFunction, Output>; export namespace query { /** * Creates a batch query function that collects multiple calls and executes them in a single request @@ -3423,10 +3302,7 @@ declare module '$app/server' { * * @since 2.35 */ - function batch( - validate: 'unchecked', - fn: (args: Input[]) => MaybePromise<(arg: Input, idx: number) => Output> - ): RemoteQueryFunction; + function batch(validate: "unchecked", fn: (args: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>): RemoteQueryFunction; /** * Creates a batch query function that collects multiple calls and executes them in a single request * @@ -3434,12 +3310,7 @@ declare module '$app/server' { * * @since 2.35 */ - function batch( - schema: Schema, - fn: ( - args: StandardSchemaV1.InferOutput[] - ) => MaybePromise<(arg: StandardSchemaV1.InferOutput, idx: number) => Output> - ): RemoteQueryFunction, Output>; + function batch(schema: Schema, fn: (args: StandardSchemaV1.InferOutput[]) => MaybePromise<(arg: StandardSchemaV1.InferOutput, idx: number) => Output>): RemoteQueryFunction, Output>; } type RemotePrerenderInputsGenerator = () => MaybePromise; type MaybePromise = T | Promise; @@ -3484,21 +3355,19 @@ declare module '$app/state' { * On the server, values can only be read during rendering (in other words _not_ in e.g. `load` functions). In the browser, the values can be read at any time. * * */ - export const page: import('@sveltejs/kit').Page; + export const page: import("@sveltejs/kit").Page; /** * A read-only object representing an in-progress navigation, with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. * Values are `null` when no navigation is occurring, or during server rendering. * */ - export const navigating: - | import('@sveltejs/kit').Navigation - | { - from: null; - to: null; - type: null; - willUnload: null; - delta: null; - complete: null; - }; + export const navigating: import("@sveltejs/kit").Navigation | { + from: null; + to: null; + type: null; + willUnload: null; + delta: null; + complete: null; + }; /** * A read-only reactive value that's initially `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update `current` to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * */ @@ -3512,10 +3381,11 @@ declare module '$app/state' { declare module '$app/stores' { export function getStores(): { + page: typeof page; - + navigating: typeof navigating; - + updated: typeof updated; }; /** @@ -3525,7 +3395,7 @@ declare module '$app/stores' { * * @deprecated Use `page` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const page: import('svelte/store').Readable; + export const page: import("svelte/store").Readable; /** * A readable store. * When navigating starts, its value is a `Navigation` object with `from`, `to`, `type` and (if `type === 'popstate'`) `delta` properties. @@ -3535,9 +3405,7 @@ declare module '$app/stores' { * * @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const navigating: import('svelte/store').Readable< - import('@sveltejs/kit').Navigation | null - >; + export const navigating: import("svelte/store").Readable; /** * A readable store whose initial value is `false`. If [`version.pollInterval`](https://svelte.dev/docs/kit/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling. * @@ -3545,12 +3413,12 @@ declare module '$app/stores' { * * @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, [see docs for more info](https://svelte.dev/docs/kit/migrating-to-sveltekit-2#SvelteKit-2.12:-$app-stores-deprecated)) * */ - export const updated: import('svelte/store').Readable & { + export const updated: import("svelte/store").Readable & { check(): Promise; }; export {}; -} /** +}/** * It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following: * * ```ts @@ -3686,4 +3554,4 @@ declare module '$app/types' { export type Asset = ReturnType; } -//# sourceMappingURL=index.d.ts.map +//# sourceMappingURL=index.d.ts.map \ No newline at end of file From 6ea71a8282bd113df4352b6eae565ed39008c43a Mon Sep 17 00:00:00 2001 From: jycouet Date: Thu, 4 Dec 2025 23:37:31 +0100 Subject: [PATCH 07/21] move to sync --- packages/kit/src/exports/vite/dev/index.js | 9 +-- .../src/exports/vite/static_analysis/index.js | 56 ++++++++++++++----- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 31c13c1ead8b..0417ee069954 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -129,12 +129,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { return; } - const node_analyser = create_node_analyser({ - resolve: async (server_node) => { - const { module } = await resolve(server_node); - return module; - } - }); + const node_analyser = create_node_analyser(); invalidate_page_options = node_analyser.invalidate_page_options; manifest = { @@ -215,7 +210,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { } if (node.universal) { - const page_options = await node_analyser.get_page_options(node); + const page_options = node_analyser.get_page_options(node); if (page_options?.ssr === false) { result.universal = page_options; } else { diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index 3809446f1285..0992f4a8e5e0 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -200,17 +200,16 @@ export function get_name(node) { /** * @param {{ - * resolve: (file: string) => Promise>; * static_exports?: Map | null, children: string[] }>; * }} opts */ -export function create_node_analyser({ resolve, static_exports = new Map() }) { +export function create_node_analyser({ 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 | null>} + * @returns {Record | null} */ - const get_page_options = async (node) => { + const get_page_options = (node) => { const key = node.universal || node.server; if (key && static_exports.has(key)) { return { ...static_exports.get(key)?.page_options }; @@ -220,7 +219,7 @@ export function create_node_analyser({ resolve, static_exports = new Map() }) { let page_options = {}; if (node.parent) { - const parent_options = await get_page_options(node.parent); + const parent_options = get_page_options(node.parent); const parent_key = node.parent.universal || node.parent.server; if (key && parent_key) { @@ -240,24 +239,51 @@ export function create_node_analyser({ resolve, static_exports = new Map() }) { } 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]; + try { + const input = read(node.server); + const server_page_options = statically_analyse_page_options(node.server, input); + + if (server_page_options === null) { + if (key) { + static_exports.set(key, { page_options: null, children: [] }); + } + return null; + } + + for (const page_option in inheritable_page_options) { + if (page_option in server_page_options) { + page_options[page_option] = server_page_options[page_option]; + } + } + } catch { + // If we can't read or analyze the file, mark it as unanalysable + if (key) { + static_exports.set(key, { page_options: null, children: [] }); } + return null; } } if (node.universal) { - const input = read(node.universal); - const universal_page_options = statically_analyse_page_options(node.universal, input); + try { + const input = read(node.universal); + const universal_page_options = statically_analyse_page_options(node.universal, input); + + if (universal_page_options === null) { + if (key) { + static_exports.set(key, { page_options: null, children: [] }); + } + return null; + } - if (universal_page_options === null) { - static_exports.set(node.universal, { page_options: null, children: [] }); + page_options = { ...page_options, ...universal_page_options }; + } catch { + // If we can't read or analyze the file, mark it as unanalysable + if (key) { + static_exports.set(key, { page_options: null, children: [] }); + } return null; } - - page_options = { ...page_options, ...universal_page_options }; } if (key) { From c0d7f8f689ebaaa5dd7d443e2e01f6359b12d89a Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 5 Dec 2025 00:34:47 +0100 Subject: [PATCH 08/21] using node_analyser --- .../core/sync/create_manifest_data/index.js | 79 +++++++------------ .../src/exports/vite/static_analysis/index.js | 6 +- 2 files changed, 30 insertions(+), 55 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index d1451fe98494..4712898dc92f 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -4,11 +4,11 @@ import process from 'node:process'; import colors from 'kleur'; import { lookup } from 'mrmime'; import { list_files, runtime_directory } from '../../utils.js'; -import { posixify, read, resolve_entry } from '../../../utils/filesystem.js'; +import { posixify, resolve_entry } from '../../../utils/filesystem.js'; import { parse_route_id } from '../../../utils/routing.js'; import { sort_routes } from './sort.js'; import { isSvelte5Plus } from '../utils.js'; -import { statically_analyse_page_options } from '../../../exports/vite/static_analysis/index.js'; +import { create_node_analyser } from '../../../exports/vite/static_analysis/index.js'; /** * Generates the manifest data used for the client-side manifest and types generation. @@ -127,12 +127,6 @@ function create_routes_and_nodes(cwd, config, fallback) { /** @type {import('types').PageNode[]} */ const nodes = []; - /** @type {Map} */ - const node_trailing_slash = new Map(); - - /** @type {Map} */ - const endpoint_trailing_slash = new Map(); - if (fs.existsSync(config.kit.files.routes)) { /** * @param {number} depth @@ -332,20 +326,6 @@ function create_routes_and_nodes(cwd, config, fallback) { } route.layout[item.kind] = project_relative; - - // Extract trailingSlash from server and universal files - if (item.kind === 'server' || item.kind === 'universal') { - const file_path = path.join(cwd, project_relative); - if (fs.existsSync(file_path)) { - try { - const input = read(file_path); - const page_options = statically_analyse_page_options(project_relative, input); - if (page_options?.trailingSlash !== undefined) { - node_trailing_slash.set(route.layout, page_options.trailingSlash); - } - } catch {} - } - } } else if (item.is_page) { if (!route.leaf) { route.leaf = { depth }; @@ -357,20 +337,6 @@ function create_routes_and_nodes(cwd, config, fallback) { } route.leaf[item.kind] = project_relative; - - // Extract trailingSlash from server and universal files - if (item.kind === 'server' || item.kind === 'universal') { - const file_path = path.join(cwd, project_relative); - if (fs.existsSync(file_path)) { - try { - const input = read(file_path); - const page_options = statically_analyse_page_options(project_relative, input); - if (page_options?.trailingSlash !== undefined) { - node_trailing_slash.set(route.leaf, page_options.trailingSlash); - } - } catch {} - } - } } else { if (route.endpoint) { throw duplicate_files_error('endpoint', route.endpoint.file); @@ -379,18 +345,6 @@ function create_routes_and_nodes(cwd, config, fallback) { route.endpoint = { file: project_relative }; - - // Extract trailingSlash from endpoint files - const file_path = path.join(cwd, project_relative); - if (fs.existsSync(file_path)) { - try { - const input = read(file_path); - const page_options = statically_analyse_page_options(project_relative, input); - if (page_options?.trailingSlash !== undefined) { - endpoint_trailing_slash.set(route.id, page_options.trailingSlash); - } - } catch {} - } } } @@ -506,14 +460,39 @@ function create_routes_and_nodes(cwd, config, fallback) { } } - // Extract and propagate trailingSlash from routes + // Extract and propagate trailingSlash from routes using static analysis + const node_analyser = create_node_analyser(); + /** @type {Map} */ + const node_trailing_slash = new Map(); + /** @type {Map} */ + const route_trailing_slash = new Map(); + + // Extract trailingSlash from nodes + for (const node of nodes) { + const page_options = node_analyser.get_page_options(node); + if (page_options?.trailingSlash !== undefined) { + node_trailing_slash.set(node, page_options.trailingSlash); + } + } + + // Extract trailingSlash from server files + for (const route of routes) { + if (route.leaf?.server) { + const page_options = node_analyser.get_page_options(route.leaf); + if (page_options?.trailingSlash !== undefined) { + route_trailing_slash.set(route.id, page_options.trailingSlash); + } + } + } + + // Propagate trailingSlash to routes for (const route of routes) { /** @type {import('types').TrailingSlash | undefined} */ let trailing_slash; if (route.endpoint) { // For endpoints, use the endpoint's trailingSlash directly - trailing_slash = endpoint_trailing_slash.get(route.id); + trailing_slash = route_trailing_slash.get(route.id); } else if (route.leaf) { // For pages, check leaf first, then walk up parent layouts trailing_slash = node_trailing_slash.get(route.leaf); diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index 0992f4a8e5e0..1511d85e9ea9 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -250,11 +250,7 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { return null; } - for (const page_option in inheritable_page_options) { - if (page_option in server_page_options) { - page_options[page_option] = server_page_options[page_option]; - } - } + page_options = { ...page_options, ...server_page_options }; } catch { // If we can't read or analyze the file, mark it as unanalysable if (key) { From 0b9e5b0087df2aa29cc006220009b34ae9ea6289 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 5 Dec 2025 00:38:43 +0100 Subject: [PATCH 09/21] simplify --- .../core/sync/create_manifest_data/index.js | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 4712898dc92f..1e10a6655e9e 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -464,10 +464,8 @@ function create_routes_and_nodes(cwd, config, fallback) { const node_analyser = create_node_analyser(); /** @type {Map} */ const node_trailing_slash = new Map(); - /** @type {Map} */ - const route_trailing_slash = new Map(); - // Extract trailingSlash from nodes + // Extract trailingSlash from all nodes for (const node of nodes) { const page_options = node_analyser.get_page_options(node); if (page_options?.trailingSlash !== undefined) { @@ -475,38 +473,23 @@ function create_routes_and_nodes(cwd, config, fallback) { } } - // Extract trailingSlash from server files - for (const route of routes) { - if (route.leaf?.server) { - const page_options = node_analyser.get_page_options(route.leaf); - if (page_options?.trailingSlash !== undefined) { - route_trailing_slash.set(route.id, page_options.trailingSlash); - } - } - } - // Propagate trailingSlash to routes for (const route of routes) { /** @type {import('types').TrailingSlash | undefined} */ let trailing_slash; - if (route.endpoint) { - // For endpoints, use the endpoint's trailingSlash directly - trailing_slash = route_trailing_slash.get(route.id); - } else if (route.leaf) { + if (route.leaf) { // For pages, check leaf first, then walk up parent layouts trailing_slash = node_trailing_slash.get(route.leaf); // Walk up the parent chain to find trailingSlash from layouts - if (trailing_slash === undefined) { + for ( let current_route = route.parent; - while (current_route && trailing_slash === undefined) { - if (current_route.layout) { - trailing_slash = node_trailing_slash.get(current_route.layout); - } - if (trailing_slash === undefined) { - current_route = current_route.parent; - } + current_route && trailing_slash === undefined; + current_route = current_route.parent + ) { + if (current_route.layout) { + trailing_slash = node_trailing_slash.get(current_route.layout); } } } From de6c420d237690754757b3cc8e002cfdf54c55f6 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 5 Dec 2025 13:39:57 +0100 Subject: [PATCH 10/21] rmv --- packages/kit/src/exports/vite/build/build_server.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 7af4073ef09d..e1e761e3047b 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -57,13 +57,7 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes } } - 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 - }); + const { get_page_options } = create_node_analyser({ static_exports }); for (let i = 0; i < manifest_data.nodes.length; i++) { const node = manifest_data.nodes[i]; From 519f832037fb1b6a7428277e8abd9f012e15d901 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 5 Dec 2025 14:12:25 +0100 Subject: [PATCH 11/21] one more time --- packages/kit/src/exports/vite/build/build_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index e1e761e3047b..a20309a32f4c 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -3,7 +3,7 @@ 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 { basename } from 'node:path'; import { create_node_analyser } from '../static_analysis/index.js'; From 2e361a51b1585326e5c838ec58f78750bb647c1c Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 04:54:03 +0800 Subject: [PATCH 12/21] deduplicate static analysis --- eslint.config.js | 1 + packages/kit/src/core/postbuild/analyse.js | 8 +- .../core/sync/create_manifest_data/index.js | 46 ++----- .../kit/src/core/sync/write_non_ambient.js | 6 +- .../src/exports/vite/build/build_server.js | 12 +- packages/kit/src/exports/vite/dev/index.js | 17 +-- packages/kit/src/exports/vite/index.js | 5 +- .../src/exports/vite/static_analysis/index.js | 117 +++++++++--------- packages/kit/src/types/internal.d.ts | 4 +- packages/kit/src/types/private.d.ts | 1 + packages/kit/types/index.d.ts | 7 +- 11 files changed, 90 insertions(+), 134 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b6a6419237a4..8a36524cccc6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -32,6 +32,7 @@ export default [ '**/.custom-out-dir', 'packages/adapter-*/files', 'packages/kit/src/core/config/fixtures/multiple', // dir contains svelte config with multiple extensions tripping eslint + 'packages/kit/types/index.d.ts', // generated file 'packages/package/test/fixtures/typescript-svelte-config/expected', 'packages/package/test/errors/**/*', 'packages/package/test/fixtures/**/*' diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 997c8fc957fd..2b4435d12e68 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -65,11 +65,8 @@ async function analyse({ internal.set_manifest(manifest); internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`)); - /** @type {Map | null, children: string[] }>} */ - const static_exports = new Map(); - // first, build server nodes without the client manifest so we can analyse it - await build_server_nodes( + build_server_nodes( out, config, manifest_data, @@ -78,7 +75,6 @@ async function analyse({ null, null, output_config, - static_exports ); /** @type {import('types').ServerMetadata} */ @@ -188,7 +184,7 @@ async function analyse({ metadata.remotes.set(remote.hash, exports); } - return { metadata, static_exports }; + return { metadata }; } /** diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 1e10a6655e9e..c7c3601c0ad7 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -201,7 +201,8 @@ function create_routes_and_nodes(cwd, config, fallback) { error: null, leaf: null, page: null, - endpoint: null + endpoint: null, + page_options: null }; // important to do this before walking children, so that child @@ -379,7 +380,8 @@ function create_routes_and_nodes(cwd, config, fallback) { error: null, leaf: null, page: null, - endpoint: null + endpoint: null, + page_options: null }); } @@ -416,6 +418,8 @@ function create_routes_and_nodes(cwd, config, fallback) { const indexes = new Map(nodes.map((node, i) => [node, i])); + const node_analyser = create_node_analyser(); + for (const route of routes) { if (!route.leaf) continue; @@ -425,6 +429,8 @@ function create_routes_and_nodes(cwd, config, fallback) { leaf: /** @type {number} */ (indexes.get(route.leaf)) }; + route.page_options = node_analyser.get_page_options(route.leaf); + /** @type {import('types').RouteData | null} */ let current_route = route; let current_node = route.leaf; @@ -460,42 +466,8 @@ function create_routes_and_nodes(cwd, config, fallback) { } } - // Extract and propagate trailingSlash from routes using static analysis - const node_analyser = create_node_analyser(); - /** @type {Map} */ - const node_trailing_slash = new Map(); - - // Extract trailingSlash from all nodes for (const node of nodes) { - const page_options = node_analyser.get_page_options(node); - if (page_options?.trailingSlash !== undefined) { - node_trailing_slash.set(node, page_options.trailingSlash); - } - } - - // Propagate trailingSlash to routes - for (const route of routes) { - /** @type {import('types').TrailingSlash | undefined} */ - let trailing_slash; - - if (route.leaf) { - // For pages, check leaf first, then walk up parent layouts - trailing_slash = node_trailing_slash.get(route.leaf); - - // Walk up the parent chain to find trailingSlash from layouts - for ( - let current_route = route.parent; - current_route && trailing_slash === undefined; - current_route = current_route.parent - ) { - if (current_route.layout) { - trailing_slash = node_trailing_slash.get(current_route.layout); - } - } - } - - // Set trailingSlash on route (default to 'never' if not found) - route.trailingSlash = trailing_slash ?? 'never'; + node.page_options = node_analyser.get_page_options(node); } return { diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index ee63e4a81688..669a509b40a2 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -80,8 +80,6 @@ function generate_app_types(manifest_data) { const layouts = []; for (const route of manifest_data.routes) { - const trailing_slash = route.trailingSlash ?? 'never'; - if (route.params.length > 0) { const params = route.params.map((p) => `${p.name}${p.optional ? '?:' : ':'} string`); const route_type = `${s(route.id)}: { ${params.join('; ')} }`; @@ -91,12 +89,12 @@ function generate_app_types(manifest_data) { const pathname = remove_group_segments(route.id); const replaced_pathname = replace_required_params(replace_optional_params(pathname)); - for (const p of get_pathnames_for_trailing_slash(replaced_pathname, trailing_slash)) { + for (const p of get_pathnames_for_trailing_slash(replaced_pathname, route.page_options?.trailingSlash)) { pathnames.add(`\`${p}\` & {}`); } } else { const pathname = remove_group_segments(route.id); - for (const p of get_pathnames_for_trailing_slash(pathname, trailing_slash)) { + for (const p of get_pathnames_for_trailing_slash(pathname, route.page_options?.trailingSlash)) { pathnames.add(s(p)); } } diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index a20309a32f4c..28a5fc2c1541 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -4,8 +4,6 @@ import { filter_fonts, find_deps, resolve_symlinks } from './utils.js'; import { s } from '../../../utils/misc.js'; import { normalizePath } from 'vite'; import { basename } from 'node:path'; -import { create_node_analyser } from '../static_analysis/index.js'; - /** * @param {string} out @@ -16,9 +14,8 @@ import { create_node_analyser } from '../static_analysis/index.js'; * @param {import('vite').Rollup.OutputBundle | null} server_bundle * @param {import('vite').Rollup.RollupOutput['output'] | null} client_chunks * @param {import('types').RecursiveRequired} output_config - * @param {Map | 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) { +export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, server_bundle, client_chunks, output_config) { mkdirp(`${out}/server/nodes`); mkdirp(`${out}/server/stylesheets`); @@ -57,8 +54,6 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes } } - const { get_page_options } = create_node_analyser({ static_exports }); - for (let i = 0; i < manifest_data.nodes.length; i++) { const node = manifest_data.nodes[i]; @@ -89,9 +84,8 @@ export async function build_server_nodes(out, kit, manifest_data, server_manifes } 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)};`) + if (!!node.page_options && node.page_options.ssr === false) { + exports.push(`export const universal = ${s(node.page_options, null, 2)};`) } else { imports.push( `import * as universal from '../${resolve_symlinks(server_manifest, node.universal).chunk.file}';` diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 0417ee069954..9d69008638c3 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -19,7 +19,6 @@ 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 @@ -103,9 +102,6 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { return { module, module_node, url }; } - /** @type {(file: string) => void} */ - let invalidate_page_options; - function update_manifest() { try { ({ manifest_data } = sync.create(svelte_config)); @@ -129,9 +125,6 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { return; } - const node_analyser = create_node_analyser(); - invalidate_page_options = node_analyser.invalidate_page_options; - manifest = { appDir: svelte_config.kit.appDir, appPath: svelte_config.kit.appDir, @@ -210,9 +203,8 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { } if (node.universal) { - const page_options = node_analyser.get_page_options(node); - if (page_options?.ssr === false) { - result.universal = page_options; + if (node.page_options?.ssr === false) { + result.universal = node.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); @@ -366,11 +358,6 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { 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); }); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index c1b12dbd06e6..f68bcdf3a923 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1054,7 +1054,7 @@ async function kit({ svelte_config }) { log.info('Analysing routes'); - const { metadata, static_exports } = await analyse({ + const { metadata } = await analyse({ hash: kit.router.type === 'hash', manifest_path, manifest_data, @@ -1254,7 +1254,7 @@ async function kit({ svelte_config }) { ); // regenerate nodes with the client manifest... - await build_server_nodes( + build_server_nodes( out, kit, manifest_data, @@ -1263,7 +1263,6 @@ async function kit({ svelte_config }) { bundle, client_chunks, svelte_config.kit.output, - static_exports ); // ...and prerender diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index 1511d85e9ea9..9ab21d790e02 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -2,9 +2,21 @@ 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 valid_page_options_array = /** @type {const} */ ([ + 'ssr', + 'prerender', + 'csr', + 'trailingSlash', + 'config', + 'entries', + 'load' +]); + +/** @type {Set} */ +const valid_page_options = new Set(valid_page_options_array); + +/** @typedef {typeof valid_page_options_array[number]} ValidPageOption */ +/** @typedef {Partial>} PageOptions */ const skip_parsing_regex = new RegExp( `${Array.from(valid_page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)` @@ -18,7 +30,7 @@ const parser = Parser.extend(tsPlugin()); * 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 | null} + * @returns {PageOptions | null} */ export function statically_analyse_page_options(filename, input) { // if there's a chance there are no page exports or export all declaration, @@ -194,20 +206,48 @@ export function statically_analyse_page_options(filename, input) { * @param {import('acorn').Identifier | import('acorn').Literal} node * @returns {string} */ -export function get_name(node) { +function get_name(node) { return node.type === 'Identifier' ? node.name : /** @type {string} */ (node.value); } +/** + * Reads and statically analyses a file for page options + * @param {string} filepath + * @returns {PageOptions | null} Returns the page options for the file or `null` if unanalysable + */ +function get_file_page_options(filepath) { + try { + const input = read(filepath); + const page_options = statically_analyse_page_options(filepath, input); + + if (page_options === null) { + return null; + } + + return page_options; + } catch { + return null; + } +} + /** * @param {{ - * static_exports?: Map | null, children: string[] }>; + * static_exports?: Map; * }} opts */ export function create_node_analyser({ static_exports = new Map() } = {}) { + /** + * @param {string | undefined} key + * @param {PageOptions | null} page_options + */ + const cache_static_analysis = (key, page_options) => { + if (key) static_exports.set(key, { page_options, children: [] }); + }; + /** * 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 {Record | null} + * @returns {PageOptions | null} */ const get_page_options = (node) => { const key = node.universal || node.server; @@ -215,7 +255,7 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { return { ...static_exports.get(key)?.page_options }; } - /** @type {Record} */ + /** @type {PageOptions} */ let page_options = {}; if (node.parent) { @@ -229,9 +269,7 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { 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: [] }); - } + cache_static_analysis(key, null); return null; } @@ -239,66 +277,29 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { } if (node.server) { - try { - const input = read(node.server); - const server_page_options = statically_analyse_page_options(node.server, input); - - if (server_page_options === null) { - if (key) { - static_exports.set(key, { page_options: null, children: [] }); - } - return null; - } - - page_options = { ...page_options, ...server_page_options }; - } catch { - // If we can't read or analyze the file, mark it as unanalysable - if (key) { - static_exports.set(key, { page_options: null, children: [] }); - } + const server_page_options = get_file_page_options(node.server); + if (server_page_options === null) { + cache_static_analysis(key, null); return null; } + page_options = { ...page_options, ...server_page_options }; } if (node.universal) { - try { - const input = read(node.universal); - const universal_page_options = statically_analyse_page_options(node.universal, input); - - if (universal_page_options === null) { - if (key) { - static_exports.set(key, { page_options: null, children: [] }); - } - return null; - } - - page_options = { ...page_options, ...universal_page_options }; - } catch { - // If we can't read or analyze the file, mark it as unanalysable - if (key) { - static_exports.set(key, { page_options: null, children: [] }); - } + const universal_page_options = get_file_page_options(node.universal); + if (universal_page_options === null) { + cache_static_analysis(key, null); return null; } + page_options = { ...page_options, ...universal_page_options }; } - if (key) { - static_exports.set(key, { page_options, children: [] }); - } + cache_static_analysis(key, page_options); 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 + get_page_options }; } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 2048d6658edd..c6815103f2ba 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -32,6 +32,7 @@ import { TrailingSlash } from './private.js'; import { Span } from '@opentelemetry/api'; +import type { PageOptions } from '../exports/vite/static_analysis/index.js'; export interface ServerModule { Server: typeof InternalServer; @@ -214,6 +215,7 @@ export interface PageNode { parent?: PageNode; /** Filled with the pages that reference this layout (if this is a layout). */ child_pages?: PageNode[]; + page_options?: PageOptions | null; } export interface PrerenderDependency { @@ -280,7 +282,7 @@ export interface RouteData { file: string; } | null; - trailingSlash?: TrailingSlash; + page_options: PageOptions | null; } export type ServerRedirectNode = { diff --git a/packages/kit/src/types/private.d.ts b/packages/kit/src/types/private.d.ts index da512ed777c5..183609929ce0 100644 --- a/packages/kit/src/types/private.d.ts +++ b/packages/kit/src/types/private.d.ts @@ -240,4 +240,5 @@ export interface RouteSegment { rest: boolean; } +/** @default 'never' */ export type TrailingSlash = 'never' | 'always' | 'ignore'; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 220add5587a1..17ba381b4dfb 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2390,6 +2390,7 @@ declare module '@sveltejs/kit' { rest: boolean; } + /** @default 'never' */ type TrailingSlash = 'never' | 'always' | 'ignore'; interface Asset { file: string; @@ -2465,6 +2466,7 @@ declare module '@sveltejs/kit' { parent?: PageNode; /** Filled with the pages that reference this layout (if this is a layout). */ child_pages?: PageNode[]; + page_options?: PageOptions | null; } type RecursiveRequired = { @@ -2511,7 +2513,7 @@ declare module '@sveltejs/kit' { file: string; } | null; - trailingSlash?: TrailingSlash; + page_options: PageOptions | null; } interface SSRComponent { @@ -2754,6 +2756,9 @@ declare module '@sveltejs/kit' { }; export type LessThan = TNumber extends TArray["length"] ? TArray[number] : LessThan; export type NumericRange = Exclude, LessThan>; + type ValidPageOption = (typeof valid_page_options_array)[number]; + type PageOptions = Partial>; + const valid_page_options_array: readonly ["ssr", "prerender", "csr", "trailingSlash", "config", "entries", "load"]; export const VERSION: string; class HttpError_1 { From 4d04c98934b6188b0081544089f979a38e85af38 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 04:57:18 +0800 Subject: [PATCH 13/21] format --- packages/kit/src/core/postbuild/analyse.js | 11 +---------- packages/kit/src/core/sync/write_non_ambient.js | 10 ++++++++-- packages/kit/src/exports/vite/index.js | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 2b4435d12e68..aa99609e6a04 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -66,16 +66,7 @@ async function analyse({ internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`)); // first, build server nodes without the client manifest so we can analyse it - build_server_nodes( - out, - config, - manifest_data, - server_manifest, - null, - null, - null, - output_config, - ); + build_server_nodes(out, config, manifest_data, server_manifest, null, null, null, output_config); /** @type {import('types').ServerMetadata} */ const metadata = { diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index 669a509b40a2..a631922ccb8d 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -89,12 +89,18 @@ function generate_app_types(manifest_data) { const pathname = remove_group_segments(route.id); const replaced_pathname = replace_required_params(replace_optional_params(pathname)); - for (const p of get_pathnames_for_trailing_slash(replaced_pathname, route.page_options?.trailingSlash)) { + for (const p of get_pathnames_for_trailing_slash( + replaced_pathname, + route.page_options?.trailingSlash + )) { pathnames.add(`\`${p}\` & {}`); } } else { const pathname = remove_group_segments(route.id); - for (const p of get_pathnames_for_trailing_slash(pathname, route.page_options?.trailingSlash)) { + for (const p of get_pathnames_for_trailing_slash( + pathname, + route.page_options?.trailingSlash + )) { pathnames.add(s(p)); } } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index f68bcdf3a923..414823646b98 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1262,7 +1262,7 @@ async function kit({ svelte_config }) { client_manifest, bundle, client_chunks, - svelte_config.kit.output, + svelte_config.kit.output ); // ...and prerender From 2e9d6011451b5984104d9848e507e308614bb514 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 05:17:14 +0800 Subject: [PATCH 14/21] fixes --- packages/kit/src/core/sync/create_manifest_data/index.js | 4 ++-- packages/kit/src/core/sync/write_non_ambient.js | 9 +++++---- packages/kit/src/exports/vite/static_analysis/index.js | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index c7c3601c0ad7..b77b42bd3afb 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -423,14 +423,14 @@ function create_routes_and_nodes(cwd, config, fallback) { for (const route of routes) { if (!route.leaf) continue; + node_analyser.get_page_options(route.leaf); + route.page = { layouts: [], errors: [], leaf: /** @type {number} */ (indexes.get(route.leaf)) }; - route.page_options = node_analyser.get_page_options(route.leaf); - /** @type {import('types').RouteData | null} */ let current_route = route; let current_node = route.leaf; diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index a631922ccb8d..301705c0fe89 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -24,13 +24,14 @@ function get_pathnames_for_trailing_slash(pathname, trailing_slash) { return [pathname]; } - if (trailing_slash === 'never') { - return [pathname]; + // 'ignore' → both versions + if (trailing_slash === 'ignore') { + return [pathname, pathname + '/']; } else if (trailing_slash === 'always') { return [pathname + '/']; } else { - // 'ignore' → both versions - return [pathname, pathname + '/']; + // 'never' or undefined → no trailing slash + return [pathname]; } } diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index 9ab21d790e02..64f2ebeacad8 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -33,8 +33,8 @@ const parser = Parser.extend(tsPlugin()); * @returns {PageOptions | 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 there's a chance there are no page exports or an unparseable + // export all declaration, then we can skip the AST parsing which is expensive if (!skip_parsing_regex.test(input)) { return {}; } From 30d895135c7d1d454cb1c3381ddbd124399f2d29 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 05:24:49 +0800 Subject: [PATCH 15/21] ok real fix --- packages/kit/src/core/sync/create_manifest_data/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index b77b42bd3afb..409db676699d 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -423,8 +423,6 @@ function create_routes_and_nodes(cwd, config, fallback) { for (const route of routes) { if (!route.leaf) continue; - node_analyser.get_page_options(route.leaf); - route.page = { layouts: [], errors: [], @@ -470,6 +468,10 @@ function create_routes_and_nodes(cwd, config, fallback) { node.page_options = node_analyser.get_page_options(node); } + for (const route of routes) { + if (route.leaf) route.page_options = node_analyser.get_page_options(route.leaf); + } + return { nodes, routes: sort_routes(routes) From 472760b2683e29b5aa1020b6bffd2b4900806fce Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 05:30:09 +0800 Subject: [PATCH 16/21] comment --- packages/kit/src/types/internal.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index c6815103f2ba..134147e99beb 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -215,6 +215,7 @@ export interface PageNode { parent?: PageNode; /** Filled with the pages that reference this layout (if this is a layout). */ child_pages?: PageNode[]; + /** The final page options for a node if it was statically analysable */ page_options?: PageOptions | null; } From 47429a456e134d548a5774b57936fb611596ebd9 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 06:42:22 +0800 Subject: [PATCH 17/21] handle endpoints --- .../core/sync/create_manifest_data/index.js | 24 +++-- .../sync/create_manifest_data/index.spec.js | 94 +++++++++++-------- .../kit/src/core/sync/write_non_ambient.js | 44 +++++---- .../(group)/path-a/{+page.ts => +page.js} | 0 .../trailing-slash/always/endpoint/+server.js | 1 + .../trailing-slash/ignore/endpoint/+server.js | 1 + .../path-a/trailing-slash/mixed/+page.js} | 0 .../path-a/trailing-slash/mixed/+server.js | 1 + .../trailing-slash/never/endpoint/+server.js | 1 + .../test/app-types/{+page.ts => +page.js} | 40 +++++--- .../test/app-types/foo/[bar]/[baz]/+page.js | 0 .../src/exports/vite/static_analysis/index.js | 32 +++---- packages/kit/src/types/internal.d.ts | 6 +- packages/kit/types/index.d.ts | 7 +- 14 files changed, 148 insertions(+), 103 deletions(-) rename packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/{+page.ts => +page.js} (100%) create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js rename packages/kit/src/core/sync/write_types/test/app-types/{foo/[bar]/[baz]/+page.ts => (group)/path-a/trailing-slash/mixed/+page.js} (100%) create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js rename packages/kit/src/core/sync/write_types/test/app-types/{+page.ts => +page.js} (51%) create mode 100644 packages/kit/src/core/sync/write_types/test/app-types/foo/[bar]/[baz]/+page.js diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 409db676699d..a2341821585a 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -8,7 +8,10 @@ import { posixify, resolve_entry } from '../../../utils/filesystem.js'; import { parse_route_id } from '../../../utils/routing.js'; import { sort_routes } from './sort.js'; import { isSvelte5Plus } from '../utils.js'; -import { create_node_analyser } from '../../../exports/vite/static_analysis/index.js'; +import { + create_node_analyser, + get_page_options +} from '../../../exports/vite/static_analysis/index.js'; /** * Generates the manifest data used for the client-side manifest and types generation. @@ -201,8 +204,7 @@ function create_routes_and_nodes(cwd, config, fallback) { error: null, leaf: null, page: null, - endpoint: null, - page_options: null + endpoint: null }; // important to do this before walking children, so that child @@ -344,7 +346,8 @@ function create_routes_and_nodes(cwd, config, fallback) { } route.endpoint = { - file: project_relative + file: project_relative, + page_options: null // will be filled later }; } } @@ -380,8 +383,7 @@ function create_routes_and_nodes(cwd, config, fallback) { error: null, leaf: null, page: null, - endpoint: null, - page_options: null + endpoint: null }); } @@ -426,7 +428,8 @@ function create_routes_and_nodes(cwd, config, fallback) { route.page = { layouts: [], errors: [], - leaf: /** @type {number} */ (indexes.get(route.leaf)) + leaf: /** @type {number} */ (indexes.get(route.leaf)), + page_options: null }; /** @type {import('types').RouteData | null} */ @@ -469,7 +472,12 @@ function create_routes_and_nodes(cwd, config, fallback) { } for (const route of routes) { - if (route.leaf) route.page_options = node_analyser.get_page_options(route.leaf); + if (route.leaf && route.page) { + route.page.page_options = node_analyser.get_page_options(route.leaf); + } + if (route.endpoint) { + route.endpoint.page_options = get_page_options(route.endpoint.file); + } } return { diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js index c321507a5b42..1e7a774b40ba 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js @@ -77,34 +77,35 @@ test('creates routes', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 2 } + page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } }, { id: '/about', pattern: '/^/about/?$/', - page: { layouts: [0], errors: [1], leaf: 3 } + page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } }, { id: '/blog.json', pattern: '/^/blog.json/?$/', - endpoint: { file: 'samples/basic/blog.json/+server.js' } + endpoint: { file: 'samples/basic/blog.json/+server.js', page_options: null } }, { id: '/blog', pattern: '/^/blog/?$/', - page: { layouts: [0], errors: [1], leaf: 4 } + page: { layouts: [0], errors: [1], leaf: 4, page_options: {} } }, { id: '/blog/[slug].json', pattern: '/^/blog/([^/]+?).json/?$/', endpoint: { - file: 'samples/basic/blog/[slug].json/+server.ts' + file: 'samples/basic/blog/[slug].json/+server.ts', + page_options: null } }, { id: '/blog/[slug]', pattern: '/^/blog/([^/]+?)/?$/', - page: { layouts: [0], errors: [1], leaf: 5 } + page: { layouts: [0], errors: [1], leaf: 5, page_options: {} } } ]); }); @@ -155,13 +156,13 @@ test('creates routes with layout', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 3 } + page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } }, { id: '/foo', pattern: '/^/foo/?$/', - page: { layouts: [0, 2], errors: [1, undefined], leaf: 4 } + page: { layouts: [0, 2], errors: [1, undefined], leaf: 4, page_options: {} } } ]); }); @@ -269,7 +270,7 @@ test('sorts routes with rest correctly', () => { { id: '/a/[...rest]', pattern: '/^/a(?:/([^]*))?/?$/', - page: { layouts: [0], errors: [1], leaf: 2 } + page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } }, { id: '/b', @@ -278,7 +279,7 @@ test('sorts routes with rest correctly', () => { { id: '/b/[...rest]', pattern: '/^/b(?:/([^]*))?/?$/', - page: { layouts: [0], errors: [1], leaf: 3 } + page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } } ]); }); @@ -302,13 +303,14 @@ test('allows rest parameters inside segments', () => { { id: '/prefix-[...rest]', pattern: '/^/prefix-([^]*?)/?$/', - page: { layouts: [0], errors: [1], leaf: 2 } + page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } }, { id: '/[...rest].json', pattern: '/^/([^]*?).json/?$/', endpoint: { - file: 'samples/rest-prefix-suffix/[...rest].json/+server.js' + file: 'samples/rest-prefix-suffix/[...rest].json/+server.js', + page_options: null } } ]); @@ -346,7 +348,7 @@ test('optional parameters', () => { { id: '/[[foo]]bar', pattern: '/^/([^/]*)?bar/?$/', - endpoint: { file: 'samples/optional/[[foo]]bar/+server.js' } + endpoint: { file: 'samples/optional/[[foo]]bar/+server.js', page_options: null } }, { id: '/nested', pattern: '/^/nested/?$/' }, { @@ -356,7 +358,8 @@ test('optional parameters', () => { layouts: [0], errors: [1], // see above, linux/windows difference -> find the index dynamically - leaf: nodes.findIndex((node) => node.component?.includes('nested/[[optional]]')) + leaf: nodes.findIndex((node) => node.component?.includes('nested/[[optional]]')), + page_options: {} } }, { id: '/nested/[[optional]]', pattern: '/^/nested(?:/([^/]+))?/?$/' }, @@ -367,7 +370,8 @@ test('optional parameters', () => { layouts: [0], errors: [1], // see above, linux/windows difference -> find the index dynamically - leaf: nodes.findIndex((node) => node.component?.includes('prefix[[suffix]]')) + leaf: nodes.findIndex((node) => node.component?.includes('prefix[[suffix]]')), + page_options: {} } }, { @@ -377,7 +381,8 @@ test('optional parameters', () => { layouts: [0], errors: [1], // see above, linux/windows difference -> find the index dynamically - leaf: nodes.findIndex((node) => node.component?.includes('optional/[[optional]]')) + leaf: nodes.findIndex((node) => node.component?.includes('optional/[[optional]]')), + page_options: {} } } ]); @@ -402,7 +407,8 @@ test('nested optionals', () => { page: { layouts: [0], errors: [1], - leaf: nodes.findIndex((node) => node.component?.includes('/[[a]]/[[b]]')) + leaf: nodes.findIndex((node) => node.component?.includes('/[[a]]/[[b]]')), + page_options: {} } }, { @@ -444,7 +450,8 @@ test('group preceding optional parameters', () => { // see above, linux/windows difference -> find the index dynamically leaf: nodes.findIndex((node) => node.component?.includes('optional-group/[[optional]]/(group)') - ) + ), + page_options: {} } }, { @@ -478,7 +485,8 @@ test('allows multiple slugs', () => { id: '/[file].[ext]', pattern: '/^/([^/]+?).([^/]+?)/?$/', endpoint: { - file: 'samples/multiple-slugs/[file].[ext]/+server.js' + file: 'samples/multiple-slugs/[file].[ext]/+server.js', + page_options: null } } ]); @@ -502,7 +510,8 @@ test('ignores things that look like lockfiles', () => { id: '/foo', pattern: '/^/foo/?$/', endpoint: { - file: 'samples/lockfiles/foo/+server.js' + file: 'samples/lockfiles/foo/+server.js', + page_options: null } } ]); @@ -526,36 +535,38 @@ test('works with custom extensions', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 2 } + page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } }, { id: '/about', pattern: '/^/about/?$/', - page: { layouts: [0], errors: [1], leaf: 3 } + page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } }, { id: '/blog.json', pattern: '/^/blog.json/?$/', endpoint: { - file: 'samples/custom-extension/blog.json/+server.js' + file: 'samples/custom-extension/blog.json/+server.js', + page_options: null } }, { id: '/blog', pattern: '/^/blog/?$/', - page: { layouts: [0], errors: [1], leaf: 4 } + page: { layouts: [0], errors: [1], leaf: 4, page_options: {} } }, { id: '/blog/[slug].json', pattern: '/^/blog/([^/]+?).json/?$/', endpoint: { - file: 'samples/custom-extension/blog/[slug].json/+server.js' + file: 'samples/custom-extension/blog/[slug].json/+server.js', + page_options: null } }, { id: '/blog/[slug]', pattern: '/^/blog/([^/]+?)/?$/', - page: { layouts: [0], errors: [1], leaf: 5 } + page: { layouts: [0], errors: [1], leaf: 5, page_options: {} } } ]); }); @@ -606,7 +617,12 @@ test('includes nested error components', () => { { id: '/foo/bar/baz', pattern: '/^/foo/bar/baz/?$/', - page: { layouts: [0, 2, undefined, 4], errors: [1, undefined, 3, 5], leaf: 6 } + page: { + layouts: [0, 2, undefined, 4], + errors: [1, undefined, 3, 5], + leaf: 6, + page_options: {} + } } ]); }); @@ -647,42 +663,42 @@ test('creates routes with named layouts', () => { { id: '/a/a1', pattern: '/^/a/a1/?$/', - page: { layouts: [0, 4], errors: [1, undefined], leaf: 10 } + page: { layouts: [0, 4], errors: [1, undefined], leaf: 10, page_options: {} } }, { id: '/(special)/a/a2', pattern: '/^/a/a2/?$/', - page: { layouts: [0, 2], errors: [1, undefined], leaf: 9 } + page: { layouts: [0, 2], errors: [1, undefined], leaf: 9, page_options: {} } }, { id: '/(special)/(alsospecial)/b/c/c1', pattern: '/^/b/c/c1/?$/', - page: { layouts: [0, 2, 3], errors: [1, undefined, undefined], leaf: 8 } + page: { layouts: [0, 2, 3], errors: [1, undefined, undefined], leaf: 8, page_options: {} } }, { id: '/b/c/c2', pattern: '/^/b/c/c2/?$/', - page: { layouts: [0], errors: [1], leaf: 11 } + page: { layouts: [0], errors: [1], leaf: 11, page_options: {} } }, { id: '/b/d/(special)', pattern: '/^/b/d/?$/', - page: { layouts: [0, 6], errors: [1, undefined], leaf: 12 } + page: { layouts: [0, 6], errors: [1, undefined], leaf: 12, page_options: {} } }, { id: '/b/d/d1', pattern: '/^/b/d/d1/?$/', - page: { layouts: [0], errors: [1], leaf: 15 } + page: { layouts: [0], errors: [1], leaf: 15, page_options: {} } }, { id: '/b/d/(special)/(extraspecial)/d2', pattern: '/^/b/d/d2/?$/', - page: { layouts: [0, 6, 7], errors: [1, undefined, undefined], leaf: 13 } + page: { layouts: [0, 6, 7], errors: [1, undefined, undefined], leaf: 13, page_options: {} } }, { id: '/b/d/(special)/(extraspecial)/d3', pattern: '/^/b/d/d3/?$/', - page: { layouts: [0, 6], errors: [1, undefined], leaf: 14 } + page: { layouts: [0, 6], errors: [1, undefined], leaf: 14, page_options: {} } } ]); }); @@ -706,7 +722,7 @@ test('handles pages without .svelte file', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 5 } + page: { layouts: [0], errors: [1], leaf: 5, page_options: {} } }, { id: '/error', @@ -715,7 +731,7 @@ test('handles pages without .svelte file', () => { { id: '/error/[...path]', pattern: '/^/error(?:/([^]*))?/?$/', - page: { layouts: [0, undefined], errors: [1, 2], leaf: 6 } + page: { layouts: [0, undefined], errors: [1, 2], leaf: 6, page_options: {} } }, { id: '/layout', @@ -724,12 +740,12 @@ test('handles pages without .svelte file', () => { { id: '/layout/exists', pattern: '/^/layout/exists/?$/', - page: { layouts: [0, 3, 4], errors: [1, undefined, undefined], leaf: 7 } + page: { layouts: [0, 3, 4], errors: [1, undefined, undefined], leaf: 7, page_options: {} } }, { id: '/layout/redirect', pattern: '/^/layout/redirect/?$/', - page: { layouts: [0, 3], errors: [1, undefined], leaf: 8 } + page: { layouts: [0, 3], errors: [1, undefined], leaf: 8, page_options: {} } } ]); }); diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index 301705c0fe89..bde2e76f1794 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -14,25 +14,37 @@ const remove_group_segments = (/** @type {string} */ id) => { }; /** - * Get pathnames to add based on trailingSlash setting + * Get pathnames to add based on trailingSlash settings * @param {string} pathname - * @param {import('types').TrailingSlash} trailing_slash + * @param {import('types').RouteData} route * @returns {string[]} */ -function get_pathnames_for_trailing_slash(pathname, trailing_slash) { +function get_pathnames_for_trailing_slash(pathname, route) { if (pathname === '/') { return [pathname]; } - // 'ignore' → both versions - if (trailing_slash === 'ignore') { - return [pathname, pathname + '/']; - } else if (trailing_slash === 'always') { - return [pathname + '/']; - } else { - // 'never' or undefined → no trailing slash - return [pathname]; + /** @type {({ trailingSlash?: import('types').TrailingSlash } | null)[]} */ + const routes = []; + + if (route.page) routes.push(route.page.page_options); + if (route.endpoint) routes.push(route.endpoint.page_options); + + /** @type {Set} */ + const pathnames = new Set(); + + for (const page_options of routes) { + if (page_options === null || page_options.trailingSlash === 'ignore') { + pathnames.add(pathname); + pathnames.add(pathname + '/'); + } else if (page_options.trailingSlash === 'always') { + pathnames.add(pathname + '/'); + } else { + pathnames.add(pathname); + } } + + return Array.from(pathnames); } // `declare module "svelte/elements"` needs to happen in a non-ambient module, and dts-buddy generates one big ambient module, @@ -90,18 +102,12 @@ function generate_app_types(manifest_data) { const pathname = remove_group_segments(route.id); const replaced_pathname = replace_required_params(replace_optional_params(pathname)); - for (const p of get_pathnames_for_trailing_slash( - replaced_pathname, - route.page_options?.trailingSlash - )) { + for (const p of get_pathnames_for_trailing_slash(replaced_pathname, route)) { pathnames.add(`\`${p}\` & {}`); } } else { const pathname = remove_group_segments(route.id); - for (const p of get_pathnames_for_trailing_slash( - pathname, - route.page_options?.trailingSlash - )) { + for (const p of get_pathnames_for_trailing_slash(pathname, route)) { pathnames.add(s(p)); } } diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/+page.js similarity index 100% rename from packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/+page.ts rename to packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/+page.js diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js new file mode 100644 index 000000000000..aa819621c4a5 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js @@ -0,0 +1 @@ +export const trailingSlash = 'always'; \ No newline at end of file diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js new file mode 100644 index 000000000000..cb2351149c51 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js @@ -0,0 +1 @@ +export const trailingSlash = 'ignore'; \ No newline at end of file diff --git a/packages/kit/src/core/sync/write_types/test/app-types/foo/[bar]/[baz]/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+page.js similarity index 100% rename from packages/kit/src/core/sync/write_types/test/app-types/foo/[bar]/[baz]/+page.ts rename to packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+page.js diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js new file mode 100644 index 000000000000..aa819621c4a5 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js @@ -0,0 +1 @@ +export const trailingSlash = 'always'; \ No newline at end of file diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js new file mode 100644 index 000000000000..1d98330fba9c --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js @@ -0,0 +1 @@ +export const trailingSlash = 'never'; \ No newline at end of file diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/+page.js similarity index 51% rename from packages/kit/src/core/sync/write_types/test/app-types/+page.ts rename to packages/kit/src/core/sync/write_types/test/app-types/+page.js index a136087be5a6..17d6f66b3cac 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.js @@ -1,6 +1,5 @@ -import type { RouteId, RouteParams, Pathname } from '$app/types'; - -declare let id: RouteId; +/** @type {import('$app/types').RouteId} */ +let id; // okay id = '/'; @@ -8,26 +7,31 @@ id = '/foo/[bar]/[baz]'; id = '/(group)/path-a'; // @ts-expect-error +// eslint-disable-next-line @typescript-eslint/no-unused-vars id = '/nope'; -// read `id` otherwise it is treated as unused -id; - -declare let params: RouteParams<'/foo/[bar]/[baz]'>; +/** @type {import('$app/types').RouteParams<'/foo/[bar]/[baz]'>} */ +const params = { + bar: 'A', + baz: 'B' +} -// @ts-expect-error -params.foo; // not okay +// @ts-expect-error foo is not a param +params.foo; params.bar; // okay params.baz; // okay -declare let pathname: Pathname; +/** @type {import('$app/types').Pathname} */ +let pathname; -// @ts-expect-error +// @ts-expect-error route doesn't exist pathname = '/nope'; +// @ts-expect-error route doesn't exist pathname = '/foo'; -pathname = '/foo/1/2'; +// @ts-expect-error route doesn't exist pathname = '/foo/'; -pathname = '/foo/1/2/'; +pathname = '/foo/1/2'; // okay +pathname = '/foo/1/2/'; // okay // Test layout groups pathname = '/path-a'; @@ -38,17 +42,23 @@ pathname = '/(group)/path-a'; // Test trailing-slash - always pathname = '/path-a/trailing-slash/always/'; +pathname = '/path-a/trailing-slash/always/endpoint/'; pathname = '/path-a/trailing-slash/always/layout/inside/'; // Test trailing-slash - ignore pathname = '/path-a/trailing-slash/ignore'; pathname = '/path-a/trailing-slash/ignore/'; +pathname = '/path-a/trailing-slash/ignore/endpoint'; +pathname = '/path-a/trailing-slash/ignore/endpoint/'; pathname = '/path-a/trailing-slash/ignore/layout/inside'; pathname = '/path-a/trailing-slash/ignore/layout/inside/'; // Test trailing-slash - never (default) pathname = '/path-a/trailing-slash/never'; +pathname = '/path-a/trailing-slash/never/endpoint'; pathname = '/path-a/trailing-slash/never/layout/inside'; -// read `pathname` otherwise it is treated as unused -pathname; +// Test trailing-slash - always (endpoint) and never (page) +pathname = '/path-a/trailing-slash/mixed'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +pathname = '/path-a/trailing-slash/mixed/'; \ No newline at end of file diff --git a/packages/kit/src/core/sync/write_types/test/app-types/foo/[bar]/[baz]/+page.js b/packages/kit/src/core/sync/write_types/test/app-types/foo/[bar]/[baz]/+page.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/src/exports/vite/static_analysis/index.js b/packages/kit/src/exports/vite/static_analysis/index.js index 64f2ebeacad8..6d4248032da5 100644 --- a/packages/kit/src/exports/vite/static_analysis/index.js +++ b/packages/kit/src/exports/vite/static_analysis/index.js @@ -215,11 +215,10 @@ function get_name(node) { * @param {string} filepath * @returns {PageOptions | null} Returns the page options for the file or `null` if unanalysable */ -function get_file_page_options(filepath) { +export function get_page_options(filepath) { try { const input = read(filepath); const page_options = statically_analyse_page_options(filepath, input); - if (page_options === null) { return null; } @@ -230,17 +229,14 @@ function get_file_page_options(filepath) { } } -/** - * @param {{ - * static_exports?: Map; - * }} opts - */ -export function create_node_analyser({ static_exports = new Map() } = {}) { +export function create_node_analyser() { + const static_exports = new Map(); + /** * @param {string | undefined} key * @param {PageOptions | null} page_options */ - const cache_static_analysis = (key, page_options) => { + const cache = (key, page_options) => { if (key) static_exports.set(key, { page_options, children: [] }); }; @@ -249,7 +245,7 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { * @param {import('types').PageNode} node * @returns {PageOptions | null} */ - const get_page_options = (node) => { + const crawl = (node) => { const key = node.universal || node.server; if (key && static_exports.has(key)) { return { ...static_exports.get(key)?.page_options }; @@ -259,7 +255,7 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { let page_options = {}; if (node.parent) { - const parent_options = get_page_options(node.parent); + const parent_options = crawl(node.parent); const parent_key = node.parent.universal || node.parent.server; if (key && parent_key) { @@ -269,7 +265,7 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { 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 - cache_static_analysis(key, null); + cache(key, null); return null; } @@ -277,29 +273,29 @@ export function create_node_analyser({ static_exports = new Map() } = {}) { } if (node.server) { - const server_page_options = get_file_page_options(node.server); + const server_page_options = get_page_options(node.server); if (server_page_options === null) { - cache_static_analysis(key, null); + cache(key, null); return null; } page_options = { ...page_options, ...server_page_options }; } if (node.universal) { - const universal_page_options = get_file_page_options(node.universal); + const universal_page_options = get_page_options(node.universal); if (universal_page_options === null) { - cache_static_analysis(key, null); + cache(key, null); return null; } page_options = { ...page_options, ...universal_page_options }; } - cache_static_analysis(key, page_options); + cache(key, page_options); return page_options; }; return { - get_page_options + get_page_options: crawl }; } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 134147e99beb..78e08466bec7 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -277,13 +277,15 @@ export interface RouteData { layouts: Array; errors: Array; leaf: number; + /** The final page options for the page if it was statically analysable */ + page_options: PageOptions | null; } | null; endpoint: { file: string; + /** The final page options for the endpoint if it was statically analysable */ + page_options: PageOptions | null; } | null; - - page_options: PageOptions | null; } export type ServerRedirectNode = { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 17ba381b4dfb..b64a6712de65 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2466,6 +2466,7 @@ declare module '@sveltejs/kit' { parent?: PageNode; /** Filled with the pages that reference this layout (if this is a layout). */ child_pages?: PageNode[]; + /** The final page options for a node if it was statically analysable */ page_options?: PageOptions | null; } @@ -2507,13 +2508,15 @@ declare module '@sveltejs/kit' { layouts: Array; errors: Array; leaf: number; + /** The final page options for the page if it was statically analysable */ + page_options: PageOptions | null; } | null; endpoint: { file: string; + /** The final page options for the endpoint if it was statically analysable */ + page_options: PageOptions | null; } | null; - - page_options: PageOptions | null; } interface SSRComponent { From 8c7685598da1282e67c4f4d152f9aa2a35b5f85b Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 06:43:58 +0800 Subject: [PATCH 18/21] format --- .../path-a/trailing-slash/always/endpoint/+server.js | 2 +- .../path-a/trailing-slash/ignore/endpoint/+server.js | 2 +- .../(group)/path-a/trailing-slash/mixed/+server.js | 2 +- .../path-a/trailing-slash/never/endpoint/+server.js | 2 +- .../kit/src/core/sync/write_types/test/app-types/+page.js | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js index aa819621c4a5..d3c325085ed2 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/endpoint/+server.js @@ -1 +1 @@ -export const trailingSlash = 'always'; \ No newline at end of file +export const trailingSlash = 'always'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js index cb2351149c51..42a828c116a3 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/ignore/endpoint/+server.js @@ -1 +1 @@ -export const trailingSlash = 'ignore'; \ No newline at end of file +export const trailingSlash = 'ignore'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js index aa819621c4a5..d3c325085ed2 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+server.js @@ -1 +1 @@ -export const trailingSlash = 'always'; \ No newline at end of file +export const trailingSlash = 'always'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js index 1d98330fba9c..844f51956c3e 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/never/endpoint/+server.js @@ -1 +1 @@ -export const trailingSlash = 'never'; \ No newline at end of file +export const trailingSlash = 'never'; diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.js b/packages/kit/src/core/sync/write_types/test/app-types/+page.js index 17d6f66b3cac..a83dce322ce7 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.js @@ -12,9 +12,9 @@ id = '/nope'; /** @type {import('$app/types').RouteParams<'/foo/[bar]/[baz]'>} */ const params = { - bar: 'A', - baz: 'B' -} + bar: 'A', + baz: 'B' +}; // @ts-expect-error foo is not a param params.foo; @@ -61,4 +61,4 @@ pathname = '/path-a/trailing-slash/never/layout/inside'; // Test trailing-slash - always (endpoint) and never (page) pathname = '/path-a/trailing-slash/mixed'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -pathname = '/path-a/trailing-slash/mixed/'; \ No newline at end of file +pathname = '/path-a/trailing-slash/mixed/'; From 7d293824a48d9b7220cb1b3ae60368ce51525b26 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 13:57:48 +0800 Subject: [PATCH 19/21] reduce duplication --- .../core/sync/create_manifest_data/index.js | 6 +- .../sync/create_manifest_data/index.spec.js | 68 +++++++++---------- .../kit/src/core/sync/write_non_ambient.js | 2 +- .../kit/src/core/sync/write_types/index.js | 2 + packages/kit/src/exports/vite/dev/index.js | 3 +- packages/kit/src/types/internal.d.ts | 2 - packages/kit/types/index.d.ts | 2 - 7 files changed, 37 insertions(+), 48 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index a2341821585a..5facdc78dd5c 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -428,8 +428,7 @@ function create_routes_and_nodes(cwd, config, fallback) { route.page = { layouts: [], errors: [], - leaf: /** @type {number} */ (indexes.get(route.leaf)), - page_options: null + leaf: /** @type {number} */ (indexes.get(route.leaf)) }; /** @type {import('types').RouteData | null} */ @@ -472,9 +471,6 @@ function create_routes_and_nodes(cwd, config, fallback) { } for (const route of routes) { - if (route.leaf && route.page) { - route.page.page_options = node_analyser.get_page_options(route.leaf); - } if (route.endpoint) { route.endpoint.page_options = get_page_options(route.endpoint.file); } diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js index 1e7a774b40ba..bbc952188785 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js @@ -77,12 +77,12 @@ test('creates routes', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 2 } }, { id: '/about', pattern: '/^/about/?$/', - page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 3 } }, { id: '/blog.json', @@ -92,7 +92,7 @@ test('creates routes', () => { { id: '/blog', pattern: '/^/blog/?$/', - page: { layouts: [0], errors: [1], leaf: 4, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 4 } }, { id: '/blog/[slug].json', @@ -105,7 +105,7 @@ test('creates routes', () => { { id: '/blog/[slug]', pattern: '/^/blog/([^/]+?)/?$/', - page: { layouts: [0], errors: [1], leaf: 5, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 5 } } ]); }); @@ -156,13 +156,13 @@ test('creates routes with layout', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 3 } }, { id: '/foo', pattern: '/^/foo/?$/', - page: { layouts: [0, 2], errors: [1, undefined], leaf: 4, page_options: {} } + page: { layouts: [0, 2], errors: [1, undefined], leaf: 4 } } ]); }); @@ -270,7 +270,7 @@ test('sorts routes with rest correctly', () => { { id: '/a/[...rest]', pattern: '/^/a(?:/([^]*))?/?$/', - page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 2 } }, { id: '/b', @@ -279,7 +279,7 @@ test('sorts routes with rest correctly', () => { { id: '/b/[...rest]', pattern: '/^/b(?:/([^]*))?/?$/', - page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 3 } } ]); }); @@ -303,7 +303,7 @@ test('allows rest parameters inside segments', () => { { id: '/prefix-[...rest]', pattern: '/^/prefix-([^]*?)/?$/', - page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 2 } }, { id: '/[...rest].json', @@ -358,8 +358,7 @@ test('optional parameters', () => { layouts: [0], errors: [1], // see above, linux/windows difference -> find the index dynamically - leaf: nodes.findIndex((node) => node.component?.includes('nested/[[optional]]')), - page_options: {} + leaf: nodes.findIndex((node) => node.component?.includes('nested/[[optional]]')) } }, { id: '/nested/[[optional]]', pattern: '/^/nested(?:/([^/]+))?/?$/' }, @@ -370,8 +369,7 @@ test('optional parameters', () => { layouts: [0], errors: [1], // see above, linux/windows difference -> find the index dynamically - leaf: nodes.findIndex((node) => node.component?.includes('prefix[[suffix]]')), - page_options: {} + leaf: nodes.findIndex((node) => node.component?.includes('prefix[[suffix]]')) } }, { @@ -381,8 +379,7 @@ test('optional parameters', () => { layouts: [0], errors: [1], // see above, linux/windows difference -> find the index dynamically - leaf: nodes.findIndex((node) => node.component?.includes('optional/[[optional]]')), - page_options: {} + leaf: nodes.findIndex((node) => node.component?.includes('optional/[[optional]]')) } } ]); @@ -407,8 +404,7 @@ test('nested optionals', () => { page: { layouts: [0], errors: [1], - leaf: nodes.findIndex((node) => node.component?.includes('/[[a]]/[[b]]')), - page_options: {} + leaf: nodes.findIndex((node) => node.component?.includes('/[[a]]/[[b]]')) } }, { @@ -450,8 +446,7 @@ test('group preceding optional parameters', () => { // see above, linux/windows difference -> find the index dynamically leaf: nodes.findIndex((node) => node.component?.includes('optional-group/[[optional]]/(group)') - ), - page_options: {} + ) } }, { @@ -535,12 +530,12 @@ test('works with custom extensions', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 2, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 2 } }, { id: '/about', pattern: '/^/about/?$/', - page: { layouts: [0], errors: [1], leaf: 3, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 3 } }, { id: '/blog.json', @@ -553,7 +548,7 @@ test('works with custom extensions', () => { { id: '/blog', pattern: '/^/blog/?$/', - page: { layouts: [0], errors: [1], leaf: 4, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 4 } }, { id: '/blog/[slug].json', @@ -566,7 +561,7 @@ test('works with custom extensions', () => { { id: '/blog/[slug]', pattern: '/^/blog/([^/]+?)/?$/', - page: { layouts: [0], errors: [1], leaf: 5, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 5 } } ]); }); @@ -620,8 +615,7 @@ test('includes nested error components', () => { page: { layouts: [0, 2, undefined, 4], errors: [1, undefined, 3, 5], - leaf: 6, - page_options: {} + leaf: 6 } } ]); @@ -663,42 +657,42 @@ test('creates routes with named layouts', () => { { id: '/a/a1', pattern: '/^/a/a1/?$/', - page: { layouts: [0, 4], errors: [1, undefined], leaf: 10, page_options: {} } + page: { layouts: [0, 4], errors: [1, undefined], leaf: 10 } }, { id: '/(special)/a/a2', pattern: '/^/a/a2/?$/', - page: { layouts: [0, 2], errors: [1, undefined], leaf: 9, page_options: {} } + page: { layouts: [0, 2], errors: [1, undefined], leaf: 9 } }, { id: '/(special)/(alsospecial)/b/c/c1', pattern: '/^/b/c/c1/?$/', - page: { layouts: [0, 2, 3], errors: [1, undefined, undefined], leaf: 8, page_options: {} } + page: { layouts: [0, 2, 3], errors: [1, undefined, undefined], leaf: 8 } }, { id: '/b/c/c2', pattern: '/^/b/c/c2/?$/', - page: { layouts: [0], errors: [1], leaf: 11, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 11 } }, { id: '/b/d/(special)', pattern: '/^/b/d/?$/', - page: { layouts: [0, 6], errors: [1, undefined], leaf: 12, page_options: {} } + page: { layouts: [0, 6], errors: [1, undefined], leaf: 12 } }, { id: '/b/d/d1', pattern: '/^/b/d/d1/?$/', - page: { layouts: [0], errors: [1], leaf: 15, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 15 } }, { id: '/b/d/(special)/(extraspecial)/d2', pattern: '/^/b/d/d2/?$/', - page: { layouts: [0, 6, 7], errors: [1, undefined, undefined], leaf: 13, page_options: {} } + page: { layouts: [0, 6, 7], errors: [1, undefined, undefined], leaf: 13 } }, { id: '/b/d/(special)/(extraspecial)/d3', pattern: '/^/b/d/d3/?$/', - page: { layouts: [0, 6], errors: [1, undefined], leaf: 14, page_options: {} } + page: { layouts: [0, 6], errors: [1, undefined], leaf: 14 } } ]); }); @@ -722,7 +716,7 @@ test('handles pages without .svelte file', () => { { id: '/', pattern: '/^/$/', - page: { layouts: [0], errors: [1], leaf: 5, page_options: {} } + page: { layouts: [0], errors: [1], leaf: 5 } }, { id: '/error', @@ -731,7 +725,7 @@ test('handles pages without .svelte file', () => { { id: '/error/[...path]', pattern: '/^/error(?:/([^]*))?/?$/', - page: { layouts: [0, undefined], errors: [1, 2], leaf: 6, page_options: {} } + page: { layouts: [0, undefined], errors: [1, 2], leaf: 6 } }, { id: '/layout', @@ -740,12 +734,12 @@ test('handles pages without .svelte file', () => { { id: '/layout/exists', pattern: '/^/layout/exists/?$/', - page: { layouts: [0, 3, 4], errors: [1, undefined, undefined], leaf: 7, page_options: {} } + page: { layouts: [0, 3, 4], errors: [1, undefined, undefined], leaf: 7 } }, { id: '/layout/redirect', pattern: '/^/layout/redirect/?$/', - page: { layouts: [0, 3], errors: [1, undefined], leaf: 8, page_options: {} } + page: { layouts: [0, 3], errors: [1, undefined], leaf: 8 } } ]); }); diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index bde2e76f1794..62d590ad7410 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -27,7 +27,7 @@ function get_pathnames_for_trailing_slash(pathname, route) { /** @type {({ trailingSlash?: import('types').TrailingSlash } | null)[]} */ const routes = []; - if (route.page) routes.push(route.page.page_options); + if (route.leaf) routes.push(route.leaf.page_options ?? null); if (route.endpoint) routes.push(route.endpoint.page_options); /** @type {Set} */ diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index a7f48109548d..64587e41c39a 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -153,6 +153,8 @@ export function write_types(config, manifest_data, file) { if (!route) return; if (!route.leaf && !route.layout && !route.endpoint) return; // nothing to do + // TODO: statically analyse page options for the file + update_types(config, create_routes_map(manifest_data), route); } diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 9d69008638c3..031856f1eaca 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -357,7 +357,8 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { 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; + // Unless it's a file where the trailing slash page option might have changed + if (timeout || restarting || !/\+(page|layout|server).*$/.test(file)) return; sync.update(svelte_config, manifest_data, file); }); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 78e08466bec7..57af755bb503 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -277,8 +277,6 @@ export interface RouteData { layouts: Array; errors: Array; leaf: number; - /** The final page options for the page if it was statically analysable */ - page_options: PageOptions | null; } | null; endpoint: { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b64a6712de65..20bfeb94a6c5 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2508,8 +2508,6 @@ declare module '@sveltejs/kit' { layouts: Array; errors: Array; leaf: number; - /** The final page options for the page if it was statically analysable */ - page_options: PageOptions | null; } | null; endpoint: { From 10d2a8f6866c37fa8adec3338561bb028b412ef1 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 14:04:34 +0800 Subject: [PATCH 20/21] add todos --- packages/kit/src/core/sync/sync.js | 2 ++ packages/kit/src/core/sync/write_types/index.js | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index c77bbca30f27..9415f1925b0b 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -45,6 +45,8 @@ export function create(config) { * @param {string} file */ export function update(config, manifest_data, file) { + // TODO: statically analyse page options for the file and update the manifest_data + write_types(config, manifest_data, file); } diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 64587e41c39a..e558d24ca2f1 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -153,8 +153,6 @@ export function write_types(config, manifest_data, file) { if (!route) return; if (!route.leaf && !route.layout && !route.endpoint) return; // nothing to do - // TODO: statically analyse page options for the file - update_types(config, create_routes_map(manifest_data), route); } @@ -364,6 +362,10 @@ function update_types(config, routes, route, to_delete = new Set()) { exports.push('export type RequestEvent = Kit.RequestEvent;'); } + if (route.leaf || route.endpoint) { + // TODO: update Pathname app type + } + const output = [imports.join('\n'), declarations.join('\n'), exports.join('\n')] .filter(Boolean) .join('\n\n'); From f45080da80a003ff26cdfde2bf14c96d95d26530 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 6 Dec 2025 14:06:34 +0800 Subject: [PATCH 21/21] reduce diff --- .../kit/src/core/sync/create_manifest_data/index.spec.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js index bbc952188785..8a075bd8f403 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js @@ -612,11 +612,7 @@ test('includes nested error components', () => { { id: '/foo/bar/baz', pattern: '/^/foo/bar/baz/?$/', - page: { - layouts: [0, 2, undefined, 4], - errors: [1, undefined, 3, 5], - leaf: 6 - } + page: { layouts: [0, 2, undefined, 4], errors: [1, undefined, 3, 5], leaf: 6 } } ]); });