diff --git a/examples/basic/.gitignore b/examples/basic/.gitignore index 2af291991..18d9cbc46 100644 --- a/examples/basic/.gitignore +++ b/examples/basic/.gitignore @@ -10,6 +10,7 @@ # shuvi.js /.shuvi/ +build/ # production /dist/ diff --git a/examples/basic/package.json b/examples/basic/package.json index 33819a904..cc6bc9b9c 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -1,17 +1,18 @@ { "private": true, + "name": "example-basic", "scripts": { "dev": "shuvi dev", "build": "shuvi build", "start": "shuvi start" }, "dependencies": { - "shuvi": "1.0.0-rc.5" + "shuvi": "workspace:*" }, "devDependencies": { "@types/node": "^18.6.1", - "@types/react": "^18.0.17", - "@types/react-dom": "^18.0.6", + "@types/react": "18.0.9", + "@types/react-dom": "18.0.6", "typescript": "^4.7.4" } } diff --git a/examples/basic/src/routes/$/layout.tsx b/examples/basic/src/routes/$/layout.tsx new file mode 100644 index 000000000..32a7947fc --- /dev/null +++ b/examples/basic/src/routes/$/layout.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { RouterView } from '@shuvi/runtime'; + +export default function Layout() { + return ( +
+ /$/layout.tsx + +
+ ); +} + +export const loader = async () => { + console.log('[demo] /$/layout loader'); + return {}; +}; diff --git a/examples/basic/src/routes/$/page.tsx b/examples/basic/src/routes/$/page.tsx new file mode 100644 index 000000000..8647b4862 --- /dev/null +++ b/examples/basic/src/routes/$/page.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export default function Home() { + return <>/$/pages.tsx; +} +export const loader = async () => { + console.log('[demo] /$/page loader'); + return {}; +}; diff --git a/examples/basic/src/routes/$symbol/calc/layout.tsx b/examples/basic/src/routes/$symbol/calc/layout.tsx new file mode 100644 index 000000000..fd3c7759e --- /dev/null +++ b/examples/basic/src/routes/$symbol/calc/layout.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { RouterView } from '@shuvi/runtime'; + +export default function Layout() { + return ( +
+ /calc/layout.tsx + +
+ ); +} + +export const loader = async () => { + console.log('[demo] /calc/layout loader'); + return {}; +}; diff --git a/examples/basic/src/routes/$symbol/calc/page.tsx b/examples/basic/src/routes/$symbol/calc/page.tsx new file mode 100644 index 000000000..a12ecdecb --- /dev/null +++ b/examples/basic/src/routes/$symbol/calc/page.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export default function Home() { + return <>/calc/pages.tsx; +} +export const loader = async () => { + console.log('[demo] /calc/page loader'); + return {}; +}; diff --git a/examples/basic/src/routes/home/layout.tsx b/examples/basic/src/routes/home/layout.tsx new file mode 100644 index 000000000..90b665dbf --- /dev/null +++ b/examples/basic/src/routes/home/layout.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { RouterView } from '@shuvi/runtime'; + +export default function Layout() { + return ( +
+ /home/layout.tsx + +
+ ); +} + +export const loader = async () => { + console.log('[demo] /home/layout loader'); + return {}; +}; diff --git a/examples/basic/src/routes/home/page.tsx b/examples/basic/src/routes/home/page.tsx new file mode 100644 index 000000000..18cbd12a1 --- /dev/null +++ b/examples/basic/src/routes/home/page.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export default function Home() { + return <>/home/pages.tsx; +} +export const loader = async () => { + console.log('[demo] /home/page loader'); + return {}; +}; diff --git a/packages/router/src/__tests__/createRoutesFromArray.test.ts b/packages/router/src/__tests__/createRoutesFromArray.test.ts index f7fe00164..4467255e9 100644 --- a/packages/router/src/__tests__/createRoutesFromArray.test.ts +++ b/packages/router/src/__tests__/createRoutesFromArray.test.ts @@ -47,4 +47,20 @@ describe('createRoutesFromArray', () => { { caseSensitive: false, path: '/' } ]); }); + + it('allow children with empty path', () => { + expect( + createRoutesFromArray([ + { path: '/' }, + { path: '/*', children: [{ path: '' }] } + ]) + ).toStrictEqual([ + { path: '/', caseSensitive: false }, + { + path: '/*', + children: [{ path: '', caseSensitive: false }], + caseSensitive: false + } + ]); + }); }); diff --git a/packages/router/src/__tests__/pathRanking.test.ts b/packages/router/src/__tests__/pathRanking.test.ts index 76b35b663..f2fff652e 100644 --- a/packages/router/src/__tests__/pathRanking.test.ts +++ b/packages/router/src/__tests__/pathRanking.test.ts @@ -4,6 +4,56 @@ import { tokensToParser, comparePathParserScore } from '../pathParserRanker'; type PathParserOptions = Parameters[1]; +const LeafPageRoute = { + path: '' +}; + +describe('tokensToParser', () => { + it('scores is correct', () => { + expect(tokensToParser(tokenizePath('/')).score).toStrictEqual([[80]]); + expect(tokensToParser(tokenizePath('/home')).score).toStrictEqual([[80]]); + expect(tokensToParser(tokenizePath('/:symbol')).score).toStrictEqual([ + [60] + ]); + expect(tokensToParser(tokenizePath('/:symbol/calc')).score).toStrictEqual([ + [60], + [80] + ]); + expect(tokensToParser(tokenizePath('/*')).score).toStrictEqual([[19]]); + }); + + it('patch score for pageBranch which ends with empty route', () => { + expect( + tokensToParser( + tokenizePath('/home'), + {}, + { routes: [{ path: '/home' }, LeafPageRoute] } + ).score + ).toStrictEqual([[80], [0.1]]); + expect( + tokensToParser( + tokenizePath('/:symbol'), + {}, + { routes: [{ path: '/:symbol' }, LeafPageRoute] } + ).score + ).toStrictEqual([[60], [0.1]]); + expect( + tokensToParser( + tokenizePath('/:symbol/calc'), + {}, + { routes: [{ path: '/:symbol' }, { path: '/calc' }, LeafPageRoute] } + ).score + ).toStrictEqual([[60], [80], [0.1]]); + expect( + tokensToParser( + tokenizePath('/*'), + {}, + { routes: [{ path: '/*' }, LeafPageRoute] } + ).score + ).toStrictEqual([[19], [0.1]]); + }); +}); + describe('Path ranking', () => { describe('comparePathParser', () => { function compare(a: number[][], b: number[][]): number { @@ -241,6 +291,10 @@ describe('Path ranking', () => { }); }); + it('catchAll /* should be the last one', () => { + checkPathOrder(['/home', '/:symbol/calc', '/*']); + }); + it('handles sub segments', () => { checkPathOrder([ '/a/_2_', diff --git a/packages/router/src/createRoutesFromArray.ts b/packages/router/src/createRoutesFromArray.ts index 9e2bdf8b2..b2f35d9f3 100644 --- a/packages/router/src/createRoutesFromArray.ts +++ b/packages/router/src/createRoutesFromArray.ts @@ -3,16 +3,24 @@ import { IPartialRouteRecord, IRouteRecord } from './types'; export function createRoutesFromArray< T extends IPartialRouteRecord, U extends IRouteRecord ->(array: T[]): U[] { +>( + array: T[], + /** + * allowEmptyPath: allow empty path for children + * A pageBranch could be ended with a page route with empty path. + */ + allowEmptyPath = false +): U[] { return array.map(partialRoute => { + const defaultPath = allowEmptyPath ? '' : '/'; let route: U = { ...(partialRoute as any), caseSensitive: !!partialRoute.caseSensitive, - path: partialRoute.path || '/' + path: partialRoute.path || defaultPath }; if (partialRoute.children) { - route.children = createRoutesFromArray(partialRoute.children); + route.children = createRoutesFromArray(partialRoute.children, true); } return route; diff --git a/packages/router/src/matchRoutes.ts b/packages/router/src/matchRoutes.ts index 0fc7e308c..219736a3e 100644 --- a/packages/router/src/matchRoutes.ts +++ b/packages/router/src/matchRoutes.ts @@ -62,9 +62,9 @@ export function rankRouteBranches( } const normalizedPaths = branches.map((branch, index) => { - const [path] = branch; + const [path, routes] = branch; return { - ...tokensToParser(tokenizePath(path)), + ...tokensToParser(tokenizePath(path), undefined, { routes }), path, index }; @@ -89,7 +89,15 @@ function flattenRoutes( parentIndexes: number[] = [] ): IRouteBranch[] { routes.forEach((route, index) => { - let path = joinPaths([parentPath, route.path]); + let path; + if (route.path === '') { + /** + * An empty path is allowed, don't append a slash. + */ + path = parentPath; + } else { + path = joinPaths([parentPath, route.path]); + } let routes = parentRoutes.concat(route); let indexes = parentIndexes.concat(index); diff --git a/packages/router/src/pathParserRanker.ts b/packages/router/src/pathParserRanker.ts index e4e0a5125..fc620929f 100644 --- a/packages/router/src/pathParserRanker.ts +++ b/packages/router/src/pathParserRanker.ts @@ -1,4 +1,5 @@ import { Token, TokenType, TokenCatchAll } from './pathTokenizer'; +import { IRouteRecord } from './types'; type PathParams = Record; @@ -113,7 +114,8 @@ const enum PathScore { BonusOptional = -0.8 * _multiplier, // /:w? or /:w* // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b BonusStrict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/? - BonusCaseSensitive = 0.025 * _multiplier // when options strict: true is passed, as the regex omits \/? + BonusCaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/? + BonusEmptyStringPath = 0.01 * _multiplier // when the path is an empty string } // Special Regex characters that must be escaped in static tokens @@ -128,7 +130,8 @@ const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g; */ export function tokensToParser( segments: Array, - extraOptions?: _PathParserOptions + extraOptions?: _PathParserOptions, + branchInfo?: { routes: IRouteRecord[] } ): PathParser { const options = Object.assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions); @@ -354,6 +357,18 @@ export function tokensToParser( return path; } + /** + * To make sure the score of pageBranch is always higher priority + * than the layoutRoute. + * We append a bonus score if the last route is an empty path. + */ + if (branchInfo?.routes) { + const lastRoutePath = branchInfo.routes[branchInfo.routes.length - 1]?.path; + if (lastRoutePath === '') { + score.push([PathScore.BonusEmptyStringPath]); + } + } + return { re, score, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af12d6b64..20a360386 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,25 @@ importers: specifier: 4.8.4 version: 4.8.4 + examples/basic: + dependencies: + shuvi: + specifier: workspace:* + version: link:../../packages/shuvi + devDependencies: + '@types/node': + specifier: ^18.6.1 + version: 18.19.34 + '@types/react': + specifier: 18.0.9 + version: 18.0.9 + '@types/react-dom': + specifier: 18.0.6 + version: 18.0.6 + typescript: + specifier: ^4.7.4 + version: 4.8.4 + packages/compiler: dependencies: '@napi-rs/triples': @@ -1007,6 +1026,12 @@ importers: specifier: workspace:* version: link:../../../packages/shuvi + test/fixtures/catch-all: + dependencies: + shuvi: + specifier: workspace:* + version: link:../../../packages/shuvi + test/fixtures/compiler: dependencies: '@emotion/react': @@ -5015,7 +5040,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 18.0.6 + '@types/node': 12.12.7 '@types/responselike': 1.0.0 dev: true @@ -5093,7 +5118,7 @@ packages: /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: true /@types/glob@7.2.0: @@ -5116,7 +5141,7 @@ packages: /@types/http-proxy@1.17.4: resolution: {integrity: sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==} dependencies: - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: false /@types/istanbul-lib-coverage@2.0.1: @@ -5185,7 +5210,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: true /@types/loader-utils@2.0.1: @@ -5229,7 +5254,6 @@ packages: /@types/node@12.12.7: resolution: {integrity: sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==} - dev: true /@types/node@17.0.23: resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==} @@ -5237,6 +5261,13 @@ packages: /@types/node@18.0.6: resolution: {integrity: sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==} + dev: true + + /@types/node@18.19.34: + resolution: {integrity: sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==} + dependencies: + undici-types: 5.26.5 + dev: true /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -5313,7 +5344,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: true /@types/rimraf@3.0.0: @@ -5327,7 +5358,7 @@ packages: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} dependencies: '@types/glob': 7.2.0 - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: true /@types/scheduler@0.16.2: @@ -5337,14 +5368,14 @@ packages: /@types/semver@7.3.1: resolution: {integrity: sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag==} dependencies: - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: true /@types/send@0.14.5: resolution: {integrity: sha512-0mwoiK3DXXBu0GIfo+jBv4Wo5s1AcsxdpdwNUtflKm99VEMvmBPJ+/NBNRZy2R5JEYfWL/u4nAHuTUTA3wFecQ==} dependencies: '@types/mime': 2.0.1 - '@types/node': 18.0.6 + '@types/node': 12.12.7 dev: true /@types/source-list-map@0.1.2: @@ -5410,7 +5441,7 @@ packages: resolution: {integrity: sha512-tWkdf9nO0zFgAY/EumUKwrDUhraHKDqCPhwfFR/R8l0qnPdgb9le0Gzhvb7uzVpouuDGBgiE//ZdY+5jcZy2TA==} dependencies: '@types/anymatch': 1.3.1 - '@types/node': 18.0.6 + '@types/node': 12.12.7 '@types/tapable': 1.0.4 '@types/uglify-js': 3.0.4 '@types/webpack-sources': 0.1.5 @@ -11020,7 +11051,7 @@ packages: '@jest/fake-timers': 28.1.1 '@jest/types': 28.1.1 '@types/jsdom': 16.2.14 - '@types/node': 18.0.6 + '@types/node': 12.12.7 jest-mock: 28.1.1 jest-util: 28.1.1 jsdom: 19.0.0 @@ -11296,7 +11327,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.0.6 + '@types/node': 12.12.7 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false @@ -16295,6 +16326,10 @@ packages: through: 2.3.8 dev: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + /unicode-canonical-property-names-ecmascript@1.0.4: resolution: {integrity: sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==} engines: {node: '>=4'} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1c4b3a8a0..fbb7c2c6d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - 'test/fixtures/*' - 'test/packages/*' - 'test/utils' + - 'examples/*' 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;