Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-streets-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: `resolve` will narrow types to follow trailing slash page settings
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*'
Expand Down
17 changes: 2 additions & 15 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,8 @@ async function analyse({
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));

/** @type {Map<string, { page_options: Record<string, any> | 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 = {
Expand Down Expand Up @@ -188,7 +175,7 @@ async function analyse({
metadata.remotes.set(remote.hash, exports);
}

return { metadata, static_exports };
return { metadata };
}

/**
Expand Down
19 changes: 18 additions & 1 deletion packages/kit/src/core/sync/create_manifest_data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
};
}
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 14 additions & 8 deletions packages/kit/src/core/sync/create_manifest_data/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
}
},
{
Expand Down Expand Up @@ -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
}
}
]);
Expand Down Expand Up @@ -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/?$/' },
{
Expand Down Expand Up @@ -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
}
}
]);
Expand All @@ -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
}
}
]);
Expand Down Expand Up @@ -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
}
},
{
Expand All @@ -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
}
},
{
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/core/sync/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
47 changes: 38 additions & 9 deletions packages/kit/src/core/sync/write_non_ambient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} */
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
Expand Down Expand Up @@ -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));
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/core/sync/write_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ function update_types(config, routes, route, to_delete = new Set()) {
exports.push('export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;');
}

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');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'always';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'always';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'always';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'ignore';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'ignore';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'ignore';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'always';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'never';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'never';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const trailingSlash = 'never';
64 changes: 64 additions & 0 deletions packages/kit/src/core/sync/write_types/test/app-types/+page.js
Original file line number Diff line number Diff line change
@@ -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/';
39 changes: 0 additions & 39 deletions packages/kit/src/core/sync/write_types/test/app-types/+page.ts

This file was deleted.

Loading
Loading