Skip to content

Commit 7d6b9a9

Browse files
tylersayshidai-shi
andauthored
feat(create-pages): support for exactPath (#1215)
escape hatch for allowing any routing conventions at all --------- Co-authored-by: Tyler <[email protected]> Co-authored-by: daishi <[email protected]>
1 parent 0093cc7 commit 7d6b9a9

File tree

7 files changed

+270
-7
lines changed

7 files changed

+270
-7
lines changed

e2e/create-pages.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,12 @@ for (const mode of ['DEV', 'PRD'] as const) {
143143
expect(res.status).toBe(200);
144144
expect(await res.text()).toBe('POST to hello world! from the test!');
145145
});
146+
147+
test('exactPath', async ({ page }) => {
148+
await page.goto(`http://localhost:${port}/exact/[slug]/[...wild]`);
149+
await expect(
150+
page.getByRole('heading', { name: 'EXACTLY!!' }),
151+
).toBeVisible();
152+
});
146153
});
147154
}

e2e/fixtures/create-pages/src/components/HomeLayout.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ const HomeLayout = ({ children }: { children: ReactNode }) => (
6161
<li>
6262
<Link to="/error">Error</Link>
6363
</li>
64+
<li>
65+
<Link to="/exact/[slug]/[...wild]">Exact Path</Link>
66+
</li>
6467
</ul>
6568
{children}
6669
</div>

e2e/fixtures/create-pages/src/entries.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ const pages: ReturnType<typeof createPages> = createPages(
133133
return new Response(null);
134134
},
135135
}),
136+
137+
createPage({
138+
render: 'static',
139+
path: '/exact/[slug]/[...wild]',
140+
exactPath: true,
141+
component: () => <h1>EXACTLY!!</h1>,
142+
}),
136143
],
137144
);
138145

packages/waku/src/lib/utils/path.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ export const joinPath = (...paths: string[]) => {
6161

6262
export const extname = (filePath: string) => {
6363
const index = filePath.lastIndexOf('.');
64-
return index > 0 ? filePath.slice(index) : '';
64+
if (index <= 0) {
65+
return '';
66+
}
67+
if (['/', '.'].includes(filePath[index - 1]!)) {
68+
return '';
69+
}
70+
return filePath.slice(index);
6571
};
6672

6773
export type PathSpecItem =
@@ -89,6 +95,12 @@ export const parsePathWithSlug = (path: string): PathSpec =>
8995
return { type, name };
9096
});
9197

98+
export const parseExactPath = (path: string): PathSpec =>
99+
path
100+
.split('/')
101+
.filter(Boolean)
102+
.map((name) => ({ type: 'literal', name }));
103+
92104
/**
93105
* Transform a path spec to a regular expression.
94106
*/

packages/waku/src/router/create-pages.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
parsePathWithSlug,
99
getPathMapping,
1010
pathSpecAsString,
11+
parseExactPath,
1112
} from '../lib/utils/path.js';
1213
import type { PathSpec } from '../lib/utils/path.js';
1314
import type {
@@ -107,19 +108,19 @@ export type CreatePage = <
107108
WildSlugKey extends string,
108109
Render extends 'static' | 'dynamic',
109110
StaticPaths extends StaticSlugRoutePaths<Path>,
111+
ExactPath extends boolean | undefined = undefined,
110112
>(
111113
page: (
112114
| {
113115
render: Extract<Render, 'static'>;
114116
path: PathWithoutSlug<Path>;
115117
component: FunctionComponent<PropsForPages<Path>>;
116118
}
117-
| {
119+
| ({
118120
render: Extract<Render, 'static'>;
119121
path: PathWithStaticSlugs<Path>;
120-
staticPaths: StaticPaths;
121122
component: FunctionComponent<PropsForPages<Path>>;
122-
}
123+
} & (ExactPath extends true ? {} : { staticPaths: StaticPaths }))
123124
| {
124125
render: Extract<Render, 'dynamic'>;
125126
path: PathWithoutSlug<Path>;
@@ -130,7 +131,14 @@ export type CreatePage = <
130131
path: PathWithWildcard<Path, SlugKey, WildSlugKey>;
131132
component: FunctionComponent<PropsForPages<Path>>;
132133
}
133-
) & { unstable_disableSSR?: boolean },
134+
) & {
135+
unstable_disableSSR?: boolean;
136+
/**
137+
* If true, the path will be matched exactly, without wildcards or slugs.
138+
* This is intended for extending support to create custom routers.
139+
*/
140+
exactPath?: ExactPath;
141+
},
134142
) => Omit<
135143
Exclude<typeof page, { path: never } | { render: never }>,
136144
'unstable_disableSSR'
@@ -337,7 +345,19 @@ export const createPages = <
337345
}
338346
return { numSlugs, numWildcards };
339347
})();
340-
if (page.render === 'static' && numSlugs === 0) {
348+
349+
if (page.exactPath) {
350+
const spec = parseExactPath(page.path);
351+
if (page.render === 'static') {
352+
staticPathMap.set(page.path, {
353+
literalSpec: parseExactPath(page.path),
354+
});
355+
const id = joinPath(page.path, 'page').replace(/^\//, '');
356+
registerStaticComponent(id, page.component);
357+
} else {
358+
dynamicPagePathMap.set(page.path, [spec, page.component]);
359+
}
360+
} else if (page.render === 'static' && numSlugs === 0) {
341361
staticPathMap.set(page.path, { literalSpec: pathSpec });
342362
const id = joinPath(page.path, 'page').replace(/^\//, '');
343363
registerStaticComponent(id, page.component);

packages/waku/tests/create-pages.test.ts

+201
Original file line numberDiff line numberDiff line change
@@ -1385,3 +1385,204 @@ describe('createPages api', () => {
13851385
expect(res.status).toEqual(200);
13861386
});
13871387
});
1388+
1389+
describe('createPages - exactPath', () => {
1390+
it('creates a simple static page', async () => {
1391+
const TestPage = () => null;
1392+
createPages(async ({ createPage }) => [
1393+
createPage({
1394+
render: 'static',
1395+
path: '/test',
1396+
exactPath: true,
1397+
component: TestPage,
1398+
}),
1399+
]);
1400+
const { getRouteConfig, handleRoute } = injectedFunctions();
1401+
expect(await getRouteConfig()).toEqual([
1402+
{
1403+
elements: {
1404+
'page:/test': { isStatic: true },
1405+
},
1406+
rootElement: { isStatic: true },
1407+
routeElement: { isStatic: true },
1408+
noSsr: false,
1409+
path: [{ name: 'test', type: 'literal' }],
1410+
},
1411+
]);
1412+
const route = await handleRoute('/test', {
1413+
query: '?skip=[]',
1414+
});
1415+
expect(route).toBeDefined();
1416+
expect(route.rootElement).toBeDefined();
1417+
expect(route.routeElement).toBeDefined();
1418+
expect(Object.keys(route.elements)).toEqual(['page:/test']);
1419+
});
1420+
1421+
it('creates a simple dynamic page', async () => {
1422+
const TestPage = () => null;
1423+
createPages(async ({ createPage }) => [
1424+
createPage({
1425+
render: 'dynamic',
1426+
path: '/test',
1427+
exactPath: true,
1428+
component: TestPage,
1429+
}),
1430+
]);
1431+
const { getRouteConfig, handleRoute } = injectedFunctions();
1432+
expect(await getRouteConfig()).toEqual([
1433+
{
1434+
elements: {
1435+
'page:/test': { isStatic: false },
1436+
},
1437+
rootElement: { isStatic: true },
1438+
routeElement: { isStatic: true },
1439+
noSsr: false,
1440+
path: [{ name: 'test', type: 'literal' }],
1441+
},
1442+
]);
1443+
const route = await handleRoute('/test', {
1444+
query: '?skip=[]',
1445+
});
1446+
expect(route).toBeDefined();
1447+
expect(route.rootElement).toBeDefined();
1448+
expect(route.routeElement).toBeDefined();
1449+
expect(Object.keys(route.elements)).toEqual(['page:/test']);
1450+
});
1451+
1452+
it('works with a slug path', async () => {
1453+
const TestPage = vi.fn();
1454+
createPages(async ({ createPage }) => [
1455+
createPage({
1456+
render: 'static',
1457+
path: '/test/[slug]',
1458+
exactPath: true,
1459+
component: TestPage,
1460+
}),
1461+
]);
1462+
const { getRouteConfig, handleRoute } = injectedFunctions();
1463+
expect(await getRouteConfig()).toEqual([
1464+
{
1465+
elements: {
1466+
'page:/test/[slug]': { isStatic: true },
1467+
},
1468+
rootElement: { isStatic: true },
1469+
routeElement: { isStatic: true },
1470+
noSsr: false,
1471+
path: [
1472+
{ name: 'test', type: 'literal' },
1473+
{ name: '[slug]', type: 'literal' },
1474+
],
1475+
},
1476+
]);
1477+
const route = await handleRoute('/test/[slug]', {
1478+
query: '?skip=[]',
1479+
});
1480+
expect(route).toBeDefined();
1481+
expect(route.rootElement).toBeDefined();
1482+
expect(route.routeElement).toBeDefined();
1483+
expect(Object.keys(route.elements)).toEqual(['page:/test/[slug]']);
1484+
});
1485+
1486+
it('works with a wildcard path', async () => {
1487+
const TestPage = vi.fn();
1488+
createPages(async ({ createPage }) => [
1489+
createPage({
1490+
render: 'static',
1491+
path: '/test/[...wildcard]',
1492+
exactPath: true,
1493+
component: TestPage,
1494+
}),
1495+
]);
1496+
const { getRouteConfig, handleRoute } = injectedFunctions();
1497+
expect(await getRouteConfig()).toEqual([
1498+
{
1499+
elements: {
1500+
'page:/test/[...wildcard]': { isStatic: true },
1501+
},
1502+
rootElement: { isStatic: true },
1503+
routeElement: { isStatic: true },
1504+
noSsr: false,
1505+
path: [
1506+
{ name: 'test', type: 'literal' },
1507+
{ name: '[...wildcard]', type: 'literal' },
1508+
],
1509+
},
1510+
]);
1511+
const route = await handleRoute('/test/[...wildcard]', {
1512+
query: '?skip=[]',
1513+
});
1514+
expect(route).toBeDefined();
1515+
expect(route.rootElement).toBeDefined();
1516+
expect(route.routeElement).toBeDefined();
1517+
expect(Object.keys(route.elements)).toEqual(['page:/test/[...wildcard]']);
1518+
});
1519+
1520+
it('works with wildcard and slug path', async () => {
1521+
const TestPage = vi.fn();
1522+
createPages(async ({ createPage }) => [
1523+
createPage({
1524+
render: 'static',
1525+
path: '/test/[...wildcard]/[slug]',
1526+
exactPath: true,
1527+
component: TestPage,
1528+
}),
1529+
]);
1530+
const { getRouteConfig, handleRoute } = injectedFunctions();
1531+
expect(await getRouteConfig()).toEqual([
1532+
{
1533+
elements: {
1534+
'page:/test/[...wildcard]/[slug]': { isStatic: true },
1535+
},
1536+
rootElement: { isStatic: true },
1537+
routeElement: { isStatic: true },
1538+
noSsr: false,
1539+
path: [
1540+
{ name: 'test', type: 'literal' },
1541+
{ name: '[...wildcard]', type: 'literal' },
1542+
{ name: '[slug]', type: 'literal' },
1543+
],
1544+
},
1545+
]);
1546+
const route = await handleRoute('/test/[...wildcard]/[slug]', {
1547+
query: '?skip=[]',
1548+
});
1549+
expect(route).toBeDefined();
1550+
expect(route.rootElement).toBeDefined();
1551+
expect(route.routeElement).toBeDefined();
1552+
expect(Object.keys(route.elements)).toEqual([
1553+
'page:/test/[...wildcard]/[slug]',
1554+
]);
1555+
});
1556+
1557+
it('does not work with slug match', async () => {
1558+
const TestPage = vi.fn();
1559+
createPages(async ({ createPage }) => [
1560+
createPage({
1561+
render: 'static',
1562+
path: '/test/[slug]',
1563+
exactPath: true,
1564+
component: TestPage,
1565+
}),
1566+
]);
1567+
const { getRouteConfig, handleRoute } = injectedFunctions();
1568+
expect(await getRouteConfig()).toEqual([
1569+
{
1570+
elements: {
1571+
'page:/test/[slug]': { isStatic: true },
1572+
},
1573+
rootElement: { isStatic: true },
1574+
routeElement: { isStatic: true },
1575+
noSsr: false,
1576+
path: [
1577+
{ name: 'test', type: 'literal' },
1578+
{ name: '[slug]', type: 'literal' },
1579+
],
1580+
},
1581+
]);
1582+
await expect(async () => {
1583+
return handleRoute('/test/foo', {
1584+
query: '?skip=[]',
1585+
});
1586+
}).rejects.toThrowError();
1587+
});
1588+
});

packages/waku/tests/path.test.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import { test, describe, expect } from 'vitest';
2-
import { parsePathWithSlug, path2regexp } from '../src/lib/utils/path.js';
2+
import {
3+
extname,
4+
parsePathWithSlug,
5+
path2regexp,
6+
} from '../src/lib/utils/path.js';
37

48
function matchPath(path: string, input: string) {
59
return new RegExp(path2regexp(parsePathWithSlug(path))).test(input);
610
}
711

12+
describe('extname', () => {
13+
test('returns the extension of a path', () => {
14+
expect(extname('foo/bar/baz.js')).toBe('.js');
15+
expect(extname('foo/bar/baz')).toBe('');
16+
expect(extname('foo/bar/.baz')).toBe('');
17+
expect(extname('foo/bar/..baz')).toBe('');
18+
});
19+
});
20+
821
describe('path2regexp', () => {
922
test('handles paths without slugs', () => {
1023
expect(matchPath('/foo/bar', '/foo/bar')).toBe(true);

0 commit comments

Comments
 (0)