Skip to content

Commit

Permalink
Remove dependency on path-to-regexp
Browse files Browse the repository at this point in the history
  • Loading branch information
uwej711 committed Sep 15, 2024
1 parent 27d19f6 commit a504d0b
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-worms-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Remove dependency on path-to-regexp
1 change: 0 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@
"ora": "^8.1.0",
"p-limit": "^6.1.0",
"p-queue": "^8.0.1",
"path-to-regexp": "6.2.2",
"preferred-pm": "^4.0.0",
"prompts": "^2.4.2",
"rehype": "^13.0.1",
Expand Down
84 changes: 40 additions & 44 deletions packages/astro/src/core/routing/manifest/generator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import type { AstroConfig, RoutePart } from '../../../@types/astro.js';

import { compile } from 'path-to-regexp';

/**
* Sanitizes the parameters object by normalizing string values and replacing certain characters with their URL-encoded equivalents.
* @param {Record<string, string | number | undefined>} params - The parameters object to be sanitized.
* @returns {Record<string, string | number | undefined>} The sanitized parameters object.
* @param {Record<string, string | number>} params - The parameters object to be sanitized.
* @returns {Record<string, string | number>} The sanitized parameters object.
*/
function sanitizeParams(
params: Record<string, string | number | undefined>,
): Record<string, string | number | undefined> {
function sanitizeParams(params: Record<string, string | number>): Record<string, string | number> {
return Object.fromEntries(
Object.entries(params).map(([key, value]) => {
if (typeof value === 'string') {
Expand All @@ -20,49 +16,49 @@ function sanitizeParams(
);
}

function getParameter(part: RoutePart, params: Record<string, string | number>): string | number {
if (part.spread) {
return params[part.content.slice(3)] || '';
}

if (part.dynamic) {
if (!params[part.content]) {
throw new TypeError(`Missing parameter: ${part.content}`);
}

return params[part.content];
}

return part.content
.normalize()
.replace(/\?/g, '%3F')
.replace(/#/g, '%23')
.replace(/%5B/g, '[')
.replace(/%5D/g, ']');
}

function getSegment(segment: RoutePart[], params: Record<string, string | number>): string {
const segmentPath = segment.map((part) => getParameter(part, params)).join('');

return segmentPath ? '/' + segmentPath : '';
}

export function getRouteGenerator(
segments: RoutePart[][],
addTrailingSlash: AstroConfig['trailingSlash'],
) {
const template = segments
.map((segment) => {
return (
'/' +
segment
.map((part) => {
if (part.spread) {
return `:${part.content.slice(3)}(.*)?`;
} else if (part.dynamic) {
return `:${part.content}`;
} else {
return part.content
.normalize()
.replace(/\?/g, '%3F')
.replace(/#/g, '%23')
.replace(/%5B/g, '[')
.replace(/%5D/g, ']')
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
})
.join('')
);
})
.join('');

// Unless trailingSlash config is set to 'always', don't automatically append it.
let trailing: '/' | '' = '';
if (addTrailingSlash === 'always' && segments.length) {
trailing = '/';
}
const toPath = compile(template + trailing);
return (params: Record<string, string | number | undefined>): string => {
return (params: Record<string, string | number>): string => {
const sanitizedParams = sanitizeParams(params);
const path = toPath(sanitizedParams);

// When generating an index from a rest parameter route, `path-to-regexp` will return an
// empty string instead "/". This causes an inconsistency with static indexes that may result
// in the incorrect routes being rendered.
// To fix this, we return "/" when the path is empty.
// Unless trailingSlash config is set to 'always', don't automatically append it.
let trailing: '/' | '' = '';
if (addTrailingSlash === 'always' && segments.length) {
trailing = '/';
}

const path =
segments.map((segment) => getSegment(segment, sanitizedParams)).join('') + trailing;

return path || '/';
};
}
152 changes: 152 additions & 0 deletions packages/astro/test/units/routing/generator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { getRouteGenerator } from '../../../dist/core/routing/manifest/generator.js';

describe('routing - generator', () => {
[
{
routeData: [],
trailingSlash: 'never',
params: {},
path: '/',
},
{
routeData: [],
trailingSlash: 'always',
params: {},
path: '/',
},
{
routeData: [[{ spread: false, content: 'test', dynamic: false }]],
trailingSlash: 'never',
params: {},
path: '/test',
},
{
routeData: [[{ spread: false, content: 'test', dynamic: false }]],
trailingSlash: 'always',
params: {},
path: '/test/',
},
{
routeData: [[{ spread: false, content: 'test', dynamic: false }]],
trailingSlash: 'always',
params: { foo: 'bar' },
path: '/test/',
},
{
routeData: [[{ spread: false, content: 'foo', dynamic: true }]],
trailingSlash: 'always',
params: { foo: 'bar' },
path: '/bar/',
},
{
routeData: [[{ spread: false, content: 'foo', dynamic: true }]],
trailingSlash: 'never',
params: { foo: 'bar' },
path: '/bar',
},
{
routeData: [[{ spread: true, content: '...foo', dynamic: true }]],
trailingSlash: 'never',
params: {},
path: '/',
},
{
routeData: [
[
{ spread: true, content: '...foo', dynamic: true },
{ spread: false, content: '-', dynamic: false },
{ spread: true, content: '...bar', dynamic: true },
],
],
trailingSlash: 'never',
params: { foo: 'one', bar: 'two' },
path: '/one-two',
},
{
routeData: [
[
{ spread: true, content: '...foo', dynamic: true },
{ spread: false, content: '-', dynamic: false },
{ spread: true, content: '...bar', dynamic: true },
],
],
trailingSlash: 'never',
params: {},
path: '/-',
},
{
routeData: [
[{ spread: true, content: '...foo', dynamic: true }],
[{ spread: true, content: '...bar', dynamic: true }],
],
trailingSlash: 'never',
params: { foo: 'one' },
path: '/one',
},
{
routeData: [
[{ spread: false, content: 'fix', dynamic: false }],
[{ spread: true, content: '...foo', dynamic: true }],
[{ spread: true, content: '...bar', dynamic: true }],
],
trailingSlash: 'never',
params: { foo: 'one' },
path: '/fix/one',
},
{
routeData: [
[{ spread: false, content: 'fix', dynamic: false }],
[{ spread: true, content: '...foo', dynamic: true }],
[{ spread: true, content: '...bar', dynamic: true }],
],
trailingSlash: 'always',
params: { foo: 'one' },
path: '/fix/one/',
},
{
routeData: [
[{ spread: false, content: 'fix', dynamic: false }],
[{ spread: true, content: '...foo', dynamic: true }],
[{ spread: true, content: '...bar', dynamic: true }],
],
trailingSlash: 'never',
params: { foo: 'one', bar: 'two' },
path: '/fix/one/two',
},
{
routeData: [
[{ spread: false, content: 'fix', dynamic: false }],
[{ spread: true, content: '...foo', dynamic: true }],
[{ spread: true, content: '...bar', dynamic: true }],
],
trailingSlash: 'never',
params: { foo: 'one&two' },
path: '/fix/one&two',
},
{
routeData: [
[{ spread: false, content: 'fix', dynamic: false }],
[{ spread: false, content: 'page', dynamic: true }],
],
trailingSlash: 'never',
params: { page: 1 },
path: '/fix/1',
},
].forEach(({ routeData, trailingSlash, params, path }) => {
it(`generates ${path}`, () => {
const generator = getRouteGenerator(routeData, trailingSlash);
assert.equal(generator(params), path);
});
});

it('should throw an error when a dynamic parameter is missing', () => {
const generator = getRouteGenerator(
[[{ spread: false, content: 'foo', dynamic: true }]],
'never',
);
assert.throws(() => generator({}), TypeError);
});
});
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a504d0b

Please sign in to comment.