element in src/app.html
- target: '#svelte'
+ target: '#svelte',
+
+ alternateRoutes: localizeRoutes
}
};
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 80a38033597b..6b788bbd91f8 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,13 @@
# @sveltejs/kit
+## 1.0.0-next.109
+
+### Patch Changes
+
+- 261ee1c: Update compatible Node versions
+- ec156c6: let hash only changes be handled by router
+- 586785d: Allow passing HTTPS key pair in Vite section of config
+
## 1.0.0-next.108
### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 063492884754..a2d132977f90 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
{
"name": "@sveltejs/kit",
- "version": "1.0.0-next.108",
+ "version": "1.0.0-next.109",
"type": "module",
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.10",
diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js
index 3137728009e7..3963776f08ef 100644
--- a/packages/kit/src/core/build/index.js
+++ b/packages/kit/src/core/build/index.js
@@ -337,6 +337,7 @@ async function build_server(
const params = get_params(route.params);
return `{
+ id: ${s(route.id)},
type: 'page',
pattern: ${route.pattern},
params: ${params},
diff --git a/packages/kit/src/core/create_app/index.js b/packages/kit/src/core/create_app/index.js
index 299ea247d056..ccb57510d720 100644
--- a/packages/kit/src/core/create_app/index.js
+++ b/packages/kit/src/core/create_app/index.js
@@ -86,7 +86,8 @@ function generate_client_manifest(manifest_data, base) {
'})';
const tuple = [route.pattern, get_indices(route.a), get_indices(route.b)];
- if (params) tuple.push(params);
+ tuple.push(params);
+ tuple.push(route.id);
return `// ${route.a[route.a.length - 1]}\n\t\t[${tuple.join(', ')}]`;
} else {
@@ -149,6 +150,7 @@ function generate_app(manifest_data, base) {
// stores
export let stores;
export let page;
+ export let routes;
export let components;
${levels.map((l) => `export let props_${l} = null;`).join('\n\t\t\t')}
@@ -158,6 +160,8 @@ function generate_app(manifest_data, base) {
$: stores.page.set(page);
afterUpdate(stores.page.notify);
+ if (routes) setContext('__svelte_routes__', routes);
+
let mounted = false;
let navigated = false;
let title = null;
diff --git a/packages/kit/src/core/create_manifest_data/index.js b/packages/kit/src/core/create_manifest_data/index.js
index e03d2223e58a..357295798bb0 100644
--- a/packages/kit/src/core/create_manifest_data/index.js
+++ b/packages/kit/src/core/create_manifest_data/index.js
@@ -52,11 +52,10 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
/**
* @param {string} dir
* @param {Part[][]} parent_segments
- * @param {string[]} parent_params
* @param {string[]} layout_stack // accumulated __layout.svelte components
* @param {string[]} error_stack // accumulated __error.svelte components
*/
- function walk(dir, parent_segments, parent_params, layout_stack, error_stack) {
+ function walk(dir, parent_segments, layout_stack, error_stack) {
/** @type {Item[]} */
const items = fs
.readdirSync(dir)
@@ -157,9 +156,6 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
segments.push(item.parts);
}
- const params = parent_params.slice();
- params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content));
-
if (item.is_dir) {
const layout_reset = find_layout('__layout.reset', item.file);
const layout = find_layout('__layout', item.file);
@@ -176,54 +172,73 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
walk(
path.join(dir, item.basename),
segments,
- params,
layout_reset ? [layout_reset] : layout_stack.concat(layout),
layout_reset ? [error] : error_stack.concat(error)
);
- } else if (item.is_page) {
- components.push(item.file);
-
- const a = layout_stack.concat(item.file);
- const b = error_stack;
-
- const pattern = get_pattern(segments, true);
+ } else {
+ const alternates = config.kit.alternateRoutes
+ ? config.kit.alternateRoutes(segments, item.is_page ? 'page' : 'endpoint')
+ : [segments];
+
+ if (item.is_page) {
+ const id = components.length.toString();
+ components.push(item.file);
+
+ const a = layout_stack.concat(item.file);
+ const b = error_stack;
+
+ alternates.forEach((segments) => {
+ const pattern = get_pattern(segments, true);
+ const params = segments.flatMap((parts) =>
+ parts.filter((p) => p.dynamic).map((p) => p.content)
+ );
+
+ let i = a.length;
+ while (i--) {
+ if (!b[i] && !a[i]) {
+ b.splice(i, 1);
+ a.splice(i, 1);
+ }
+ }
- let i = a.length;
- while (i--) {
- if (!b[i] && !a[i]) {
- b.splice(i, 1);
- a.splice(i, 1);
- }
- }
+ i = b.length;
+ while (i--) {
+ if (b[i]) break;
+ }
- i = b.length;
- while (i--) {
- if (b[i]) break;
+ b.splice(i + 1);
+
+ const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
+ ? `/${segments.map((segment) => segment[0].content).join('/')}${
+ config.kit.trailingSlash === 'always' && segments.length > 0 ? '/' : ''
+ }`
+ : null;
+
+ routes.push({
+ id,
+ type: 'page',
+ pattern,
+ params,
+ path,
+ a,
+ b
+ });
+ });
+ } else {
+ alternates.forEach((segments) => {
+ const pattern = get_pattern(segments, !item.route_suffix);
+ const params = segments.flatMap((parts) =>
+ parts.filter((p) => p.dynamic).map((p) => p.content)
+ );
+
+ routes.push({
+ type: 'endpoint',
+ pattern,
+ file: item.file,
+ params
+ });
+ });
}
-
- b.splice(i + 1);
-
- const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
- ? `/${segments.map((segment) => segment[0].content).join('/')}`
- : null;
-
- routes.push({
- type: 'page',
- pattern,
- params,
- path,
- a,
- b
- });
- } else {
- const pattern = get_pattern(segments, !item.route_suffix);
-
- routes.push({
- type: 'endpoint',
- pattern,
- file: item.file,
- params
- });
}
});
}
@@ -235,7 +250,7 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
components.push(layout, error);
- walk(config.kit.files.routes, [], [], [layout], [error]);
+ walk(config.kit.files.routes, [], [layout], [error]);
const assets_dir = config.kit.files.assets;
diff --git a/packages/kit/src/core/create_manifest_data/index.spec.js b/packages/kit/src/core/create_manifest_data/index.spec.js
index 2b543a4613e8..9843529cd7ef 100644
--- a/packages/kit/src/core/create_manifest_data/index.spec.js
+++ b/packages/kit/src/core/create_manifest_data/index.spec.js
@@ -44,6 +44,7 @@ test('creates routes', () => {
assert.equal(routes, [
{
+ id: '2',
type: 'page',
pattern: /^\/$/,
params: [],
@@ -53,6 +54,7 @@ test('creates routes', () => {
},
{
+ id: '3',
type: 'page',
pattern: /^\/about\/?$/,
params: [],
@@ -69,6 +71,7 @@ test('creates routes', () => {
},
{
+ id: '4',
type: 'page',
pattern: /^\/blog\/?$/,
params: [],
@@ -85,6 +88,7 @@ test('creates routes', () => {
},
{
+ id: '5',
type: 'page',
pattern: /^\/blog\/([^/]+?)\/?$/,
params: ['slug'],
@@ -108,6 +112,7 @@ test('creates routes with layout', () => {
assert.equal(routes, [
{
+ id: '2',
type: 'page',
pattern: /^\/$/,
params: [],
@@ -117,6 +122,7 @@ test('creates routes with layout', () => {
},
{
+ id: '4',
type: 'page',
pattern: /^\/foo\/?$/,
params: [],
@@ -263,6 +269,7 @@ test('works with custom extensions', () => {
assert.equal(routes, [
{
+ id: '2',
type: 'page',
pattern: /^\/$/,
params: [],
@@ -272,6 +279,7 @@ test('works with custom extensions', () => {
},
{
+ id: '3',
type: 'page',
pattern: /^\/about\/?$/,
params: [],
@@ -288,6 +296,7 @@ test('works with custom extensions', () => {
},
{
+ id: '4',
type: 'page',
pattern: /^\/blog\/?$/,
params: [],
@@ -304,6 +313,7 @@ test('works with custom extensions', () => {
},
{
+ id: '5',
type: 'page',
pattern: /^\/blog\/([^/]+?)\/?$/,
params: ['slug'],
@@ -336,6 +346,7 @@ test('includes nested error components', () => {
assert.equal(routes, [
{
+ id: '6',
type: 'page',
pattern: /^\/foo\/bar\/baz\/?$/,
params: [],
@@ -362,6 +373,7 @@ test('resets layout', () => {
assert.equal(routes, [
{
+ id: '2',
type: 'page',
pattern: /^\/$/,
params: [],
@@ -370,6 +382,7 @@ test('resets layout', () => {
b: [error]
},
{
+ id: '4',
type: 'page',
pattern: /^\/foo\/?$/,
params: [],
@@ -382,6 +395,7 @@ test('resets layout', () => {
b: [error]
},
{
+ id: '7',
type: 'page',
pattern: /^\/foo\/bar\/?$/,
params: [],
diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js
index 349673bfe358..85b3987e803b 100644
--- a/packages/kit/src/core/dev/index.js
+++ b/packages/kit/src/core/dev/index.js
@@ -334,6 +334,7 @@ class Watcher extends EventEmitter {
routes: manifest_data.routes.map((route) => {
if (route.type === 'page') {
return {
+ id: route.id,
type: 'page',
pattern: route.pattern,
params: get_params(route.params),
diff --git a/packages/kit/src/core/load_config/index.spec.js b/packages/kit/src/core/load_config/index.spec.js
index 4678a7300126..285373d3141d 100644
--- a/packages/kit/src/core/load_config/index.spec.js
+++ b/packages/kit/src/core/load_config/index.spec.js
@@ -12,6 +12,7 @@ test('fills in defaults', () => {
extensions: ['.svelte'],
kit: {
adapter: null,
+ alternateRoutes: null,
amp: false,
appDir: '_app',
files: {
@@ -96,6 +97,7 @@ test('fills in partial blanks', () => {
extensions: ['.svelte'],
kit: {
adapter: null,
+ alternateRoutes: null,
amp: false,
appDir: '_app',
files: {
diff --git a/packages/kit/src/core/load_config/options.js b/packages/kit/src/core/load_config/options.js
index 8c63e9748885..18af95f36710 100644
--- a/packages/kit/src/core/load_config/options.js
+++ b/packages/kit/src/core/load_config/options.js
@@ -54,6 +54,18 @@ const options = {
}
},
+ alternateRoutes: {
+ type: 'leaf',
+ default: null,
+ validate: (option, keypath) => {
+ if (typeof option !== 'function') {
+ throw new Error(`${keypath} must be a function that processes route segments`);
+ }
+
+ return option;
+ }
+ },
+
amp: expect_boolean(false),
appDir: expect_string('_app', false),
diff --git a/packages/kit/src/core/load_config/test/index.js b/packages/kit/src/core/load_config/test/index.js
index c86f90e55307..1a9b00e249e8 100644
--- a/packages/kit/src/core/load_config/test/index.js
+++ b/packages/kit/src/core/load_config/test/index.js
@@ -22,6 +22,7 @@ async function testLoadDefaultConfig(path) {
extensions: ['.svelte'],
kit: {
adapter: null,
+ alternateRoutes: null,
amp: false,
appDir: '_app',
files: {
diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js
index b8f2266bdac5..a8ae40686363 100644
--- a/packages/kit/src/runtime/app/navigation.js
+++ b/packages/kit/src/runtime/app/navigation.js
@@ -1,5 +1,6 @@
import { router } from '../client/singletons.js';
import { get_base_uri } from '../client/utils.js';
+import { getContext } from 'svelte';
/**
* @param {string} name
@@ -49,3 +50,46 @@ async function prefetchRoutes_(pathnames) {
await Promise.all(promises);
}
+
+/**
+ * @param {RegExp} pattern
+ * @param {string[]} params
+ * @returns {string}
+ */
+function pathFromPattern(pattern, params) {
+ let index = 0;
+ return pattern.source
+ .slice(1, -1)
+ .replace(/\\\//g, '/')
+ .replace(/\(\[\^\/\]\+\?\)/g, () => params[index++])
+ .replace(/\/\?$/, '');
+}
+
+/**
+ * @param {any} value
+ * @return {value is import('types/internal').SSRPage}
+ */
+function isSSRPage(value) {
+ return typeof value === 'object' && value.type === 'page';
+}
+
+/**
+ * @type {import('$app/navigation').alternates}
+ */
+export function alternates(href) {
+ if (!import.meta.env.SSR) {
+ const hrefRoute = router?.routes?.find((route) => route[0].test(href));
+ if (!hrefRoute) return null;
+ const [, ...params] = href.match(hrefRoute[0]);
+ const alternates = router.routes.filter((route) => route[4] === hrefRoute[4]);
+ return alternates.map((route) => pathFromPattern(route[0], params));
+ } else {
+ /** @type {import('types/internal').SSRRoute[]} */
+ const routes = getContext('__svelte_routes__');
+ const hrefRoute = routes.find((route) => route.pattern.test(href));
+ if (!hrefRoute || !isSSRPage(hrefRoute)) return null;
+ const [, ...params] = href.match(hrefRoute.pattern);
+ const alternates = routes.filter((route) => isSSRPage(route) && route.id === hrefRoute.id);
+ return alternates.map((route) => pathFromPattern(route.pattern, params));
+ }
+}
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index e6538758ba6c..85218a4ffc50 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -66,7 +66,8 @@ export async function render_response({
session
},
page,
- components: branch.map(({ node }) => node.module.default)
+ components: branch.map(({ node }) => node.module.default),
+ routes: options.manifest.routes
};
// props_n (instead of props[n]) makes it easy to avoid
diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts
index 9e52c1f250bf..a9883aed8232 100644
--- a/packages/kit/types/ambient-modules.d.ts
+++ b/packages/kit/types/ambient-modules.d.ts
@@ -50,6 +50,10 @@ declare module '$app/navigation' {
* Returns a Promise that resolves when the routes have been prefetched.
*/
export function prefetchRoutes(routes?: string[]): Promise
;
+ /**
+ * Returns alternate routes for the given page
+ */
+ export function alternates(href: string): string[];
}
declare module '$app/paths' {
diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts
index cd3d7e54ade4..a2ecafff8898 100644
--- a/packages/kit/types/config.d.ts
+++ b/packages/kit/types/config.d.ts
@@ -1,5 +1,6 @@
import { Logger, TrailingSlash } from './internal';
import { UserConfig as ViteConfig } from 'vite';
+import { Part } from '../src/core/create_manifest_data';
export type AdapterUtils = {
log: Logger;
@@ -30,6 +31,7 @@ export type Config = {
extensions?: string[];
kit?: {
adapter?: Adapter;
+ alternateRoutes?: (segments: Part[][], type: 'page' | 'endpoint') => Part[][][];
amp?: boolean;
appDir?: string;
files?: {
@@ -68,6 +70,7 @@ export type ValidatedConfig = {
extensions: string[];
kit: {
adapter: Adapter;
+ alternateRoutes: (segments: Part[][], type: 'page' | 'endpoint') => Part[][][];
amp: boolean;
appDir: string;
files: {
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 6e8189f1c9b7..f7f5f42bc9fb 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -73,6 +73,7 @@ export type SSRPagePart = {
export type GetParams = (match: RegExpExecArray) => Record;
export type SSRPage = {
+ id: string;
type: 'page';
pattern: RegExp;
params: GetParams;
@@ -95,7 +96,7 @@ export type SSREndpoint = {
export type SSRRoute = SSREndpoint | SSRPage;
-export type CSRPage = [RegExp, CSRComponentLoader[], CSRComponentLoader[], GetParams?];
+export type CSRPage = [RegExp, CSRComponentLoader[], CSRComponentLoader[], GetParams?, string?];
export type CSREndpoint = [RegExp];
@@ -168,6 +169,7 @@ export type Asset = {
};
export type PageData = {
+ id: string;
type: 'page';
pattern: RegExp;
params: string[];