diff --git a/apps/router-e2e/__e2e__/server/app/(a,b)/multi-group-api+api.ts b/apps/router-e2e/__e2e__/server/app/(a,b)/multi-group-api+api.ts new file mode 100644 index 0000000000000..5f9b9db05aeac --- /dev/null +++ b/apps/router-e2e/__e2e__/server/app/(a,b)/multi-group-api+api.ts @@ -0,0 +1,8 @@ +/** @type {import('expo-router/server').RequestHandler} */ +export function GET() { + return new Response( + JSON.stringify({ + value: 'multi-group-api-get', + }) + ); +} diff --git a/apps/router-e2e/__e2e__/server/app/(a,b)/multi-group.tsx b/apps/router-e2e/__e2e__/server/app/(a,b)/multi-group.tsx new file mode 100644 index 0000000000000..f3c1c081308b2 --- /dev/null +++ b/apps/router-e2e/__e2e__/server/app/(a,b)/multi-group.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
Beta
; +} diff --git a/packages/@expo/cli/CHANGELOG.md b/packages/@expo/cli/CHANGELOG.md index 79c8f39f52e7f..fe22b7222ea38 100644 --- a/packages/@expo/cli/CHANGELOG.md +++ b/packages/@expo/cli/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug fixes +- Fix using array syntax `(a,b)` with server output. ([#27462](https://github.com/expo/expo/pull/27462) by [@EvanBacon](https://github.com/EvanBacon)) - Prevent `console.log` statements from colliding with Metro logs. ([#27217](https://github.com/expo/expo/pull/27217) by [@EvanBacon](https://github.com/EvanBacon)) - Fix using dev server URL in development. ([#27213](https://github.com/expo/expo/pull/27213) by [@EvanBacon](https://github.com/EvanBacon)) - Always reset production bundler cache in run command. ([#27114](https://github.com/expo/expo/pull/27114) by [@EvanBacon](https://github.com/EvanBacon)) diff --git a/packages/@expo/cli/e2e/__tests__/export/server.test.ts b/packages/@expo/cli/e2e/__tests__/export/server.test.ts index f4a77f7452762..c9d789a993e13 100644 --- a/packages/@expo/cli/e2e/__tests__/export/server.test.ts +++ b/packages/@expo/cli/e2e/__tests__/export/server.test.ts @@ -29,6 +29,20 @@ describe('server-output', () => { 560 * 1000 ); + function getFiles() { + // List output files with sizes for snapshotting. + // This is to make sure that any changes to the output are intentional. + // Posix path formatting is used to make paths the same across OSes. + return klawSync(outputDir) + .map((entry) => { + if (entry.path.includes('node_modules') || !entry.stats.isFile()) { + return null; + } + return path.posix.relative(outputDir, entry.path); + }) + .filter(Boolean); + } + describe('requests', () => { beforeAll(async () => { await ensurePortFreeAsync(3000); @@ -126,6 +140,60 @@ describe('server-output', () => { ).toMatch(/
/); }); + it(`can serve up static html in array group`, async () => { + expect(getFiles()).not.toContain('server/multi-group.html'); + expect(getFiles()).not.toContain('server/(a,b)/multi-group.html'); + expect(getFiles()).toContain('server/(a)/multi-group.html'); + expect(getFiles()).toContain('server/(b)/multi-group.html'); + expect(await fetch('http://localhost:3000/multi-group').then((res) => res.text())).toMatch( + /
/ + ); + }); + + it(`can serve up static html in specific array group`, async () => { + expect( + await fetch('http://localhost:3000/(a)/multi-group').then((res) => res.text()) + ).toMatch(/
/); + + expect( + await fetch('http://localhost:3000/(b)/multi-group').then((res) => res.text()) + ).toMatch(/
/); + }); + + it(`can not serve up static html in retained array group syntax`, async () => { + // Should not be able to match the array syntax + expect( + await fetch('http://localhost:3000/(a,b)/multi-group').then((res) => res.status) + ).toEqual(404); + }); + + it(`can serve up API route in array group`, async () => { + expect(getFiles()).toContain('server/_expo/functions/(a,b)/multi-group-api+api.js'); + expect(getFiles()).not.toContain('server/_expo/functions/(a)/multi-group-api+api.js'); + expect(getFiles()).not.toContain('server/_expo/functions/(b)/multi-group-api+api.js'); + + expect( + await fetch('http://localhost:3000/multi-group-api').then((res) => res.json()) + ).toEqual({ value: 'multi-group-api-get' }); + }); + + it(`can serve up API route in specific array group`, async () => { + // Should be able to match all the group variations + expect( + await fetch('http://localhost:3000/(a)/multi-group-api').then((res) => res.json()) + ).toEqual({ value: 'multi-group-api-get' }); + expect( + await fetch('http://localhost:3000/(b)/multi-group-api').then((res) => res.json()) + ).toEqual({ value: 'multi-group-api-get' }); + }); + + it(`can not serve up API route in retained array group syntax`, async () => { + // Should not be able to match the array syntax + expect( + await fetch('http://localhost:3000/(a,b)/multi-group-api').then((res) => res.status) + ).toEqual(404); + }); + it( 'can use environment variables', async () => { @@ -269,14 +337,7 @@ describe('server-output', () => { // List output files with sizes for snapshotting. // This is to make sure that any changes to the output are intentional. // Posix path formatting is used to make paths the same across OSes. - const files = klawSync(outputDir) - .map((entry) => { - if (entry.path.includes('node_modules') || !entry.stats.isFile()) { - return null; - } - return path.posix.relative(outputDir, entry.path); - }) - .filter(Boolean); + const files = getFiles(); // The wrapper should not be included as a route. expect(files).not.toContain('server/+html.html'); diff --git a/packages/@expo/cli/src/export/__tests__/exportStaticAsync.test.ts b/packages/@expo/cli/src/export/__tests__/exportStaticAsync.test.ts index fab7e23845ce5..d2585404f57e7 100644 --- a/packages/@expo/cli/src/export/__tests__/exportStaticAsync.test.ts +++ b/packages/@expo/cli/src/export/__tests__/exportStaticAsync.test.ts @@ -1,10 +1,22 @@ -import { ExpoRouterRuntimeManifest } from '../../start/server/metro/MetroBundlerDevServer'; +import { getMockConfig } from 'expo-router/build/testing-library/mock-config'; + import { getHtmlFiles, getPathVariations, getFilesToExportFromServerAsync, } from '../exportStaticAsync'; +jest.mock('expo-router/build/views/Navigator', () => ({})); + +jest.mock('react-native', () => ({})); +jest.mock('expo-linking', () => ({})); +jest.mock('expo-modules-core', () => ({})); +jest.mock('@react-navigation/native', () => ({})); + +function Route() { + return null; +} + describe(getPathVariations, () => { it(`should get path variations`, () => { expect(getPathVariations('(foo)/bar/(bax)/baz').sort()).toEqual([ @@ -67,62 +79,35 @@ describe(getPathVariations, () => { }); }); -function mockExpandRuntimeManifest(manifest: ExpoRouterRuntimeManifest) { - function mockExpandRuntimeManifestScreens(screens: ExpoRouterRuntimeManifest['screens']) { - return Object.fromEntries( - Object.entries(screens).map(([key, value]) => { - if (typeof value === 'string') { - return [ - key, - { - path: value, - screens: {}, - _route: {}, - }, - ]; - } else if (Object.keys(value.screens).length) { - return [ - key, - { - ...value, - screens: mockExpandRuntimeManifestScreens(value.screens), - }, - ]; - } - return [key, value]; - }) - ); - } - - return { - ...manifest, - screens: mockExpandRuntimeManifestScreens(manifest.screens), - }; -} - describe(getHtmlFiles, () => { it(`should get html files`, () => { expect( getHtmlFiles({ includeGroupVariations: true, - manifest: mockExpandRuntimeManifest({ - initialRouteName: undefined, - screens: { - alpha: { - path: 'alpha', - screens: { index: '', second: 'second' }, - initialRouteName: 'index', + + manifest: getMockConfig( + { + './alpha/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, - '(app)': { - path: '(app)', - screens: { compose: 'compose', index: '', 'note/[note]': 'note/:note' }, - initialRouteName: 'index', + './alpha/index.tsx': Route, + './alpha/second.tsx': Route, + // + './(app)/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, - '(auth)/sign-in': '(auth)/sign-in', - _sitemap: '_sitemap', - '[...404]': '*404', + './(app)/compose.tsx': Route, + './(app)/index.tsx': Route, + './(app)/note/[note].tsx': Route, + // + './(auth)/sign-in.js': Route, + './_sitemap.tsx': Route, + './[...404].tsx': Route, }, - }), + false + ), }) .map((a) => a.filePath) .sort((a, b) => a.length - b.length) @@ -141,42 +126,80 @@ describe(getHtmlFiles, () => { '(app)/note/[note].html', ]); }); + + it(`should get html files with top-level array syntax`, () => { + expect( + getHtmlFiles({ + includeGroupVariations: true, + + manifest: getMockConfig( + { + './(a,b)/index.tsx': Route, + }, + false + ), + }) + .map(({ filePath, pathname }) => ({ filePath, pathname })) + .sort((a, b) => a.filePath.length - b.filePath.length) + ).toEqual([ + { filePath: 'index.html', pathname: '' }, + { filePath: '(a)/index.html', pathname: '(a)' }, + { filePath: '(b)/index.html', pathname: '(b)' }, + ]); + }); + it(`should get html files with nested array syntax`, () => { + expect( + getHtmlFiles({ + includeGroupVariations: true, + manifest: getMockConfig( + { + './(a,b)/foo.tsx': Route, + }, + false + ), + }) + .map(({ filePath, pathname }) => ({ filePath, pathname })) + .sort((a, b) => a.filePath.length - b.filePath.length) + ).toEqual([ + { filePath: 'foo.html', pathname: 'foo' }, + { filePath: '(a)/foo.html', pathname: '(a)/foo' }, + { filePath: '(b)/foo.html', pathname: '(b)/foo' }, + ]); + }); + it(`should get html files 2`, () => { expect( getHtmlFiles({ includeGroupVariations: true, - manifest: mockExpandRuntimeManifest({ - initialRouteName: undefined, - screens: { - '(root)': { - path: '(root)', - screens: { - '(index)': { - path: '(index)', - screens: { - '[...missing]': '*missing', - index: '', - notifications: 'notifications', - }, - initialRouteName: 'index', - }, - }, - initialRouteName: '(index)', + + manifest: getMockConfig( + { + './(root)/_layout.tsx': { + unstable_settings: { initialRouteName: '(index)' }, + default: Route, + }, + './(root)/(index)/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, + './(root)/(index)/index.tsx': Route, + './(root)/(index)/[...missing].tsx': Route, + './(root)/(index)/notifications.tsx': Route, }, - }), + false + ), }) .map((a) => a.filePath) .sort((a, b) => a.length - b.length) ).toEqual([ 'index.html', - '[...missing].html', '(root)/index.html', + '[...missing].html', '(index)/index.html', 'notifications.html', '(root)/[...missing].html', - '(index)/[...missing].html', '(root)/(index)/index.html', + '(index)/[...missing].html', '(root)/notifications.html', '(index)/notifications.html', '(root)/(index)/[...missing].html', @@ -187,26 +210,22 @@ describe(getHtmlFiles, () => { expect( getHtmlFiles({ includeGroupVariations: false, - manifest: mockExpandRuntimeManifest({ - initialRouteName: undefined, - screens: { - '(root)': { - path: '(root)', - screens: { - '(index)': { - path: '(index)', - screens: { - '[...missing]': '*missing', - index: '', - notifications: 'notifications', - }, - initialRouteName: 'index', - }, - }, - initialRouteName: '(index)', + manifest: getMockConfig( + { + './(root)/_layout.tsx': { + unstable_settings: { initialRouteName: '(index)' }, + default: Route, + }, + './(root)/(index)/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, + './(root)/(index)/index.tsx': Route, + './(root)/(index)/[...missing].tsx': Route, + './(root)/(index)/notifications.tsx': Route, }, - }), + false + ), }) .map((a) => a.filePath) .sort((a, b) => a.length - b.length) @@ -219,24 +238,30 @@ describe(getHtmlFiles, () => { expect( getHtmlFiles({ includeGroupVariations: false, - manifest: mockExpandRuntimeManifest({ - initialRouteName: undefined, - screens: { - alpha: { - path: 'alpha', - screens: { index: '', second: 'second' }, - initialRouteName: 'index', + manifest: getMockConfig( + { + './alpha/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, - '(app)': { - path: '(app)', - screens: { compose: 'compose', index: '', 'note/[note]': 'note/:note' }, - initialRouteName: 'index', + './alpha/index.tsx': Route, + './alpha/second.tsx': Route, + + './(app)/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, - '(auth)/sign-in': '(auth)/sign-in', - _sitemap: '_sitemap', - '[...404]': '*404', + './(app)/compose.tsx': Route, + './(app)/index.tsx': Route, + './(app)/note/[note].tsx': Route, + + './(auth)/sign-in.js': Route, + + './_sitemap.tsx': Route, + './[...404].tsx': Route, }, - }), + false + ), }) .map((a) => a.filePath) .sort((a, b) => a.length - b.length) @@ -259,42 +284,100 @@ describe(getFilesToExportFromServerAsync, () => { const files = await getFilesToExportFromServerAsync('/', { exportServer: false, - manifest: mockExpandRuntimeManifest({ - initialRouteName: undefined, - screens: { - alpha: { - path: 'alpha', - screens: { index: '', second: 'second' }, - initialRouteName: 'index', + manifest: getMockConfig( + { + './alpha/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, - '(app)': { - path: '(app)', - screens: { compose: 'compose', index: '', 'note/[note]': 'note/:note' }, - initialRouteName: 'index', + './alpha/index.tsx': Route, + './alpha/second.tsx': Route, + + './(app)/_layout.tsx': { + unstable_settings: { initialRouteName: 'index' }, + default: Route, }, - '(auth)/sign-in': '(auth)/sign-in', - _sitemap: '_sitemap', - '[...404]': '*404', + './(app)/compose.tsx': Route, + './(app)/index.tsx': Route, + './(app)/note/[note].tsx': Route, + + './(auth)/sign-in.js': Route, + + './_sitemap.tsx': Route, + './[...404].tsx': Route, }, - }), + false + ), renderAsync, }); - expect([...files.keys()]).toEqual([ + expect([...files.keys()].sort()).toEqual([ + '(app)/compose.html', + '(app)/index.html', + '(app)/note/[note].html', + '(auth)/sign-in.html', + '[...404].html', + '_sitemap.html', 'alpha/index.html', 'alpha/second.html', - '(app)/compose.html', 'compose.html', - '(app)/index.html', 'index.html', - '(app)/note/[note].html', 'note/[note].html', - '(auth)/sign-in.html', 'sign-in.html', - '_sitemap.html', - '[...404].html', ]); expect([...files.values()].every((file) => file.targetDomain === 'client')).toBeTruthy(); }); + + it(`should export from server with array syntax`, async () => { + const renderAsync = jest.fn(async () => ''); + + const files = await getFilesToExportFromServerAsync('/', { + exportServer: true, + manifest: getMockConfig( + { + './(a,b)/multi-group.tsx': Route, + }, + false + ), + renderAsync, + }); + + expect(renderAsync).toHaveBeenNthCalledWith(1, { + filePath: '(a)/multi-group.html', + pathname: '(a)/multi-group', + route: expect.anything(), + }); + expect(renderAsync).toHaveBeenNthCalledWith(2, { + filePath: '(b)/multi-group.html', + pathname: '(b)/multi-group', + route: expect.anything(), + }); + expect([...files.keys()]).toEqual(['(a)/multi-group.html', '(b)/multi-group.html']); + + expect([...files.values()].every((file) => file.targetDomain === 'server')).toBeTruthy(); + }); + + it(`should export from server with top-level array syntax`, async () => { + const renderAsync = jest.fn(async () => ''); + + const files = await getFilesToExportFromServerAsync('/', { + exportServer: true, + manifest: getMockConfig( + { + './(a,b)/index.tsx': Route, + }, + false + ), + renderAsync, + }); + + expect(renderAsync).toHaveBeenNthCalledWith(1, { + filePath: '(a)/index.html', + pathname: '(a)', + route: expect.anything(), + }); + + expect([...files.keys()]).toEqual(['(a)/index.html', '(b)/index.html']); + }); }); diff --git a/packages/@expo/cli/src/export/exportStaticAsync.ts b/packages/@expo/cli/src/export/exportStaticAsync.ts index 5328d40703468..f5c3f7760fe21 100644 --- a/packages/@expo/cli/src/export/exportStaticAsync.ts +++ b/packages/@expo/cli/src/export/exportStaticAsync.ts @@ -7,6 +7,7 @@ import assert from 'assert'; import chalk from 'chalk'; import { RouteNode } from 'expo-router/build/Route'; +import { stripGroupSegmentsFromPath } from 'expo-router/build/matchers'; import path from 'path'; import resolveFrom from 'resolve-from'; import { inspect } from 'util'; @@ -285,6 +286,7 @@ export function getHtmlFiles({ if (leaf != null) { let filePath = baseUrl + leaf; + if (leaf === '') { filePath = baseUrl === '' @@ -292,6 +294,11 @@ export function getHtmlFiles({ : baseUrl.endsWith('/') ? baseUrl + 'index' : baseUrl.slice(0, -1); + } else if ( + // If the path is a collection of group segments leading to an index route, append `/index`. + stripGroupSegmentsFromPath(filePath) === '' + ) { + filePath += '/index'; } // This should never happen, the type of `string | object` originally comes from React Navigation. diff --git a/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts b/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts index 5becbd35f7cb8..07ca68dcacfee 100644 --- a/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts +++ b/packages/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts @@ -125,7 +125,7 @@ export function createRouteHandlerMiddleware( warnInvalidWebOutput(); } - const resolvedFunctionPath = await resolveAsync(route.page, { + const resolvedFunctionPath = await resolveAsync(route.file, { extensions: ['.js', '.jsx', '.ts', '.tsx'], basedir: options.appDir, })!; diff --git a/packages/@expo/server/CHANGELOG.md b/packages/@expo/server/CHANGELOG.md index 5ce603fea8b6f..f12ab4da976eb 100644 --- a/packages/@expo/server/CHANGELOG.md +++ b/packages/@expo/server/CHANGELOG.md @@ -10,6 +10,7 @@ ### 🐛 Bug fixes +- Fix using array syntax `(a,b)` with server output. ([#27462](https://github.com/expo/expo/pull/27462) by [@EvanBacon](https://github.com/EvanBacon)) - Fix issue with `duplex` streams not being properly handled. ([#27436](https://github.com/expo/expo/pull/27436) by [@EvanBacon](https://github.com/EvanBacon)) - Throw "method not found" when an API route has no exports. ([#27024](https://github.com/expo/expo/pull/27024) by [@EvanBacon](https://github.com/EvanBacon)) diff --git a/packages/expo-router/CHANGELOG.md b/packages/expo-router/CHANGELOG.md index 5f1d8fab56e9d..15f9f49d54885 100644 --- a/packages/expo-router/CHANGELOG.md +++ b/packages/expo-router/CHANGELOG.md @@ -10,6 +10,7 @@ ### 🐛 Bug fixes +- Fix using array syntax `(a,b)` with server output. ([#27462](https://github.com/expo/expo/pull/27462) by [@EvanBacon](https://github.com/EvanBacon)) - Fix issue with skipping all imports. ([#27238](https://github.com/expo/expo/pull/27238) by [@EvanBacon](https://github.com/EvanBacon)) - Include search parameters in the default Screen.getId() function. ([#26710](https://github.com/expo/expo/pull/26710) by [@marklawlor](https://github.com/marklawlor)) - Fix sitemap missing paths ([#26507](https://github.com/expo/expo/pull/26507) by [@marklawlor](https://github.com/marklawlor)) diff --git a/packages/expo-router/build/getLinkingConfig.d.ts b/packages/expo-router/build/getLinkingConfig.d.ts index 987618e2cfbbe..4aecd17933e2d 100644 --- a/packages/expo-router/build/getLinkingConfig.d.ts +++ b/packages/expo-router/build/getLinkingConfig.d.ts @@ -2,13 +2,13 @@ import { LinkingOptions } from '@react-navigation/native'; import { RouteNode } from './Route'; import { Screen } from './getReactNavigationConfig'; import { getPathFromState } from './link/linking'; -export declare function getNavigationConfig(routes: RouteNode): { +export declare function getNavigationConfig(routes: RouteNode, metaOnly?: boolean): { initialRouteName?: string; screens: Record; }; export type ExpoLinkingOptions = LinkingOptions & { getPathFromState?: typeof getPathFromState; }; -export declare function getLinkingConfig(routes: RouteNode): ExpoLinkingOptions; +export declare function getLinkingConfig(routes: RouteNode, metaOnly?: boolean): ExpoLinkingOptions; export declare const stateCache: Map; //# sourceMappingURL=getLinkingConfig.d.ts.map \ No newline at end of file diff --git a/packages/expo-router/build/getLinkingConfig.d.ts.map b/packages/expo-router/build/getLinkingConfig.d.ts.map index a8fc78b808804..2586c33c13f9b 100644 --- a/packages/expo-router/build/getLinkingConfig.d.ts.map +++ b/packages/expo-router/build/getLinkingConfig.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"getLinkingConfig.d.ts","sourceRoot":"","sources":["../src/getLinkingConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE9E,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,OAAO,EAA4B,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAGL,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,CAEA;AAED,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG;IACxD,gBAAgB,CAAC,EAAE,OAAO,gBAAgB,CAAC;CAC5C,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,kBAAkB,CA0BtE;AAED,eAAO,MAAM,UAAU,kBAAyB,CAAC"} \ No newline at end of file +{"version":3,"file":"getLinkingConfig.d.ts","sourceRoot":"","sources":["../src/getLinkingConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE9E,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,OAAO,EAA4B,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAGL,gBAAgB,EAEjB,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,QAAQ,GAAE,OAAc,GACvB;IACD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,CAEA;AAED,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG;IACxD,gBAAgB,CAAC,EAAE,OAAO,gBAAgB,CAAC;CAC5C,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,GAAE,OAAc,GAAG,kBAAkB,CA0BhG;AAED,eAAO,MAAM,UAAU,kBAAyB,CAAC"} \ No newline at end of file diff --git a/packages/expo-router/build/getLinkingConfig.js b/packages/expo-router/build/getLinkingConfig.js index b6d4703ef5942..ca15bc91b65ef 100644 --- a/packages/expo-router/build/getLinkingConfig.js +++ b/packages/expo-router/build/getLinkingConfig.js @@ -4,15 +4,15 @@ exports.stateCache = exports.getLinkingConfig = exports.getNavigationConfig = vo const native_1 = require("@react-navigation/native"); const getReactNavigationConfig_1 = require("./getReactNavigationConfig"); const linking_1 = require("./link/linking"); -function getNavigationConfig(routes) { - return (0, getReactNavigationConfig_1.getReactNavigationConfig)(routes, true); +function getNavigationConfig(routes, metaOnly = true) { + return (0, getReactNavigationConfig_1.getReactNavigationConfig)(routes, metaOnly); } exports.getNavigationConfig = getNavigationConfig; -function getLinkingConfig(routes) { +function getLinkingConfig(routes, metaOnly = true) { return { prefixes: [], // @ts-expect-error - config: getNavigationConfig(routes), + config: getNavigationConfig(routes, metaOnly), // A custom getInitialURL is used on native to ensure the app always starts at // the root path if it's launched from something other than a deep link. // This helps keep the native functionality working like the web functionality. diff --git a/packages/expo-router/build/getLinkingConfig.js.map b/packages/expo-router/build/getLinkingConfig.js.map index 3f4c9e343b719..56ae9a9be0c53 100644 --- a/packages/expo-router/build/getLinkingConfig.js.map +++ b/packages/expo-router/build/getLinkingConfig.js.map @@ -1 +1 @@ -{"version":3,"file":"getLinkingConfig.js","sourceRoot":"","sources":["../src/getLinkingConfig.ts"],"names":[],"mappings":";;;AAAA,qDAA8E;AAI9E,yEAA8E;AAC9E,4CAKwB;AAExB,SAAgB,mBAAmB,CAAC,MAAiB;IAInD,OAAO,IAAA,mDAAwB,EAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC;AALD,kDAKC;AAMD,SAAgB,gBAAgB,CAAC,MAAiB;IAChD,OAAO;QACL,QAAQ,EAAE,EAAE;QACZ,mBAAmB;QACnB,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC;QACnC,8EAA8E;QAC9E,wEAAwE;QACxE,+EAA+E;QAC/E,8GAA8G;QAC9G,8EAA8E;QAC9E,aAAa,EAAb,uBAAa;QACb,SAAS,EAAE,0BAAgB;QAC3B,gBAAgB,EAAE,wBAAwB;QAC1C,gBAAgB,CAAC,KAAY,EAAE,OAA+C;YAC5E,OAAO,CACL,IAAA,0BAAgB,EAAC,KAAK,EAAE;gBACtB,OAAO,EAAE,EAAE;gBACX,GAAG,IAAI,CAAC,MAAM;gBACd,GAAG,OAAO;aACX,CAAC,IAAI,GAAG,CACV,CAAC;QACJ,CAAC;QACD,gEAAgE;QAChE,kDAAkD;QAClD,kBAAkB,EAAlB,2BAAkB;KACnB,CAAC;AACJ,CAAC;AA1BD,4CA0BC;AAEY,QAAA,UAAU,GAAG,IAAI,GAAG,EAAe,CAAC;AAEjD,mJAAmJ;AACnJ,SAAS,wBAAwB,CAAC,IAAY,EAAE,OAA+C;IAC7F,MAAM,MAAM,GAAG,kBAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,MAAM,EAAE;QACV,OAAO,MAAM,CAAC;KACf;IACD,MAAM,MAAM,GAAG,IAAA,0BAAgB,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/C,kBAAU,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { getActionFromState, LinkingOptions } from '@react-navigation/native';\n\nimport { RouteNode } from './Route';\nimport { State } from './fork/getPathFromState';\nimport { getReactNavigationConfig, Screen } from './getReactNavigationConfig';\nimport {\n addEventListener,\n getInitialURL,\n getPathFromState,\n getStateFromPath,\n} from './link/linking';\n\nexport function getNavigationConfig(routes: RouteNode): {\n initialRouteName?: string;\n screens: Record;\n} {\n return getReactNavigationConfig(routes, true);\n}\n\nexport type ExpoLinkingOptions = LinkingOptions & {\n getPathFromState?: typeof getPathFromState;\n};\n\nexport function getLinkingConfig(routes: RouteNode): ExpoLinkingOptions {\n return {\n prefixes: [],\n // @ts-expect-error\n config: getNavigationConfig(routes),\n // A custom getInitialURL is used on native to ensure the app always starts at\n // the root path if it's launched from something other than a deep link.\n // This helps keep the native functionality working like the web functionality.\n // For example, if you had a root navigator where the first screen was `/settings` and the second was `/index`\n // then `/index` would be used on web and `/settings` would be used on native.\n getInitialURL,\n subscribe: addEventListener,\n getStateFromPath: getStateFromPathMemoized,\n getPathFromState(state: State, options: Parameters[1]) {\n return (\n getPathFromState(state, {\n screens: [],\n ...this.config,\n ...options,\n }) ?? '/'\n );\n },\n // Add all functions to ensure the types never need to fallback.\n // This is a convenience for usage in the package.\n getActionFromState,\n };\n}\n\nexport const stateCache = new Map();\n\n/** We can reduce work by memoizing the state by the pathname. This only works because the options (linking config) theoretically never change. */\nfunction getStateFromPathMemoized(path: string, options: Parameters[1]) {\n const cached = stateCache.get(path);\n if (cached) {\n return cached;\n }\n const result = getStateFromPath(path, options);\n stateCache.set(path, result);\n return result;\n}\n"]} \ No newline at end of file +{"version":3,"file":"getLinkingConfig.js","sourceRoot":"","sources":["../src/getLinkingConfig.ts"],"names":[],"mappings":";;;AAAA,qDAA8E;AAI9E,yEAA8E;AAC9E,4CAKwB;AAExB,SAAgB,mBAAmB,CACjC,MAAiB,EACjB,WAAoB,IAAI;IAKxB,OAAO,IAAA,mDAAwB,EAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACpD,CAAC;AARD,kDAQC;AAMD,SAAgB,gBAAgB,CAAC,MAAiB,EAAE,WAAoB,IAAI;IAC1E,OAAO;QACL,QAAQ,EAAE,EAAE;QACZ,mBAAmB;QACnB,MAAM,EAAE,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC;QAC7C,8EAA8E;QAC9E,wEAAwE;QACxE,+EAA+E;QAC/E,8GAA8G;QAC9G,8EAA8E;QAC9E,aAAa,EAAb,uBAAa;QACb,SAAS,EAAE,0BAAgB;QAC3B,gBAAgB,EAAE,wBAAwB;QAC1C,gBAAgB,CAAC,KAAY,EAAE,OAA+C;YAC5E,OAAO,CACL,IAAA,0BAAgB,EAAC,KAAK,EAAE;gBACtB,OAAO,EAAE,EAAE;gBACX,GAAG,IAAI,CAAC,MAAM;gBACd,GAAG,OAAO;aACX,CAAC,IAAI,GAAG,CACV,CAAC;QACJ,CAAC;QACD,gEAAgE;QAChE,kDAAkD;QAClD,kBAAkB,EAAlB,2BAAkB;KACnB,CAAC;AACJ,CAAC;AA1BD,4CA0BC;AAEY,QAAA,UAAU,GAAG,IAAI,GAAG,EAAe,CAAC;AAEjD,mJAAmJ;AACnJ,SAAS,wBAAwB,CAAC,IAAY,EAAE,OAA+C;IAC7F,MAAM,MAAM,GAAG,kBAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,MAAM,EAAE;QACV,OAAO,MAAM,CAAC;KACf;IACD,MAAM,MAAM,GAAG,IAAA,0BAAgB,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/C,kBAAU,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { getActionFromState, LinkingOptions } from '@react-navigation/native';\n\nimport { RouteNode } from './Route';\nimport { State } from './fork/getPathFromState';\nimport { getReactNavigationConfig, Screen } from './getReactNavigationConfig';\nimport {\n addEventListener,\n getInitialURL,\n getPathFromState,\n getStateFromPath,\n} from './link/linking';\n\nexport function getNavigationConfig(\n routes: RouteNode,\n metaOnly: boolean = true\n): {\n initialRouteName?: string;\n screens: Record;\n} {\n return getReactNavigationConfig(routes, metaOnly);\n}\n\nexport type ExpoLinkingOptions = LinkingOptions & {\n getPathFromState?: typeof getPathFromState;\n};\n\nexport function getLinkingConfig(routes: RouteNode, metaOnly: boolean = true): ExpoLinkingOptions {\n return {\n prefixes: [],\n // @ts-expect-error\n config: getNavigationConfig(routes, metaOnly),\n // A custom getInitialURL is used on native to ensure the app always starts at\n // the root path if it's launched from something other than a deep link.\n // This helps keep the native functionality working like the web functionality.\n // For example, if you had a root navigator where the first screen was `/settings` and the second was `/index`\n // then `/index` would be used on web and `/settings` would be used on native.\n getInitialURL,\n subscribe: addEventListener,\n getStateFromPath: getStateFromPathMemoized,\n getPathFromState(state: State, options: Parameters[1]) {\n return (\n getPathFromState(state, {\n screens: [],\n ...this.config,\n ...options,\n }) ?? '/'\n );\n },\n // Add all functions to ensure the types never need to fallback.\n // This is a convenience for usage in the package.\n getActionFromState,\n };\n}\n\nexport const stateCache = new Map();\n\n/** We can reduce work by memoizing the state by the pathname. This only works because the options (linking config) theoretically never change. */\nfunction getStateFromPathMemoized(path: string, options: Parameters[1]) {\n const cached = stateCache.get(path);\n if (cached) {\n return cached;\n }\n const result = getStateFromPath(path, options);\n stateCache.set(path, result);\n return result;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/getServerManifest.d.ts b/packages/expo-router/build/getServerManifest.d.ts index 4bc7cbb9bb5d7..e5bc6714976de 100644 --- a/packages/expo-router/build/getServerManifest.d.ts +++ b/packages/expo-router/build/getServerManifest.d.ts @@ -30,7 +30,6 @@ export interface RouteRegex { re: RegExp; } export declare function getServerManifest(route: RouteNode): ExpoRouterServerManifestV1; -export declare function getNamedRouteRegex(normalizedRoute: string, page: string): ExpoRouterServerManifestV1Route; export declare function parseParameter(param: string): { name: string; repeat: boolean; diff --git a/packages/expo-router/build/getServerManifest.d.ts.map b/packages/expo-router/build/getServerManifest.d.ts.map index 13d8d8c9659ed..e0adc1429e119 100644 --- a/packages/expo-router/build/getServerManifest.d.ts.map +++ b/packages/expo-router/build/getServerManifest.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"getServerManifest.d.ts","sourceRoot":"","sources":["../src/getServerManifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAKzC,MAAM,MAAM,+BAA+B,CAAC,MAAM,GAAG,MAAM,IAAI;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,MAAM,GAAG,MAAM,IAAI;IACxD,SAAS,EAAE,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;IACrD,UAAU,EAAE,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;IACtD,cAAc,EAAE,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9B,EAAE,EAAE,MAAM,CAAC;CACZ;AAWD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,0BAA0B,CA8B9E;AAiBD,wBAAgB,kBAAkB,CAChC,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,GACX,+BAA+B,CAQjC;AA2GD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM;;;;EAgB3C"} \ No newline at end of file +{"version":3,"file":"getServerManifest.d.ts","sourceRoot":"","sources":["../src/getServerManifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAKzC,MAAM,MAAM,+BAA+B,CAAC,MAAM,GAAG,MAAM,IAAI;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,MAAM,GAAG,MAAM,IAAI;IACxD,SAAS,EAAE,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;IACrD,UAAU,EAAE,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;IACtD,cAAc,EAAE,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;CAC3D,CAAC;AAEF,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9B,EAAE,EAAE,MAAM,CAAC;CACZ;AAmBD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,0BAA0B,CA6C9E;AAmJD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM;;;;EAgB3C"} \ No newline at end of file diff --git a/packages/expo-router/build/getServerManifest.js b/packages/expo-router/build/getServerManifest.js index 5cf1d12194736..baa42104b3ef9 100644 --- a/packages/expo-router/build/getServerManifest.js +++ b/packages/expo-router/build/getServerManifest.js @@ -1,28 +1,46 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseParameter = exports.getNamedRouteRegex = exports.getServerManifest = void 0; +exports.parseParameter = exports.getServerManifest = void 0; const matchers_1 = require("./matchers"); const sortRoutes_1 = require("./sortRoutes"); -function isApiRoute(route) { - return !route.children.length && !!route.contextKey.match(/\+api\.[jt]sx?$/); -} function isNotFoundRoute(route) { return route.dynamic && route.dynamic[route.dynamic.length - 1].notFound; } +function uniqueBy(arr, key) { + const seen = new Set(); + return arr.filter((item) => { + const id = key(item); + if (seen.has(id)) { + return false; + } + seen.add(id); + return true; + }); +} // Given a nested route tree, return a flattened array of all routes that can be matched. function getServerManifest(route) { function getFlatNodes(route) { if (route.children.length) { return route.children.map((child) => getFlatNodes(child)).flat(); } - const key = (0, matchers_1.getContextKey)(route.contextKey).replace(/\/index$/, '') ?? '/'; + // API Routes are handled differently to HTML routes because they have no nested behavior. + // An HTML route can be different based on parent segments due to layout routes, therefore multiple + // copies should be rendered. However, an API route is always the same regardless of parent segments. + let key; + if (route.type === 'api') { + key = (0, matchers_1.getContextKey)(route.contextKey).replace(/\/index$/, '') ?? '/'; + } + else { + key = (0, matchers_1.getContextKey)(route.route).replace(/\/index$/, '') ?? '/'; + } return [[key, route]]; } + // Remove duplicates from the runtime manifest which expands array syntax. const flat = getFlatNodes(route) .sort(([, a], [, b]) => (0, sortRoutes_1.sortRoutes)(b, a)) .reverse(); - const apiRoutes = flat.filter(([, route]) => isApiRoute(route)); - const otherRoutes = flat.filter(([, route]) => !isApiRoute(route)); + const apiRoutes = uniqueBy(flat.filter(([, route]) => route.type === 'api'), ([path]) => path); + const otherRoutes = uniqueBy(flat.filter(([, route]) => route.type === 'route'), ([path]) => path); const standardRoutes = otherRoutes.filter(([, route]) => !isNotFoundRoute(route)); const notFoundRoutes = otherRoutes.filter(([, route]) => isNotFoundRoute(route)); return { @@ -34,23 +52,22 @@ function getServerManifest(route) { exports.getServerManifest = getServerManifest; function getMatchableManifestForPaths(paths) { return paths.map((normalizedRoutePath) => { - const matcher = getNamedRouteRegex(normalizedRoutePath[0], normalizedRoutePath[1].contextKey); + const matcher = getNamedRouteRegex(normalizedRoutePath[0], (0, matchers_1.getContextKey)(normalizedRoutePath[1].route), normalizedRoutePath[1].contextKey); if (normalizedRoutePath[1].generated) { matcher.generated = true; } return matcher; }); } -function getNamedRouteRegex(normalizedRoute, page) { +function getNamedRouteRegex(normalizedRoute, page, file) { const result = getNamedParametrizedRoute(normalizedRoute); return { - file: page, - page: page.replace(/\.[jt]sx?$/, ''), + file, + page, namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`, routeKeys: result.routeKeys, }; } -exports.getNamedRouteRegex = getNamedRouteRegex; /** * Builds a function to generate a minimal routeKey using only a-z and minimal * number of characters. @@ -125,8 +142,19 @@ function getNamedParametrizedRoute(route) { : `/(?<${cleanedKey}>[^/]+?)`; } else if (/^\(.*\)$/.test(segment)) { - // Make section optional - return `(?:/${escapeStringRegexp(segment)})?`; + const groupName = (0, matchers_1.matchGroupName)(segment) + .split(',') + .map((group) => group.trim()) + .filter(Boolean); + if (groupName.length > 1) { + const optionalSegment = `\\((?:${groupName.map(escapeStringRegexp).join('|')})\\)`; + // Make section optional + return `(?:/${optionalSegment})?`; + } + else { + // Use simpler regex for single groups + return `(?:/${escapeStringRegexp(segment)})?`; + } } else { return `/${escapeStringRegexp(segment)}`; diff --git a/packages/expo-router/build/getServerManifest.js.map b/packages/expo-router/build/getServerManifest.js.map index cc29d486a5cd7..33a4445a8ffb1 100644 --- a/packages/expo-router/build/getServerManifest.js.map +++ b/packages/expo-router/build/getServerManifest.js.map @@ -1 +1 @@ -{"version":3,"file":"getServerManifest.js","sourceRoot":"","sources":["../src/getServerManifest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Copyright © 2023 650 Industries.\n * Copyright © 2023 Vercel, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * Based on https://github.com/vercel/next.js/blob/1df2686bc9964f1a86c444701fa5cbf178669833/packages/next/src/shared/lib/router/utils/route-regex.ts\n */\nimport type { RouteNode } from './Route';\nimport { getContextKey } from './matchers';\nimport { sortRoutes } from './sortRoutes';\n\n// TODO: Share these types across cli, server, router, etc.\nexport type ExpoRouterServerManifestV1Route = {\n file: string;\n page: string;\n routeKeys: Record;\n namedRegex: TRegex;\n generated?: boolean;\n};\n\nexport type ExpoRouterServerManifestV1 = {\n apiRoutes: ExpoRouterServerManifestV1Route[];\n htmlRoutes: ExpoRouterServerManifestV1Route[];\n notFoundRoutes: ExpoRouterServerManifestV1Route[];\n};\n\nexport interface Group {\n pos: number;\n repeat: boolean;\n optional: boolean;\n}\n\nexport interface RouteRegex {\n groups: Record;\n re: RegExp;\n}\n\nfunction isApiRoute(route: RouteNode) {\n return !route.children.length && !!route.contextKey.match(/\\+api\\.[jt]sx?$/);\n}\n\nfunction isNotFoundRoute(route: RouteNode) {\n return route.dynamic && route.dynamic[route.dynamic.length - 1].notFound;\n}\n\n// Given a nested route tree, return a flattened array of all routes that can be matched.\nexport function getServerManifest(route: RouteNode): ExpoRouterServerManifestV1 {\n function getFlatNodes(route: RouteNode): [string, RouteNode][] {\n if (route.children.length) {\n return route.children.map((child) => getFlatNodes(child)).flat();\n }\n\n const key = getContextKey(route.contextKey).replace(/\\/index$/, '') ?? '/';\n return [[key, route]];\n }\n\n const flat = getFlatNodes(route)\n .sort(([, a], [, b]) => sortRoutes(b, a))\n .reverse();\n\n const apiRoutes = flat.filter(([, route]) => isApiRoute(route));\n const otherRoutes = flat.filter(([, route]) => !isApiRoute(route));\n const standardRoutes = otherRoutes.filter(([, route]) => !isNotFoundRoute(route));\n const notFoundRoutes = otherRoutes.filter(([, route]) => isNotFoundRoute(route));\n\n return {\n apiRoutes: getMatchableManifestForPaths(\n apiRoutes.map(([normalizedRoutePath, node]) => [normalizedRoutePath, node])\n ),\n htmlRoutes: getMatchableManifestForPaths(\n standardRoutes.map(([normalizedRoutePath, node]) => [normalizedRoutePath, node])\n ),\n notFoundRoutes: getMatchableManifestForPaths(\n notFoundRoutes.map(([normalizedRoutePath, node]) => [normalizedRoutePath, node])\n ),\n };\n}\n\nfunction getMatchableManifestForPaths(\n paths: [string, RouteNode][]\n): ExpoRouterServerManifestV1Route[] {\n return paths.map((normalizedRoutePath) => {\n const matcher: ExpoRouterServerManifestV1Route = getNamedRouteRegex(\n normalizedRoutePath[0],\n normalizedRoutePath[1].contextKey\n );\n if (normalizedRoutePath[1].generated) {\n matcher.generated = true;\n }\n return matcher;\n });\n}\n\nexport function getNamedRouteRegex(\n normalizedRoute: string,\n page: string\n): ExpoRouterServerManifestV1Route {\n const result = getNamedParametrizedRoute(normalizedRoute);\n return {\n file: page,\n page: page.replace(/\\.[jt]sx?$/, ''),\n namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,\n routeKeys: result.routeKeys,\n };\n}\n\n/**\n * Builds a function to generate a minimal routeKey using only a-z and minimal\n * number of characters.\n */\nfunction buildGetSafeRouteKey() {\n let currentCharCode = 96; // Starting one before 'a' to make the increment logic simpler\n let currentLength = 1;\n\n return () => {\n let result = '';\n let incrementNext = true;\n\n // Iterate from right to left to build the key\n for (let i = 0; i < currentLength; i++) {\n if (incrementNext) {\n currentCharCode++;\n if (currentCharCode > 122) {\n currentCharCode = 97; // Reset to 'a'\n incrementNext = true; // Continue to increment the next character\n } else {\n incrementNext = false;\n }\n }\n result = String.fromCharCode(currentCharCode) + result;\n }\n\n // If all characters are 'z', increase the length of the key\n if (incrementNext) {\n currentLength++;\n currentCharCode = 96; // This will make the next key start with 'a'\n }\n\n return result;\n };\n}\n\nfunction removeTrailingSlash(route: string): string {\n return route.replace(/\\/$/, '') || '/';\n}\n\nfunction getNamedParametrizedRoute(route: string) {\n const segments = removeTrailingSlash(route).slice(1).split('/');\n const getSafeRouteKey = buildGetSafeRouteKey();\n const routeKeys: Record = {};\n return {\n namedParameterizedRoute: segments\n .map((segment, index) => {\n if (segment === '+not-found' && index === segments.length - 1) {\n segment = '[...not-found]';\n }\n if (/^\\[.*\\]$/.test(segment)) {\n const { name, optional, repeat } = parseParameter(segment);\n // replace any non-word characters since they can break\n // the named regex\n let cleanedKey = name.replace(/\\W/g, '');\n let invalidKey = false;\n\n // check if the key is still invalid and fallback to using a known\n // safe key\n if (cleanedKey.length === 0 || cleanedKey.length > 30) {\n invalidKey = true;\n }\n if (!isNaN(parseInt(cleanedKey.slice(0, 1), 10))) {\n invalidKey = true;\n }\n\n // Prevent duplicates after sanitizing the key\n if (cleanedKey in routeKeys) {\n invalidKey = true;\n }\n\n if (invalidKey) {\n cleanedKey = getSafeRouteKey();\n }\n\n routeKeys[cleanedKey] = name;\n return repeat\n ? optional\n ? `(?:/(?<${cleanedKey}>.+?))?`\n : `/(?<${cleanedKey}>.+?)`\n : `/(?<${cleanedKey}>[^/]+?)`;\n } else if (/^\\(.*\\)$/.test(segment)) {\n // Make section optional\n return `(?:/${escapeStringRegexp(segment)})?`;\n } else {\n return `/${escapeStringRegexp(segment)}`;\n }\n })\n .join(''),\n routeKeys,\n };\n}\n\n// regexp is based on https://github.com/sindresorhus/escape-string-regexp\nconst reHasRegExp = /[|\\\\{}()[\\]^$+*?.-]/;\nconst reReplaceRegExp = /[|\\\\{}()[\\]^$+*?.-]/g;\n\nfunction escapeStringRegexp(str: string) {\n // see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23\n if (reHasRegExp.test(str)) {\n return str.replace(reReplaceRegExp, '\\\\$&');\n }\n return str;\n}\n\nexport function parseParameter(param: string) {\n let repeat = false;\n let optional = false;\n let name = param;\n\n if (/^\\[.*\\]$/.test(name)) {\n optional = true;\n name = name.slice(1, -1);\n }\n\n if (/^\\.\\.\\./.test(name)) {\n repeat = true;\n name = name.slice(3);\n }\n\n return { name, repeat, optional };\n}\n"]} \ No newline at end of file +{"version":3,"file":"getServerManifest.js","sourceRoot":"","sources":["../src/getServerManifest.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Copyright © 2023 650 Industries.\n * Copyright © 2023 Vercel, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * Based on https://github.com/vercel/next.js/blob/1df2686bc9964f1a86c444701fa5cbf178669833/packages/next/src/shared/lib/router/utils/route-regex.ts\n */\nimport type { RouteNode } from './Route';\nimport { getContextKey, matchGroupName } from './matchers';\nimport { sortRoutes } from './sortRoutes';\n\n// TODO: Share these types across cli, server, router, etc.\nexport type ExpoRouterServerManifestV1Route = {\n file: string;\n page: string;\n routeKeys: Record;\n namedRegex: TRegex;\n generated?: boolean;\n};\n\nexport type ExpoRouterServerManifestV1 = {\n apiRoutes: ExpoRouterServerManifestV1Route[];\n htmlRoutes: ExpoRouterServerManifestV1Route[];\n notFoundRoutes: ExpoRouterServerManifestV1Route[];\n};\n\nexport interface Group {\n pos: number;\n repeat: boolean;\n optional: boolean;\n}\n\nexport interface RouteRegex {\n groups: Record;\n re: RegExp;\n}\n\nfunction isNotFoundRoute(route: RouteNode) {\n return route.dynamic && route.dynamic[route.dynamic.length - 1].notFound;\n}\n\nfunction uniqueBy(arr: T[], key: (item: T) => string): T[] {\n const seen = new Set();\n return arr.filter((item) => {\n const id = key(item);\n if (seen.has(id)) {\n return false;\n }\n seen.add(id);\n return true;\n });\n}\n\n// Given a nested route tree, return a flattened array of all routes that can be matched.\nexport function getServerManifest(route: RouteNode): ExpoRouterServerManifestV1 {\n function getFlatNodes(route: RouteNode): [string, RouteNode][] {\n if (route.children.length) {\n return route.children.map((child) => getFlatNodes(child)).flat();\n }\n\n // API Routes are handled differently to HTML routes because they have no nested behavior.\n // An HTML route can be different based on parent segments due to layout routes, therefore multiple\n // copies should be rendered. However, an API route is always the same regardless of parent segments.\n let key: string;\n if (route.type === 'api') {\n key = getContextKey(route.contextKey).replace(/\\/index$/, '') ?? '/';\n } else {\n key = getContextKey(route.route).replace(/\\/index$/, '') ?? '/';\n }\n return [[key, route]];\n }\n\n // Remove duplicates from the runtime manifest which expands array syntax.\n const flat = getFlatNodes(route)\n .sort(([, a], [, b]) => sortRoutes(b, a))\n .reverse();\n\n const apiRoutes = uniqueBy(\n flat.filter(([, route]) => route.type === 'api'),\n ([path]) => path\n );\n const otherRoutes = uniqueBy(\n flat.filter(([, route]) => route.type === 'route'),\n ([path]) => path\n );\n const standardRoutes = otherRoutes.filter(([, route]) => !isNotFoundRoute(route));\n const notFoundRoutes = otherRoutes.filter(([, route]) => isNotFoundRoute(route));\n\n return {\n apiRoutes: getMatchableManifestForPaths(\n apiRoutes.map(([normalizedRoutePath, node]) => [normalizedRoutePath, node])\n ),\n htmlRoutes: getMatchableManifestForPaths(\n standardRoutes.map(([normalizedRoutePath, node]) => [normalizedRoutePath, node])\n ),\n notFoundRoutes: getMatchableManifestForPaths(\n notFoundRoutes.map(([normalizedRoutePath, node]) => [normalizedRoutePath, node])\n ),\n };\n}\n\nfunction getMatchableManifestForPaths(\n paths: [string, RouteNode][]\n): ExpoRouterServerManifestV1Route[] {\n return paths.map((normalizedRoutePath) => {\n const matcher: ExpoRouterServerManifestV1Route = getNamedRouteRegex(\n normalizedRoutePath[0],\n getContextKey(normalizedRoutePath[1].route),\n normalizedRoutePath[1].contextKey\n );\n if (normalizedRoutePath[1].generated) {\n matcher.generated = true;\n }\n return matcher;\n });\n}\n\nfunction getNamedRouteRegex(\n normalizedRoute: string,\n page: string,\n file: string\n): ExpoRouterServerManifestV1Route {\n const result = getNamedParametrizedRoute(normalizedRoute);\n return {\n file,\n page,\n namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,\n routeKeys: result.routeKeys,\n };\n}\n\n/**\n * Builds a function to generate a minimal routeKey using only a-z and minimal\n * number of characters.\n */\nfunction buildGetSafeRouteKey() {\n let currentCharCode = 96; // Starting one before 'a' to make the increment logic simpler\n let currentLength = 1;\n\n return () => {\n let result = '';\n let incrementNext = true;\n\n // Iterate from right to left to build the key\n for (let i = 0; i < currentLength; i++) {\n if (incrementNext) {\n currentCharCode++;\n if (currentCharCode > 122) {\n currentCharCode = 97; // Reset to 'a'\n incrementNext = true; // Continue to increment the next character\n } else {\n incrementNext = false;\n }\n }\n result = String.fromCharCode(currentCharCode) + result;\n }\n\n // If all characters are 'z', increase the length of the key\n if (incrementNext) {\n currentLength++;\n currentCharCode = 96; // This will make the next key start with 'a'\n }\n\n return result;\n };\n}\n\nfunction removeTrailingSlash(route: string): string {\n return route.replace(/\\/$/, '') || '/';\n}\n\nfunction getNamedParametrizedRoute(route: string) {\n const segments = removeTrailingSlash(route).slice(1).split('/');\n const getSafeRouteKey = buildGetSafeRouteKey();\n const routeKeys: Record = {};\n return {\n namedParameterizedRoute: segments\n .map((segment, index) => {\n if (segment === '+not-found' && index === segments.length - 1) {\n segment = '[...not-found]';\n }\n if (/^\\[.*\\]$/.test(segment)) {\n const { name, optional, repeat } = parseParameter(segment);\n // replace any non-word characters since they can break\n // the named regex\n let cleanedKey = name.replace(/\\W/g, '');\n let invalidKey = false;\n\n // check if the key is still invalid and fallback to using a known\n // safe key\n if (cleanedKey.length === 0 || cleanedKey.length > 30) {\n invalidKey = true;\n }\n if (!isNaN(parseInt(cleanedKey.slice(0, 1), 10))) {\n invalidKey = true;\n }\n\n // Prevent duplicates after sanitizing the key\n if (cleanedKey in routeKeys) {\n invalidKey = true;\n }\n\n if (invalidKey) {\n cleanedKey = getSafeRouteKey();\n }\n\n routeKeys[cleanedKey] = name;\n return repeat\n ? optional\n ? `(?:/(?<${cleanedKey}>.+?))?`\n : `/(?<${cleanedKey}>.+?)`\n : `/(?<${cleanedKey}>[^/]+?)`;\n } else if (/^\\(.*\\)$/.test(segment)) {\n const groupName = matchGroupName(segment)!\n .split(',')\n .map((group) => group.trim())\n .filter(Boolean);\n if (groupName.length > 1) {\n const optionalSegment = `\\\\((?:${groupName.map(escapeStringRegexp).join('|')})\\\\)`;\n // Make section optional\n return `(?:/${optionalSegment})?`;\n } else {\n // Use simpler regex for single groups\n return `(?:/${escapeStringRegexp(segment)})?`;\n }\n } else {\n return `/${escapeStringRegexp(segment)}`;\n }\n })\n .join(''),\n routeKeys,\n };\n}\n\n// regexp is based on https://github.com/sindresorhus/escape-string-regexp\nconst reHasRegExp = /[|\\\\{}()[\\]^$+*?.-]/;\nconst reReplaceRegExp = /[|\\\\{}()[\\]^$+*?.-]/g;\n\nfunction escapeStringRegexp(str: string) {\n // see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23\n if (reHasRegExp.test(str)) {\n return str.replace(reReplaceRegExp, '\\\\$&');\n }\n return str;\n}\n\nexport function parseParameter(param: string) {\n let repeat = false;\n let optional = false;\n let name = param;\n\n if (/^\\[.*\\]$/.test(name)) {\n optional = true;\n name = name.slice(1, -1);\n }\n\n if (/^\\.\\.\\./.test(name)) {\n repeat = true;\n name = name.slice(3);\n }\n\n return { name, repeat, optional };\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/testing-library/index.d.ts b/packages/expo-router/build/testing-library/index.d.ts index 5bd64069ef3ae..f4fd805309c4f 100644 --- a/packages/expo-router/build/testing-library/index.d.ts +++ b/packages/expo-router/build/testing-library/index.d.ts @@ -1,6 +1,6 @@ import './expect'; import { render } from '@testing-library/react-native'; -import { FileStub } from './context-stubs'; +import { MockContextConfig, getMockConfig, getMockContext } from './mock-config'; export * from '@testing-library/react-native'; type RenderRouterOptions = Parameters[1] & { initialUrl?: any; @@ -11,19 +11,7 @@ type Result = ReturnType & { getSegments(): string[]; getSearchParams(): Record; }; -export type MockContextConfig = string | string[] | Record | { - appDir: string; - overrides: Record; -}; -export declare function getMockConfig(context: MockContextConfig): { - initialRouteName?: string | undefined; - screens: Record; -}; -export declare function getMockContext(context: MockContextConfig): ((id: string) => any) & { - keys: () => string[]; - resolve: (key: string) => string; - id: string; -}; +export { MockContextConfig, getMockConfig, getMockContext }; export declare function renderRouter(context?: MockContextConfig, { initialUrl, ...options }?: RenderRouterOptions): Result; export declare const testRouter: { /** Navigate to the provided pathname and the pathname */ diff --git a/packages/expo-router/build/testing-library/index.d.ts.map b/packages/expo-router/build/testing-library/index.d.ts.map index 035d8a0ffcd37..0f38f558d737f 100644 --- a/packages/expo-router/build/testing-library/index.d.ts.map +++ b/packages/expo-router/build/testing-library/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing-library/index.tsx"],"names":[],"mappings":"AACA,OAAO,UAAU,CAAC;AAElB,OAAO,EAAO,MAAM,EAAwB,MAAM,+BAA+B,CAAC;AAIlF,OAAO,EACL,QAAQ,EAIT,MAAM,iBAAiB,CAAC;AAUzB,cAAc,+BAA+B,CAAC;AAE9C,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG;IACxD,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB,CAAC;AAEF,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,GAAG;IACxC,WAAW,IAAI,MAAM,CAAC;IACtB,qBAAqB,IAAI,MAAM,CAAC;IAChC,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CACtD,CAAC;AAQF,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,MAAM,EAAE,GACR,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GACxB;IAEE,MAAM,EAAE,MAAM,CAAC;IAEf,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CACrC,CAAC;AAEN,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB;;;EAEvD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB;;;;EAYxD;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,iBAA2B,EACpC,EAAE,UAAgB,EAAE,GAAG,OAAO,EAAE,GAAE,mBAAwB,GACzD,MAAM,CAsCR;AAED,eAAO,MAAM,UAAU;IACrB,yDAAyD;mBAC1C,MAAM;IAIrB,yDAAyD;eAC9C,MAAM;IAIjB,6DAA6D;kBAC/C,MAAM;IAIpB,oDAAoD;gBACxC,MAAM;IAOlB,qEAAqE;;IAIrE,wEAAwE;uBACrD,OAAO,MAAM,EAAE,MAAM,CAAC,SAAS,MAAM;IAMxD,qEAAqE;;CAItE,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing-library/index.tsx"],"names":[],"mappings":"AACA,OAAO,UAAU,CAAC;AAElB,OAAO,EAAO,MAAM,EAAwB,MAAM,+BAA+B,CAAC;AAGlF,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AASjF,cAAc,+BAA+B,CAAC;AAE9C,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG;IACxD,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB,CAAC;AAEF,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,GAAG;IACxC,WAAW,IAAI,MAAM,CAAC;IACtB,qBAAqB,IAAI,MAAM,CAAC;IAChC,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CACtD,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AAE5D,wBAAgB,YAAY,CAC1B,OAAO,GAAE,iBAA2B,EACpC,EAAE,UAAgB,EAAE,GAAG,OAAO,EAAE,GAAE,mBAAwB,GACzD,MAAM,CAsCR;AAED,eAAO,MAAM,UAAU;IACrB,yDAAyD;mBAC1C,MAAM;IAIrB,yDAAyD;eAC9C,MAAM;IAIjB,6DAA6D;kBAC/C,MAAM;IAIpB,oDAAoD;gBACxC,MAAM;IAOlB,qEAAqE;;IAIrE,wEAAwE;uBACrD,OAAO,MAAM,EAAE,MAAM,CAAC,SAAS,MAAM;IAMxD,qEAAqE;;CAItE,CAAC"} \ No newline at end of file diff --git a/packages/expo-router/build/testing-library/index.js b/packages/expo-router/build/testing-library/index.js index ace1b506ce40d..18726bf57173d 100644 --- a/packages/expo-router/build/testing-library/index.js +++ b/packages/expo-router/build/testing-library/index.js @@ -21,43 +21,21 @@ exports.testRouter = exports.renderRouter = exports.getMockContext = exports.get /// require("./expect"); const react_native_1 = require("@testing-library/react-native"); -const path_1 = __importDefault(require("path")); const react_1 = __importDefault(require("react")); -const context_stubs_1 = require("./context-stubs"); +const mock_config_1 = require("./mock-config"); +Object.defineProperty(exports, "getMockConfig", { enumerable: true, get: function () { return mock_config_1.getMockConfig; } }); +Object.defineProperty(exports, "getMockContext", { enumerable: true, get: function () { return mock_config_1.getMockContext; } }); const mocks_1 = require("./mocks"); const ExpoRoot_1 = require("../ExpoRoot"); const getPathFromState_1 = __importDefault(require("../fork/getPathFromState")); const getLinkingConfig_1 = require("../getLinkingConfig"); -const getRoutes_1 = require("../getRoutes"); const router_store_1 = require("../global-state/router-store"); const imperative_api_1 = require("../imperative-api"); // re-export everything __exportStar(require("@testing-library/react-native"), exports); -function isOverrideContext(context) { - return Boolean(typeof context === 'object' && 'appDir' in context); -} -function getMockConfig(context) { - return (0, getLinkingConfig_1.getNavigationConfig)((0, getRoutes_1.getExactRoutes)(getMockContext(context))); -} -exports.getMockConfig = getMockConfig; -function getMockContext(context) { - if (typeof context === 'string') { - return (0, context_stubs_1.requireContext)(path_1.default.resolve(process.cwd(), context)); - } - else if (Array.isArray(context)) { - return (0, context_stubs_1.inMemoryContext)(Object.fromEntries(context.map((filename) => [filename, { default: () => null }]))); - } - else if (isOverrideContext(context)) { - return (0, context_stubs_1.requireContextWithOverrides)(context.appDir, context.overrides); - } - else { - return (0, context_stubs_1.inMemoryContext)(context); - } -} -exports.getMockContext = getMockContext; function renderRouter(context = './app', { initialUrl = '/', ...options } = {}) { jest.useFakeTimers(); - const mockContext = getMockContext(context); + const mockContext = (0, mock_config_1.getMockContext)(context); // Reset the initial URL (0, mocks_1.setInitialUrl)(initialUrl); // Force the render to be synchronous diff --git a/packages/expo-router/build/testing-library/index.js.map b/packages/expo-router/build/testing-library/index.js.map index 7c7bf9a468d91..591642808004a 100644 --- a/packages/expo-router/build/testing-library/index.js.map +++ b/packages/expo-router/build/testing-library/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing-library/index.tsx"],"names":[],"mappings":"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","sourcesContent":["/// \nimport './expect';\n\nimport { act, render, RenderResult, screen } from '@testing-library/react-native';\nimport path from 'path';\nimport React from 'react';\n\nimport {\n FileStub,\n inMemoryContext,\n requireContext,\n requireContextWithOverrides,\n} from './context-stubs';\nimport { setInitialUrl } from './mocks';\nimport { ExpoRoot } from '../ExpoRoot';\nimport getPathFromState from '../fork/getPathFromState';\nimport { getNavigationConfig, stateCache } from '../getLinkingConfig';\nimport { getExactRoutes } from '../getRoutes';\nimport { store } from '../global-state/router-store';\nimport { router } from '../imperative-api';\n\n// re-export everything\nexport * from '@testing-library/react-native';\n\ntype RenderRouterOptions = Parameters[1] & {\n initialUrl?: any;\n};\n\ntype Result = ReturnType & {\n getPathname(): string;\n getPathnameWithParams(): string;\n getSegments(): string[];\n getSearchParams(): Record;\n};\n\nfunction isOverrideContext(\n context: object\n): context is { appDir: string; overrides: Record } {\n return Boolean(typeof context === 'object' && 'appDir' in context);\n}\n\nexport type MockContextConfig =\n | string // Pathname to a directory\n | string[] // Array of filenames to mock as empty components, e.g () => null\n | Record // Map of filenames and their exports\n | {\n // Directory to load as context\n appDir: string;\n // Map of filenames and their exports. Will override contents of files loaded in `appDir\n overrides: Record;\n };\n\nexport function getMockConfig(context: MockContextConfig) {\n return getNavigationConfig(getExactRoutes(getMockContext(context))!);\n}\n\nexport function getMockContext(context: MockContextConfig) {\n if (typeof context === 'string') {\n return requireContext(path.resolve(process.cwd(), context));\n } else if (Array.isArray(context)) {\n return inMemoryContext(\n Object.fromEntries(context.map((filename) => [filename, { default: () => null }]))\n );\n } else if (isOverrideContext(context)) {\n return requireContextWithOverrides(context.appDir, context.overrides);\n } else {\n return inMemoryContext(context);\n }\n}\n\nexport function renderRouter(\n context: MockContextConfig = './app',\n { initialUrl = '/', ...options }: RenderRouterOptions = {}\n): Result {\n jest.useFakeTimers();\n\n const mockContext = getMockContext(context);\n\n // Reset the initial URL\n setInitialUrl(initialUrl);\n\n // Force the render to be synchronous\n process.env.EXPO_ROUTER_IMPORT_MODE = 'sync';\n stateCache.clear();\n\n let location: URL | undefined;\n\n if (typeof initialUrl === 'string') {\n location = new URL(initialUrl, 'test://');\n } else if (initialUrl instanceof URL) {\n location = initialUrl;\n }\n\n const result = render(, {\n ...options,\n });\n\n return Object.assign(result, {\n getPathname(this: RenderResult): string {\n return store.routeInfoSnapshot().pathname;\n },\n getSegments(this: RenderResult): string[] {\n return store.routeInfoSnapshot().segments;\n },\n getSearchParams(this: RenderResult): Record {\n return store.routeInfoSnapshot().params;\n },\n getPathnameWithParams(this: RenderResult): string {\n return getPathFromState(store.rootState!, store.linking!.config);\n },\n });\n}\n\nexport const testRouter = {\n /** Navigate to the provided pathname and the pathname */\n navigate(path: string) {\n act(() => router.navigate(path));\n expect(screen).toHavePathnameWithParams(path);\n },\n /** Push the provided pathname and assert the pathname */\n push(path: string) {\n act(() => router.push(path));\n expect(screen).toHavePathnameWithParams(path);\n },\n /** Replace with provided pathname and assert the pathname */\n replace(path: string) {\n act(() => router.replace(path));\n expect(screen).toHavePathnameWithParams(path);\n },\n /** Go back in history and asset the new pathname */\n back(path?: string) {\n expect(router.canGoBack()).toBe(true);\n act(() => router.back());\n if (path) {\n expect(screen).toHavePathnameWithParams(path);\n }\n },\n /** If there's history that supports invoking the `back` function. */\n canGoBack() {\n return router.canGoBack();\n },\n /** Update the current route query params and assert the new pathname */\n setParams(params?: Record, path?: string) {\n router.setParams(params);\n if (path) {\n expect(screen).toHavePathnameWithParams(path);\n }\n },\n /** If there's history that supports invoking the `back` function. */\n dismissAll() {\n act(() => router.dismissAll());\n },\n};\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing-library/index.tsx"],"names":[],"mappings":"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","sourcesContent":["/// \nimport './expect';\n\nimport { act, render, RenderResult, screen } from '@testing-library/react-native';\nimport React from 'react';\n\nimport { MockContextConfig, getMockConfig, getMockContext } from './mock-config';\nimport { setInitialUrl } from './mocks';\nimport { ExpoRoot } from '../ExpoRoot';\nimport getPathFromState from '../fork/getPathFromState';\nimport { stateCache } from '../getLinkingConfig';\nimport { store } from '../global-state/router-store';\nimport { router } from '../imperative-api';\n\n// re-export everything\nexport * from '@testing-library/react-native';\n\ntype RenderRouterOptions = Parameters[1] & {\n initialUrl?: any;\n};\n\ntype Result = ReturnType & {\n getPathname(): string;\n getPathnameWithParams(): string;\n getSegments(): string[];\n getSearchParams(): Record;\n};\n\nexport { MockContextConfig, getMockConfig, getMockContext };\n\nexport function renderRouter(\n context: MockContextConfig = './app',\n { initialUrl = '/', ...options }: RenderRouterOptions = {}\n): Result {\n jest.useFakeTimers();\n\n const mockContext = getMockContext(context);\n\n // Reset the initial URL\n setInitialUrl(initialUrl);\n\n // Force the render to be synchronous\n process.env.EXPO_ROUTER_IMPORT_MODE = 'sync';\n stateCache.clear();\n\n let location: URL | undefined;\n\n if (typeof initialUrl === 'string') {\n location = new URL(initialUrl, 'test://');\n } else if (initialUrl instanceof URL) {\n location = initialUrl;\n }\n\n const result = render(, {\n ...options,\n });\n\n return Object.assign(result, {\n getPathname(this: RenderResult): string {\n return store.routeInfoSnapshot().pathname;\n },\n getSegments(this: RenderResult): string[] {\n return store.routeInfoSnapshot().segments;\n },\n getSearchParams(this: RenderResult): Record {\n return store.routeInfoSnapshot().params;\n },\n getPathnameWithParams(this: RenderResult): string {\n return getPathFromState(store.rootState!, store.linking!.config);\n },\n });\n}\n\nexport const testRouter = {\n /** Navigate to the provided pathname and the pathname */\n navigate(path: string) {\n act(() => router.navigate(path));\n expect(screen).toHavePathnameWithParams(path);\n },\n /** Push the provided pathname and assert the pathname */\n push(path: string) {\n act(() => router.push(path));\n expect(screen).toHavePathnameWithParams(path);\n },\n /** Replace with provided pathname and assert the pathname */\n replace(path: string) {\n act(() => router.replace(path));\n expect(screen).toHavePathnameWithParams(path);\n },\n /** Go back in history and asset the new pathname */\n back(path?: string) {\n expect(router.canGoBack()).toBe(true);\n act(() => router.back());\n if (path) {\n expect(screen).toHavePathnameWithParams(path);\n }\n },\n /** If there's history that supports invoking the `back` function. */\n canGoBack() {\n return router.canGoBack();\n },\n /** Update the current route query params and assert the new pathname */\n setParams(params?: Record, path?: string) {\n router.setParams(params);\n if (path) {\n expect(screen).toHavePathnameWithParams(path);\n }\n },\n /** If there's history that supports invoking the `back` function. */\n dismissAll() {\n act(() => router.dismissAll());\n },\n};\n"]} \ No newline at end of file diff --git a/packages/expo-router/build/testing-library/mock-config.d.ts b/packages/expo-router/build/testing-library/mock-config.d.ts new file mode 100644 index 0000000000000..70e4492c3c75e --- /dev/null +++ b/packages/expo-router/build/testing-library/mock-config.d.ts @@ -0,0 +1,15 @@ +import { FileStub } from './context-stubs'; +export type MockContextConfig = string | string[] | Record | { + appDir: string; + overrides: Record; +}; +export declare function getMockConfig(context: MockContextConfig, metaOnly?: boolean): { + initialRouteName?: string | undefined; + screens: Record; +}; +export declare function getMockContext(context: MockContextConfig): ((id: string) => any) & { + keys: () => string[]; + resolve: (key: string) => string; + id: string; +}; +//# sourceMappingURL=mock-config.d.ts.map \ No newline at end of file diff --git a/packages/expo-router/build/testing-library/mock-config.d.ts.map b/packages/expo-router/build/testing-library/mock-config.d.ts.map new file mode 100644 index 0000000000000..807fc181e482c --- /dev/null +++ b/packages/expo-router/build/testing-library/mock-config.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"mock-config.d.ts","sourceRoot":"","sources":["../../src/testing-library/mock-config.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,QAAQ,EAIT,MAAM,iBAAiB,CAAC;AAUzB,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,MAAM,EAAE,GACR,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GACxB;IAEE,MAAM,EAAE,MAAM,CAAC;IAEf,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CACrC,CAAC;AAEN,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,GAAE,OAAc;;;EAEjF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB;;;;EAYxD"} \ No newline at end of file diff --git a/packages/expo-router/build/testing-library/mock-config.js b/packages/expo-router/build/testing-library/mock-config.js new file mode 100644 index 0000000000000..29f60456e1f2b --- /dev/null +++ b/packages/expo-router/build/testing-library/mock-config.js @@ -0,0 +1,33 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getMockContext = exports.getMockConfig = void 0; +const path_1 = __importDefault(require("path")); +const context_stubs_1 = require("./context-stubs"); +const getLinkingConfig_1 = require("../getLinkingConfig"); +const getRoutes_1 = require("../getRoutes"); +function isOverrideContext(context) { + return Boolean(typeof context === 'object' && 'appDir' in context); +} +function getMockConfig(context, metaOnly = true) { + return (0, getLinkingConfig_1.getNavigationConfig)((0, getRoutes_1.getExactRoutes)(getMockContext(context)), metaOnly); +} +exports.getMockConfig = getMockConfig; +function getMockContext(context) { + if (typeof context === 'string') { + return (0, context_stubs_1.requireContext)(path_1.default.resolve(process.cwd(), context)); + } + else if (Array.isArray(context)) { + return (0, context_stubs_1.inMemoryContext)(Object.fromEntries(context.map((filename) => [filename, { default: () => null }]))); + } + else if (isOverrideContext(context)) { + return (0, context_stubs_1.requireContextWithOverrides)(context.appDir, context.overrides); + } + else { + return (0, context_stubs_1.inMemoryContext)(context); + } +} +exports.getMockContext = getMockContext; +//# sourceMappingURL=mock-config.js.map \ No newline at end of file diff --git a/packages/expo-router/build/testing-library/mock-config.js.map b/packages/expo-router/build/testing-library/mock-config.js.map new file mode 100644 index 0000000000000..6b47d26c0d954 --- /dev/null +++ b/packages/expo-router/build/testing-library/mock-config.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mock-config.js","sourceRoot":"","sources":["../../src/testing-library/mock-config.ts"],"names":[],"mappings":";;;;;;AAAA,gDAAwB;AAExB,mDAKyB;AACzB,0DAA0D;AAC1D,4CAA8C;AAE9C,SAAS,iBAAiB,CACxB,OAAe;IAEf,OAAO,OAAO,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,QAAQ,IAAI,OAAO,CAAC,CAAC;AACrE,CAAC;AAaD,SAAgB,aAAa,CAAC,OAA0B,EAAE,WAAoB,IAAI;IAChF,OAAO,IAAA,sCAAmB,EAAC,IAAA,0BAAc,EAAC,cAAc,CAAC,OAAO,CAAC,CAAE,EAAE,QAAQ,CAAC,CAAC;AACjF,CAAC;AAFD,sCAEC;AAED,SAAgB,cAAc,CAAC,OAA0B;IACvD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;QAC/B,OAAO,IAAA,8BAAc,EAAC,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;KAC7D;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QACjC,OAAO,IAAA,+BAAe,EACpB,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CACnF,CAAC;KACH;SAAM,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE;QACrC,OAAO,IAAA,2CAA2B,EAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;KACvE;SAAM;QACL,OAAO,IAAA,+BAAe,EAAC,OAAO,CAAC,CAAC;KACjC;AACH,CAAC;AAZD,wCAYC","sourcesContent":["import path from 'path';\n\nimport {\n FileStub,\n inMemoryContext,\n requireContext,\n requireContextWithOverrides,\n} from './context-stubs';\nimport { getNavigationConfig } from '../getLinkingConfig';\nimport { getExactRoutes } from '../getRoutes';\n\nfunction isOverrideContext(\n context: object\n): context is { appDir: string; overrides: Record } {\n return Boolean(typeof context === 'object' && 'appDir' in context);\n}\n\nexport type MockContextConfig =\n | string // Pathname to a directory\n | string[] // Array of filenames to mock as empty components, e.g () => null\n | Record // Map of filenames and their exports\n | {\n // Directory to load as context\n appDir: string;\n // Map of filenames and their exports. Will override contents of files loaded in `appDir\n overrides: Record;\n };\n\nexport function getMockConfig(context: MockContextConfig, metaOnly: boolean = true) {\n return getNavigationConfig(getExactRoutes(getMockContext(context))!, metaOnly);\n}\n\nexport function getMockContext(context: MockContextConfig) {\n if (typeof context === 'string') {\n return requireContext(path.resolve(process.cwd(), context));\n } else if (Array.isArray(context)) {\n return inMemoryContext(\n Object.fromEntries(context.map((filename) => [filename, { default: () => null }]))\n );\n } else if (isOverrideContext(context)) {\n return requireContextWithOverrides(context.appDir, context.overrides);\n } else {\n return inMemoryContext(context);\n }\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/src/__tests__/getServerManifest.test.web.ts b/packages/expo-router/src/__tests__/getServerManifest.test.web.ts index defb25c80205a..790eb21eb9a63 100644 --- a/packages/expo-router/src/__tests__/getServerManifest.test.web.ts +++ b/packages/expo-router/src/__tests__/getServerManifest.test.web.ts @@ -29,20 +29,20 @@ it(`sorts different route types`, () => { ).toEqual({ apiRoutes: [ expect.objectContaining({ - page: './b+api', + file: './b+api.tsx', }), ], htmlRoutes: [ expect.objectContaining({ - page: './a', + file: './a.js', }), ], notFoundRoutes: [ expect.objectContaining({ - page: './c/+not-found', + file: './c/+not-found.tsx', }), expect.objectContaining({ - page: './+not-found', + file: './+not-found.ts', }), ], }); @@ -54,21 +54,15 @@ it(`converts a server manifest`, () => { { file: './api/[post]+api.tsx', namedRegex: '^/api/(?[^/]+?)(?:/)?$', - page: './api/[post]+api', + page: '/api/[post]', routeKeys: { post: 'post' }, }, ], - htmlRoutes: [{ file: './home.js', namedRegex: '^/home(?:/)?$', page: './home', routeKeys: {} }], + htmlRoutes: [{ file: './home.js', namedRegex: '^/home(?:/)?$', page: '/home', routeKeys: {} }], notFoundRoutes: [], }); }); -xit(`converts single basic`, () => { - expect(getServerManifest(getRoutesFor(['./home.js']))).toEqual([ - { namedRegex: '^/home(?:/)?$', routeKeys: {} }, - ]); -}); - describe(parseParameter, () => { it(`matches optionals using non-standard from router v1`, () => { expect(parseParameter('[...all]')).toEqual({ @@ -101,7 +95,7 @@ it(`supports groups`, () => { { file: './(a)/b.tsx', namedRegex: '^(?:/\\(a\\))?/b(?:/)?$', - page: './(a)/b', + page: '/(a)/b', routeKeys: {}, }, ], @@ -115,17 +109,17 @@ it(`converts index routes`, () => { ).toEqual({ apiRoutes: [], htmlRoutes: [ - { file: './index.tsx', namedRegex: '^/(?:/)?$', page: './index', routeKeys: {} }, + { file: './index.tsx', namedRegex: '^/(?:/)?$', page: '/index', routeKeys: {} }, { file: './a/index/b.tsx', namedRegex: '^/a/index/b(?:/)?$', - page: './a/index/b', + page: '/a/index/b', routeKeys: {}, }, { file: './a/index/index.js', namedRegex: '^/a/index(?:/)?$', - page: './a/index/index', + page: '/a/index/index', routeKeys: {}, }, ], @@ -199,19 +193,19 @@ it(`converts dynamic routes`, () => { { file: './c/[d]/e/[...f].js', namedRegex: '^/c/(?[^/]+?)/e(?:/(?.+?))?(?:/)?$', - page: './c/[d]/e/[...f]', + page: '/c/[d]/e/[...f]', routeKeys: { d: 'd', f: 'f' }, }, { file: './[a].tsx', namedRegex: '^/(?[^/]+?)(?:/)?$', - page: './[a]', + page: '/[a]', routeKeys: { a: 'a' }, }, { file: './[...b].tsx', namedRegex: '^(?:/(?.+?))?(?:/)?$', - page: './[...b]', + page: '/[...b]', routeKeys: { b: 'b' }, }, ], @@ -229,31 +223,31 @@ it(`converts dynamic routes on same level with specificity`, () => { { file: './index.tsx', namedRegex: '^/(?:/)?$', - page: './index', + page: '/index', routeKeys: {}, }, { file: './a.tsx', namedRegex: '^/a(?:/)?$', - page: './a', + page: '/a', routeKeys: {}, }, { file: './(a)/[a].tsx', namedRegex: '^(?:/\\(a\\))?/(?[^/]+?)(?:/)?$', - page: './(a)/[a]', + page: '/(a)/[a]', routeKeys: { a: 'a' }, }, { file: './[a].tsx', namedRegex: '^/(?[^/]+?)(?:/)?$', - page: './[a]', + page: '/[a]', routeKeys: { a: 'a' }, }, { file: './[...a].tsx', namedRegex: '^(?:/(?.+?))?(?:/)?$', - page: './[...a]', + page: '/[...a]', routeKeys: { a: 'a' }, }, ], @@ -261,16 +255,244 @@ it(`converts dynamic routes on same level with specificity`, () => { }); for (const [matcher, page] of [ - ['/', './index'], - ['/a', './a'], - ['/b', './(a)/[a]'], + ['/', './index.tsx'], + ['/a', './a.tsx'], + ['/b', './(a)/[a].tsx'], ]) { - expect(routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(matcher)).page).toBe( + expect(routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(matcher)).file).toBe( page ); } }); +it(`converts array syntax API routes`, () => { + const routesFor = getRoutesFor(['./(a,b)/foo+api.tsx']); + expect(routesFor).toEqual({ + children: [ + { + children: [], + contextKey: './(a,b)/foo+api.tsx', + dynamic: null, + loadRoute: expect.anything(), + route: '(a)/foo', + type: 'api', + }, + { + children: [], + contextKey: './(a,b)/foo+api.tsx', + dynamic: null, + loadRoute: expect.anything(), + route: '(b)/foo', + type: 'api', + }, + ], + contextKey: 'expo-router/build/views/Navigator.js', + dynamic: null, + loadRoute: expect.anything(), + generated: true, + route: '', + type: 'layout', + }); + const routesManifest = getServerManifest(routesFor); + expect(routesManifest).toEqual({ + apiRoutes: [ + // Should only be one API route entry. + { + file: './(a,b)/foo+api.tsx', + namedRegex: '^(?:/\\((?:a|b)\\))?/foo(?:/)?$', + routeKeys: {}, + // NOTE: This isn't correct, but page isn't used with API Routes. + page: '/(b)/foo', + }, + ], + htmlRoutes: [], + notFoundRoutes: [], + }); + + const match = (url: string) => { + return routesManifest.apiRoutes.find((r) => new RegExp(r.namedRegex).test(url))?.file; + }; + + const matches = (url: string) => { + expect(match(url)).toBe('./(a,b)/foo+api.tsx'); + }; + + matches('/foo'); + matches('/(a)/foo'); + matches('/(b)/foo'); + + // Cannot match the exact array syntax + expect(match('/(a,b)/foo')).toBeUndefined(); + // No special variation + expect(match('/(a,)/foo')).toBeUndefined(); + expect(match('/(a )/foo')).toBeUndefined(); + expect(match('/(, a )/foo')).toBeUndefined(); +}); + +it(`converts array syntax HTML routes`, () => { + const routesFor = getRoutesFor(['./(a,b)/foo.tsx']); + expect(routesFor).toEqual({ + children: [ + { + children: [], + contextKey: './(a,b)/foo.tsx', + entryPoints: ['expo-router/build/views/Navigator.js', './(a,b)/foo.tsx'], + dynamic: null, + loadRoute: expect.anything(), + route: '(a)/foo', + type: 'route', + }, + { + children: [], + contextKey: './(a,b)/foo.tsx', + entryPoints: ['expo-router/build/views/Navigator.js', './(a,b)/foo.tsx'], + dynamic: null, + loadRoute: expect.anything(), + route: '(b)/foo', + type: 'route', + }, + ], + contextKey: 'expo-router/build/views/Navigator.js', + dynamic: null, + loadRoute: expect.anything(), + generated: true, + route: '', + type: 'layout', + }); + const routesManifest = getServerManifest(routesFor); + expect(routesManifest).toEqual({ + apiRoutes: [], + htmlRoutes: [ + { + file: './(a,b)/foo.tsx', + namedRegex: '^(?:/\\(b\\))?/foo(?:/)?$', + page: '/(b)/foo', + routeKeys: {}, + }, + { + file: './(a,b)/foo.tsx', + namedRegex: '^(?:/\\(a\\))?/foo(?:/)?$', + page: '/(a)/foo', + routeKeys: {}, + }, + ], + notFoundRoutes: [], + }); + + const match = (url: string) => { + return routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(url))?.file; + }; + + const matches = (url: string) => { + expect(match(url)).toBe('./(a,b)/foo.tsx'); + }; + + matches('/foo'); + matches('/(a)/foo'); + matches('/(b)/foo'); + + // Cannot match the exact array syntax + expect(match('/(a,b)/foo')).toBeUndefined(); + // No special variation + expect(match('/(a,)/foo')).toBeUndefined(); + expect(match('/(a )/foo')).toBeUndefined(); + expect(match('/(, a )/foo')).toBeUndefined(); +}); + +it(`converts top-level array syntax HTML routes`, () => { + const routesManifest = getServerManifest(getRoutesFor(['./(a,b)/index.tsx'])); + expect(routesManifest).toEqual({ + apiRoutes: [], + htmlRoutes: [ + { + file: './(a,b)/index.tsx', + namedRegex: '^(?:/\\(b\\))?(?:/)?$', + page: '/(b)/index', + routeKeys: {}, + }, + { + file: './(a,b)/index.tsx', + namedRegex: '^(?:/\\(a\\))?(?:/)?$', + page: '/(a)/index', + routeKeys: {}, + }, + ], + notFoundRoutes: [], + }); + + const match = (url: string) => { + return routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(url))?.file; + }; + + expect(match('/')).toBeDefined(); + expect(match('/(a)')).toBeDefined(); + expect(match('/(a)/')).toBeDefined(); + expect(match('/(b)')).toBeDefined(); + // + expect(match('/(a,b)')).toBeUndefined(); +}); + +it(`converts nested array syntax HTML routes`, () => { + const routesFor = getRoutesFor(['./(a,b)/(c, d)/foo.tsx']); + const routesManifest = getServerManifest(routesFor); + expect(routesManifest).toEqual({ + apiRoutes: [], + htmlRoutes: [ + { + file: './(a,b)/(c, d)/foo.tsx', + namedRegex: '^(?:/\\(b\\))?(?:/\\(d\\))?/foo(?:/)?$', + page: '/(b)/(d)/foo', + routeKeys: {}, + }, + { + file: './(a,b)/(c, d)/foo.tsx', + namedRegex: '^(?:/\\(b\\))?(?:/\\(c\\))?/foo(?:/)?$', + page: '/(b)/(c)/foo', + routeKeys: {}, + }, + { + file: './(a,b)/(c, d)/foo.tsx', + namedRegex: '^(?:/\\(a\\))?(?:/\\(d\\))?/foo(?:/)?$', + page: '/(a)/(d)/foo', + routeKeys: {}, + }, + { + file: './(a,b)/(c, d)/foo.tsx', + namedRegex: '^(?:/\\(a\\))?(?:/\\(c\\))?/foo(?:/)?$', + page: '/(a)/(c)/foo', + routeKeys: {}, + }, + ], + notFoundRoutes: [], + }); + + const match = (url: string) => { + return routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(url))?.file; + }; + + const matches = (url: string) => { + expect(match(url)).toBe('./(a,b)/(c, d)/foo.tsx'); + }; + + matches('/foo'); + matches('/(a)/foo'); + matches('/(b)/foo'); + matches('/(c)/foo'); + matches('/(d)/foo'); + matches('/(a)/(c)/foo'); + matches('/(b)/(d)/foo'); + + // Cannot match the exact array syntax + expect(match('/(a,b)/foo')).toBeUndefined(); + expect(match('/(a)/(b)/foo')).toBeUndefined(); + expect(match('/(a)/(c,d)/foo')).toBeUndefined(); + expect(match('/(c,d)/foo')).toBeUndefined(); + // No special variation + expect(match('/(a,)/foo')).toBeUndefined(); + expect(match('/(a )/foo')).toBeUndefined(); + expect(match('/(, a )/foo')).toBeUndefined(); +}); + it(`matches top-level catch-all before +not-found route`, () => { const routesManifest = getServerManifest(getRoutesFor(['./[...a].tsx', './+not-found.tsx'])); expect(routesManifest).toEqual({ @@ -279,7 +501,7 @@ it(`matches top-level catch-all before +not-found route`, () => { { file: './[...a].tsx', namedRegex: '^(?:/(?.+?))?(?:/)?$', - page: './[...a]', + page: '/[...a]', routeKeys: { a: 'a' }, }, ], @@ -287,20 +509,20 @@ it(`matches top-level catch-all before +not-found route`, () => { { file: './+not-found.tsx', namedRegex: '^(?:/(?.+?))?(?:/)?$', - page: './+not-found', + page: '/+not-found', routeKeys: { notfound: 'not-found' }, }, ], }); for (const [matcher, page] of [ - ['', './[...a]'], - ['/', './[...a]'], - ['//', './[...a]'], - ['/a', './[...a]'], - ['/b/c/', './[...a]'], + ['', './[...a].tsx'], + ['/', './[...a].tsx'], + ['//', './[...a].tsx'], + ['/a', './[...a].tsx'], + ['/b/c/', './[...a].tsx'], ]) { - expect(routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(matcher)).page).toBe( + expect(routesManifest.htmlRoutes.find((r) => new RegExp(r.namedRegex).test(matcher)).file).toBe( page ); } diff --git a/packages/expo-router/src/getLinkingConfig.ts b/packages/expo-router/src/getLinkingConfig.ts index 798c9128703b8..f53e4b0b03c83 100644 --- a/packages/expo-router/src/getLinkingConfig.ts +++ b/packages/expo-router/src/getLinkingConfig.ts @@ -10,22 +10,25 @@ import { getStateFromPath, } from './link/linking'; -export function getNavigationConfig(routes: RouteNode): { +export function getNavigationConfig( + routes: RouteNode, + metaOnly: boolean = true +): { initialRouteName?: string; screens: Record; } { - return getReactNavigationConfig(routes, true); + return getReactNavigationConfig(routes, metaOnly); } export type ExpoLinkingOptions = LinkingOptions & { getPathFromState?: typeof getPathFromState; }; -export function getLinkingConfig(routes: RouteNode): ExpoLinkingOptions { +export function getLinkingConfig(routes: RouteNode, metaOnly: boolean = true): ExpoLinkingOptions { return { prefixes: [], // @ts-expect-error - config: getNavigationConfig(routes), + config: getNavigationConfig(routes, metaOnly), // A custom getInitialURL is used on native to ensure the app always starts at // the root path if it's launched from something other than a deep link. // This helps keep the native functionality working like the web functionality. diff --git a/packages/expo-router/src/getServerManifest.ts b/packages/expo-router/src/getServerManifest.ts index 3a91fcd37cbf0..58789a5fb8b18 100644 --- a/packages/expo-router/src/getServerManifest.ts +++ b/packages/expo-router/src/getServerManifest.ts @@ -8,7 +8,7 @@ * Based on https://github.com/vercel/next.js/blob/1df2686bc9964f1a86c444701fa5cbf178669833/packages/next/src/shared/lib/router/utils/route-regex.ts */ import type { RouteNode } from './Route'; -import { getContextKey } from './matchers'; +import { getContextKey, matchGroupName } from './matchers'; import { sortRoutes } from './sortRoutes'; // TODO: Share these types across cli, server, router, etc. @@ -37,14 +37,22 @@ export interface RouteRegex { re: RegExp; } -function isApiRoute(route: RouteNode) { - return !route.children.length && !!route.contextKey.match(/\+api\.[jt]sx?$/); -} - function isNotFoundRoute(route: RouteNode) { return route.dynamic && route.dynamic[route.dynamic.length - 1].notFound; } +function uniqueBy(arr: T[], key: (item: T) => string): T[] { + const seen = new Set(); + return arr.filter((item) => { + const id = key(item); + if (seen.has(id)) { + return false; + } + seen.add(id); + return true; + }); +} + // Given a nested route tree, return a flattened array of all routes that can be matched. export function getServerManifest(route: RouteNode): ExpoRouterServerManifestV1 { function getFlatNodes(route: RouteNode): [string, RouteNode][] { @@ -52,16 +60,31 @@ export function getServerManifest(route: RouteNode): ExpoRouterServerManifestV1 return route.children.map((child) => getFlatNodes(child)).flat(); } - const key = getContextKey(route.contextKey).replace(/\/index$/, '') ?? '/'; + // API Routes are handled differently to HTML routes because they have no nested behavior. + // An HTML route can be different based on parent segments due to layout routes, therefore multiple + // copies should be rendered. However, an API route is always the same regardless of parent segments. + let key: string; + if (route.type === 'api') { + key = getContextKey(route.contextKey).replace(/\/index$/, '') ?? '/'; + } else { + key = getContextKey(route.route).replace(/\/index$/, '') ?? '/'; + } return [[key, route]]; } + // Remove duplicates from the runtime manifest which expands array syntax. const flat = getFlatNodes(route) .sort(([, a], [, b]) => sortRoutes(b, a)) .reverse(); - const apiRoutes = flat.filter(([, route]) => isApiRoute(route)); - const otherRoutes = flat.filter(([, route]) => !isApiRoute(route)); + const apiRoutes = uniqueBy( + flat.filter(([, route]) => route.type === 'api'), + ([path]) => path + ); + const otherRoutes = uniqueBy( + flat.filter(([, route]) => route.type === 'route'), + ([path]) => path + ); const standardRoutes = otherRoutes.filter(([, route]) => !isNotFoundRoute(route)); const notFoundRoutes = otherRoutes.filter(([, route]) => isNotFoundRoute(route)); @@ -84,6 +107,7 @@ function getMatchableManifestForPaths( return paths.map((normalizedRoutePath) => { const matcher: ExpoRouterServerManifestV1Route = getNamedRouteRegex( normalizedRoutePath[0], + getContextKey(normalizedRoutePath[1].route), normalizedRoutePath[1].contextKey ); if (normalizedRoutePath[1].generated) { @@ -93,14 +117,15 @@ function getMatchableManifestForPaths( }); } -export function getNamedRouteRegex( +function getNamedRouteRegex( normalizedRoute: string, - page: string + page: string, + file: string ): ExpoRouterServerManifestV1Route { const result = getNamedParametrizedRoute(normalizedRoute); return { - file: page, - page: page.replace(/\.[jt]sx?$/, ''), + file, + page, namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`, routeKeys: result.routeKeys, }; @@ -188,8 +213,18 @@ function getNamedParametrizedRoute(route: string) { : `/(?<${cleanedKey}>.+?)` : `/(?<${cleanedKey}>[^/]+?)`; } else if (/^\(.*\)$/.test(segment)) { - // Make section optional - return `(?:/${escapeStringRegexp(segment)})?`; + const groupName = matchGroupName(segment)! + .split(',') + .map((group) => group.trim()) + .filter(Boolean); + if (groupName.length > 1) { + const optionalSegment = `\\((?:${groupName.map(escapeStringRegexp).join('|')})\\)`; + // Make section optional + return `(?:/${optionalSegment})?`; + } else { + // Use simpler regex for single groups + return `(?:/${escapeStringRegexp(segment)})?`; + } } else { return `/${escapeStringRegexp(segment)}`; } diff --git a/packages/expo-router/src/testing-library/index.tsx b/packages/expo-router/src/testing-library/index.tsx index 8dbbfa39f00af..24912ccb728e4 100644 --- a/packages/expo-router/src/testing-library/index.tsx +++ b/packages/expo-router/src/testing-library/index.tsx @@ -2,20 +2,13 @@ import './expect'; import { act, render, RenderResult, screen } from '@testing-library/react-native'; -import path from 'path'; import React from 'react'; -import { - FileStub, - inMemoryContext, - requireContext, - requireContextWithOverrides, -} from './context-stubs'; +import { MockContextConfig, getMockConfig, getMockContext } from './mock-config'; import { setInitialUrl } from './mocks'; import { ExpoRoot } from '../ExpoRoot'; import getPathFromState from '../fork/getPathFromState'; -import { getNavigationConfig, stateCache } from '../getLinkingConfig'; -import { getExactRoutes } from '../getRoutes'; +import { stateCache } from '../getLinkingConfig'; import { store } from '../global-state/router-store'; import { router } from '../imperative-api'; @@ -33,40 +26,7 @@ type Result = ReturnType & { getSearchParams(): Record; }; -function isOverrideContext( - context: object -): context is { appDir: string; overrides: Record } { - return Boolean(typeof context === 'object' && 'appDir' in context); -} - -export type MockContextConfig = - | string // Pathname to a directory - | string[] // Array of filenames to mock as empty components, e.g () => null - | Record // Map of filenames and their exports - | { - // Directory to load as context - appDir: string; - // Map of filenames and their exports. Will override contents of files loaded in `appDir - overrides: Record; - }; - -export function getMockConfig(context: MockContextConfig) { - return getNavigationConfig(getExactRoutes(getMockContext(context))!); -} - -export function getMockContext(context: MockContextConfig) { - if (typeof context === 'string') { - return requireContext(path.resolve(process.cwd(), context)); - } else if (Array.isArray(context)) { - return inMemoryContext( - Object.fromEntries(context.map((filename) => [filename, { default: () => null }])) - ); - } else if (isOverrideContext(context)) { - return requireContextWithOverrides(context.appDir, context.overrides); - } else { - return inMemoryContext(context); - } -} +export { MockContextConfig, getMockConfig, getMockContext }; export function renderRouter( context: MockContextConfig = './app', diff --git a/packages/expo-router/src/testing-library/mock-config.ts b/packages/expo-router/src/testing-library/mock-config.ts new file mode 100644 index 0000000000000..b730bc26151dc --- /dev/null +++ b/packages/expo-router/src/testing-library/mock-config.ts @@ -0,0 +1,45 @@ +import path from 'path'; + +import { + FileStub, + inMemoryContext, + requireContext, + requireContextWithOverrides, +} from './context-stubs'; +import { getNavigationConfig } from '../getLinkingConfig'; +import { getExactRoutes } from '../getRoutes'; + +function isOverrideContext( + context: object +): context is { appDir: string; overrides: Record } { + return Boolean(typeof context === 'object' && 'appDir' in context); +} + +export type MockContextConfig = + | string // Pathname to a directory + | string[] // Array of filenames to mock as empty components, e.g () => null + | Record // Map of filenames and their exports + | { + // Directory to load as context + appDir: string; + // Map of filenames and their exports. Will override contents of files loaded in `appDir + overrides: Record; + }; + +export function getMockConfig(context: MockContextConfig, metaOnly: boolean = true) { + return getNavigationConfig(getExactRoutes(getMockContext(context))!, metaOnly); +} + +export function getMockContext(context: MockContextConfig) { + if (typeof context === 'string') { + return requireContext(path.resolve(process.cwd(), context)); + } else if (Array.isArray(context)) { + return inMemoryContext( + Object.fromEntries(context.map((filename) => [filename, { default: () => null }])) + ); + } else if (isOverrideContext(context)) { + return requireContextWithOverrides(context.appDir, context.overrides); + } else { + return inMemoryContext(context); + } +}