Skip to content

allow prefixes and suffixes around rest parameters #3240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

Allow prefixes and suffixes around rest parameters
60 changes: 31 additions & 29 deletions packages/kit/src/core/create_manifest_data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ export default function create_manifest_data({ config, output, cwd = process.cwd
throw new Error(`Invalid route ${file} — brackets are unbalanced`);
}

if (/.+\[\.\.\.[^\]]+\]/.test(segment) || /\[\.\.\.[^\]]+\].+/.test(segment)) {
throw new Error(`Invalid route ${file} — rest parameter must be a standalone segment`);
}

const parts = get_parts(segment, file);
const is_index = is_dir ? false : basename.startsWith('index.');
const is_page = config.extensions.indexOf(ext) !== -1;
Expand Down Expand Up @@ -367,31 +363,37 @@ function get_parts(part, file) {
function get_pattern(segments, add_trailing_slash) {
const path = segments
.map((segment) => {
return segment[0].rest
? '(?:\\/(.*))?'
: '\\/' +
segment
.map((part) => {
return part.dynamic
? '([^/]+?)'
: // allow users to specify characters on the file system in an encoded manner
part.content
.normalize()
// We use [ and ] to denote parameters, so users must encode these on the file
// system to match against them. We don't decode all characters since others
// can already be epressed and so that '%' can be easily used directly in filenames
.replace(/%5[Bb]/g, '[')
.replace(/%5[Dd]/g, ']')
// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
// They will not be touched by decodeURI so need to be encoded here, so
// that we can match against them.
// We skip '/' since you can't create a file with it on any OS
.replace(/#/g, '%23')
.replace(/\?/g, '%3F')
// escape characters that have special meaning in regex
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
})
.join('');
if (segment.length === 1 && segment[0].rest) {
// special case — `src/routes/foo/[...bar]/baz` matches `/foo/baz`
// so we need to make the leading slash optional
return '(?:\\/(.*))?';
}

const parts = segment.map((part) => {
if (part.rest) return '(.*?)';
if (part.dynamic) return '([^/]+?)';

return (
part.content
// allow users to specify characters on the file system in an encoded manner
.normalize()
// We use [ and ] to denote parameters, so users must encode these on the file
// system to match against them. We don't decode all characters since others
// can already be epressed and so that '%' can be easily used directly in filenames
.replace(/%5[Bb]/g, '[')
.replace(/%5[Dd]/g, ']')
// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
// They will not be touched by decodeURI so need to be encoded here, so
// that we can match against them.
// We skip '/' since you can't create a file with it on any OS
.replace(/#/g, '%23')
.replace(/\?/g, '%3F')
// escape characters that have special meaning in regex
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
);
});

return '\\/' + parts.join('');
})
.join('');

Expand Down
40 changes: 30 additions & 10 deletions packages/kit/src/core/create_manifest_data/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,38 @@ test('sorts routes with rest correctly', () => {
});

test('disallows rest parameters inside segments', () => {
assert.throws(
() => {
create('samples/invalid-rest');
const { routes } = create('samples/rest-prefix-suffix');

assert.equal(routes, [
{
type: 'page',
segments: [
{
dynamic: true,
rest: true,
content: 'prefix-[...rest]'
}
],
pattern: /^\/prefix-(.*?)\/?$/,
params: ['...rest'],
path: '',
a: ['components/layout.svelte', 'samples/rest-prefix-suffix/prefix-[...rest].svelte'],
b: ['components/error.svelte']
},
/** @param {Error} e */
(e) => {
return (
e.message ===
'Invalid route samples/invalid-rest/foo-[...rest]-bar.svelte — rest parameter must be a standalone segment'
);
{
type: 'endpoint',
segments: [
{
dynamic: true,
rest: true,
content: '[...rest].json'
}
],
pattern: /^\/(.*?)\.json$/,
file: 'samples/rest-prefix-suffix/[...rest].json.js',
params: ['...rest']
}
);
]);
});

test('ignores files and directories with leading underscores', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('@sveltejs/kit').RequestHandler} */
export function get({ params }) {
return {
body: {
parts: params.parts
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ fetch, params }) {
const res = await fetch(`/routing/rest/complex/${params.parts}.json`);
const { parts } = await res.json();

return {
props: { parts }
};
}
</script>

<script>
/** @type {string} */
export let parts;
</script>

<h1>parts: {parts}</h1>
5 changes: 5 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,11 @@ test.describe.parallel('Routing', () => {
await clicknav('[href="/routing/rest/path/three"]');
expect(await page.textContent('h1')).toBe('path: /routing/rest/path/three');
});

test('allows rest routes to have prefixes and suffixes', async ({ page }) => {
await page.goto('/routing/rest/complex/prefix-one/two/three');
expect(await page.textContent('h1')).toBe('parts: one/two/three');
});
});

test.describe.parallel('Session', () => {
Expand Down