Skip to content
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

Matchers #4358

Merged
merged 5 commits into from
Mar 16, 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/spicy-apples-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] Rename validators to matchers
10 changes: 5 additions & 5 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,14 +318,14 @@ A route can have multiple dynamic parameters, for example `src/routes/[category]

> `src/routes/a/[...rest]/z.svelte` will match `/a/z` as well as `/a/b/z` and `/a/b/c/z` and so on. Make sure you check that the value of the rest parameter is valid.

#### Validation
#### Matching

A route like `src/routes/archive/[page]` would match `/archive/3`, but it would also match `/archive/potato`. We don't want that. You can ensure that route parameters are well-formed by adding a _validator_ — which takes the parameter string (`"3"` or `"potato"`) and returns `true` if it is valid — to your [`params`](/docs/configuration#files) directory...
A route like `src/routes/archive/[page]` would match `/archive/3`, but it would also match `/archive/potato`. We don't want that. You can ensure that route parameters are well-formed by adding a _matcher_ — which takes the parameter string (`"3"` or `"potato"`) and returns `true` if it is valid — to your [`params`](/docs/configuration#files) directory...

```js
/// file: src/params/integer.js
/** @type {import('@sveltejs/kit').ParamValidator} */
export function validate(param) {
/** @type {import('@sveltejs/kit').ParamMatcher} */
export function match(param) {
return /^\d+$/.test(param);
}
```
Expand All @@ -337,7 +337,7 @@ export function validate(param) {
+src/routes/archive/[page=integer]
```

If the pathname doesn't validate, SvelteKit will try to match other routes (using the sort order specified below), before eventually returning a 404.
If the pathname doesn't match, SvelteKit will try to match other routes (using the sort order specified below), before eventually returning a 404.

#### Sorting

Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/core/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ export async function build_server(
input[name] = resolved;
});

// ...and every validator
Object.entries(manifest_data.validators).forEach(([key, file]) => {
const name = posixify(path.join('entries/validators', key));
// ...and every matcher
Object.entries(manifest_data.matchers).forEach(([key, file]) => {
const name = posixify(path.join('entries/matchers', key));
input[name] = path.resolve(cwd, file);
});

Expand Down
18 changes: 9 additions & 9 deletions packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,23 @@ export async function create_plugin(config, cwd) {
}
};
}),
validators: async () => {
/** @type {Record<string, import('types').ParamValidator>} */
const validators = {};
matchers: async () => {
/** @type {Record<string, import('types').ParamMatcher>} */
const matchers = {};

for (const key in manifest_data.validators) {
const file = manifest_data.validators[key];
for (const key in manifest_data.matchers) {
const file = manifest_data.matchers[key];
const url = path.resolve(cwd, file);
const module = await vite.ssrLoadModule(url);

if (module.validate) {
validators[key] = module.validate;
if (module.match) {
matchers[key] = module.match;
} else {
throw new Error(`${file} does not export a \`validate\` function`);
throw new Error(`${file} does not export a \`match\` function`);
}
}

return validators;
return matchers;
}
}
};
Expand Down
10 changes: 5 additions & 5 deletions packages/kit/src/core/generate_manifest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function generate_manifest({ build_data, relative_path, routes, format =
/** @param {string} id */
const get_index = (id) => id && /** @type {LookupEntry} */ (bundled_nodes.get(id)).index;

const validators = new Set();
const matchers = new Set();

// prettier-ignore
return `{
Expand All @@ -75,7 +75,7 @@ export function generate_manifest({ build_data, relative_path, routes, format =
const { pattern, names, types } = parse_route_id(route.id);

types.forEach(type => {
if (type) validators.add(type);
if (type) matchers.add(type);
});

if (route.type === 'page') {
Expand Down Expand Up @@ -108,9 +108,9 @@ export function generate_manifest({ build_data, relative_path, routes, format =
}
}).filter(Boolean).join(',\n\t\t\t\t')}
],
validators: async () => {
${Array.from(validators).map(type => `const { validate: ${type} } = await ${load(`${relative_path}/entries/validators/${type}.js`)}`).join('\n\t\t\t\t')}
return { ${Array.from(validators).join(', ')} };
matchers: async () => {
${Array.from(matchers).map(type => `const { match: ${type} } = await ${load(`${relative_path}/entries/matchers/${type}.js`)}`).join('\n\t\t\t\t')}
return { ${Array.from(matchers).join(', ')} };
}
}
}`.replace(/^\t/gm, '');
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/core/sync/create_manifest_data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,14 @@ export default function create_manifest_data({
const params_base = path.relative(cwd, config.kit.files.params);

/** @type {Record<string, string>} */
const validators = {};
const matchers = {};
if (fs.existsSync(config.kit.files.params)) {
for (const file of fs.readdirSync(config.kit.files.params)) {
const ext = path.extname(file);
const type = file.slice(0, -ext.length);

if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(type)) {
validators[type] = path.join(params_base, file);
matchers[type] = path.join(params_base, file);
} else {
throw new Error(
`Validator names must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ — "${file}" is invalid`
Expand All @@ -314,7 +314,7 @@ export default function create_manifest_data({
error,
components,
routes,
validators
matchers
};
}

Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/core/sync/create_manifest_data/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,10 @@ test('errors on encountering an illegal __file', () => {
);
});

test('creates param validators', () => {
const { validators } = create('samples/basic'); // directory doesn't matter for the test
test('creates param matchers', () => {
const { matchers } = create('samples/basic'); // directory doesn't matter for the test

assert.equal(validators, {
assert.equal(matchers, {
foo: path.join('params', 'foo.js'),
bar: path.join('params', 'bar.js')
});
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/sync/write_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function write_manifest(manifest_data, base, output) {
write_if_changed(
`${output}/client-manifest.js`,
trim(`
export { validators } from './client-validators.js';
export { matchers } from './client-matchers.js';

export const components = ${components};

Expand Down
16 changes: 8 additions & 8 deletions packages/kit/src/core/sync/write_validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ import { write_if_changed } from './utils.js';
*/
export function write_validators(manifest_data, output) {
const imports = [];
const validators = [];
const matchers = [];

for (const key in manifest_data.validators) {
const src = manifest_data.validators[key];
for (const key in manifest_data.matchers) {
const src = manifest_data.matchers[key];

imports.push(`import { validate as ${key} } from ${s(path.relative(output, src))};`);
validators.push(key);
imports.push(`import { match as ${key} } from ${s(path.relative(output, src))};`);
matchers.push(key);
}

const module = imports.length
? `${imports.join('\n')}\n\nexport const validators = { ${validators.join(', ')} };`
: 'export const validators = {};';
? `${imports.join('\n')}\n\nexport const matchers = { ${matchers.join(', ')} };`
: 'export const matchers = {};';

write_if_changed(`${output}/client-validators.js`, module);
write_if_changed(`${output}/client-matchers.js`, module);
}
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/client/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare module '__GENERATED__/client-manifest.js' {
import { CSRComponentLoader, ParamValidator } from 'types';
import { CSRComponentLoader, ParamMatcher } from 'types';

/**
* A list of all the layout/pages components used in the app
Expand All @@ -12,5 +12,5 @@ declare module '__GENERATED__/client-manifest.js' {
*/
export const dictionary: Record<string, [number[], number[], 1]>;

export const validators: Record<string, ParamValidator>;
export const matchers: Record<string, ParamMatcher>;
}
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import {
import { parse } from './parse.js';

import Root from '__GENERATED__/root.svelte';
import { components, dictionary, validators } from '__GENERATED__/client-manifest.js';
import { components, dictionary, matchers } from '__GENERATED__/client-manifest.js';

const SCROLL_KEY = 'sveltekit:scroll';
const INDEX_KEY = 'sveltekit:index';

const routes = parse(components, dictionary, validators);
const routes = parse(components, dictionary, matchers);

// we import the root layout/error components eagerly, so that
// connectivity errors after initialisation don't nuke the app
Expand Down Expand Up @@ -660,7 +660,7 @@ export function create_client({ target, session, base, trailing_slash }) {
// @ts-expect-error
if (node.loaded.fallthrough) {
throw new Error(
'fallthrough is no longer supported. Use validators instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/client/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { exec, parse_route_id } from '../../utils/routing.js';
/**
* @param {import('types').CSRComponentLoader[]} components
* @param {Record<string, [number[], number[], 1?]>} dictionary
* @param {Record<string, (param: string) => boolean>} validators
* @param {Record<string, (param: string) => boolean>} matchers
* @returns {import('types').CSRRoute[]}
*/
export function parse(components, dictionary, validators) {
export function parse(components, dictionary, matchers) {
const routes = Object.entries(dictionary).map(([id, [a, b, has_shadow]]) => {
const { pattern, names, types } = parse_route_id(id);

Expand All @@ -15,7 +15,7 @@ export function parse(components, dictionary, validators) {
/** @param {string} path */
exec: (path) => {
const match = pattern.exec(path);
if (match) return exec(match, names, types, validators);
if (match) return exec(match, names, types, matchers);
},
a: a.map((n) => components[n]),
b: b.map((n) => components[n]),
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function render_endpoint(event, mod) {
// @ts-expect-error
if (response.fallthrough) {
throw new Error(
'fallthrough is no longer supported. Use validators instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching'
);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function respond(request, options, state) {
/** @type {Record<string, string>} */
let params = {};

if (options.paths.base) {
if (options.paths.base && !state.prerender?.fallback) {
if (!decoded.startsWith(options.paths.base)) {
return new Response(undefined, { status: 404 });
}
Expand All @@ -81,13 +81,13 @@ export async function respond(request, options, state) {
}

if (!state.prerender || !state.prerender.fallback) {
const validators = await options.manifest._.validators();
const matchers = await options.manifest._.matchers();

for (const candidate of options.manifest._.routes) {
const match = candidate.pattern.exec(decoded);
if (!match) continue;

const matched = exec(match, candidate.names, candidate.types, validators);
const matched = exec(match, candidate.names, candidate.types, matchers);
if (matched) {
route = candidate;
params = decode_params(matched);
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/server/page/load_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export async function load_node({
// @ts-expect-error
if (loaded.fallthrough) {
throw new Error(
'fallthrough is no longer supported. Use validators instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching'
);
}
} else if (shadow.body) {
Expand Down Expand Up @@ -407,7 +407,7 @@ async function load_shadow_data(route, event, options, prerender) {
// @ts-expect-error
if (result.fallthrough) {
throw new Error(
'fallthrough is no longer supported. Use validators instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching'
);
}

Expand Down Expand Up @@ -438,7 +438,7 @@ async function load_shadow_data(route, event, options, prerender) {
// @ts-expect-error
if (result.fallthrough) {
throw new Error(
'fallthrough is no longer supported. Use validators instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching'
);
}

Expand Down
10 changes: 5 additions & 5 deletions packages/kit/src/utils/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export function parse_route_id(key) {
* @param {RegExpMatchArray} match
* @param {string[]} names
* @param {string[]} types
* @param {Record<string, import('types').ParamValidator>} validators
* @param {Record<string, import('types').ParamMatcher>} matchers
*/
export function exec(match, names, types, validators) {
export function exec(match, names, types, matchers) {
/** @type {Record<string, string>} */
const params = {};

Expand All @@ -52,10 +52,10 @@ export function exec(match, names, types, validators) {
const value = match[i + 1] || '';

if (type) {
const validator = validators[type];
if (!validator) throw new Error(`Missing "${type}" param validator`); // TODO do this ahead of time?
const matcher = matchers[type];
if (!matcher) throw new Error(`Missing "${type}" param matcher`); // TODO do this ahead of time?

if (!validator(value)) return;
if (!matcher(value)) return;
}

params[name] = value;
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/test/apps/basics/src/params/lowercase.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function validate(param) {
export function match(param) {
return /^[a-z]+$/.test(param);
}
2 changes: 1 addition & 1 deletion packages/kit/test/apps/basics/src/params/numeric.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function validate(param) {
export function match(param) {
return !isNaN(param);
}
2 changes: 1 addition & 1 deletion packages/kit/test/apps/basics/src/params/uppercase.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function validate(param) {
export function match(param) {
return /^[A-Z]+$/.test(param);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<a href="/routing/matched/a">/routing/matched/a</a>
<a href="/routing/matched/B">/routing/matched/B</a>
<a href="/routing/matched/1">/routing/matched/1</a>
<a href="/routing/matched/everything-else">/routing/matched/everything-else</a>

<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Matchers</h1>

This file was deleted.

This file was deleted.

14 changes: 7 additions & 7 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2229,20 +2229,20 @@ test.describe.parallel('Static files', () => {
});
});

test.describe.parallel('Validators', () => {
test('Validates parameters', async ({ page, clicknav }) => {
await page.goto('/routing/validated');
test.describe.parallel('Matchers', () => {
test('Matches parameters', async ({ page, clicknav }) => {
await page.goto('/routing/matched');

await clicknav('[href="/routing/validated/a"]');
await clicknav('[href="/routing/matched/a"]');
expect(await page.textContent('h1')).toBe('lowercase: a');

await clicknav('[href="/routing/validated/B"]');
await clicknav('[href="/routing/matched/B"]');
expect(await page.textContent('h1')).toBe('uppercase: B');

await clicknav('[href="/routing/validated/1"]');
await clicknav('[href="/routing/matched/1"]');
expect(await page.textContent('h1')).toBe('number: 1');

await clicknav('[href="/routing/validated/everything-else"]');
await clicknav('[href="/routing/matched/everything-else"]');
expect(await page.textContent('h1')).toBe('fallback: everything-else');
});
});
Expand Down
Loading