diff --git a/packages/router/src/__tests__/pathRanking.test.ts b/packages/router/src/__tests__/pathRanking.test.ts index 76b35b663..ad76f8c2d 100644 --- a/packages/router/src/__tests__/pathRanking.test.ts +++ b/packages/router/src/__tests__/pathRanking.test.ts @@ -241,6 +241,25 @@ describe('Path ranking', () => { }); }); + it('catchAll /* should be the last one', () => { + checkPathOrder(['/a', '/*']); + checkPathOrder(['/a/*', '/a', '/*']); + + possibleOptions.forEach(options => { + checkPathOrder([['', options], '/*']); + checkPathOrder([['/', options], '/*']); + checkPathOrder([['/ab', options], '/*']); + checkPathOrder([['/:a', options], '/*']); + checkPathOrder([['/:a?', options], '/*']); + checkPathOrder([['/:a+', options], '/*']); + checkPathOrder([['/:a*', options], '/*']); + checkPathOrder([['/:a(\\d+)', options], '/*']); + checkPathOrder([['/:a(\\d+)?', options], '/*']); + checkPathOrder([['/:a(\\d+)+', options], '/*']); + checkPathOrder([['/:a(\\d+)*', options], '/*']); + }); + }); + it('handles sub segments', () => { checkPathOrder([ '/a/_2_', diff --git a/packages/router/src/matchRoutes.ts b/packages/router/src/matchRoutes.ts index 0fc7e308c..ccfff6361 100644 --- a/packages/router/src/matchRoutes.ts +++ b/packages/router/src/matchRoutes.ts @@ -106,6 +106,32 @@ function flattenRoutes( return branches; } +/** + * 1. Remove layout branches. + * Only keep the page branches which the last route is a page. + * 2. Remove path ending with slash + * + * @param branches + * @returns branches + */ +function onlyPageBranches( + branches: IRouteBranch[] = [] +): IRouteBranch[] { + // 1. remove layout branches + const newBranches = branches.filter(([_path, routes]) => { + const lastRoute = routes[routes.length - 1]; + // @ts-expect-error todo + return lastRoute?.__componentSource__?.endsWith('page'); + }); + + // 2. remove path ending with slash + newBranches.forEach(branch => { + branch[0] = branch[0].slice(0, -1); + }); + + return newBranches; +} + export function matchRoutes( routes: T[], location: string | PartialLocation, @@ -127,6 +153,7 @@ export function matchRoutes( } let branches = flattenRoutes(routes); + branches = onlyPageBranches(branches); branches = rankRouteBranches(branches); let matches: IRouteMatch[] | null = null; diff --git a/test/e2e/catch-all.test.ts b/test/e2e/catch-all.test.ts new file mode 100644 index 000000000..5db3182e9 --- /dev/null +++ b/test/e2e/catch-all.test.ts @@ -0,0 +1,52 @@ +import { AppCtx, Page, devFixture } from '../utils'; + +let ctx: AppCtx; +let page: Page; + +jest.setTimeout(5 * 60 * 1000); + +describe('catch-all', () => { + beforeAll(async () => { + ctx = await devFixture('catch-all'); + }); + afterAll(async () => { + await ctx.close(); + }); + afterEach(async () => { + await page.close(); + }); + + test('should exact match home page', async () => { + page = await ctx.browser.page(ctx.url('/home')); + await page.waitForSelector('[id="home-page"]'); + expect(await page.$text('[id="global-layout"]')).toBe('/layout.js'); + expect(await page.$text('[id="home-page"]')).toBe('/home/page.js'); + expect(await page.$text('[id="home-layout"]')).toBe('/home/layout.js'); + }); + + test('should match catchAll', async () => { + page = await ctx.browser.page(ctx.url('/other')); + await page.waitForSelector('[id="catchAll-page"]'); + expect(await page.$text('[id="global-layout"]')).toBe('/layout.js'); + expect(await page.$text('[id="catchAll-page"]')).toBe('/$/page.js'); + expect(await page.$text('[id="catchAll-layout"]')).toBe('/$/layout.js'); + }); + + test('should match /:symbol/calc', async () => { + page = await ctx.browser.page(ctx.url('/symbol/calc')); + await page.waitForSelector('[id="calc-page"]'); + expect(await page.$text('[id="global-layout"]')).toBe('/layout.js'); + expect(await page.$text('[id="calc-page"]')).toBe('/$symbol/calc/page.js'); + expect(await page.$text('[id="calc-layout"]')).toBe( + '/$symbol/calc/layout.js' + ); + }); + + test('/symbol/calc2 should match catchAll', async () => { + page = await ctx.browser.page(ctx.url('/symbol/calc2')); + await page.waitForSelector('[id="catchAll-page"]'); + expect(await page.$text('[id="global-layout"]')).toBe('/layout.js'); + expect(await page.$text('[id="catchAll-page"]')).toBe('/$/page.js'); + expect(await page.$text('[id="catchAll-layout"]')).toBe('/$/layout.js'); + }); +}); diff --git a/test/fixtures/catch-all/package.json b/test/fixtures/catch-all/package.json new file mode 100644 index 000000000..bf450f976 --- /dev/null +++ b/test/fixtures/catch-all/package.json @@ -0,0 +1,6 @@ +{ + "name": "fixture-catch-all", + "dependencies": { + "shuvi": "workspace:*" + } +} diff --git a/test/fixtures/catch-all/shuvi.config.js b/test/fixtures/catch-all/shuvi.config.js new file mode 100644 index 000000000..8f9018e7a --- /dev/null +++ b/test/fixtures/catch-all/shuvi.config.js @@ -0,0 +1,3 @@ +export default { + ssr: true +}; diff --git a/test/fixtures/catch-all/src/routes/$/layout.js b/test/fixtures/catch-all/src/routes/$/layout.js new file mode 100644 index 000000000..6b91715c2 --- /dev/null +++ b/test/fixtures/catch-all/src/routes/$/layout.js @@ -0,0 +1,10 @@ +import { RouterView } from '@shuvi/runtime'; + +const Layout = () => ( +
+
/$/layout.js
+ +
+); + +export default Layout; diff --git a/test/fixtures/catch-all/src/routes/$/page.js b/test/fixtures/catch-all/src/routes/$/page.js new file mode 100644 index 000000000..345e54fea --- /dev/null +++ b/test/fixtures/catch-all/src/routes/$/page.js @@ -0,0 +1,7 @@ +export default function Index() { + return ( + <> +
/$/page.js
+ + ); +} diff --git a/test/fixtures/catch-all/src/routes/$symbol/calc/layout.js b/test/fixtures/catch-all/src/routes/$symbol/calc/layout.js new file mode 100644 index 000000000..888ec2312 --- /dev/null +++ b/test/fixtures/catch-all/src/routes/$symbol/calc/layout.js @@ -0,0 +1,10 @@ +import { RouterView } from '@shuvi/runtime'; + +const Layout = () => ( +
+
/$symbol/calc/layout.js
+ +
+); + +export default Layout; diff --git a/test/fixtures/catch-all/src/routes/$symbol/calc/page.js b/test/fixtures/catch-all/src/routes/$symbol/calc/page.js new file mode 100644 index 000000000..b7bd64667 --- /dev/null +++ b/test/fixtures/catch-all/src/routes/$symbol/calc/page.js @@ -0,0 +1,7 @@ +export default function Index() { + return ( + <> +
/$symbol/calc/page.js
+ + ); +} diff --git a/test/fixtures/catch-all/src/routes/home/layout.js b/test/fixtures/catch-all/src/routes/home/layout.js new file mode 100644 index 000000000..5a6fbafb3 --- /dev/null +++ b/test/fixtures/catch-all/src/routes/home/layout.js @@ -0,0 +1,10 @@ +import { RouterView } from '@shuvi/runtime'; + +const Layout = () => ( +
+
/home/layout.js
+ +
+); + +export default Layout; diff --git a/test/fixtures/catch-all/src/routes/home/page.js b/test/fixtures/catch-all/src/routes/home/page.js new file mode 100644 index 000000000..b31c7c5c5 --- /dev/null +++ b/test/fixtures/catch-all/src/routes/home/page.js @@ -0,0 +1,7 @@ +export default function Index() { + return ( + <> +
/home/page.js
+ + ); +} diff --git a/test/fixtures/catch-all/src/routes/layout.js b/test/fixtures/catch-all/src/routes/layout.js new file mode 100644 index 000000000..6be37a89f --- /dev/null +++ b/test/fixtures/catch-all/src/routes/layout.js @@ -0,0 +1,10 @@ +import { RouterView } from '@shuvi/runtime'; + +const GlobalLayout = () => ( +
+
/layout.js
+ +
+); + +export default GlobalLayout;