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 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..aa99609e6a04 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -65,21 +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( - out, - config, - manifest_data, - server_manifest, - null, - null, - null, - output_config, - static_exports - ); + build_server_nodes(out, config, manifest_data, server_manifest, null, null, null, output_config); /** @type {import('types').ServerMetadata} */ const metadata = { @@ -188,7 +175,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 b7c5e93d658d..5facdc78dd5c 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -8,6 +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, + get_page_options +} from '../../../exports/vite/static_analysis/index.js'; /** * Generates the manifest data used for the client-side manifest and types generation. @@ -342,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 }; } } @@ -415,6 +420,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; @@ -459,6 +466,16 @@ function create_routes_and_nodes(cwd, config, fallback) { } } + for (const node of nodes) { + node.page_options = node_analyser.get_page_options(node); + } + + for (const route of routes) { + if (route.endpoint) { + route.endpoint.page_options = get_page_options(route.endpoint.file); + } + } + return { nodes, routes: sort_routes(routes) 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..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 @@ -87,7 +87,7 @@ test('creates routes', () => { { 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', @@ -98,7 +98,8 @@ test('creates routes', () => { 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 } }, { @@ -308,7 +309,8 @@ test('allows rest parameters inside segments', () => { 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/?$/' }, { @@ -478,7 +480,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 +505,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 } } ]); @@ -537,7 +541,8 @@ test('works with custom extensions', () => { 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 } }, { @@ -549,7 +554,8 @@ test('works with custom extensions', () => { 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 } }, { 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_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index c4161720879a..62d590ad7410 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -13,6 +13,40 @@ const remove_group_segments = (/** @type {string} */ id) => { return '/' + get_route_segments(id).join('/'); }; +/** + * Get pathnames to add based on trailingSlash settings + * @param {string} pathname + * @param {import('types').RouteData} route + * @returns {string[]} + */ +function get_pathnames_for_trailing_slash(pathname, route) { + if (pathname === '/') { + return [pathname]; + } + + /** @type {({ trailingSlash?: import('types').TrailingSlash } | null)[]} */ + const routes = []; + + if (route.leaf) routes.push(route.leaf.page_options ?? null); + 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, // 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 @@ -67,19 +101,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, route)) { + 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, route)) { + pathnames.add(s(p)); } } diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index a7f48109548d..e558d24ca2f1 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -362,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'); 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/+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/(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..d3c325085ed2 --- /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'; 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/foo/[bar]/[baz]/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/always/layout/inside/+page.svelte 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/always/layout/inside/+page.svelte 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/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..42a828c116a3 --- /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'; 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/mixed/+page.js b/packages/kit/src/core/sync/write_types/test/app-types/(group)/path-a/trailing-slash/mixed/+page.js 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/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..d3c325085ed2 --- /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'; 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/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..844f51956c3e --- /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'; 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.js b/packages/kit/src/core/sync/write_types/test/app-types/+page.js new file mode 100644 index 000000000000..a83dce322ce7 --- /dev/null +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.js @@ -0,0 +1,64 @@ +/** @type {import('$app/types').RouteId} */ +let id; + +// okay +id = '/'; +id = '/foo/[bar]/[baz]'; +id = '/(group)/path-a'; + +// @ts-expect-error +// eslint-disable-next-line @typescript-eslint/no-unused-vars +id = '/nope'; + +/** @type {import('$app/types').RouteParams<'/foo/[bar]/[baz]'>} */ +const params = { + bar: 'A', + baz: 'B' +}; + +// @ts-expect-error foo is not a param +params.foo; +params.bar; // okay +params.baz; // okay + +/** @type {import('$app/types').Pathname} */ +let pathname; + +// @ts-expect-error route doesn't exist +pathname = '/nope'; +// @ts-expect-error route doesn't exist +pathname = '/foo'; +// @ts-expect-error route doesn't exist +pathname = '/foo/'; +pathname = '/foo/1/2'; // okay +pathname = '/foo/1/2/'; // okay + +// Test layout groups +pathname = '/path-a'; +// @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 - 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'; + +// 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/'; 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 deleted file mode 100644 index c47c696e7203..000000000000 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { RouteId, RouteParams, Pathname } from '$app/types'; - -declare let id: RouteId; - -// okay -id = '/'; -id = '/foo/[bar]/[baz]'; -id = '/(group)/path-a'; - -// @ts-expect-error -id = '/nope'; - -// read `id` otherwise it is treated as unused -id; - -declare let params: RouteParams<'/foo/[bar]/[baz]'>; - -// @ts-expect-error -params.foo; // not okay -params.bar; // okay -params.baz; // okay - -declare let pathname: Pathname; - -// @ts-expect-error -pathname = '/nope'; -pathname = '/foo'; -pathname = '/foo/1/2'; -pathname = '/foo/'; -pathname = '/foo/1/2/'; - -// Test layout groups -pathname = '/path-a'; -pathname = '/path-a/'; -// @ts-expect-error layout group names are NOT part of the pathname type -pathname = '/(group)/path-a'; - -// read `pathname` otherwise it is treated as unused -pathname; 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/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 7af4073ef09d..28a5fc2c1541 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -3,9 +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 { create_node_analyser } from '../static_analysis/index.js'; - +import { basename } from 'node:path'; /** * @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,14 +54,6 @@ 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 - }); - for (let i = 0; i < manifest_data.nodes.length; i++) { const node = manifest_data.nodes[i]; @@ -95,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 31c13c1ead8b..031856f1eaca 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,14 +125,6 @@ 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; - } - }); - invalidate_page_options = node_analyser.invalidate_page_options; - manifest = { appDir: svelte_config.kit.appDir, appPath: svelte_config.kit.appDir, @@ -215,9 +203,8 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { } if (node.universal) { - const page_options = await 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); @@ -370,12 +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; - - if (/\+(page|layout).*$/.test(file)) { - invalidate_page_options(path.relative(cwd, file)); - } - + // 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/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index c1b12dbd06e6..414823646b98 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, @@ -1262,8 +1262,7 @@ async function kit({ svelte_config }) { client_manifest, bundle, client_chunks, - svelte_config.kit.output, - static_exports + svelte_config.kit.output ); // ...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 3809446f1285..6d4248032da5 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,11 +30,11 @@ 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, - // 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 {}; } @@ -194,33 +206,56 @@ 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); } /** - * @param {{ - * resolve: (file: string) => Promise>; - * static_exports?: Map | null, children: string[] }>; - * }} opts + * 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 */ -export function create_node_analyser({ resolve, static_exports = new Map() }) { +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; + } + + return page_options; + } catch { + return null; + } +} + +export function create_node_analyser() { + const static_exports = new Map(); + + /** + * @param {string | undefined} key + * @param {PageOptions | null} page_options + */ + const cache = (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 {Promise | null>} + * @returns {PageOptions | null} */ - const get_page_options = async (node) => { + const crawl = (node) => { const key = node.universal || node.server; if (key && static_exports.has(key)) { return { ...static_exports.get(key)?.page_options }; } - /** @type {Record} */ + /** @type {PageOptions} */ let page_options = {}; if (node.parent) { - const parent_options = await get_page_options(node.parent); + const parent_options = crawl(node.parent); const parent_key = node.parent.universal || node.parent.server; if (key && parent_key) { @@ -230,9 +265,7 @@ export function create_node_analyser({ resolve, 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(key, null); return null; } @@ -240,43 +273,29 @@ 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]; - } + const server_page_options = get_page_options(node.server); + if (server_page_options === null) { + cache(key, null); + return null; } + page_options = { ...page_options, ...server_page_options }; } if (node.universal) { - const input = read(node.universal); - const universal_page_options = statically_analyse_page_options(node.universal, input); - + const universal_page_options = get_page_options(node.universal); if (universal_page_options === null) { - static_exports.set(node.universal, { page_options: null, children: [] }); + cache(key, null); return null; } - page_options = { ...page_options, ...universal_page_options }; } - if (key) { - static_exports.set(key, { page_options, children: [] }); - } + cache(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: crawl }; } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6384201af551..57af755bb503 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,8 @@ 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; } export interface PrerenderDependency { @@ -278,6 +281,8 @@ export interface RouteData { endpoint: { file: string; + /** The final page options for the endpoint if it was statically analysable */ + page_options: PageOptions | null; } | null; } 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 b3611645dc44..20bfeb94a6c5 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,8 @@ 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; } type RecursiveRequired = { @@ -2509,6 +2512,8 @@ declare module '@sveltejs/kit' { endpoint: { file: string; + /** The final page options for the endpoint if it was statically analysable */ + page_options: PageOptions | null; } | null; } @@ -2752,6 +2757,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 {