diff --git a/.changeset/many-forks-sniff.md b/.changeset/many-forks-sniff.md
new file mode 100644
index 000000000000..190723e5f584
--- /dev/null
+++ b/.changeset/many-forks-sniff.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] implement new layout system (see the PR for migration instructions)
diff --git a/documentation/docs/04-advanced-routing.md b/documentation/docs/04-advanced-routing.md
index 8c82a8be0ec2..11eca78bac83 100644
--- a/documentation/docs/04-advanced-routing.md
+++ b/documentation/docs/04-advanced-routing.md
@@ -126,75 +126,95 @@ assert.equal(
To express a `%` character, use `%25`, otherwise the result will be malformed.
-### Named layouts
+### Advanced layouts
-Some parts of your app might need something other than the default layout. For these cases you can create _named layouts_...
+By default, the _layout hierarchy_ mirrors the _route hierarchy_. In some cases, that might not be what you want.
-```svelte
-/// file: src/routes/+layout-foo.svelte
-
-
-
-```
+#### (group)
-...and then use them by referencing the layout name (`foo`, in the example above) in the filename:
+Perhaps you have some routes that are 'app' routes that should have one layout (e.g. `/dashboard` or `/item`), and others that are 'marketing' routes that should have a different layout (`/blog` or `/testimonials`). We can group these routes with a directory whose name is wrapped in parentheses — unlike normal directories, `(app)` and `(marketing)` do not affect the URL pathname of the routes inside them:
-```svelte
-/// file: src/routes/my-special-page/+page@foo.svelte
-I am inside +layout-foo
+```diff
+src/routes/
++│ (app)/
+│ ├ dashboard/
+│ ├ item/
+│ └ +layout.svelte
++│ (marketing)/
+│ ├ about/
+│ ├ testimonials/
+│ └ +layout.svelte
+├ admin/
+└ +layout.svelte
```
-> Named layout should only be referenced from Svelte files
-
-Named layouts are very powerful, but it can take a minute to get your head round them. Don't worry if this doesn't make sense all at once.
+You can also put a `+page` directly inside a `(group)`, for example if `/` should be an `(app)` or a `(marketing)` page.
-#### Scoping
+#### +page@
-Named layouts can be created at any depth, and will apply to any components in the same subtree. For example, `+layout-foo` will apply to `/x/one` and `/x/two`, but not `/x/three` or `/four`:
+Conversely, some routes of your app might need to break out of the layout hierarchy. Let's add an `/item/[id]/embed` route inside the `(app)` group from the previous example:
-```bash
+```diff
src/routes/
-├ x/
-│ ├ +layout-foo.svelte
-│ ├ one/+page@foo.svelte # ✅ page has `@foo`
-│ ├ two/+page@foo.svelte # ✅ page has `@foo`
-│ └ three/+page.svelte # ❌ page does not have `@foo`
-└ four/+page@foo.svelte # ❌ page has `@foo`, but +layout-foo is not 'in scope'
+├ (app)/
+│ ├ item/
+│ │ ├ [id]/
+│ │ │ ├ embed/
++│ │ │ │ └ +page.svelte
+│ │ │ └ +layout.svelte
+│ │ └ +layout.svelte
+│ └ +layout.svelte
+└ +layout.svelte
```
-#### Inheritance chains
-
-Layouts can themselves choose to inherit from named layouts, from the same directory or a parent directory. For example, `x/y/+layout@root.svelte` is the default layout for `/x/y` (meaning `/x/y/one`, `/x/y/two` and `/x/y/three` all inherit from it) because it has no name. Because it specifies `@root`, it will inherit directly from the nearest `+layout-root.svelte`, skipping `+layout.svelte` and `x/+layout.svelte`.
+Ordinarily, this would inherit the root layout, the `(app)` layout, the `item` layout and the `[id]` layout. We can reset to one of those layouts by appending `@` followed by the segment name — or, for the root layout, the empty string. In this example, we can choose from `+page@.svelte`, `+page@(app).svelte`, `+page@item.svelte` or `+page@[id].svelte`:
-```
+```diff
src/routes/
-├ x/
-│ ├ y/
-│ │ ├ +layout@root.svelte
-│ │ ├ one/+page.svelte
-│ │ ├ two/+page.svelte
-│ │ └ three/+page.svelte
+├ (app)/
+│ ├ item/
+│ │ ├ [id]/
+│ │ │ ├ embed/
++│ │ │ │ └ +page@(app).svelte
+│ │ │ └ +layout.svelte
+│ │ └ +layout.svelte
│ └ +layout.svelte
-├ +layout.svelte
-└ +layout-root.svelte
+└ +layout.svelte
```
-> In the case where `+layout-root.svelte` contains a lone ``, this effectively means we're able to 'reset' to a blank layout for any page or nested layout in the app by adding `@root`.
+#### +layout@
-If no parent is specified, a layout will inherit from the nearest default (i.e. unnamed) layout _above_ it in the tree. In some cases, it's helpful for a named layout to inherit from a default layout _alongside_ it in the tree, such as `+layout-root.svelte` inheriting from `+layout.svelte`. We can do this by explicitly specifying `@default`, allowing `/x/y/one` and siblings to use the app's default layout without using `x/+layout.svelte`:
+Like pages, layouts can _themselves_ break out of their parent layout hierarchy, using the same technique. For example, a `+layout@.svelte` component would reset the hierarchy for all its child routes.
-```diff
-src/routes/
-├ x/
-│ ├ y/
-│ │ ├ +layout@root.svelte
-│ │ ├ one/+page.svelte
-│ │ ├ two/+page.svelte
-│ │ └ three/+page.svelte
-│ └ +layout.svelte
-├ +layout.svelte
--└ +layout-root.svelte
-+└ +layout-root@default.svelte
+#### When to use layout groups
+
+Not all use cases are suited for layout grouping, nor should you feel compelled to use them. It might be that your use case would result in complex `(group)` nesting, or that you don't want to introduce a `(group)` for a single outlier. It's perfectly fine to use other means such as composition (reusable `load` functions or Svelte components) or if-statements to achieve what you want. The following example shows a layout that rewinds to the root layout and reuses components and functions that other layouts can also use:
+
+```svelte
+/// file: src/routes/nested/route/+layout@.svelte
+
+
+
+
+
```
-> `default` is a reserved name — in other words, you can't have a `+layout-default.svelte` file.
+```js
+/// file: src/routes/nested/route/+layout.js
+// @filename: ambient.d.ts
+declare module "$lib/reusable-load-function" {
+ export function reusableLoad(event: import('@sveltejs/kit').LoadEvent): Promise>;
+}
+// @filename: index.js
+// ---cut---
+import { reusableLoad } from '$lib/reusable-load-function';
+
+/** @type {import('./$types').PageLoad} */
+export function load(event) {
+ // Add additional logic here, if needed
+ return reusableLoad(event);
+}
+```
diff --git a/packages/kit/package.json b/packages/kit/package.json
index e67591d42e13..c641a32b4aef 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -59,7 +59,6 @@
],
"scripts": {
"build": "npm run types",
- "dev": "rollup -cw",
"lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
"check": "tsc",
"check:all": "tsc && pnpm -r --filter=\"./**\" check",
diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js
index f5d3a6451020..03b7bb68a3c3 100644
--- a/packages/kit/src/core/adapt/builder.js
+++ b/packages/kit/src/core/adapt/builder.js
@@ -23,7 +23,7 @@ export function create_builder({ config, build_data, prerendered, log }) {
/** @param {import('types').RouteData} route */
// TODO routes should come pre-filtered
function not_prerendered(route) {
- const path = route.type === 'page' && !route.id.includes('[') && `/${route.id}`;
+ const path = route.page && !route.id.includes('[') && `/${route.id}`;
if (path) {
return !prerendered_paths.has(path) && !prerendered_paths.has(path + '/');
}
@@ -68,17 +68,31 @@ export function create_builder({ config, build_data, prerendered, log }) {
const { routes } = build_data.manifest_data;
/** @type {import('types').RouteDefinition[]} */
- const facades = routes.map((route) => ({
- id: route.id,
- type: route.type,
- segments: route.id.split('/').map((segment) => ({
- dynamic: segment.includes('['),
- rest: segment.includes('[...'),
- content: segment
- })),
- pattern: route.pattern,
- methods: route.type === 'page' ? ['GET'] : build_data.server.methods[route.file]
- }));
+ const facades = routes.map((route) => {
+ const methods = new Set();
+
+ if (route.page) {
+ methods.add('SET');
+ }
+
+ if (route.endpoint) {
+ for (const method of build_data.server.methods[route.endpoint.file]) {
+ methods.add(method);
+ }
+ }
+
+ return {
+ id: route.id,
+ type: route.page ? 'page' : 'endpoint', // TODO change this if support pages+endpoints
+ segments: route.id.split('/').map((segment) => ({
+ dynamic: segment.includes('['),
+ rest: segment.includes('[...'),
+ content: segment
+ })),
+ pattern: route.pattern,
+ methods: Array.from(methods)
+ };
+ });
const seen = new Set();
@@ -102,8 +116,9 @@ export function create_builder({ config, build_data, prerendered, log }) {
// heuristic: if /foo/[bar] is included, /foo/[bar].json should
// also be included, since the page likely needs the endpoint
+ // TODO is this still necessary, given the new way of doing things?
filtered.forEach((route) => {
- if (route.type === 'page') {
+ if (route.page) {
const endpoint = routes.find((candidate) => candidate.id === route.id + '.json');
if (endpoint) {
diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js
index 946043484bb9..8f4529cb93aa 100644
--- a/packages/kit/src/core/generate_manifest/index.js
+++ b/packages/kit/src/core/generate_manifest/index.js
@@ -1,5 +1,4 @@
import { s } from '../../utils/misc.js';
-import { parse_route_id } from '../../utils/routing.js';
import { get_mime_lookup } from '../utils.js';
/**
@@ -38,12 +37,11 @@ export function generate_manifest({ build_data, relative_path, routes, format =
assets.push(build_data.service_worker);
}
- /** @param {import('types').PageNode | undefined} id */
- const get_index = (id) => id && /** @type {LookupEntry} */ (bundled_nodes.get(id)).index;
-
const matchers = new Set();
// prettier-ignore
+ // String representation of
+ /** @type {import('types').SSRManifest} */
return `{
appDir: ${s(build_data.app_dir)},
assets: new Set(${s(assets)}),
@@ -55,39 +53,20 @@ export function generate_manifest({ build_data, relative_path, routes, format =
],
routes: [
${routes.map(route => {
- const { pattern, names, types } = parse_route_id(route.id);
-
- types.forEach(type => {
+ route.types.forEach(type => {
if (type) matchers.add(type);
});
- if (route.type === 'page') {
- return `{
- type: 'page',
- id: ${s(route.id)},
- pattern: ${pattern},
- names: ${s(names)},
- types: ${s(types)},
- errors: ${s(route.errors.map(get_index))},
- layouts: ${s(route.layouts.map(get_index))},
- leaf: ${s(get_index(route.leaf))}
- }`.replace(/^\t\t/gm, '');
- } else {
- if (!build_data.server.vite_manifest[route.file]) {
- // this is necessary in cases where a .css file snuck in —
- // perhaps it would be better to disallow these (and others?)
- return null;
- }
+ if (!route.page && !route.endpoint) return;
- return `{
- type: 'endpoint',
- id: ${s(route.id)},
- pattern: ${pattern},
- names: ${s(names)},
- types: ${s(types)},
- load: ${loader(`${relative_path}/${build_data.server.vite_manifest[route.file].file}`)}
- }`.replace(/^\t\t/gm, '');
- }
+ return `{
+ id: ${s(route.id)},
+ pattern: ${route.pattern},
+ names: ${s(route.names)},
+ types: ${s(route.types)},
+ page: ${s(route.page)},
+ endpoint: ${route.endpoint ? loader(`${relative_path}/${build_data.server.vite_manifest[route.endpoint.file].file}`) : 'null'}
+ }`;
}).filter(Boolean).join(',\n\t\t\t\t')}
],
matchers: async () => {
diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js
index 93d535efdd6c..a2cfbd79c118 100644
--- a/packages/kit/src/core/prerender/prerender.js
+++ b/packages/kit/src/core/prerender/prerender.js
@@ -343,7 +343,7 @@ export async function prerender() {
/** @type {import('types').ManifestData} */
const { routes } = (await import(pathToFileURL(manifest_path).href)).manifest._;
const entries = routes
- .map((route) => (route.type === 'page' && !route.id.includes('[') ? `/${route.id}` : ''))
+ .map((route) => (route.page && !route.id.includes('[') ? `/${route.id}` : ''))
.filter(Boolean);
for (const entry of entries) {
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 ce8944511ce1..57e4b6ec630d 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -3,9 +3,7 @@ import path from 'path';
import mime from 'mime';
import { runtime_directory } from '../../utils.js';
import { posixify } from '../../../utils/filesystem.js';
-import { parse_route_id } from '../../../utils/routing.js';
-
-const DEFAULT = 'default';
+import { parse_route_id, is_no_group } from '../../../utils/routing.js';
/**
* @param {{
@@ -20,261 +18,296 @@ export default function create_manifest_data({
fallback = `${runtime_directory}/components`,
cwd = process.cwd()
}) {
- /** @type {Map} */
- const route_map = new Map();
+ const assets = create_assets(config);
+ const matchers = create_matchers(config, cwd);
+ const { nodes, routes } = create_routes_and_nodes(cwd, config, fallback);
- /** @type {Map} */
- const segment_map = new Map();
+ return {
+ assets,
+ matchers,
+ nodes,
+ routes
+ };
+}
- /** @type {import('./types').RouteTree} */
- const tree = new Map();
+/**
+ * @param {import('types').ValidatedConfig} config
+ */
+function create_assets(config) {
+ return list_files(config.kit.files.assets).map((file) => ({
+ file,
+ size: fs.statSync(path.resolve(config.kit.files.assets, file)).size,
+ type: mime.getType(file)
+ }));
+}
- const default_layout = {
- component: posixify(path.relative(cwd, `${fallback}/layout.svelte`))
- };
+/**
+ * @param {import('types').ValidatedConfig} config
+ * @param {string} cwd
+ */
+function create_matchers(config, cwd) {
+ const params_base = path.relative(cwd, config.kit.files.params);
- // set default root layout/error
- tree.set('', {
- error: {
- component: posixify(path.relative(cwd, `${fallback}/error.svelte`))
- },
- layouts: { [DEFAULT]: default_layout }
- });
-
- /** @param {string} id */
- function tree_node(id) {
- if (!tree.has(id)) {
- tree.set(id, {
- error: undefined,
- layouts: {}
- });
- }
+ /** @type {Record} */
+ const matchers = {};
+ if (fs.existsSync(config.kit.files.params)) {
+ for (const file of fs.readdirSync(config.kit.files.params)) {
+ const ext = path.extname(file);
+ if (!config.kit.moduleExtensions.includes(ext)) continue;
+ const type = file.slice(0, -ext.length);
- return /** @type {import('./types').RouteTreeNode} */ (tree.get(id));
- }
+ if (/^\w+$/.test(type)) {
+ const matcher_file = path.join(params_base, file);
- const routes_base = posixify(path.relative(cwd, config.kit.files.routes));
- const valid_extensions = [...config.extensions, ...config.kit.moduleExtensions];
+ // Disallow same matcher with different extensions
+ if (matchers[type]) {
+ throw new Error(`Duplicate matchers: ${matcher_file} and ${matchers[type]}`);
+ } else {
+ matchers[type] = matcher_file;
+ }
+ } else {
+ throw new Error(
+ `Matcher names can only have underscores and alphanumeric characters — "${file}" is invalid`
+ );
+ }
+ }
+ }
- if (fs.existsSync(config.kit.files.routes)) {
- list_files(config.kit.files.routes).forEach((filepath) => {
- const extension = valid_extensions.find((ext) => filepath.endsWith(ext));
- if (!extension) return;
+ return matchers;
+}
- const project_relative = `${routes_base}/${filepath}`;
- const segments = filepath.split('/');
- const file = /** @type {string} */ (segments.pop());
+/**
+ * @param {import('types').ValidatedConfig} config
+ * @param {string} cwd
+ * @param {string} fallback
+ */
+function create_routes_and_nodes(cwd, config, fallback) {
+ const route_map = new Map();
- if (file[0] !== '+') return; // not a route file
+ /** @type {Map} */
+ const segment_map = new Map();
- const item = analyze(project_relative, file, config.extensions, config.kit.moduleExtensions);
- const id = segments.join('/');
+ const routes_base = posixify(path.relative(cwd, config.kit.files.routes));
- if (/\]\[/.test(id)) {
- throw new Error(`Invalid route ${project_relative} — parameters must be separated`);
- }
+ const valid_extensions = [...config.extensions, ...config.kit.moduleExtensions];
- if (count_occurrences('[', id) !== count_occurrences(']', id)) {
- throw new Error(`Invalid route ${project_relative} — brackets are unbalanced`);
- }
+ /** @type {import('types').PageNode[]} */
+ const nodes = [];
- // error/layout files should be added to the tree, but don't result
- // in a route being created, so deal with them first. note: we are
- // relying on the fact that the +error and +layout files precede
- // +page files alphabetically, and will therefore be processes
- // before we reach the page
- if (item.kind === 'component' && item.is_error) {
- tree_node(id).error = {
- component: project_relative
- };
-
- return;
- }
+ if (fs.existsSync(config.kit.files.routes)) {
+ /**
+ * @param {number} depth
+ * @param {string} id
+ * @param {string} segment
+ * @param {import('types').RouteData | null} parent
+ */
+ const walk = (depth, id, segment, parent) => {
+ const { pattern, names, types } = parse_route_id(id);
+
+ const segments = id.split('/');
+
+ segment_map.set(
+ id,
+ segments.filter(Boolean).map((segment) => {
+ /** @type {import('./types').Part[]} */
+ const parts = [];
+ segment.split(/\[(.+?)\]/).map((content, i) => {
+ const dynamic = !!(i % 2);
+
+ if (!content) return;
+
+ parts.push({
+ content,
+ dynamic,
+ rest: dynamic && content.startsWith('...'),
+ type: (dynamic && content.split('=')[1]) || null
+ });
+ });
+ return parts;
+ })
+ );
- if (item.is_layout) {
- if (item.declares_layout === DEFAULT) {
- throw new Error(`${project_relative} cannot use reserved "${DEFAULT}" name`);
- }
+ /** @type {import('types').RouteData} */
+ const route = {
+ id,
+ parent,
- const layout_id = item.declares_layout || DEFAULT;
+ segment,
+ pattern,
+ names,
+ types,
- const group = tree_node(id);
+ layout: null,
+ error: null,
+ leaf: null,
+ page: null,
+ endpoint: null
+ };
- const defined = group.layouts[layout_id] || (group.layouts[layout_id] = {});
+ // important to do this before walking children, so that child
+ // routes appear later
+ route_map.set(id, route);
- if (defined[item.kind] && layout_id !== DEFAULT) {
- // edge case
- throw new Error(
- `Duplicate layout ${project_relative} already defined at ${defined[item.kind]}`
- );
- }
+ // if we don't do this, the route map becomes unwieldy to console.log
+ Object.defineProperty(route, 'parent', { enumerable: false });
- defined[item.kind] = project_relative;
+ const dir = path.join(cwd, routes_base, id);
- return;
- }
+ const files = fs.readdirSync(dir, {
+ withFileTypes: true
+ });
- const type = item.kind === 'server' && !item.is_layout && !item.is_page ? 'endpoint' : 'page';
+ // process files first
+ for (const file of files) {
+ if (file.isDirectory()) continue;
+ if (!file.name.startsWith('+')) continue;
+ if (!valid_extensions.find((ext) => file.name.endsWith(ext))) continue;
- if (type === 'endpoint' && route_map.has(id)) {
- // note that we are relying on +server being lexically ordered after
- // all other route files — if we added +view or something this is
- // potentially brittle, since the server might be added before
- // another route file. a problem for another day
- throw new Error(
- `${file} cannot share a directory with other route files (${project_relative})`
- );
- }
+ const project_relative = posixify(path.relative(cwd, path.join(dir, file.name)));
- if (!route_map.has(id)) {
- const pattern = parse_route_id(id).pattern;
-
- segment_map.set(
- id,
- segments.filter(Boolean).map((segment) => {
- /** @type {import('./types').Part[]} */
- const parts = [];
- segment.split(/\[(.+?)\]/).map((content, i) => {
- const dynamic = !!(i % 2);
-
- if (!content) return;
-
- parts.push({
- content,
- dynamic,
- rest: dynamic && content.startsWith('...'),
- type: (dynamic && content.split('=')[1]) || null
- });
- });
- return parts;
- })
+ const item = analyze(
+ project_relative,
+ file.name,
+ config.extensions,
+ config.kit.moduleExtensions
);
- if (type === 'endpoint') {
- route_map.set(id, {
- type,
- id,
- pattern,
- file: project_relative
- });
+ if (item.kind === 'component') {
+ if (item.is_error) {
+ route.error = {
+ depth,
+ component: project_relative
+ };
+ } else if (item.is_layout) {
+ if (!route.layout) route.layout = { depth };
+ route.layout.component = project_relative;
+ if (item.uses_layout !== undefined) route.layout.parent_id = item.uses_layout;
+ } else {
+ if (!route.leaf) route.leaf = { depth };
+ route.leaf.component = project_relative;
+ if (item.uses_layout !== undefined) route.leaf.parent_id = item.uses_layout;
+ }
+ } else if (item.is_layout) {
+ if (!route.layout) route.layout = { depth };
+ route.layout[item.kind] = project_relative;
+ } else if (item.is_page) {
+ if (!route.leaf) route.leaf = { depth };
+ route.leaf[item.kind] = project_relative;
} else {
- route_map.set(id, {
- type,
- id,
- pattern,
- errors: [],
- layouts: [],
- leaf: {}
- });
+ route.endpoint = {
+ file: project_relative
+ };
}
}
- if (item.is_page) {
- const route = /** @type {import('types').PageData} */ (route_map.get(id));
-
- // This ensures that layouts and errors are set for pages that have no Svelte file
- // and only redirect or throw an error, but are set to the Svelte file definition if it exists.
- // This ensures the proper error page is used and rendered in the proper layout.
- if (item.kind === 'component' || route.layouts.length === 0) {
- const { layouts, errors } = trace(
- tree,
- id,
- item.kind === 'component' ? item.uses_layout : undefined,
- project_relative
- );
- route.layouts = layouts;
- route.errors = errors;
- }
-
- if (item.kind === 'component') {
- route.leaf.component = project_relative;
- } else if (item.kind === 'server') {
- route.leaf.server = project_relative;
- } else {
- route.leaf.shared = project_relative;
+ // then handle children
+ for (const file of files) {
+ if (file.isDirectory()) {
+ walk(depth + 1, path.posix.join(id, file.name), file.name, route);
}
}
- });
+ };
- // TODO remove for 1.0
- if (route_map.size === 0) {
- throw new Error(
- 'The filesystem router API has changed, see https://github.com/sveltejs/kit/discussions/5774 for details'
- );
- }
- }
+ walk(0, '', '', null);
- /** @type {import('types').PageNode[]} */
- const nodes = [];
+ const root = /** @type {import('types').RouteData} */ (route_map.get(''));
- tree.forEach(({ layouts, error }) => {
- // we do [default, error, ...other_layouts] so that components[0] and [1]
- // are the root layout/error. kinda janky, there's probably a nicer way
- if (layouts[DEFAULT]) {
- nodes.push(layouts[DEFAULT]);
+ // TODO remove for 1.0
+ if (route_map.size === 1) {
+ if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
+ throw new Error(
+ 'The filesystem router API has changed, see https://github.com/sveltejs/kit/discussions/5774 for details'
+ );
+ }
}
- if (error) {
- nodes.push(error);
+ if (!root.layout?.component) {
+ if (!root.layout) root.layout = { depth: 0 };
+ root.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
}
- for (const id in layouts) {
- if (id !== DEFAULT) {
- nodes.push(layouts[id]);
- }
+ if (!root.error?.component) {
+ if (!root.error) root.error = { depth: 0 };
+ root.error.component = posixify(path.relative(cwd, `${fallback}/error.svelte`));
}
- });
- route_map.forEach((route) => {
- if (route.type === 'page') {
+ // we do layouts/errors first as they are more likely to be reused,
+ // and smaller indexes take fewer bytes. also, this guarantees that
+ // the default error/layout are 0/1
+ route_map.forEach((route) => {
+ if (route.layout) nodes.push(route.layout);
+ if (route.error) nodes.push(route.error);
+ });
+
+ /** @type {Map} */
+ const conflicts = new Map();
+
+ route_map.forEach((route) => {
+ if (!route.leaf) return;
+
nodes.push(route.leaf);
- }
- });
- const routes = Array.from(route_map.values()).sort((a, b) => compare(a, b, segment_map));
+ const normalized = route.id.split('/').filter(is_no_group).join('/');
- /** @type {import('types').Asset[]} */
- const assets = fs.existsSync(config.kit.files.assets)
- ? list_files(config.kit.files.assets).map((file) => ({
- file,
- size: fs.statSync(`${config.kit.files.assets}/${file}`).size,
- type: mime.getType(file)
- }))
- : [];
+ if (conflicts.has(normalized)) {
+ throw new Error(`${conflicts.get(normalized)} and ${route.id} occupy the same route`);
+ }
- const params_base = path.relative(cwd, config.kit.files.params);
+ conflicts.set(normalized, route.id);
+ });
- /** @type {Record} */
- const matchers = {};
- if (fs.existsSync(config.kit.files.params)) {
- for (const file of fs.readdirSync(config.kit.files.params)) {
- const ext = path.extname(file);
- if (!config.kit.moduleExtensions.includes(ext)) continue;
- const type = file.slice(0, -ext.length);
+ const indexes = new Map(nodes.map((node, i) => [node, i]));
- if (/^\w+$/.test(type)) {
- const matcher_file = path.join(params_base, file);
+ route_map.forEach((route) => {
+ if (!route.leaf) return;
- // Disallow same matcher with different extensions
- if (matchers[type]) {
- throw new Error(`Duplicate matchers: ${matcher_file} and ${matchers[type]}`);
- } else {
- matchers[type] = matcher_file;
+ if (route.leaf && route.endpoint) {
+ // TODO possibly relax this https://github.com/sveltejs/kit/issues/5896
+ throw new Error(`${route.endpoint.file} cannot share a directory with other route files`);
+ }
+
+ route.page = {
+ layouts: [],
+ errors: [],
+ leaf: /** @type {number} */ (indexes.get(route.leaf))
+ };
+
+ /** @type {import('types').RouteData | null} */
+ let current_route = route;
+ let current_node = route.leaf;
+ let parent_id = route.leaf.parent_id;
+
+ while (current_route) {
+ if (parent_id === undefined || current_route.segment === parent_id) {
+ if (current_route.layout || current_route.error) {
+ route.page.layouts.unshift(
+ current_route.layout ? indexes.get(current_route.layout) : undefined
+ );
+ route.page.errors.unshift(
+ current_route.error ? indexes.get(current_route.error) : undefined
+ );
+ }
+
+ if (current_route.layout) {
+ current_node.parent = current_node = current_route.layout;
+ parent_id = current_node.parent_id;
+ } else {
+ parent_id = undefined;
+ }
}
- } else {
- throw new Error(
- `Matcher names can only have underscores and alphanumeric characters — "${file}" is invalid`
- );
+
+ current_route = current_route.parent;
}
- }
+
+ if (parent_id !== undefined) {
+ throw new Error(`${current_node.component} references missing segment "${parent_id}"`);
+ }
+ });
}
- return {
- assets,
- nodes,
- routes,
- matchers
- };
+ const routes = Array.from(route_map.values()).sort((a, b) => compare(a, b, segment_map));
+
+ return { nodes, routes };
}
/**
@@ -288,10 +321,16 @@ function analyze(project_relative, file, component_extensions, module_extensions
const component_extension = component_extensions.find((ext) => file.endsWith(ext));
if (component_extension) {
const name = file.slice(0, -component_extension.length);
- const pattern =
- /^\+(?:(page(?:@([a-zA-Z0-9_-]+))?)|(layout(?:-([a-zA-Z0-9_-]+))?(?:@([a-zA-Z0-9_-]+))?)|(error))$/;
+ const pattern = /^\+(?:(page(?:@([a-zA-Z0-9_-]*))?)|(layout(?:@([a-zA-Z0-9_-]*))?)|(error))$/;
const match = pattern.exec(name);
if (!match) {
+ // TODO remove for 1.0
+ if (/^\+layout-/.test(name)) {
+ throw new Error(
+ `${project_relative} should be reimplemented with layout groups: https://kit.svelte.dev/docs/advanced-routing#advanced-layouts`
+ );
+ }
+
throw new Error(`Files prefixed with + are reserved (saw ${project_relative})`);
}
@@ -299,9 +338,8 @@ function analyze(project_relative, file, component_extensions, module_extensions
kind: 'component',
is_page: !!match[1],
is_layout: !!match[3],
- is_error: !!match[6],
- uses_layout: match[2] || match[5],
- declares_layout: match[4]
+ is_error: !!match[5],
+ uses_layout: match[2] ?? match[4]
};
}
@@ -309,96 +347,29 @@ function analyze(project_relative, file, component_extensions, module_extensions
if (module_extension) {
const name = file.slice(0, -module_extension.length);
const pattern =
- /^\+(?:(server)|(page(?:@([a-zA-Z0-9_-]+))?(\.server)?)|(layout(?:-([a-zA-Z0-9_-]+))?(?:@([a-zA-Z0-9_-]+))?(\.server)?))$/;
+ /^\+(?:(server)|(page(?:(@[a-zA-Z0-9_-]*))?(\.server)?)|(layout(?:(@[a-zA-Z0-9_-]*))?(\.server)?))$/;
const match = pattern.exec(name);
if (!match) {
throw new Error(`Files prefixed with + are reserved (saw ${project_relative})`);
- } else if (match[3] || match[7]) {
+ } else if (match[3] || match[6]) {
throw new Error(
// prettier-ignore
- `Only Svelte files can reference named layouts. Remove '@${match[3] || match[7]}' from ${file} (at ${project_relative})`
+ `Only Svelte files can reference named layouts. Remove '${match[3] || match[6]}' from ${file} (at ${project_relative})`
);
}
- const kind = !!(match[1] || match[4] || match[8]) ? 'server' : 'shared';
+ const kind = !!(match[1] || match[4] || match[7]) ? 'server' : 'shared';
return {
kind,
is_page: !!match[2],
- is_layout: !!match[5],
- declares_layout: match[6]
+ is_layout: !!match[5]
};
}
throw new Error(`Files and directories prefixed with + are reserved (saw ${project_relative})`);
}
-/**
- * @param {import('./types').RouteTree} tree
- * @param {string} id
- * @param {string} layout_id
- * @param {string} project_relative
- */
-function trace(tree, id, layout_id = DEFAULT, project_relative) {
- /** @type {Array} */
- const layouts = [];
-
- /** @type {Array} */
- const errors = [];
-
- const parts = id.split('/').filter(Boolean);
-
- // walk up the tree, find which +layout and +error components
- // apply to this page
- while (true) {
- const node = tree.get(parts.join('/'));
- const layout = node?.layouts[layout_id];
-
- if (layout && layouts.indexOf(layout) > -1) {
- // TODO this needs to be fixed for #5748
- throw new Error(
- `Recursive layout detected: ${layout.component} -> ${layouts
- .map((l) => l?.component)
- .join(' -> ')}`
- );
- }
-
- // any segment that has neither a +layout nor an +error can be discarded.
- // in other words these...
- // layouts: [a, , b, c]
- // errors: [d, , e, ]
- //
- // ...can be compacted to these:
- // layouts: [a, b, c]
- // errors: [d, e, ]
- if (node?.error || layout) {
- errors.unshift(node?.error);
- layouts.unshift(layout);
- }
-
- const parent_layout_id = layout?.component?.split('/').at(-1)?.split('@')[1]?.split('.')[0];
-
- if (parent_layout_id) {
- layout_id = parent_layout_id;
- } else {
- if (layout) layout_id = DEFAULT;
- if (parts.length === 0) break;
- parts.pop();
- }
- }
-
- if (layout_id !== DEFAULT) {
- throw new Error(`${project_relative} references missing layout "${layout_id}"`);
- }
-
- // trim empty space off the end of the errors array
- let i = errors.length;
- while (i--) if (errors[i]) break;
- errors.length = i + 1;
-
- return { layouts, errors };
-}
-
/**
* @param {import('types').RouteData} a
* @param {import('types').RouteData} b
@@ -445,62 +416,31 @@ function compare(a, b, segment_map) {
}
}
- const a_is_endpoint = a.type === 'endpoint';
- const b_is_endpoint = b.type === 'endpoint';
-
- if (a_is_endpoint !== b_is_endpoint) {
- return a_is_endpoint ? -1 : +1;
+ if (!!a.endpoint !== !!b.endpoint) {
+ return a.endpoint ? -1 : +1;
}
return a < b ? -1 : 1;
}
-/**
- * @param {string} needle
- * @param {string} haystack
- */
-function count_occurrences(needle, haystack) {
- let count = 0;
- for (let i = 0; i < haystack.length; i += 1) {
- if (haystack[i] === needle) count += 1;
- }
- return count;
-}
-
-/**
- * @param {string} dir
- * @param {string} [path]
- * @param {string[]} [files]
- */
-function list_files(dir, path = '', files = []) {
- fs.readdirSync(dir)
- .sort((a, b) => {
- // sort each directory in (+layout, +error, everything else) order
- // so that we can trace layouts/errors immediately
-
- if (a.startsWith('+layout') || a.startsWith('+error')) {
- if (!b.startsWith('+layout') && !b.startsWith('+error')) return -1;
- } else if (b.startsWith('+layout') || b.startsWith('+error')) {
- return 1;
- } else if (a.startsWith('__')) {
- if (!b.startsWith('__')) return -1;
- } else if (b.startsWith('__')) {
- return 1;
- }
-
- return a < b ? -1 : 1;
- })
- .forEach((file) => {
- const full = `${dir}/${file}`;
- const stats = fs.statSync(full);
- const joined = path ? `${path}/${file}` : file;
-
- if (stats.isDirectory()) {
- list_files(full, joined, files);
+/** @param {string} dir */
+function list_files(dir) {
+ /** @type {string[]} */
+ const files = [];
+
+ /** @param {string} current */
+ function walk(current) {
+ for (const file of fs.readdirSync(path.resolve(dir, current))) {
+ const child = path.posix.join(current, file);
+ if (fs.statSync(path.resolve(dir, child)).isDirectory()) {
+ walk(child);
} else {
- files.push(joined);
+ files.push(child);
}
- });
+ }
+ }
+
+ if (fs.existsSync(dir)) walk('');
return files;
}
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 5a5bc953ed16..f22ab207c262 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
@@ -34,65 +34,82 @@ const default_error = {
component: 'error.svelte'
};
+/** @param {import('types').PageNode} node */
+function simplify_node(node) {
+ /** @type {import('types').PageNode} */
+ const simplified = {};
+
+ if (node.component) simplified.component = node.component;
+ if (node.shared) simplified.shared = node.shared;
+ if (node.server) simplified.server = node.server;
+ if (node.parent_id !== undefined) simplified.parent_id = node.parent_id;
+
+ return simplified;
+}
+
+/** @param {import('types').RouteData} route */
+function simplify_route(route) {
+ /** @type {{ id: string, pattern: string, page?: import('types').PageNodeIndexes, endpoint?: { file: string } }} */
+ const simplified = {
+ id: route.id,
+ pattern: route.pattern.toString().replace(/\\\//g, '/').replace(/\\\./g, '.')
+ };
+
+ if (route.page) simplified.page = route.page;
+ if (route.endpoint) simplified.endpoint = route.endpoint;
+
+ return simplified;
+}
+
test('creates routes', () => {
const { nodes, routes } = create('samples/basic');
- const index = { component: 'samples/basic/+page.svelte' };
- const about = { component: 'samples/basic/about/+page.svelte' };
- const blog = { component: 'samples/basic/blog/+page.svelte' };
- const blog_$slug = { component: 'samples/basic/blog/[slug]/+page.svelte' };
-
- assert.equal(nodes, [default_layout, default_error, index, about, blog, blog_$slug]);
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ { component: 'samples/basic/+page.svelte' },
+ { component: 'samples/basic/about/+page.svelte' },
+ { component: 'samples/basic/blog/+page.svelte' },
+ { component: 'samples/basic/blog/[slug]/+page.svelte' }
+ ]);
- assert.equal(routes, [
+ assert.equal(routes.map(simplify_route), [
{
- type: 'page',
id: '',
- pattern: /^\/$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: index
+ pattern: '/^/$/',
+ page: { layouts: [0], errors: [1], leaf: 2 }
},
{
- type: 'endpoint',
id: 'blog.json',
- pattern: /^\/blog\.json$/,
- file: 'samples/basic/blog.json/+server.js'
+ pattern: '/^/blog.json$/',
+ endpoint: { file: 'samples/basic/blog.json/+server.js' }
},
{
- type: 'page',
id: 'about',
- pattern: /^\/about\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: about
+ pattern: '/^/about/?$/',
+ page: { layouts: [0], errors: [1], leaf: 3 }
},
{
- type: 'page',
id: 'blog',
- pattern: /^\/blog\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: blog
+ pattern: '/^/blog/?$/',
+ page: { layouts: [0], errors: [1], leaf: 4 }
},
{
- type: 'endpoint',
id: 'blog/[slug].json',
- pattern: /^\/blog\/([^/]+?)\.json$/,
- file: 'samples/basic/blog/[slug].json/+server.ts'
+ pattern: '/^/blog/([^/]+?).json$/',
+ endpoint: {
+ file: 'samples/basic/blog/[slug].json/+server.ts'
+ }
},
{
- type: 'page',
id: 'blog/[slug]',
- pattern: /^\/blog\/([^/]+?)\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: blog_$slug
+ pattern: '/^/blog/([^/]+?)/?$/',
+ page: { layouts: [0], errors: [1], leaf: 5 }
}
]);
});
@@ -106,28 +123,24 @@ const test_symlinks = symlink_survived_git ? test : test.skip;
test_symlinks('creates symlinked routes', () => {
const { nodes, routes } = create('samples/symlinks/routes');
- const index = { component: 'samples/symlinks/routes/index.svelte' };
- const symlinked_index = { component: 'samples/symlinks/routes/foo/index.svelte' };
-
- assert.equal(nodes, [default_layout, default_error, symlinked_index, index]);
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ { component: 'samples/symlinks/routes/foo/index.svelte' },
+ { component: 'samples/symlinks/routes/index.svelte' }
+ ]);
assert.equal(routes, [
{
- type: 'page',
id: '',
- pattern: /^\/$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: index
+ pattern: '/^/$/',
+ page: { layouts: [0], errors: [1], leaf: 1 }
},
{
- type: 'page',
id: 'foo',
- pattern: /^\/foo\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: symlinked_index
+ pattern: '/^/foo/?$/',
+ page: { layouts: [0], errors: [1], leaf: 2 }
}
]);
});
@@ -135,40 +148,32 @@ test_symlinks('creates symlinked routes', () => {
test('creates routes with layout', () => {
const { nodes, routes } = create('samples/basic-layout');
- const layout = { component: 'samples/basic-layout/+layout.svelte' };
- const index = { component: 'samples/basic-layout/+page.svelte' };
- const foo___layout = { component: 'samples/basic-layout/foo/+layout.svelte' };
- const foo = { component: 'samples/basic-layout/foo/+page.svelte' };
+ assert.equal(nodes.map(simplify_node), [
+ { component: 'samples/basic-layout/+layout.svelte' },
+ default_error,
+ { component: 'samples/basic-layout/foo/+layout.svelte' },
+ { component: 'samples/basic-layout/+page.svelte' },
+ { component: 'samples/basic-layout/foo/+page.svelte' }
+ ]);
- assert.equal(nodes, [layout, default_error, foo___layout, index, foo]);
+ assert.equal(routes.map(simplify_route), [
+ {
+ id: '',
+ pattern: '/^/$/',
+ page: { layouts: [0], errors: [1], leaf: 3 }
+ },
- assert.equal(
- routes.slice(1, 2),
- [
- {
- type: 'page',
- id: '',
- pattern: /^\/$/,
- errors: [default_error],
- layouts: [layout],
- leaf: index
- },
-
- {
- type: 'page',
- id: 'foo',
- pattern: /^\/foo\/?$/,
- errors: [default_error],
- layouts: [layout, foo___layout],
- leaf: foo
- }
- ].slice(1, 2)
- );
+ {
+ id: 'foo',
+ pattern: '/^/foo/?$/',
+ page: { layouts: [0, 2], errors: [1, undefined], leaf: 4 }
+ }
+ ]);
});
test('succeeds when routes does not exist', () => {
const { nodes, routes } = create('samples/basic/routes');
- assert.equal(nodes, [default_layout, default_error]);
+ assert.equal(nodes, []);
assert.equal(routes, []);
});
@@ -182,7 +187,7 @@ test('encodes invalid characters', () => {
const hash = { component: 'samples/encoding/%23/+page.svelte' };
// const question_mark = 'samples/encoding/?.svelte';
- assert.equal(nodes, [
+ assert.equal(nodes.map(simplify_node), [
default_layout,
default_error,
// quote,
@@ -193,6 +198,7 @@ test('encodes invalid characters', () => {
assert.equal(
routes.map((p) => p.pattern),
[
+ /^\/$/,
// /^\/%22\/?$/,
/^\/%23\/?$/
// /^\/%3F\/?$/
@@ -229,53 +235,74 @@ test('sorts routes correctly', () => {
});
test('sorts routes with rest correctly', () => {
- const { routes } = create('samples/rest');
+ const { nodes, routes } = create('samples/rest');
- assert.equal(routes, [
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ {
+ component: 'samples/rest/a/[...rest]/+page.svelte',
+ server: 'samples/rest/a/[...rest]/+page.server.js'
+ },
+ {
+ component: 'samples/rest/b/[...rest]/+page.svelte',
+ server: 'samples/rest/b/[...rest]/+page.server.ts'
+ }
+ ]);
+
+ assert.equal(routes.map(simplify_route), [
+ {
+ id: '',
+ pattern: '/^/$/'
+ },
+ {
+ id: 'a',
+ pattern: '/^/a/?$/'
+ },
+ {
+ id: 'b',
+ pattern: '/^/b/?$/'
+ },
{
- type: 'page',
id: 'a/[...rest]',
- pattern: /^\/a(?:\/(.*))?\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: {
- component: 'samples/rest/a/[...rest]/+page.svelte',
- server: 'samples/rest/a/[...rest]/+page.server.js'
- }
+ pattern: '/^/a(?:/(.*))?/?$/',
+ page: { layouts: [0], errors: [1], leaf: 2 }
},
{
- type: 'page',
id: 'b/[...rest]',
- pattern: /^\/b(?:\/(.*))?\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: {
- component: 'samples/rest/b/[...rest]/+page.svelte',
- server: 'samples/rest/b/[...rest]/+page.server.ts'
- }
+ pattern: '/^/b(?:/(.*))?/?$/',
+ page: { layouts: [0], errors: [1], leaf: 3 }
}
]);
});
test('allows rest parameters inside segments', () => {
- const { routes } = create('samples/rest-prefix-suffix');
+ const { nodes, routes } = create('samples/rest-prefix-suffix');
- assert.equal(routes, [
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ {
+ component: 'samples/rest-prefix-suffix/prefix-[...rest]/+page.svelte'
+ }
+ ]);
+
+ assert.equal(routes.map(simplify_route), [
+ {
+ id: '',
+ pattern: '/^/$/'
+ },
{
- type: 'page',
id: 'prefix-[...rest]',
- pattern: /^\/prefix-(.*?)\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: {
- component: 'samples/rest-prefix-suffix/prefix-[...rest]/+page.svelte'
- }
+ pattern: '/^/prefix-(.*?)/?$/',
+ page: { layouts: [0], errors: [1], leaf: 2 }
},
{
- type: 'endpoint',
id: '[...rest].json',
- pattern: /^\/(.*?)\.json$/,
- file: 'samples/rest-prefix-suffix/[...rest].json/+server.js'
+ pattern: '/^/(.*?).json$/',
+ endpoint: {
+ file: 'samples/rest-prefix-suffix/[...rest].json/+server.js'
+ }
}
]);
});
@@ -283,7 +310,7 @@ test('allows rest parameters inside segments', () => {
test('ignores files and directories with leading underscores', () => {
const { routes } = create('samples/hidden-underscore');
- assert.equal(routes.map((r) => r.type === 'endpoint' && r.file).filter(Boolean), [
+ assert.equal(routes.map((r) => r.endpoint?.file).filter(Boolean), [
'samples/hidden-underscore/e/f/g/h/+server.js'
]);
});
@@ -291,7 +318,7 @@ test('ignores files and directories with leading underscores', () => {
test('ignores files and directories with leading dots except .well-known', () => {
const { routes } = create('samples/hidden-dot');
- assert.equal(routes.map((r) => r.type === 'endpoint' && r.file).filter(Boolean), [
+ assert.equal(routes.map((r) => r.endpoint?.file).filter(Boolean), [
'samples/hidden-dot/.well-known/dnt-policy.txt/+server.js'
]);
});
@@ -299,34 +326,37 @@ test('ignores files and directories with leading dots except .well-known', () =>
test('allows multiple slugs', () => {
const { routes } = create('samples/multiple-slugs');
- assert.equal(
- routes.filter((route) => route.type === 'endpoint'),
- [
- {
- type: 'endpoint',
- id: '[file].[ext]',
- pattern: /^\/([^/]+?)\.([^/]+?)$/,
+ assert.equal(routes.filter((route) => route.endpoint).map(simplify_route), [
+ {
+ id: '[file].[ext]',
+ pattern: '/^/([^/]+?).([^/]+?)$/',
+ endpoint: {
file: 'samples/multiple-slugs/[file].[ext]/+server.js'
}
- ]
- );
+ }
+ ]);
});
test('fails if dynamic params are not separated', () => {
assert.throws(() => {
create('samples/invalid-params');
- }, /Invalid route samples\/invalid-params\/\[foo\]\[bar\]\/\+server\.js — parameters must be separated/);
+ }, /Invalid route \[foo\]\[bar\] — parameters must be separated/);
});
test('ignores things that look like lockfiles', () => {
const { routes } = create('samples/lockfiles');
- assert.equal(routes, [
+ assert.equal(routes.map(simplify_route), [
+ {
+ id: '',
+ pattern: '/^/$/'
+ },
{
- type: 'endpoint',
id: 'foo',
- file: 'samples/lockfiles/foo/+server.js',
- pattern: /^\/foo\/?$/
+ pattern: '/^/foo/?$/',
+ endpoint: {
+ file: 'samples/lockfiles/foo/+server.js'
+ }
}
]);
});
@@ -336,62 +366,54 @@ test('works with custom extensions', () => {
extensions: ['.jazz', '.beebop', '.funk', '.svelte']
});
- const index = { component: 'samples/custom-extension/+page.funk' };
- const about = { component: 'samples/custom-extension/about/+page.jazz' };
- const blog = { component: 'samples/custom-extension/blog/+page.svelte' };
- const blog_$slug = { component: 'samples/custom-extension/blog/[slug]/+page.beebop' };
-
- assert.equal(nodes, [default_layout, default_error, index, about, blog, blog_$slug]);
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ { component: 'samples/custom-extension/+page.funk' },
+ { component: 'samples/custom-extension/about/+page.jazz' },
+ { component: 'samples/custom-extension/blog/+page.svelte' },
+ { component: 'samples/custom-extension/blog/[slug]/+page.beebop' }
+ ]);
- assert.equal(routes, [
+ assert.equal(routes.map(simplify_route), [
{
- type: 'page',
id: '',
- pattern: /^\/$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: index
+ pattern: '/^/$/',
+ page: { layouts: [0], errors: [1], leaf: 2 }
},
{
- type: 'endpoint',
id: 'blog.json',
- pattern: /^\/blog\.json$/,
- file: 'samples/custom-extension/blog.json/+server.js'
+ pattern: '/^/blog.json$/',
+ endpoint: {
+ file: 'samples/custom-extension/blog.json/+server.js'
+ }
},
{
- type: 'page',
id: 'about',
- pattern: /^\/about\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: about
+ pattern: '/^/about/?$/',
+ page: { layouts: [0], errors: [1], leaf: 3 }
},
{
- type: 'page',
id: 'blog',
- pattern: /^\/blog\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: blog
+ pattern: '/^/blog/?$/',
+ page: { layouts: [0], errors: [1], leaf: 4 }
},
{
- type: 'endpoint',
id: 'blog/[slug].json',
- pattern: /^\/blog\/([^/]+?)\.json$/,
- file: 'samples/custom-extension/blog/[slug].json/+server.js'
+ pattern: '/^/blog/([^/]+?).json$/',
+ endpoint: {
+ file: 'samples/custom-extension/blog/[slug].json/+server.js'
+ }
},
{
- type: 'page',
id: 'blog/[slug]',
- pattern: /^\/blog\/([^/]+?)\/?$/,
- errors: [default_error],
- layouts: [default_layout],
- leaf: blog_$slug
+ pattern: '/^/blog/([^/]+?)/?$/',
+ page: { layouts: [0], errors: [1], leaf: 5 }
}
]);
});
@@ -414,28 +436,35 @@ test('lists static assets', () => {
});
test('includes nested error components', () => {
- const { routes } = create('samples/nested-errors');
+ const { nodes, routes } = create('samples/nested-errors');
- assert.equal(routes, [
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ { component: 'samples/nested-errors/foo/+layout.svelte' },
+ { component: 'samples/nested-errors/foo/bar/+error.svelte' },
+ { component: 'samples/nested-errors/foo/bar/baz/+layout.svelte' },
+ { component: 'samples/nested-errors/foo/bar/baz/+error.svelte' },
+ { component: 'samples/nested-errors/foo/bar/baz/+page.svelte' }
+ ]);
+
+ assert.equal(routes.map(simplify_route), [
+ {
+ id: '',
+ pattern: '/^/$/'
+ },
+ {
+ id: 'foo',
+ pattern: '/^/foo/?$/'
+ },
+ {
+ id: 'foo/bar',
+ pattern: '/^/foo/bar/?$/'
+ },
{
- type: 'page',
id: 'foo/bar/baz',
- pattern: /^\/foo\/bar\/baz\/?$/,
- errors: [
- default_error,
- undefined,
- { component: 'samples/nested-errors/foo/bar/+error.svelte' },
- { component: 'samples/nested-errors/foo/bar/baz/+error.svelte' }
- ],
- layouts: [
- default_layout,
- { component: 'samples/nested-errors/foo/+layout.svelte' },
- undefined,
- { component: 'samples/nested-errors/foo/bar/baz/+layout.svelte' }
- ],
- leaf: {
- component: 'samples/nested-errors/foo/bar/baz/+page.svelte'
- }
+ pattern: '/^/foo/bar/baz/?$/',
+ page: { layouts: [0, 2, undefined, 4], errors: [1, undefined, 3, 5], leaf: 6 }
}
]);
});
@@ -443,200 +472,112 @@ test('includes nested error components', () => {
test('creates routes with named layouts', () => {
const { nodes, routes } = create('samples/named-layouts');
- assert.equal(nodes, [
- { component: 'samples/named-layouts/+layout.svelte' },
- default_error,
- { component: 'samples/named-layouts/+layout-home@default.svelte' },
- {
- component: 'samples/named-layouts/+layout-special.svelte',
- shared: 'samples/named-layouts/+layout-special.js',
- server: 'samples/named-layouts/+layout-special.server.js'
- },
- { component: 'samples/named-layouts/a/+layout.svelte' },
- { component: 'samples/named-layouts/b/+layout-alsospecial@special.svelte' },
- { component: 'samples/named-layouts/b/c/+layout.svelte' },
- { component: 'samples/named-layouts/b/d/+layout-extraspecial@special.svelte' },
- { component: 'samples/named-layouts/b/d/+layout-special.svelte' },
- { component: 'samples/named-layouts/a/a1/+page.svelte' },
- { component: 'samples/named-layouts/a/a2/+page@special.svelte' },
- { component: 'samples/named-layouts/b/c/c1/+page@alsospecial.svelte' },
- { component: 'samples/named-layouts/b/c/c2/+page@home.svelte' },
- { component: 'samples/named-layouts/b/d/+page@special.svelte' },
- { component: 'samples/named-layouts/b/d/d1/+page.svelte' },
- { component: 'samples/named-layouts/b/d/d2/+page@extraspecial.svelte' }
+ assert.equal(nodes.map(simplify_node), [
+ // layouts
+ { component: 'samples/named-layouts/+layout.svelte' }, // 0
+ default_error, // 1
+ {
+ component: 'samples/named-layouts/(special)/+layout.svelte',
+ shared: 'samples/named-layouts/(special)/+layout.js',
+ server: 'samples/named-layouts/(special)/+layout.server.js'
+ }, // 2
+ { component: 'samples/named-layouts/(special)/(alsospecial)/+layout.svelte' }, // 3
+ { component: 'samples/named-layouts/a/+layout.svelte' }, // 4
+ { component: 'samples/named-layouts/b/c/+layout.svelte' }, // 5
+ { component: 'samples/named-layouts/b/d/(special)/+layout.svelte' }, // 6
+ { component: 'samples/named-layouts/b/d/(special)/(extraspecial)/+layout.svelte' }, // 7
+
+ // pages
+ { component: 'samples/named-layouts/(special)/(alsospecial)/b/c/c1/+page.svelte' }, // 8
+ { component: 'samples/named-layouts/(special)/a/a2/+page.svelte' }, // 9
+ { component: 'samples/named-layouts/a/a1/+page.svelte' }, // 10
+ { component: 'samples/named-layouts/b/c/c2/+page@.svelte', parent_id: '' }, // 11
+ { component: 'samples/named-layouts/b/d/(special)/+page.svelte' }, // 12
+ { component: 'samples/named-layouts/b/d/(special)/(extraspecial)/d2/+page.svelte' }, // 13
+ { component: 'samples/named-layouts/b/d/d1/+page.svelte' } // 14
]);
- assert.equal(routes, [
+ assert.equal(routes.filter((route) => route.page).map(simplify_route), [
{
- type: 'page',
id: 'a/a1',
- pattern: /^\/a\/a1\/?$/,
- errors: [default_error],
- layouts: [
- { component: 'samples/named-layouts/+layout.svelte' },
- { component: 'samples/named-layouts/a/+layout.svelte' }
- ],
- leaf: { component: 'samples/named-layouts/a/a1/+page.svelte' }
- },
- {
- type: 'page',
- id: 'a/a2',
- pattern: /^\/a\/a2\/?$/,
- errors: [default_error],
- layouts: [
- {
- component: 'samples/named-layouts/+layout-special.svelte',
- shared: 'samples/named-layouts/+layout-special.js',
- server: 'samples/named-layouts/+layout-special.server.js'
- }
- ],
- leaf: { component: 'samples/named-layouts/a/a2/+page@special.svelte' }
- },
- {
- type: 'page',
- id: 'b/d',
- pattern: /^\/b\/d\/?$/,
- errors: [default_error],
- layouts: [
- { component: 'samples/named-layouts/+layout.svelte' },
- { component: 'samples/named-layouts/b/d/+layout-special.svelte' }
- ],
- leaf: { component: 'samples/named-layouts/b/d/+page@special.svelte' }
- },
- {
- type: 'page',
- id: 'b/c/c1',
- pattern: /^\/b\/c\/c1\/?$/,
- errors: [default_error],
- layouts: [
- {
- component: 'samples/named-layouts/+layout-special.svelte',
- shared: 'samples/named-layouts/+layout-special.js',
- server: 'samples/named-layouts/+layout-special.server.js'
- },
- { component: 'samples/named-layouts/b/+layout-alsospecial@special.svelte' }
- ],
- leaf: { component: 'samples/named-layouts/b/c/c1/+page@alsospecial.svelte' }
- },
- {
- type: 'page',
+ pattern: '/^/a/a1/?$/',
+ 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 }
+ },
+ {
id: 'b/c/c2',
- pattern: /^\/b\/c\/c2\/?$/,
- errors: [default_error, default_error],
- layouts: [
- { component: 'samples/named-layouts/+layout.svelte' },
- { component: 'samples/named-layouts/+layout-home@default.svelte' }
- ],
- leaf: { component: 'samples/named-layouts/b/c/c2/+page@home.svelte' }
+ pattern: '/^/b/c/c2/?$/',
+ page: { layouts: [0], errors: [1], leaf: 11 }
+ },
+ {
+ id: 'b/d/(special)',
+ pattern: '/^/b/d/?$/',
+ page: { layouts: [0, 6], errors: [1, undefined], leaf: 12 }
},
{
- type: 'page',
id: 'b/d/d1',
- pattern: /^\/b\/d\/d1\/?$/,
- errors: [default_error],
- layouts: [{ component: 'samples/named-layouts/+layout.svelte' }],
- leaf: { component: 'samples/named-layouts/b/d/d1/+page.svelte' }
- },
- {
- type: 'page',
- id: 'b/d/d2',
- pattern: /^\/b\/d\/d2\/?$/,
- errors: [default_error],
- layouts: [
- { component: 'samples/named-layouts/+layout.svelte' },
- { component: 'samples/named-layouts/b/d/+layout-special.svelte' },
- { component: 'samples/named-layouts/b/d/+layout-extraspecial@special.svelte' }
- ],
- leaf: { component: 'samples/named-layouts/b/d/d2/+page@extraspecial.svelte' }
+ pattern: '/^/b/d/d1/?$/',
+ page: { layouts: [0], errors: [1], leaf: 14 }
+ },
+ {
+ id: '(special)/(alsospecial)/b/c/c1',
+ pattern: '/^/b/c/c1/?$/',
+ page: { layouts: [0, 2, 3], errors: [1, undefined, undefined], leaf: 8 }
+ },
+ {
+ id: 'b/d/(special)/(extraspecial)/d2',
+ pattern: '/^/b/d/d2/?$/',
+ page: { layouts: [0, 6, 7], errors: [1, undefined, undefined], leaf: 13 }
}
]);
});
test('handles pages without .svelte file', () => {
- const { routes } = create('samples/page-without-svelte-file');
+ const { nodes, routes } = create('samples/page-without-svelte-file');
- assert.equal(routes, [
+ assert.equal(nodes.map(simplify_node), [
+ default_layout,
+ default_error,
+ { component: 'samples/page-without-svelte-file/error/+error.svelte' },
+ { component: 'samples/page-without-svelte-file/layout/+layout.svelte' },
+ { component: 'samples/page-without-svelte-file/+page.svelte' },
+ { shared: 'samples/page-without-svelte-file/error/[...path]/+page.js' },
+ { component: 'samples/page-without-svelte-file/layout/exists/+page.svelte' },
+ { server: 'samples/page-without-svelte-file/layout/redirect/+page.server.js' }
+ ]);
+
+ assert.equal(routes.map(simplify_route), [
{
- type: 'page',
id: '',
- pattern: /^\/$/,
- errors: [
- {
- component: 'error.svelte'
- }
- ],
- layouts: [
- {
- component: 'layout.svelte'
- }
- ],
- leaf: {
- component: 'samples/page-without-svelte-file/+page.svelte'
- }
+ pattern: '/^/$/',
+ page: { layouts: [0], errors: [1], leaf: 4 }
+ },
+ {
+ id: 'error',
+ pattern: '/^/error/?$/'
+ },
+ {
+ id: 'layout',
+ pattern: '/^/layout/?$/'
},
{
- type: 'page',
id: 'layout/exists',
- pattern: /^\/layout\/exists\/?$/,
- errors: [
- {
- component: 'error.svelte'
- }
- ],
- layouts: [
- {
- component: 'layout.svelte'
- },
- {
- component: 'samples/page-without-svelte-file/layout/+layout.svelte'
- }
- ],
- leaf: {
- component: 'samples/page-without-svelte-file/layout/exists/+page.svelte'
- }
+ pattern: '/^/layout/exists/?$/',
+ page: { layouts: [0, 3], errors: [1, undefined], leaf: 6 }
},
{
- type: 'page',
id: 'layout/redirect',
- pattern: /^\/layout\/redirect\/?$/,
- errors: [
- {
- component: 'error.svelte'
- }
- ],
- layouts: [
- {
- component: 'layout.svelte'
- },
- {
- component: 'samples/page-without-svelte-file/layout/+layout.svelte'
- }
- ],
- leaf: {
- server: 'samples/page-without-svelte-file/layout/redirect/+page.server.js'
- }
+ pattern: '/^/layout/redirect/?$/',
+ page: { layouts: [0, 3], errors: [1, undefined], leaf: 7 }
},
{
- type: 'page',
id: 'error/[...path]',
- pattern: /^\/error(?:\/(.*))?\/?$/,
- errors: [
- {
- component: 'error.svelte'
- },
- {
- component: 'samples/page-without-svelte-file/error/+error.svelte'
- }
- ],
- layouts: [
- {
- component: 'layout.svelte'
- },
- undefined
- ],
- leaf: {
- shared: 'samples/page-without-svelte-file/error/[...path]/+page.js'
- }
+ pattern: '/^/error(?:/(.*))?/?$/',
+ page: { layouts: [0, undefined], errors: [1, 2], leaf: 5 }
}
]);
});
@@ -644,44 +585,14 @@ test('handles pages without .svelte file', () => {
test('errors on missing layout', () => {
assert.throws(
() => create('samples/named-layout-missing'),
- /samples\/named-layout-missing\/\+page@missing.svelte references missing layout "missing"/
- );
-});
-
-test('errors on layout named default', () => {
- assert.throws(
- () => create('samples/named-layout-default'),
- /samples\/named-layout-default\/\+layout-default.svelte cannot use reserved "default" name/
+ /samples\/named-layout-missing\/\+page@missing.svelte references missing segment "missing"/
);
});
test('errors on invalid named layout reference', () => {
assert.throws(
() => create('samples/invalid-named-layout-reference'),
- /Only Svelte files can reference named layouts. Remove '@named' from \+page@named.js \(at samples\/invalid-named-layout-reference\/\+page@named.js\)/
- );
-});
-
-test('errors on duplicate layout definition', () => {
- assert.throws(
- () => create('samples/duplicate-layout'),
- /Duplicate layout samples\/duplicate-layout\/\+layout-a@x.svelte already defined at samples\/duplicate-layout\/\+layout-a.svelte/
- );
-});
-
-test('errors on recursive name layout', () => {
- assert.throws(
- () => create('samples/named-layout-recursive-1'),
- /Recursive layout detected: samples\/named-layout-recursive-1\/\+layout-a@b\.svelte -> samples\/named-layout-recursive-1\/\+layout-b@a\.svelte -> samples\/named-layout-recursive-1\/\+layout-a@b\.svelte/
- );
- assert.throws(
- () => create('samples/named-layout-recursive-2'),
- /Recursive layout detected: samples\/named-layout-recursive-2\/\+layout-a@a\.svelte -> samples\/named-layout-recursive-2\/\+layout-a@a\.svelte/
- );
-
- assert.throws(
- () => create('samples/named-layout-recursive-3'),
- /Recursive layout detected: samples\/named-layout-recursive-3\/\+layout@a\.svelte -> samples\/named-layout-recursive-3\/\+layout-a@default\.svelte -> samples\/named-layout-recursive-3\/\+layout@a\.svelte/
+ /Only Svelte files can reference named layouts. Remove '@' from \+page@.js \(at samples\/invalid-named-layout-reference\/x\/\+page@.js\)/
);
});
@@ -720,4 +631,19 @@ test('errors on duplicate matchers', () => {
}
});
+test('prevents route conflicts between groups', () => {
+ assert.throws(
+ () => create('samples/conflicting-groups'),
+ /\(x\)\/a and \(y\)\/a occupy the same route/
+ );
+});
+
+// TODO remove for 1.0
+test('errors on encountering a declared layout', () => {
+ assert.throws(
+ () => create('samples/declared-layout'),
+ /samples\/declared-layout\/\+layout-foo.svelte should be reimplemented with layout groups: https:\/\/kit\.svelte\.dev\/docs\/advanced-routing#advanced-layouts/
+ );
+});
+
test.run();
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-3/+page.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/conflicting-groups/(x)/a/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-3/+page.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/conflicting-groups/(x)/a/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/duplicate-layout/+layout-a.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/conflicting-groups/(y)/a/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/duplicate-layout/+layout-a.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/conflicting-groups/(y)/a/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/duplicate-layout/+layout-a@x.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/declared-layout/+layout-foo.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/duplicate-layout/+layout-a@x.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/declared-layout/+layout-foo.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/+layout-named.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/x/+page@.js
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/+layout-named.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/x/+page@.js
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/+page@named.js b/packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/x/+page@.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/+page@named.js
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/x/+page@.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/+page@named.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/(alsospecial)/+layout.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/invalid-named-layout-reference/+page@named.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/(alsospecial)/+layout.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-default/+layout-default.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/(alsospecial)/b/c/c1/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-default/+layout-default.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/(alsospecial)/b/c/c1/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-1/+layout-a@b.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/+layout.js
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-1/+layout-a@b.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/+layout.js
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-1/+layout-b@a.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/+layout.server.js
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-1/+layout-b@a.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/+layout.server.js
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-1/+page@a.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/+layout.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-1/+page@a.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/+layout.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-2/+layout-a@a.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/a/a2/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-2/+layout-a@a.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/(special)/a/a2/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-special.server.js b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-special.server.js
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-special.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-special.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/a/a2/+page@special.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/a/a2/+page@special.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/+layout-alsospecial@special.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/+layout-alsospecial@special.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/c/c1/+page@alsospecial.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/c/c1/+page@alsospecial.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-2/+page@a.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/c/c2/+page@.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-2/+page@a.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/c/c2/+page@.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/c/c2/+page@home.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/c/c2/+page@home.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-3/+layout-a@default.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/(extraspecial)/+layout.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-3/+layout-a@default.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/(extraspecial)/+layout.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-3/+layout@a.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/(extraspecial)/d2/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layout-recursive-3/+layout@a.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/(extraspecial)/d2/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-home@default.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/+layout.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-home@default.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/+layout.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-special.js b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/+layout-special.js
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/(special)/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/+layout-extraspecial@special.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/+layout-extraspecial@special.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/+layout-special.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/+layout-special.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/+page@special.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/+page@special.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/d2/+page@extraspecial.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/named-layouts/b/d/d2/+page@extraspecial.svelte
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/packages/kit/src/core/sync/create_manifest_data/types.d.ts b/packages/kit/src/core/sync/create_manifest_data/types.d.ts
index ac63955a9001..e897cc9e6458 100644
--- a/packages/kit/src/core/sync/create_manifest_data/types.d.ts
+++ b/packages/kit/src/core/sync/create_manifest_data/types.d.ts
@@ -9,7 +9,7 @@ interface Part {
interface RouteTreeNode {
error: PageNode | undefined;
- layouts: Record;
+ layout: PageNode | undefined;
}
export type RouteTree = Map;
@@ -20,21 +20,18 @@ interface RouteComponent {
is_layout: boolean;
is_error: boolean;
uses_layout: string | undefined;
- declares_layout: string | undefined;
}
interface RouteSharedModule {
kind: 'shared';
is_page: boolean;
is_layout: boolean;
- declares_layout: string | undefined;
}
interface RouteServerModule {
kind: 'server';
is_page: boolean;
is_layout: boolean;
- declares_layout: string | undefined;
}
export type RouteFile = RouteComponent | RouteSharedModule | RouteServerModule;
diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js
index 60e71ba0f767..4d451066770b 100644
--- a/packages/kit/src/core/sync/sync.js
+++ b/packages/kit/src/core/sync/sync.js
@@ -4,7 +4,7 @@ import { write_client_manifest } from './write_client_manifest.js';
import { write_matchers } from './write_matchers.js';
import { write_root } from './write_root.js';
import { write_tsconfig } from './write_tsconfig.js';
-import { write_type, write_types } from './write_types.js';
+import { write_types, write_all_types } from './write_types.js';
import { write_ambient } from './write_ambient.js';
/**
@@ -29,7 +29,7 @@ export async function create(config) {
write_client_manifest(manifest_data, output);
write_root(manifest_data, output);
write_matchers(manifest_data, output);
- await write_types(config, manifest_data);
+ await write_all_types(config, manifest_data);
return { manifest_data };
}
@@ -43,7 +43,7 @@ export async function create(config) {
* @param {string} file
*/
export async function update(config, manifest_data, file) {
- await write_type(config, manifest_data, file);
+ await write_types(config, manifest_data, file);
return { manifest_data };
}
diff --git a/packages/kit/src/core/sync/utils.js b/packages/kit/src/core/sync/utils.js
index c0ee2f2cd278..be1862cd1927 100644
--- a/packages/kit/src/core/sync/utils.js
+++ b/packages/kit/src/core/sync/utils.js
@@ -25,17 +25,6 @@ export function write(file, code) {
fs.writeFileSync(file, code);
}
-/**
- * @param {(file: string) => boolean} should_remove
- */
-export function remove_from_previous(should_remove) {
- for (const key of previous_contents.keys()) {
- if (should_remove(key)) {
- previous_contents.delete(key);
- }
- }
-}
-
/** @param {string} str */
export function trim(str) {
const indentation = /** @type {RegExpExecArray} */ (/\n?(\s*)/.exec(str))[1];
diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js
index 9178c9d95bda..99bc0eccd24b 100644
--- a/packages/kit/src/core/sync/write_client_manifest.js
+++ b/packages/kit/src/core/sync/write_client_manifest.js
@@ -9,9 +9,6 @@ import { trim, write_if_changed } from './utils.js';
* @param {string} output
*/
export function write_client_manifest(manifest_data, output) {
- /** @type {Map} */
- const node_indexes = new Map();
-
/**
* Creates a module that exports a `CSRPageNode`
* @param {import('types').PageNode} node
@@ -41,7 +38,6 @@ export function write_client_manifest(manifest_data, output) {
const nodes = manifest_data.nodes
.map((node, i) => {
- node_indexes.set(node, i);
write_if_changed(`${output}/nodes/${i}.js`, generate_node(node));
return `() => import('./nodes/${i}')`;
})
@@ -50,17 +46,35 @@ export function write_client_manifest(manifest_data, output) {
const dictionary = `{
${manifest_data.routes
.map((route) => {
- if (route.type === 'page') {
- const errors = route.errors.map((node) => (node ? node_indexes.get(node) : '')).join(',');
- const layouts = route.layouts
- .map((node) => (node ? node_indexes.get(node) : ''))
- .join(',');
- const leaf = route.leaf ? node_indexes.get(route.leaf) : '';
+ if (route.page) {
+ const errors = route.page.errors.slice(1).map((n) => n ?? '');
+ const layouts = route.page.layouts.slice(1).map((n) => n ?? '');
+
+ while (layouts.at(-1) === '') layouts.pop();
+ while (errors.at(-1) === '') errors.pop();
+
+ /** @type {import('types').RouteData | null} */
+ let current_route = route;
+
+ /** @type {import('types').PageNode | null} */
+ let current_node = route.leaf;
+
+ let uses_server_data = false;
+ while (current_route && !uses_server_data) {
+ uses_server_data = !!current_node?.server;
+ current_route = current_route.parent;
+ current_node = current_route?.layout ?? null;
+ }
+
+ // encode whether or not the route uses the server data
+ // using the ones' complement, to save space
+ const array = [`${uses_server_data ? '~' : ''}${route.page.leaf}`];
- const uses_server_data = [...route.layouts, route.leaf].some((node) => node?.server);
- const suffix = uses_server_data ? ', 1' : '';
+ // only include non-root layout/error nodes if they exist
+ if (layouts.length > 0 || errors.length > 0) array.push(`[${layouts.join(',')}]`);
+ if (errors.length > 0) array.push(`[${errors.join(',')}]`);
- return `${s(route.id)}: [[${errors}], [${layouts}], ${leaf}${suffix}]`;
+ return `${s(route.id)}: [${array.join(',')}]`;
}
})
.filter(Boolean)
diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js
index 0dd8536bdd37..f954e3fcba48 100644
--- a/packages/kit/src/core/sync/write_root.js
+++ b/packages/kit/src/core/sync/write_root.js
@@ -9,7 +9,7 @@ export function write_root(manifest_data, output) {
const max_depth = Math.max(
...manifest_data.routes.map((route) =>
- route.type === 'page' ? route.layouts.filter(Boolean).length + 1 : 0
+ route.page ? route.page.layouts.filter(Boolean).length + 1 : 0
),
1
);
diff --git a/packages/kit/src/core/sync/write_types.js b/packages/kit/src/core/sync/write_types.js
index 624223027bbe..470bd418e16f 100644
--- a/packages/kit/src/core/sync/write_types.js
+++ b/packages/kit/src/core/sync/write_types.js
@@ -1,28 +1,8 @@
import fs from 'fs';
import path from 'path';
import MagicString from 'magic-string';
-import { posixify, rimraf } from '../../utils/filesystem.js';
-import { parse_route_id } from '../../utils/routing.js';
-import { remove_from_previous, write_if_changed } from './utils.js';
-
-/**
- * @typedef { import('types').PageNode & {
- * parent?: {
- * key: string;
- * name: string;
- * folder_depth_diff: number;
- * }
- * } } Node
- */
-
-/**
- * @typedef {{
- * leaf?: Node;
- * default_layout?: Node;
- * named_layouts: Map;
- * endpoint?: string;
- * }} NodeGroup
- */
+import { posixify, rimraf, walk } from '../../utils/filesystem.js';
+import { compact } from '../../utils/array.js';
/**
* @typedef {{
@@ -32,200 +12,133 @@ import { remove_from_previous, write_if_changed } from './utils.js';
* } | null} Proxy
*/
+/** @type {import('typescript')} */
+// @ts-ignore
+let ts = undefined;
+try {
+ ts = (await import('typescript')).default;
+} catch {}
+
const cwd = process.cwd();
const shared_names = new Set(['load']);
const server_names = new Set(['load', 'POST', 'PUT', 'PATCH', 'DELETE']); // TODO replace with a single `action`
-let first_run = true;
-
/**
* Creates types for the whole manifest
- *
* @param {import('types').ValidatedConfig} config
* @param {import('types').ManifestData} manifest_data
*/
-export async function write_types(config, manifest_data) {
- /** @type {import('typescript') | undefined} */
- let ts = undefined;
- try {
- ts = (await import('typescript')).default;
- } catch (e) {
- // No TypeScript installed - skip type generation
- return;
- }
+export async function write_all_types(config, manifest_data) {
+ if (!ts) return;
const types_dir = `${config.kit.outDir}/types`;
- if (first_run) {
- rimraf(types_dir);
- first_run = false;
- }
-
- const routes_dir = posixify(path.relative('.', config.kit.files.routes));
- const groups = get_groups(manifest_data, routes_dir);
+ // empty out files that no longer need to exist
+ const routes_dir = path.relative('.', config.kit.files.routes);
+ const expected_directories = new Set(
+ manifest_data.routes.map((route) => path.join(routes_dir, route.id))
+ );
- let written_files = new Set();
- // ...then, for each directory, write $types.d.ts
- for (const [dir] of groups) {
- const written = write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts);
- written.forEach((w) => written_files.add(w));
+ if (fs.existsSync(types_dir)) {
+ for (const file of walk(types_dir)) {
+ const dir = path.dirname(file);
+ if (!expected_directories.has(dir)) {
+ rimraf(file);
+ }
+ }
}
- // Remove all files that were not updated, which means their original was removed
- remove_from_previous((file) => {
- const was_removed = file.startsWith(types_dir) && !written_files.has(file);
- if (was_removed) {
- rimraf(file);
- }
- return was_removed;
- });
+ // For each directory, write $types.d.ts
+ for (const route of manifest_data.routes) {
+ update_types(config, manifest_data, route);
+ }
}
/**
* Creates types related to the given file. This should only be called
* if the file in question was edited, not if it was created/deleted/moved.
- *
* @param {import('types').ValidatedConfig} config
* @param {import('types').ManifestData} manifest_data
* @param {string} file
*/
-export async function write_type(config, manifest_data, file) {
+export async function write_types(config, manifest_data, file) {
+ if (!ts) return;
+
if (!path.basename(file).startsWith('+')) {
// Not a route file
return;
}
- /** @type {import('typescript') | undefined} */
- let ts = undefined;
- try {
- ts = (await import('typescript')).default;
- } catch (e) {
- // No TypeScript installed - skip type generation
- return;
- }
+ const filepath = path.relative(config.kit.files.routes, file);
+ const id = path.dirname(filepath);
- const routes_dir = posixify(path.relative('.', config.kit.files.routes));
- const file_dir = posixify(path.dirname(file).slice(config.kit.files.routes.length + 1));
- const groups = get_groups(manifest_data, routes_dir);
+ const route = manifest_data.routes.find((route) => route.id === id);
+ if (!route) return; // this shouldn't ever happen
- // We are only interested in the directory that contains the file
- write_types_for_dir(config, manifest_data, routes_dir, file_dir, groups, ts);
+ update_types(config, manifest_data, route);
}
/**
+ *
+ * @param {import('types').ValidatedConfig} config
* @param {import('types').ManifestData} manifest_data
- * @param {string} routes_dir
+ * @param {import('types').RouteData} route
*/
-function get_groups(manifest_data, routes_dir) {
- /**
- * A map of all directories : route files. We don't just use
- * manifest_data.routes, because that will exclude +layout
- * files that aren't accompanied by a +page
- * @type {Map}
- */
- const groups = new Map();
-
- /** @param {string} dir */
- function get_group(dir) {
- let group = groups.get(dir);
- if (!group) {
- group = {
- named_layouts: new Map()
- };
- groups.set(dir, group);
- }
-
- return group;
- }
-
- // first, sort nodes (necessary for finding the nearest layout more efficiently)...
- const nodes = [...manifest_data.nodes].sort((n1, n2) => {
- // Sort by path length first...
- const path_length_diff =
- /** @type {string} */ (n1.component ?? n1.shared ?? n1.server).split('/').length -
- /** @type {string} */ (n2.component ?? n2.shared ?? n2.server).split('/').length;
-
- return (
- path_length_diff ||
- // ...on ties, sort named layouts first
- (path.basename(n1.component || '').includes('-')
- ? -1
- : path.basename(n2.component || '').includes('-')
- ? 1
- : 0)
- );
- });
-
- // ...then, populate `directories` with +page/+layout files...
- for (let i = 0; i < nodes.length; i += 1) {
- /** @type {Node} */
- const node = { ...nodes[i] }; // shallow copy so we don't mutate the original when setting parent
-
- const file_path = /** @type {string} */ (node.component ?? node.shared ?? node.server);
- // skip default layout/error
- if (!file_path.startsWith(routes_dir)) continue;
-
- const parts = file_path.split('/');
-
- const file = /** @type {string} */ (parts.pop());
- const dir = parts.join('/').slice(routes_dir.length + 1);
-
- // error pages don't need types
- if (!file || file.startsWith('+error')) continue;
-
- const group = get_group(dir);
-
- if (file.startsWith('+page')) {
- group.leaf = node;
- } else {
- const match = /^\+layout(?:-([^@.]+))?/.exec(file);
+function update_types(config, manifest_data, route) {
+ const routes_dir = posixify(path.relative('.', config.kit.files.routes));
+ const outdir = path.join(config.kit.outDir, 'types', routes_dir, route.id);
- // this shouldn't happen, but belt and braces. also keeps TS happy,
- // and we live to keep TS happy
- if (!match) throw new Error(`Unexpected route file: ${file}`);
+ // first, check if the types are out of date
+ const input_files = [];
- if (match[1]) {
- group.named_layouts.set(match[1], node);
- } else {
- group.default_layout = node;
- }
- }
+ /** @type {import('types').PageNode | null} */
+ let node = route.leaf;
+ while (node) {
+ if (node.shared) input_files.push(node.shared);
+ if (node.server) input_files.push(node.server);
+ node = node.parent ?? null;
+ }
- node.parent = find_nearest_layout(routes_dir, nodes, i);
+ /** @type {import('types').PageNode | null} */
+ node = route.layout;
+ while (node) {
+ if (node.shared) input_files.push(node.shared);
+ if (node.server) input_files.push(node.server);
+ node = node.parent ?? null;
}
- // ...then add +server.js files...
- for (const route of manifest_data.routes) {
- if (route.type === 'endpoint') {
- get_group(route.id).endpoint = route.file;
- }
+ if (route.endpoint) {
+ input_files.push(route.endpoint.file);
}
- return groups;
-}
+ if (input_files.length === 0) return; // nothing to do
-/**
- *
- * @param {import('types').ValidatedConfig} config
- * @param {import('types').ManifestData} manifest_data
- * @param {string} routes_dir
- * @param {string} dir
- * @param {Map} groups
- * @param {import('typescript')} ts
- */
-function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts) {
- const group = groups.get(dir);
- if (!group) {
- return [];
- }
+ try {
+ fs.mkdirSync(outdir, { recursive: true });
+ } catch {}
+
+ const output_files = compact(
+ fs.readdirSync(outdir).map((name) => {
+ const stats = fs.statSync(path.join(outdir, name));
+ if (stats.isDirectory()) return;
+ return {
+ name,
+ updated: stats.mtimeMs
+ };
+ })
+ );
- const outdir = `${config.kit.outDir}/types/${routes_dir}/${dir}`;
+ const source_last_updated = Math.max(...input_files.map((file) => fs.statSync(file).mtimeMs));
+ const types_last_updated = Math.max(...output_files.map((file) => file?.updated));
- const imports = [`import type * as Kit from '@sveltejs/kit';`];
+ // types were generated more recently than the source files, so don't regenerate
+ if (types_last_updated > source_last_updated) return;
- /** @type {string[]} */
- const written_files = [];
+ // track which old files end up being surplus to requirements
+ const to_delete = new Set(output_files.map((file) => file.name));
+
+ const imports = [`import type * as Kit from '@sveltejs/kit';`];
/** @type {string[]} */
const declarations = [];
@@ -233,10 +146,8 @@ function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts)
/** @type {string[]} */
const exports = [];
- const route_params = parse_route_id(dir).names;
-
- if (route_params.length > 0) {
- const params = route_params.map((param) => `${param}: string`).join('; ');
+ if (route.names.length > 0) {
+ const params = route.names.map((param) => `${param}: string`).join('; ');
declarations.push(
`interface RouteParams extends Partial> { ${params} }`
);
@@ -244,15 +155,14 @@ function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts)
declarations.push(`interface RouteParams extends Partial> {}`);
}
- if (group.leaf) {
+ if (route.leaf) {
const { data, server_data, load, server_load, errors, written_proxies } = process_node(
- ts,
- group.leaf,
+ route.leaf,
outdir,
- 'RouteParams',
- groups
+ 'RouteParams'
);
- written_files.push(...written_proxies);
+
+ for (const file of written_proxies) to_delete.delete(file);
exports.push(`export type Errors = ${errors};`);
@@ -272,20 +182,18 @@ function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts)
exports.push('export type PageServerLoadEvent = Parameters[0];');
}
- if (group.leaf.server) {
+ if (route.leaf.server) {
exports.push(`export type Action = Kit.Action`);
}
}
- if (group.default_layout || group.named_layouts.size > 0) {
- // TODO to be completely rigorous, we should have a LayoutParams per
- // layout, and only include params for child pages that use each layout.
- // but that's more work than i care to do right now
+ if (route.layout) {
+ // TODO collect children in create_manifest_data, instead of this inefficient O(n^2) algorithm
const layout_params = new Set();
- manifest_data.routes.forEach((route) => {
- if (route.type === 'page' && route.id.startsWith(dir + '/')) {
+ manifest_data.routes.forEach((other) => {
+ if (other.page && other.id.startsWith(route.id + '/')) {
// TODO this is O(n^2), see if we need to speed it up
- for (const name of parse_route_id(route.id.slice(dir.length + 1)).names) {
+ for (const name of other.names) {
layout_params.add(name);
}
}
@@ -298,95 +206,32 @@ function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts)
declarations.push(`interface LayoutParams extends RouteParams {}`);
}
- if (group.default_layout) {
- const { data, server_data, load, server_load, written_proxies } = process_node(
- ts,
- group.default_layout,
- outdir,
- 'LayoutParams',
- groups
- );
- written_files.push(...written_proxies);
-
- exports.push(`export type LayoutData = ${data};`);
- if (load) {
- exports.push(
- `export type LayoutLoad | void = Record | void> = ${load};`
- );
- exports.push('export type LayoutLoadEvent = Parameters[0];');
- }
-
- exports.push(`export type LayoutServerData = ${server_data};`);
- if (server_load) {
- exports.push(
- `export type LayoutServerLoad | void = Record | void> = ${server_load};`
- );
- exports.push('export type LayoutServerLoadEvent = Parameters[0];');
- }
- }
+ const { data, server_data, load, server_load, written_proxies } = process_node(
+ route.layout,
+ outdir,
+ 'LayoutParams'
+ );
- if (group.named_layouts.size > 0) {
- /** @type {string[]} */
- const data_exports = [];
-
- /** @type {string[]} */
- const server_data_exports = [];
-
- /** @type {string[]} */
- const load_exports = [];
-
- /** @type {string[]} */
- const load_event_exports = [];
-
- /** @type {string[]} */
- const server_load_exports = [];
-
- /** @type {string[]} */
- const server_load_event_exports = [];
-
- for (const [name, node] of group.named_layouts) {
- const { data, server_data, load, server_load, written_proxies } = process_node(
- ts,
- node,
- outdir,
- 'LayoutParams',
- groups
- );
- written_files.push(...written_proxies);
- data_exports.push(`export type ${name} = ${data};`);
- server_data_exports.push(`export type ${name} = ${server_data};`);
- if (load) {
- load_exports.push(
- `export type ${name} | void = Record | void> = ${load};`
- );
- load_event_exports.push(`export type ${name} = Parameters[0];`);
- }
- if (server_load) {
- server_load_exports.push(
- `export type ${name} | void = Record | void> = ${server_load};`
- );
- server_load_event_exports.push(
- `export type ${name} = Parameters[0];`
- );
- }
- }
+ for (const file of written_proxies) to_delete.delete(file);
- exports.push(`\nexport namespace LayoutData {\n\t${data_exports.join('\n\t')}\n}`);
- exports.push(`\nexport namespace LayoutLoad {\n\t${load_exports.join('\n\t')}\n}`);
- exports.push(`\nexport namespace LayoutLoadEvent {\n\t${load_event_exports.join('\n\t')}\n}`);
- exports.push(
- `\nexport namespace LayoutServerData {\n\t${server_data_exports.join('\n\t')}\n}`
- );
+ exports.push(`export type LayoutData = ${data};`);
+ if (load) {
exports.push(
- `\nexport namespace LayoutServerLoad {\n\t${server_load_exports.join('\n\t')}\n}`
+ `export type LayoutLoad | void = Record | void> = ${load};`
);
+ exports.push('export type LayoutLoadEvent = Parameters[0];');
+ }
+
+ exports.push(`export type LayoutServerData = ${server_data};`);
+ if (server_load) {
exports.push(
- `\nexport namespace LayoutServerLoadEvent {\n\t${server_load_event_exports.join('\n\t')}\n}`
+ `export type LayoutServerLoad | void = Record | void> = ${server_load};`
);
+ exports.push('export type LayoutServerLoadEvent = Parameters[0];');
}
}
- if (group.endpoint) {
+ if (route.endpoint) {
exports.push(`export type RequestHandler = Kit.RequestHandler;`);
exports.push(`export type RequestEvent = Kit.RequestEvent;`);
}
@@ -395,19 +240,20 @@ function write_types_for_dir(config, manifest_data, routes_dir, dir, groups, ts)
.filter(Boolean)
.join('\n\n');
- written_files.push(write(`${outdir}/$types.d.ts`, output));
+ fs.writeFileSync(`${outdir}/$types.d.ts`, output);
+ to_delete.delete('$types.d.ts');
- return written_files;
+ for (const file of to_delete) {
+ fs.unlinkSync(path.join(outdir, file));
+ }
}
/**
- * @param {import('typescript')} ts
- * @param {Node} node
+ * @param {import('types').PageNode} node
* @param {string} outdir
* @param {string} params
- * @param {Map} groups
*/
-function process_node(ts, node, outdir, params, groups) {
+function process_node(node, outdir, params) {
let data;
let load;
let server_load;
@@ -420,14 +266,18 @@ function process_node(ts, node, outdir, params, groups) {
if (node.server) {
const content = fs.readFileSync(node.server, 'utf8');
- const proxy = tweak_types(ts, content, server_names);
+ const proxy = tweak_types(content, server_names);
const basename = path.basename(node.server);
if (proxy?.modified) {
- written_proxies.push(write(`${outdir}/proxy${basename}`, proxy.code));
+ fs.writeFileSync(`${outdir}/proxy${basename}`, proxy.code);
+ written_proxies.push(`proxy${basename}`);
}
server_data = get_data_type(node.server, 'null', proxy);
- server_load = `Kit.ServerLoad<${params}, ${get_parent_type('LayoutServerData')}, OutputData>`;
+ server_load = `Kit.ServerLoad<${params}, ${get_parent_type(
+ node,
+ 'LayoutServerData'
+ )}, OutputData>`;
if (proxy) {
const types = [];
@@ -450,13 +300,14 @@ function process_node(ts, node, outdir, params, groups) {
server_data = 'null';
}
- const parent_type = get_parent_type('LayoutData');
+ const parent_type = get_parent_type(node, 'LayoutData');
if (node.shared) {
const content = fs.readFileSync(node.shared, 'utf8');
- const proxy = tweak_types(ts, content, shared_names);
+ const proxy = tweak_types(content, shared_names);
if (proxy?.modified) {
- written_proxies.push(write(`${outdir}/proxy${path.basename(node.shared)}`, proxy.code));
+ fs.writeFileSync(`${outdir}/proxy${path.basename(node.shared)}`, proxy.code);
+ written_proxies.push(`proxy${path.basename(node.shared)}`);
}
const type = get_data_type(node.shared, `${parent_type} & ${server_data}`, proxy);
@@ -492,39 +343,35 @@ function process_node(ts, node, outdir, params, groups) {
return 'unknown';
}
}
+}
- /**
- * Get the parent type string by recursively looking up the parent layout and accumulate them to one type.
- * @param {string} type
- */
- function get_parent_type(type) {
- const parent_imports = [];
- let parent = node.parent;
- let acc_diff = 0;
-
- while (parent) {
- acc_diff += parent.folder_depth_diff;
- let parent_group = /** @type {NodeGroup} */ (groups.get(parent.key));
- // unshift because we need it the other way round for the import string
- parent_imports.unshift(
- (acc_diff === 0 ? '' : `import('` + '../'.repeat(acc_diff) + '$types.js' + `').`) +
- `${type}${parent.name ? `.${parent.name}` : ''}`
- );
- let parent_layout = /** @type {Node} */ (
- parent.name ? parent_group.named_layouts.get(parent.name) : parent_group.default_layout
- );
- parent = parent_layout.parent;
- }
+/**
+ * Get the parent type string by recursively looking up the parent layout and accumulate them to one type.
+ * @param {import('types').PageNode} node
+ * @param {string} type
+ */
+function get_parent_type(node, type) {
+ const parent_imports = [];
- let parent_str = parent_imports[0] || 'Record';
- for (let i = 1; i < parent_imports.length; i++) {
- // Omit is necessary because a parent could have a property with the same key which would
- // cause a type conflict. At runtime the child overwrites the parent property in this case,
- // so reflect that in the type definition.
- parent_str = `Omit<${parent_str}, keyof ${parent_imports[i]}> & ${parent_imports[i]}`;
- }
- return parent_str;
+ let parent = node.parent;
+
+ while (parent) {
+ const d = node.depth - parent.depth;
+ // unshift because we need it the other way round for the import string
+ parent_imports.unshift(
+ `${d === 0 ? '' : `import('${'../'.repeat(d)}${'$types.js'}').`}${type}`
+ );
+ parent = parent.parent;
+ }
+
+ let parent_str = parent_imports[0] || 'Record';
+ for (let i = 1; i < parent_imports.length; i++) {
+ // Omit is necessary because a parent could have a property with the same key which would
+ // cause a type conflict. At runtime the child overwrites the parent property in this case,
+ // so reflect that in the type definition.
+ parent_str = `Omit<${parent_str}, keyof ${parent_imports[i]}> & ${parent_imports[i]}`;
}
+ return parent_str;
}
/**
@@ -546,12 +393,11 @@ function replace_ext_with_js(file_path) {
}
/**
- * @param {import('typescript')} ts
* @param {string} content
* @param {Set} names
* @returns {Proxy}
*/
-export function tweak_types(ts, content, names) {
+export function tweak_types(content, names) {
try {
let modified = false;
@@ -699,77 +545,3 @@ export function tweak_types(ts, content, names) {
return null;
}
}
-
-/**
- * @param {string} file
- * @param {string} content
- */
-function write(file, content) {
- write_if_changed(file, content);
- return file;
-}
-
-/**
- * Finds the nearest layout for given node.
- * Assumes that nodes is sorted by path length (lowest first).
- *
- * @param {string} routes_dir
- * @param {import('types').PageNode[]} nodes
- * @param {number} start_idx
- */
-export function find_nearest_layout(routes_dir, nodes, start_idx) {
- const start_file = /** @type {string} */ (
- nodes[start_idx].component || nodes[start_idx].shared || nodes[start_idx].server
- );
-
- let name = '';
- const match = /^\+(layout|page)(?:-([^@.]+))?(?:@([^@.]+))?/.exec(path.basename(start_file));
- if (!match) throw new Error(`Unexpected route file: ${start_file}`);
- if (match[3] && match[3] !== 'default') {
- name = match[3]; // a named layout is referenced
- }
-
- let common_path = path.dirname(start_file);
- if (match[1] === 'layout' && !match[2] && !name) {
- // We are a default layout, so we skip the current level
- common_path = path.dirname(common_path);
- }
-
- for (let i = start_idx - 1; i >= 0; i -= 1) {
- const node = nodes[i];
- const file = /** @type {string} */ (node.component || node.shared || node.server);
-
- const current_path = path.dirname(file);
- const common_path_length = common_path.split('/').length;
- const current_path_length = current_path.split('/').length;
-
- if (common_path_length < current_path_length) {
- // this is a layout in a different tree
- continue;
- } else if (common_path_length > current_path_length) {
- // we've gone back up a folder level
- common_path = path.dirname(common_path);
- }
- if (common_path !== current_path) {
- // this is a layout in a different tree
- continue;
- }
- if (
- path.basename(file, path.extname(file)).split('@')[0] !==
- '+layout' + (name ? `-${name}` : '')
- ) {
- // this is not the layout we are searching for
- continue;
- }
-
- // matching parent layout found
- let folder_depth_diff =
- posixify(path.relative(path.dirname(start_file), common_path + '/$types.js')).split('/')
- .length - 1;
- return {
- key: path.dirname(file).slice(routes_dir.length + 1),
- name,
- folder_depth_diff
- };
- }
-}
diff --git a/packages/kit/src/core/sync/write_types.spec.js b/packages/kit/src/core/sync/write_types.spec.js
index 4f0a85d140b7..bb56c892cb8c 100644
--- a/packages/kit/src/core/sync/write_types.spec.js
+++ b/packages/kit/src/core/sync/write_types.spec.js
@@ -1,7 +1,6 @@
import { test } from 'uvu';
import * as assert from 'uvu/assert';
-import ts from 'typescript';
-import { find_nearest_layout, tweak_types } from './write_types.js';
+import { tweak_types } from './write_types.js';
test('Rewrites types for a TypeScript module', () => {
const source = `
@@ -12,7 +11,7 @@ test('Rewrites types for a TypeScript module', () => {
};
`;
- const rewritten = tweak_types(ts, source, new Set(['GET']));
+ const rewritten = tweak_types(source, new Set(['GET']));
assert.equal(rewritten?.exports, ['GET']);
assert.equal(
@@ -36,7 +35,7 @@ test('Rewrites types for a TypeScript module without param', () => {
};
`;
- const rewritten = tweak_types(ts, source, new Set(['GET']));
+ const rewritten = tweak_types(source, new Set(['GET']));
assert.equal(rewritten?.exports, ['GET']);
assert.equal(
@@ -61,7 +60,7 @@ test('Rewrites types for a JavaScript module with `function`', () => {
};
`;
- const rewritten = tweak_types(ts, source, new Set(['GET']));
+ const rewritten = tweak_types(source, new Set(['GET']));
assert.equal(rewritten?.exports, ['GET']);
assert.equal(
@@ -87,7 +86,7 @@ test('Rewrites types for a JavaScript module with `const`', () => {
};
`;
- const rewritten = tweak_types(ts, source, new Set(['GET']));
+ const rewritten = tweak_types(source, new Set(['GET']));
assert.equal(rewritten?.exports, ['GET']);
assert.equal(
@@ -103,77 +102,4 @@ test('Rewrites types for a JavaScript module with `const`', () => {
);
});
-/** @type {import('types').PageNode[]} */
-const nodes = [
- { component: 'src/routes/+layout.svelte' }, // 0
- { component: 'src/routes/+layout-named2@default.svelte' }, // 1
- { component: 'src/routes/+layout-named.svelte' }, // 2
- {
- shared: 'src/routes/+page.js',
- component: 'src/routes/+page@named2.svelte'
- }, // 3
- {
- server: 'src/routes/docs/+layout.server.js',
- component: 'src/routes/docs/+layout.svelte'
- }, // 4
- { shared: 'src/routes/docs/+page.js' }, // 5
- {
- server: 'src/routes/faq/+page.server.js',
- component: 'src/routes/faq/+page.svelte'
- }, // 6
- {
- shared: 'src/routes/search/+page.js',
- server: 'src/routes/search/+page.server.js',
- component: 'src/routes/search/+page.svelte'
- }, // 7
- {
- server: 'src/routes/docs/[slug]/+page.server.js',
- component: 'src/routes/docs/[slug]/+page.svelte'
- }, // 8
- {
- server: 'src/routes/docs/[slug]/hi/+page.server.js',
- component: 'src/routes/docs/[slug]/hi/+page@named.svelte'
- } // 9
-];
-
-test('Finds nearest layout (nested)', () => {
- assert.equal(find_nearest_layout('src/routes', nodes, 8), {
- key: 'docs',
- folder_depth_diff: 1,
- name: ''
- });
-});
-
-test('Finds nearest layout (root)', () => {
- assert.equal(find_nearest_layout('src/routes', nodes, 6), {
- key: '',
- folder_depth_diff: 1,
- name: ''
- });
-});
-
-test('Finds nearest layout (named)', () => {
- assert.equal(find_nearest_layout('src/routes', nodes, 9), {
- key: '',
- folder_depth_diff: 3,
- name: 'named'
- });
-});
-
-test('Finds nearest named layout from layout', () => {
- assert.equal(find_nearest_layout('src/routes', nodes, 1), {
- key: '',
- folder_depth_diff: 0,
- name: ''
- });
-});
-
-test('Finds nearest layout (recursively named)', () => {
- assert.equal(find_nearest_layout('src/routes', nodes, 3), {
- key: '',
- folder_depth_diff: 0,
- name: 'named2'
- });
-});
-
test.run();
diff --git a/packages/kit/src/runtime/client/ambient.d.ts b/packages/kit/src/runtime/client/ambient.d.ts
index 7560947d5fb1..74164d55c347 100644
--- a/packages/kit/src/runtime/client/ambient.d.ts
+++ b/packages/kit/src/runtime/client/ambient.d.ts
@@ -7,11 +7,12 @@ declare module '__GENERATED__/client-manifest.js' {
export const nodes: CSRPageNodeLoader[];
/**
- * A map of `[routeId: string]: [errors, layouts, page]` tuples, which
+ * A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which
* is parsed into an array of routes on startup. The numbers refer to the
- * indices in `nodes`.
+ * indices in `nodes`. The route layout and error nodes are not referenced,
+ * they are always number 0 and 1 and always apply.
*/
- export const dictionary: Record;
+ export const dictionary: Record;
export const matchers: Record;
}
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 5a86ff58d570..d74fe9daba56 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -825,15 +825,10 @@ export function create_client({ target, base, trailing_slash }) {
}
}
- // TODO post-https://github.com/sveltejs/kit/discussions/6124, this will
- // no longer be necessary — if we get here, it's because the root layout
- // load function failed, which means we have to fall back to the server
- return await load_root_error_page({
- status,
- error,
- url,
- routeId: route.id
- });
+ // if we get here, it's because the root `load` function failed,
+ // and we need to fall back to the server
+ native_navigation(url);
+ return;
}
} else {
// push an empty slot so we can rewind past gaps to the
diff --git a/packages/kit/src/runtime/client/parse.js b/packages/kit/src/runtime/client/parse.js
index 6acf39b64d10..ec22a49b852e 100644
--- a/packages/kit/src/runtime/client/parse.js
+++ b/packages/kit/src/runtime/client/parse.js
@@ -2,14 +2,19 @@ import { exec, parse_route_id } from '../../utils/routing.js';
/**
* @param {import('types').CSRPageNodeLoader[]} nodes
- * @param {Record} dictionary
+ * @param {typeof import('__GENERATED__/client-manifest.js').dictionary} dictionary
* @param {Record boolean>} matchers
* @returns {import('types').CSRRoute[]}
*/
export function parse(nodes, dictionary, matchers) {
- return Object.entries(dictionary).map(([id, [errors, layouts, leaf, uses_server_data]]) => {
+ return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
const { pattern, names, types } = parse_route_id(id);
+ // whether or not the route uses the server data is
+ // encoded using the ones' complement, to save space
+ const uses_server_data = leaf < 0;
+ if (uses_server_data) leaf = ~leaf;
+
const route = {
id,
/** @param {string} path */
@@ -17,10 +22,10 @@ export function parse(nodes, dictionary, matchers) {
const match = pattern.exec(path);
if (match) return exec(match, names, types, matchers);
},
- errors: errors.map((n) => nodes[n]),
- layouts: layouts.map((n) => nodes[n]),
+ errors: [1, ...(errors || [])].map((n) => nodes[n]),
+ layouts: [0, ...(layouts || [])].map((n) => nodes[n]),
leaf: nodes[leaf],
- uses_server_data: !!uses_server_data
+ uses_server_data
};
// bit of a hack, but ensures that layout/error node lists are the same
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index 9d112214a478..135e22ff8072 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -3,14 +3,12 @@ import { check_method_names, method_not_allowed } from './utils.js';
/**
* @param {import('types').RequestEvent} event
- * @param {import('types').SSREndpoint} route
+ * @param {import('types').SSREndpoint} mod
* @returns {Promise}
*/
-export async function render_endpoint(event, route) {
+export async function render_endpoint(event, mod) {
const method = /** @type {import('types').HttpMethod} */ (event.request.method);
- const mod = await route.load();
-
// TODO: Remove for 1.0
check_method_names(mod);
@@ -21,12 +19,6 @@ export async function render_endpoint(event, route) {
}
if (!handler) {
- if (event.request.headers.get('x-sveltekit-load')) {
- // TODO would be nice to avoid these requests altogether,
- // by noting whether or not page endpoints export `get`
- return new Response(undefined, { status: 204 });
- }
-
return method_not_allowed(mod, method);
}
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index af7e56a84f72..42d990ba2b52 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -93,7 +93,7 @@ export async function respond(request, options, state) {
}
if (route) {
- if (route.type === 'page') {
+ if (route.page) {
const normalized = normalize_path(url.pathname, options.trailing_slash);
if (normalized !== url.pathname && !state.prerendering?.fallback) {
@@ -253,9 +253,9 @@ export async function respond(request, options, state) {
if (route) {
/** @type {Response} */
let response;
- if (is_data_request && route.type === 'page') {
+ if (is_data_request && route.page) {
try {
- const node_ids = [...route.layouts, route.leaf];
+ const node_ids = [...route.page.layouts, route.page.leaf];
const invalidated =
request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ??
@@ -317,7 +317,8 @@ export async function respond(request, options, state) {
throw error;
}
- length = i + 1; // don't include nodes after first error
+ // Math.min because array isn't guaranteed to resolve in order
+ length = Math.min(length, i + 1);
if (error instanceof HttpError) {
return /** @type {import('types').ServerErrorNode} */ ({
@@ -358,11 +359,14 @@ export async function respond(request, options, state) {
response = json(error_to_pojo(error, options.get_stack), { status: 500 });
}
}
+ } else if (route.page) {
+ response = await render_page(event, route, route.page, options, state, resolve_opts);
+ } else if (route.endpoint) {
+ response = await render_endpoint(event, await route.endpoint());
} else {
- response =
- route.type === 'endpoint'
- ? await render_endpoint(event, route)
- : await render_page(event, route, options, state, resolve_opts);
+ // a route will always have a page or an endpoint, but TypeScript
+ // doesn't know that
+ throw new Error('This should never happen');
}
if (!is_data_request) {
diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js
index 5ae3b3206f62..e6ef5aae7c89 100644
--- a/packages/kit/src/runtime/server/page/fetch.js
+++ b/packages/kit/src/runtime/server/page/fetch.js
@@ -9,7 +9,7 @@ import { domain_matches, path_matches } from './cookie.js';
* event: import('types').RequestEvent;
* options: import('types').SSROptions;
* state: import('types').SSRState;
- * route: import('types').SSRPage | import('types').SSRErrorPage;
+ * route: import('types').SSRRoute | import('types').SSRErrorPage;
* }} opts
*/
export function create_fetch({ event, options, state, route }) {
diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js
index 744af2f2d15c..d0df231da30b 100644
--- a/packages/kit/src/runtime/server/page/index.js
+++ b/packages/kit/src/runtime/server/page/index.js
@@ -17,13 +17,14 @@ import { load_data, load_server_data } from './load_data.js';
/**
* @param {import('types').RequestEvent} event
- * @param {import('types').SSRPage} route
+ * @param {import('types').SSRRoute} route
+ * @param {import('types').PageNodeIndexes} page
* @param {import('types').SSROptions} options
* @param {import('types').SSRState} state
* @param {import('types').RequiredResolveOptions} resolve_opts
* @returns {Promise}
*/
-export async function render_page(event, route, options, state, resolve_opts) {
+export async function render_page(event, route, page, options, state, resolve_opts) {
if (state.initiator === route) {
// infinite request cycle detected
return new Response(`Not found: ${event.url.pathname}`, {
@@ -41,7 +42,7 @@ export async function render_page(event, route, options, state, resolve_opts) {
event.request.method !== 'GET' &&
event.request.method !== 'HEAD'
) {
- const node = await options.manifest._.nodes[route.leaf]();
+ const node = await options.manifest._.nodes[page.leaf]();
if (node.server) {
return handle_json_request(event, options, node.server);
}
@@ -52,8 +53,8 @@ export async function render_page(event, route, options, state, resolve_opts) {
try {
const nodes = await Promise.all([
// we use == here rather than === because [undefined] serializes as "[null]"
- ...route.layouts.map((n) => (n == undefined ? n : options.manifest._.nodes[n]())),
- options.manifest._.nodes[route.leaf]()
+ ...page.layouts.map((n) => (n == undefined ? n : options.manifest._.nodes[n]())),
+ options.manifest._.nodes[page.leaf]()
]);
const leaf_node = /** @type {import('types').SSRNode} */ (nodes.at(-1));
@@ -244,8 +245,8 @@ export async function render_page(event, route, options, state, resolve_opts) {
const status = error instanceof HttpError ? error.status : 500;
while (i--) {
- if (route.errors[i]) {
- const index = /** @type {number} */ (route.errors[i]);
+ if (page.errors[i]) {
+ const index = /** @type {number} */ (page.errors[i]);
const node = await options.manifest._.nodes[index]();
let j = i;
diff --git a/packages/kit/src/utils/array.js b/packages/kit/src/utils/array.js
new file mode 100644
index 000000000000..08f93845149b
--- /dev/null
+++ b/packages/kit/src/utils/array.js
@@ -0,0 +1,9 @@
+/**
+ * Removes nullish values from an array.
+ *
+ * @template T
+ * @param {Array} arr
+ */
+export function compact(arr) {
+ return arr.filter(/** @returns {val is NonNullable} */ (val) => val != null);
+}
diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js
index 826c75a4d602..5f6dbd4fbc4f 100644
--- a/packages/kit/src/utils/routing.js
+++ b/packages/kit/src/utils/routing.js
@@ -12,12 +12,21 @@ export function parse_route_id(id) {
// const add_trailing_slash = !/\.[a-z]+$/.test(key);
let add_trailing_slash = true;
+ if (/\]\[/.test(id)) {
+ throw new Error(`Invalid route ${id} — parameters must be separated`);
+ }
+
+ if (count_occurrences('[', id) !== count_occurrences(']', id)) {
+ throw new Error(`Invalid route ${id} — brackets are unbalanced`);
+ }
+
const pattern =
id === ''
? /^\/$/
: new RegExp(
`^${id
- .split(/(?:@[a-zA-Z0-9_-]+)?(?:\/|$)/)
+ .split(/(?:\/|$)/)
+ .filter(is_no_group)
.map((segment, i, segments) => {
const decoded_segment = decodeURIComponent(segment);
// special case — /[...rest]/ could contain zero segments
@@ -79,6 +88,13 @@ export function parse_route_id(id) {
return { pattern, names, types };
}
+/**
+ * @param {string} segment
+ */
+export function is_no_group(segment) {
+ return !/^\([^)]+\)$/.test(segment);
+}
+
/**
* @param {RegExpMatchArray} match
* @param {string[]} names
@@ -106,3 +122,15 @@ export function exec(match, names, types, matchers) {
return params;
}
+
+/**
+ * @param {string} needle
+ * @param {string} haystack
+ */
+function count_occurrences(needle, haystack) {
+ let count = 0;
+ for (let i = 0; i < haystack.length; i += 1) {
+ if (haystack[i] === needle) count += 1;
+ }
+ return count;
+}
diff --git a/packages/kit/src/vite/build/build_server.js b/packages/kit/src/vite/build/build_server.js
index cb126b83c40d..793179b1c9f2 100644
--- a/packages/kit/src/vite/build/build_server.js
+++ b/packages/kit/src/vite/build/build_server.js
@@ -158,8 +158,8 @@ export async function build_server(options, client) {
// add entry points for every endpoint...
manifest_data.routes.forEach((route) => {
- if (route.type === 'endpoint') {
- const resolved = path.resolve(cwd, route.file);
+ if (route.endpoint) {
+ const resolved = path.resolve(cwd, route.endpoint.file);
const relative = decodeURIComponent(path.relative(config.kit.files.routes, resolved));
const name = posixify(path.join('entries/endpoints', relative.replace(/\.js$/, '')));
input[name] = resolved;
@@ -326,10 +326,16 @@ function get_methods(cwd, output, manifest_data) {
/** @type {Record} */
const methods = {};
manifest_data.routes.forEach((route) => {
- const file = route.type === 'endpoint' ? route.file : route.leaf.server;
+ if (route.endpoint) {
+ if (lookup[route.endpoint.file]) {
+ methods[route.endpoint.file] = lookup[route.endpoint.file].filter(is_http_method);
+ }
+ }
- if (file && lookup[file]) {
- methods[file] = lookup[file].filter(is_http_method);
+ if (route.leaf?.server) {
+ if (lookup[route.leaf.server]) {
+ methods[route.leaf.server] = lookup[route.leaf.server].filter(is_http_method);
+ }
}
});
diff --git a/packages/kit/src/vite/dev/index.js b/packages/kit/src/vite/dev/index.js
index 9cbfb128e65d..c0155862e0d6 100644
--- a/packages/kit/src/vite/dev/index.js
+++ b/packages/kit/src/vite/dev/index.js
@@ -7,12 +7,12 @@ import { getRequest, setResponse } from '../../node/index.js';
import { installPolyfills } from '../../node/polyfills.js';
import { coalesce_to_error } from '../../utils/error.js';
import { posixify } from '../../utils/filesystem.js';
-import { parse_route_id } from '../../utils/routing.js';
import { load_template } from '../../core/config/index.js';
import { SVELTE_KIT_ASSETS } from '../../core/constants.js';
import * as sync from '../../core/sync/sync.js';
import { get_mime_lookup, runtime_base, runtime_prefix } from '../../core/utils.js';
import { get_env, prevent_illegal_vite_imports, resolve_entry } from '../utils.js';
+import { compact } from '../../utils/array.js';
// Vite doesn't expose this so we just copy the list for now
// https://github.com/vitejs/vite/blob/3edd1af56e980aef56641a5a51cf2932bb580d41/packages/vite/src/node/plugins/css.ts#L96
@@ -149,36 +149,27 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
return result;
};
}),
- routes: manifest_data.routes.map((route) => {
- const { pattern, names, types } = parse_route_id(route.id);
+ routes: compact(
+ manifest_data.routes.map((route) => {
+ if (!route.page && !route.endpoint) return null;
+
+ const endpoint = route.endpoint;
- if (route.type === 'page') {
return {
- type: 'page',
id: route.id,
- pattern,
- names,
- types,
- errors: route.errors.map((id) => (id ? manifest_data.nodes.indexOf(id) : undefined)),
- layouts: route.layouts.map((id) =>
- id ? manifest_data.nodes.indexOf(id) : undefined
- ),
- leaf: manifest_data.nodes.indexOf(route.leaf)
+ pattern: route.pattern,
+ names: route.names,
+ types: route.types,
+ page: route.page,
+ endpoint: endpoint
+ ? async () => {
+ const url = path.resolve(cwd, endpoint.file);
+ return await vite.ssrLoadModule(url);
+ }
+ : null
};
- }
-
- return {
- type: 'endpoint',
- id: route.id,
- pattern,
- names,
- types,
- load: async () => {
- const url = path.resolve(cwd, route.file);
- return await vite.ssrLoadModule(url);
- }
- };
- }),
+ })
+ ),
matchers: async () => {
/** @type {Record} */
const matchers = {};
@@ -229,15 +220,16 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
to_run();
}, 100);
};
+
// Debounce add/unlink events because in case of folder deletion or moves
// they fire in rapid succession, causing needless invocations.
watch('add', () => debounce(update_manifest));
watch('unlink', () => debounce(update_manifest));
watch('change', (file) => {
// Don't run for a single file if the whole manifest is about to get updated
- if (!timeout) {
- sync.update(svelte_config, manifest_data, file);
- }
+ if (timeout) return;
+
+ sync.update(svelte_config, manifest_data, file);
});
const assets = svelte_config.kit.paths.assets ? SVELTE_KIT_ASSETS : svelte_config.kit.paths.base;
diff --git a/packages/kit/test/apps/basics/src/routes/+layout-blank.svelte b/packages/kit/test/apps/basics/src/routes/+layout-blank.svelte
deleted file mode 100644
index 4fa864ce7aa9..000000000000
--- a/packages/kit/test/apps/basics/src/routes/+layout-blank.svelte
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/packages/kit/test/apps/basics/src/routes/nested-layout/reset/+layout@blank.svelte b/packages/kit/test/apps/basics/src/routes/nested-layout/reset/+layout@.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/nested-layout/reset/+layout@blank.svelte
rename to packages/kit/test/apps/basics/src/routes/nested-layout/reset/+layout@.svelte
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index ea04845a3982..90f8f66760f4 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -981,10 +981,9 @@ test.describe('Nested layouts', () => {
test('resets layout', async ({ page }) => {
await page.goto('/nested-layout/reset');
- expect(await page.evaluate(() => document.querySelector('footer'))).toBe(null);
- expect(await page.evaluate(() => document.querySelector('p'))).toBe(null);
expect(await page.textContent('h1')).toBe('Layout reset');
expect(await page.textContent('h2')).toBe('Hello');
+ expect(await page.$('#nested')).toBeNull();
});
test('renders the closest error page', async ({ page, clicknav }) => {
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 2e43e299baff..7786b946d2bb 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -80,13 +80,6 @@ export type CSRRoute = {
uses_server_data: boolean;
};
-export interface EndpointData {
- type: 'endpoint';
- id: string;
- pattern: RegExp;
- file: string;
-}
-
export type GetParams = (match: RegExpExecArray) => Record;
export interface Hooks {
@@ -123,18 +116,12 @@ export interface MethodOverride {
}
export interface PageNode {
+ depth: number;
component?: string; // TODO supply default component if it's missing (bit of an edge case)
shared?: string;
server?: string;
-}
-
-export interface PageData {
- type: 'page';
- id: string;
- pattern: RegExp;
- errors: Array;
- layouts: Array;
- leaf: PageNode;
+ parent_id?: string;
+ parent?: PageNode;
}
export type PayloadScriptAttributes =
@@ -167,7 +154,34 @@ export interface Respond {
(request: Request, options: SSROptions, state: SSRState): Promise;
}
-export type RouteData = PageData | EndpointData;
+/**
+ * Represents a route segement in the app. It can either be an intermediate node
+ * with only layout/error pages, or a leaf, at which point either `page` and `leaf`
+ * or `endpoint` is set.
+ */
+export interface RouteData {
+ id: string;
+ parent: RouteData | null;
+
+ segment: string;
+ pattern: RegExp;
+ names: string[];
+ types: string[];
+
+ layout: PageNode | null;
+ error: PageNode | null;
+ leaf: PageNode | null;
+
+ page: {
+ layouts: Array;
+ errors: Array;
+ leaf: number;
+ } | null;
+
+ endpoint: {
+ file: string;
+ } | null;
+}
export type ServerData =
| {
@@ -228,15 +242,6 @@ export interface SSRComponent {
export type SSRComponentLoader = () => Promise;
-export interface SSREndpoint {
- type: 'endpoint';
- id: string;
- pattern: RegExp;
- names: string[];
- types: string[];
- load(): Promise>>;
-}
-
export interface SSRNode {
component: SSRComponentLoader;
/** index into the `components` array in client-manifest.js */
@@ -309,27 +314,33 @@ export interface SSROptions {
trailing_slash: TrailingSlash;
}
-export interface SSRPage {
- type: 'page';
- id: string;
- pattern: RegExp;
- names: string[];
- types: string[];
+export interface SSRErrorPage {
+ id: '__error';
+}
+
+export interface PageNodeIndexes {
errors: Array;
layouts: Array;
leaf: number;
}
-export interface SSRErrorPage {
- id: '__error';
-}
+export type SSREndpoint = Partial>;
-export type SSRRoute = SSREndpoint | SSRPage;
+export interface SSRRoute {
+ id: string;
+ pattern: RegExp;
+ names: string[];
+ types: string[];
+
+ page: PageNodeIndexes | null;
+
+ endpoint: (() => Promise) | null;
+}
export interface SSRState {
fallback?: string;
getClientAddress: () => string;
- initiator?: SSRPage | SSRErrorPage;
+ initiator?: SSRRoute | SSRErrorPage;
platform?: any;
prerendering?: PrerenderOptions;
}