Skip to content

Commit e2c098c

Browse files
author
Philipp Zerelles
committed
Added alternateRoutes() extension point
1 parent 2c5b740 commit e2c098c

File tree

13 files changed

+153
-51
lines changed

13 files changed

+153
-51
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ async function build_server(
337337
const params = get_params(route.params);
338338
339339
return `{
340+
id: ${s(route.id)},
340341
type: 'page',
341342
pattern: ${route.pattern},
342343
params: ${params},

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ function generate_client_manifest(manifest_data, base) {
8686
'})';
8787
8888
const tuple = [route.pattern, get_indices(route.a), get_indices(route.b)];
89-
if (params) tuple.push(params);
89+
tuple.push(params);
90+
tuple.push(route.id);
9091
9192
return `// ${route.a[route.a.length - 1]}\n\t\t[${tuple.join(', ')}]`;
9293
} else {
@@ -149,6 +150,7 @@ function generate_app(manifest_data, base) {
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, base) {
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: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,10 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
5252
/**
5353
* @param {string} dir
5454
* @param {Part[][]} parent_segments
55-
* @param {string[]} parent_params
5655
* @param {string[]} layout_stack // accumulated __layout.svelte components
5756
* @param {string[]} error_stack // accumulated __error.svelte components
5857
*/
59-
function walk(dir, parent_segments, parent_params, layout_stack, error_stack) {
58+
function walk(dir, parent_segments, layout_stack, error_stack) {
6059
/** @type {Item[]} */
6160
const items = fs
6261
.readdirSync(dir)
@@ -157,9 +156,6 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
157156
segments.push(item.parts);
158157
}
159158

160-
const params = parent_params.slice();
161-
params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content));
162-
163159
if (item.is_dir) {
164160
const layout_reset = find_layout('__layout.reset', item.file);
165161
const layout = find_layout('__layout', item.file);
@@ -176,54 +172,71 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
176172
walk(
177173
path.join(dir, item.basename),
178174
segments,
179-
params,
180175
layout_reset ? [layout_reset] : layout_stack.concat(layout),
181176
layout_reset ? [error] : error_stack.concat(error)
182177
);
183-
} else if (item.is_page) {
184-
components.push(item.file);
185-
186-
const a = layout_stack.concat(item.file);
187-
const b = error_stack;
188-
189-
const pattern = get_pattern(segments, true);
178+
} else {
179+
const alternates = config.kit.alternateRoutes
180+
? config.kit.alternateRoutes(segments, item.is_page ? 'page' : 'endpoint')
181+
: [segments];
182+
183+
if (item.is_page) {
184+
const id = components.length.toString();
185+
components.push(item.file);
186+
187+
const a = layout_stack.concat(item.file);
188+
const b = error_stack;
189+
190+
alternates.forEach((segments) => {
191+
const pattern = get_pattern(segments, true);
192+
const params = segments.flatMap((parts) =>
193+
parts.filter((p) => p.dynamic).map((p) => p.content)
194+
);
195+
196+
let i = a.length;
197+
while (i--) {
198+
if (!b[i] && !a[i]) {
199+
b.splice(i, 1);
200+
a.splice(i, 1);
201+
}
202+
}
190203

191-
let i = a.length;
192-
while (i--) {
193-
if (!b[i] && !a[i]) {
194-
b.splice(i, 1);
195-
a.splice(i, 1);
196-
}
197-
}
204+
i = b.length;
205+
while (i--) {
206+
if (b[i]) break;
207+
}
198208

199-
i = b.length;
200-
while (i--) {
201-
if (b[i]) break;
209+
b.splice(i + 1);
210+
211+
const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
212+
? `/${segments.map((segment) => segment[0].content).join('/')}`
213+
: null;
214+
215+
routes.push({
216+
id,
217+
type: 'page',
218+
pattern,
219+
params,
220+
path,
221+
a,
222+
b
223+
});
224+
});
225+
} else {
226+
alternates.forEach((segments) => {
227+
const pattern = get_pattern(segments, !item.route_suffix);
228+
const params = segments.flatMap((parts) =>
229+
parts.filter((p) => p.dynamic).map((p) => p.content)
230+
);
231+
232+
routes.push({
233+
type: 'endpoint',
234+
pattern,
235+
file: item.file,
236+
params
237+
});
238+
});
202239
}
203-
204-
b.splice(i + 1);
205-
206-
const path = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
207-
? `/${segments.map((segment) => segment[0].content).join('/')}`
208-
: null;
209-
210-
routes.push({
211-
type: 'page',
212-
pattern,
213-
params,
214-
path,
215-
a,
216-
b
217-
});
218-
} else {
219-
const pattern = get_pattern(segments, !item.route_suffix);
220-
221-
routes.push({
222-
type: 'endpoint',
223-
pattern,
224-
file: item.file,
225-
params
226-
});
227240
}
228241
});
229242
}
@@ -235,7 +248,7 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
235248

236249
components.push(layout, error);
237250

238-
walk(config.kit.files.routes, [], [], [layout], [error]);
251+
walk(config.kit.files.routes, [], [layout], [error]);
239252

240253
const assets_dir = config.kit.files.assets;
241254

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

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

4545
assert.equal(routes, [
4646
{
47+
id: '2',
4748
type: 'page',
4849
pattern: /^\/$/,
4950
params: [],
@@ -53,6 +54,7 @@ test('creates routes', () => {
5354
},
5455

5556
{
57+
id: '3',
5658
type: 'page',
5759
pattern: /^\/about\/?$/,
5860
params: [],
@@ -69,6 +71,7 @@ test('creates routes', () => {
6971
},
7072

7173
{
74+
id: '4',
7275
type: 'page',
7376
pattern: /^\/blog\/?$/,
7477
params: [],
@@ -85,6 +88,7 @@ test('creates routes', () => {
8588
},
8689

8790
{
91+
id: '5',
8892
type: 'page',
8993
pattern: /^\/blog\/([^/]+?)\/?$/,
9094
params: ['slug'],
@@ -108,6 +112,7 @@ test('creates routes with layout', () => {
108112

109113
assert.equal(routes, [
110114
{
115+
id: '2',
111116
type: 'page',
112117
pattern: /^\/$/,
113118
params: [],
@@ -117,6 +122,7 @@ test('creates routes with layout', () => {
117122
},
118123

119124
{
125+
id: '4',
120126
type: 'page',
121127
pattern: /^\/foo\/?$/,
122128
params: [],
@@ -263,6 +269,7 @@ test('works with custom extensions', () => {
263269

264270
assert.equal(routes, [
265271
{
272+
id: '2',
266273
type: 'page',
267274
pattern: /^\/$/,
268275
params: [],
@@ -272,6 +279,7 @@ test('works with custom extensions', () => {
272279
},
273280

274281
{
282+
id: '3',
275283
type: 'page',
276284
pattern: /^\/about\/?$/,
277285
params: [],
@@ -288,6 +296,7 @@ test('works with custom extensions', () => {
288296
},
289297

290298
{
299+
id: '4',
291300
type: 'page',
292301
pattern: /^\/blog\/?$/,
293302
params: [],
@@ -304,6 +313,7 @@ test('works with custom extensions', () => {
304313
},
305314

306315
{
316+
id: '5',
307317
type: 'page',
308318
pattern: /^\/blog\/([^/]+?)\/?$/,
309319
params: ['slug'],
@@ -336,6 +346,7 @@ test('includes nested error components', () => {
336346

337347
assert.equal(routes, [
338348
{
349+
id: '6',
339350
type: 'page',
340351
pattern: /^\/foo\/bar\/baz\/?$/,
341352
params: [],
@@ -362,6 +373,7 @@ test('resets layout', () => {
362373

363374
assert.equal(routes, [
364375
{
376+
id: '2',
365377
type: 'page',
366378
pattern: /^\/$/,
367379
params: [],
@@ -370,6 +382,7 @@ test('resets layout', () => {
370382
b: [error]
371383
},
372384
{
385+
id: '4',
373386
type: 'page',
374387
pattern: /^\/foo\/?$/,
375388
params: [],
@@ -382,6 +395,7 @@ test('resets layout', () => {
382395
b: [error]
383396
},
384397
{
398+
id: '7',
385399
type: 'page',
386400
pattern: /^\/foo\/bar\/?$/,
387401
params: [],

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ class Watcher extends EventEmitter {
341341
routes: manifest_data.routes.map((route) => {
342342
if (route.type === 'page') {
343343
return {
344+
id: route.id,
344345
type: 'page',
345346
pattern: route.pattern,
346347
params: get_params(route.params),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test('fills in defaults', () => {
1212
extensions: ['.svelte'],
1313
kit: {
1414
adapter: null,
15+
alternateRoutes: null,
1516
amp: false,
1617
appDir: '_app',
1718
files: {
@@ -107,6 +108,7 @@ test('fills in partial blanks', () => {
107108
extensions: ['.svelte'],
108109
kit: {
109110
adapter: null,
111+
alternateRoutes: null,
110112
amp: false,
111113
appDir: '_app',
112114
files: {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ const options = {
5454
}
5555
},
5656

57+
alternateRoutes: {
58+
type: 'leaf',
59+
default: null,
60+
validate: (option, keypath) => {
61+
if (typeof option !== 'function') {
62+
throw new Error(`${keypath} must be a function that processes route segments`);
63+
}
64+
65+
return option;
66+
}
67+
},
68+
5769
amp: expect_boolean(false),
5870

5971
appDir: expect_string('_app', false),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ async function testLoadDefaultConfig(path) {
2222
extensions: ['.svelte'],
2323
kit: {
2424
adapter: null,
25+
alternateRoutes: null,
2526
amp: false,
2627
appDir: '_app',
2728
files: {

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

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

45
/**
56
* @param {string} name
@@ -49,3 +50,46 @@ async function prefetchRoutes_(pathnames) {
4950

5051
await Promise.all(promises);
5152
}
53+
54+
/**
55+
* @param {RegExp} pattern
56+
* @param {string[]} params
57+
* @returns {string}
58+
*/
59+
function pathFromPattern(pattern, params) {
60+
let index = 0;
61+
return pattern.source
62+
.slice(1, -1)
63+
.replace(/\\\//g, '/')
64+
.replace(/\(\[\^\/\]\+\?\)/g, () => params[index++])
65+
.replace(/\/\?$/, '');
66+
}
67+
68+
/**
69+
* @param {any} value
70+
* @return {value is import('types/internal').SSRPage}
71+
*/
72+
function isSSRPage(value) {
73+
return typeof value === 'object' && value.type === 'page';
74+
}
75+
76+
/**
77+
* @type {import('$app/navigation').alternates}
78+
*/
79+
export function alternates(href) {
80+
if (!import.meta.env.SSR) {
81+
const hrefRoute = router?.routes?.find((route) => route[0].test(href));
82+
if (!hrefRoute) return null;
83+
const [, ...params] = href.match(hrefRoute[0]);
84+
const alternates = router.routes.filter((route) => route[4] === hrefRoute[4]);
85+
return alternates.map((route) => pathFromPattern(route[0], params));
86+
} else {
87+
/** @type {import('types/internal').SSRRoute[]} */
88+
const routes = getContext('__svelte_routes__');
89+
const hrefRoute = routes.find((route) => route.pattern.test(href));
90+
if (!hrefRoute || !isSSRPage(hrefRoute)) return null;
91+
const [, ...params] = href.match(hrefRoute.pattern);
92+
const alternates = routes.filter((route) => isSSRPage(route) && route.id === hrefRoute.id);
93+
return alternates.map((route) => pathFromPattern(route.pattern, params));
94+
}
95+
}

0 commit comments

Comments
 (0)