Skip to content

Commit eba0281

Browse files
committed
Added alternateRoutes() extension point
1 parent 3e4f3ba commit eba0281

File tree

13 files changed

+144
-37
lines changed

13 files changed

+144
-37
lines changed

packages/kit/src/core/config/index.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ test('fills in defaults', () => {
1919
kit: {
2020
adapter: null,
2121
amp: false,
22+
alternateRoutes: null,
2223
appDir: '_app',
2324
files: {
2425
assets: 'static',
@@ -131,6 +132,7 @@ test('fills in partial blanks', () => {
131132
kit: {
132133
adapter: null,
133134
amp: false,
135+
alternateRoutes: null,
134136
appDir: '_app',
135137
files: {
136138
assets: 'public',

packages/kit/src/core/config/options.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ const options = object(
3939

4040
amp: boolean(false),
4141

42+
alternateRoutes: validate(null, (option, keypath) => {
43+
if (typeof option !== 'function') {
44+
throw new Error(`${keypath} must be a function that processes route segments`);
45+
}
46+
47+
return option;
48+
}),
49+
4250
appDir: validate('_app', (input, keypath) => {
4351
assert_string(input, keypath);
4452

packages/kit/src/core/config/test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ test('load default config (esm)', async () => {
2121
kit: {
2222
adapter: null,
2323
amp: false,
24+
alternateRoutes: null,
2425
appDir: '_app',
2526
files: {
2627
assets: join(cwd, 'static'),

packages/kit/src/core/create_app/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ function generate_client_manifest(manifest_data, base) {
8383
'})';
8484
8585
const tuple = [route.pattern, get_indices(route.a), get_indices(route.b)];
86-
if (params) tuple.push(params);
86+
tuple.push(params || '');
87+
tuple.push(route.id);
8788
8889
return `// ${route.a[route.a.length - 1]}\n\t\t[${tuple.join(', ')}]`;
8990
}
@@ -149,6 +150,7 @@ function generate_app(manifest_data) {
149150
// stores
150151
export let stores;
151152
export let page;
153+
export let routes;
152154
153155
export let components;
154156
${levels.map((l) => `export let props_${l} = null;`).join('\n\t\t\t')}
@@ -158,6 +160,8 @@ function generate_app(manifest_data) {
158160
$: stores.page.set(page);
159161
afterUpdate(stores.page.notify);
160162
163+
if (routes) setContext('__svelte_routes__', routes);
164+
161165
let mounted = false;
162166
let navigated = false;
163167
let title = null;

packages/kit/src/core/create_manifest_data/index.js

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -194,50 +194,70 @@ export default function create_manifest_data({
194194
layout_reset ? [error] : error_stack.concat(error)
195195
);
196196
} else if (item.is_page) {
197+
const id = components.length.toString();
198+
const alternates = config.kit.alternateRoutes
199+
? config.kit.alternateRoutes(segments, 'page')
200+
: [segments];
201+
197202
components.push(item.file);
198203

199204
const concatenated = layout_stack.concat(item.file);
200205
const errors = error_stack.slice();
201206

202-
const pattern = get_pattern(segments, true);
203-
204-
let i = concatenated.length;
205-
while (i--) {
206-
if (!errors[i] && !concatenated[i]) {
207-
errors.splice(i, 1);
208-
concatenated.splice(i, 1);
207+
alternates.forEach((segments) => {
208+
const pattern = get_pattern(segments, true);
209+
const params = segments.flatMap((parts) =>
210+
parts.filter((p) => p.dynamic).map((p) => p.content)
211+
);
212+
213+
let i = concatenated.length;
214+
while (i--) {
215+
if (!errors[i] && !concatenated[i]) {
216+
errors.splice(i, 1);
217+
concatenated.splice(i, 1);
218+
}
209219
}
210-
}
211220

212-
i = errors.length;
213-
while (i--) {
214-
if (errors[i]) break;
215-
}
216-
217-
errors.splice(i + 1);
218-
219-
const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
220-
? `/${segments.map((segment) => segment[0].content).join('/')}`
221-
: '';
221+
i = errors.length;
222+
while (i--) {
223+
if (errors[i]) break;
224+
}
222225

223-
routes.push({
224-
type: 'page',
225-
segments: simple_segments,
226-
pattern,
227-
params,
228-
path,
229-
a: /** @type {string[]} */ (concatenated),
230-
b: /** @type {string[]} */ (errors)
226+
errors.splice(i + 1);
227+
228+
const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
229+
? `/${segments.map((segment) => segment[0].content).join('/')}`
230+
: '';
231+
232+
routes.push({
233+
id,
234+
type: 'page',
235+
segments: simple_segments,
236+
pattern,
237+
params,
238+
path,
239+
a: /** @type {string[]} */ (concatenated),
240+
b: /** @type {string[]} */ (errors)
241+
});
231242
});
232243
} else {
233-
const pattern = get_pattern(segments, !item.route_suffix);
234-
235-
routes.push({
236-
type: 'endpoint',
237-
segments: simple_segments,
238-
pattern,
239-
file: item.file,
240-
params
244+
const alternates = config.kit.alternateRoutes
245+
? config.kit.alternateRoutes(segments, 'endpoint')
246+
: [segments];
247+
248+
alternates.forEach((segments) => {
249+
const pattern = get_pattern(segments, !item.route_suffix);
250+
const params = segments.flatMap((parts) =>
251+
parts.filter((p) => p.dynamic).map((p) => p.content)
252+
);
253+
254+
routes.push({
255+
type: 'endpoint',
256+
segments: simple_segments,
257+
pattern,
258+
file: item.file,
259+
params
260+
});
241261
});
242262
}
243263
});

packages/kit/src/core/create_manifest_data/index.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ test('creates routes', () => {
4949

5050
assert.equal(routes, [
5151
{
52+
id: '2',
5253
type: 'page',
5354
segments: [],
5455
pattern: /^\/$/,
@@ -59,6 +60,7 @@ test('creates routes', () => {
5960
},
6061

6162
{
63+
id: '3',
6264
type: 'page',
6365
segments: [{ rest: false, dynamic: false, content: 'about' }],
6466
pattern: /^\/about\/?$/,
@@ -77,6 +79,7 @@ test('creates routes', () => {
7779
},
7880

7981
{
82+
id: '4',
8083
type: 'page',
8184
segments: [{ rest: false, dynamic: false, content: 'blog' }],
8285
pattern: /^\/blog\/?$/,
@@ -98,6 +101,7 @@ test('creates routes', () => {
98101
},
99102

100103
{
104+
id: '5',
101105
type: 'page',
102106
segments: [
103107
{ rest: false, dynamic: false, content: 'blog' },
@@ -125,6 +129,7 @@ test('creates routes with layout', () => {
125129

126130
assert.equal(routes, [
127131
{
132+
id: '2',
128133
type: 'page',
129134
segments: [],
130135
pattern: /^\/$/,
@@ -135,6 +140,7 @@ test('creates routes with layout', () => {
135140
},
136141

137142
{
143+
id: '4',
138144
type: 'page',
139145
segments: [{ rest: false, dynamic: false, content: 'foo' }],
140146
pattern: /^\/foo\/?$/,
@@ -319,6 +325,7 @@ test('works with custom extensions', () => {
319325

320326
assert.equal(routes, [
321327
{
328+
id: '2',
322329
type: 'page',
323330
segments: [],
324331
pattern: /^\/$/,
@@ -329,6 +336,7 @@ test('works with custom extensions', () => {
329336
},
330337

331338
{
339+
id: '3',
332340
type: 'page',
333341
segments: [{ rest: false, dynamic: false, content: 'about' }],
334342
pattern: /^\/about\/?$/,
@@ -347,6 +355,7 @@ test('works with custom extensions', () => {
347355
},
348356

349357
{
358+
id: '4',
350359
type: 'page',
351360
segments: [{ rest: false, dynamic: false, content: 'blog' }],
352361
pattern: /^\/blog\/?$/,
@@ -368,6 +377,7 @@ test('works with custom extensions', () => {
368377
},
369378

370379
{
380+
id: '5',
371381
type: 'page',
372382
segments: [
373383
{ rest: false, dynamic: false, content: 'blog' },
@@ -404,6 +414,7 @@ test('includes nested error components', () => {
404414

405415
assert.equal(routes, [
406416
{
417+
id: '6',
407418
type: 'page',
408419
segments: [
409420
{ rest: false, dynamic: false, content: 'foo' },
@@ -435,6 +446,7 @@ test('resets layout', () => {
435446

436447
assert.equal(routes, [
437448
{
449+
id: '2',
438450
type: 'page',
439451
segments: [],
440452
pattern: /^\/$/,
@@ -444,6 +456,7 @@ test('resets layout', () => {
444456
b: [error]
445457
},
446458
{
459+
id: '4',
447460
type: 'page',
448461
segments: [{ rest: false, dynamic: false, content: 'foo' }],
449462
pattern: /^\/foo\/?$/,
@@ -457,6 +470,7 @@ test('resets layout', () => {
457470
b: [error]
458471
},
459472
{
473+
id: '7',
460474
type: 'page',
461475
segments: [
462476
{ rest: false, dynamic: false, content: 'foo' },

packages/kit/src/core/dev/plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export async function create_plugin(config, cwd) {
100100
routes: manifest_data.routes.map((route) => {
101101
if (route.type === 'page') {
102102
return {
103+
id: route.id,
103104
type: 'page',
104105
pattern: route.pattern,
105106
params: get_params(route.params),

packages/kit/src/core/generate_manifest/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function generate_manifest(
6161
${routes.map(route => {
6262
if (route.type === 'page') {
6363
return `{
64+
id: ${s(route.id)},
6465
type: 'page',
6566
pattern: ${route.pattern},
6667
params: ${get_params(route.params)},

packages/kit/src/runtime/app/navigation.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { router, renderer } from '../client/singletons.js';
22
import { get_base_uri } from '../client/utils.js';
3+
import { getContext } from 'svelte';
34

45
/**
56
* @param {string} name
@@ -75,3 +76,48 @@ function beforeNavigate_(fn) {
7576
function afterNavigate_(fn) {
7677
if (router) router.after_navigate(fn);
7778
}
79+
80+
/**
81+
* @param {RegExp} pattern
82+
* @param {string[]} params
83+
* @returns {string}
84+
*/
85+
function pathFromPattern(pattern, params) {
86+
let index = 0;
87+
return pattern.source
88+
.slice(1, -1)
89+
.replace(/\\\//g, '/')
90+
.replace(/\(\[\^\/\]\+\?\)/g, () => params[index++])
91+
.replace(/\/\?$/, '');
92+
}
93+
94+
/**
95+
* @param {any} value
96+
* @return {value is import('types/internal').SSRPage}
97+
*/
98+
function isSSRPage(value) {
99+
return typeof value === 'object' && value.type === 'page';
100+
}
101+
102+
/**
103+
* @type {import('$app/navigation').alternates}
104+
*/
105+
export function alternates(href) {
106+
if (!import.meta.env.SSR && router) {
107+
const hrefRoute = router.routes?.find((route) => route && route[0].test(href));
108+
if (!hrefRoute) return [];
109+
const match = href.match(hrefRoute[0]);
110+
const params = match ? match.slice(1) : [];
111+
const alternates = router.routes.filter((route) => route && route[4] === hrefRoute[4]);
112+
return alternates.map((route) => pathFromPattern(route[0], params));
113+
} else {
114+
/** @type {import('types/internal').SSRRoute[]} */
115+
const routes = getContext('__svelte_routes__');
116+
const hrefRoute = routes.find((route) => route.pattern.test(href));
117+
if (!hrefRoute || !isSSRPage(hrefRoute)) return [];
118+
const match = href.match(hrefRoute.pattern);
119+
const params = match ? match.slice(1) : [];
120+
const alternates = routes.filter((route) => isSSRPage(route) && route.id === hrefRoute.id);
121+
return alternates.map((route) => pathFromPattern(route.pattern, params));
122+
}
123+
}

packages/kit/src/runtime/server/page/render.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ export async function render_response({
8383
error,
8484
stuff
8585
},
86-
components: branch.map(({ node }) => node.module.default)
86+
components: branch.map(({ node }) => node.module.default),
87+
routes: options.manifest._.routes
8788
};
8889

8990
// TODO remove this for 1.0

0 commit comments

Comments
 (0)