diff --git a/.changeset/tidy-tomatoes-tie.md b/.changeset/tidy-tomatoes-tie.md new file mode 100644 index 0000000000..b1c5cdf69c --- /dev/null +++ b/.changeset/tidy-tomatoes-tie.md @@ -0,0 +1,6 @@ +--- +'@ice/runtime': patch +'@ice/app': patch +--- + +feat: support remove router even if route count is greater than 1 diff --git a/examples/single-route/optimization.config.mts b/examples/single-route/optimization.config.mts index 6e27ecc934..6ca8677635 100644 --- a/examples/single-route/optimization.config.mts +++ b/examples/single-route/optimization.config.mts @@ -3,6 +3,6 @@ import { defineConfig } from '@ice/app'; export default defineConfig(() => ({ publicPath: '/', optimization: { - router: true, + disableRouter: true, }, })); diff --git a/examples/single-route/src/pages/home.tsx b/examples/single-route/src/pages/home.tsx new file mode 100644 index 0000000000..0d646b173f --- /dev/null +++ b/examples/single-route/src/pages/home.tsx @@ -0,0 +1,7 @@ +const home = () => { + return ( + <>home + ); +}; + +export default home; diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 4341a5990e..95993d4c1f 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -232,9 +232,10 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('dataLoader'); const csr = !userConfig.ssr && !userConfig.ssg; - const disableRouter = userConfig?.optimization?.router && routesInfo.routesCount <= 1; + const disableRouter = (userConfig?.optimization?.router && routesInfo.routesCount <= 1) || + userConfig?.optimization?.disableRouter; if (disableRouter) { - logger.info('`optimization.router` is enabled and only have one route, ice build will remove react-router and history which is unnecessary.'); + logger.info('`optimization.router` is enabled, ice build will remove react-router and history which is unnecessary.'); taskConfigs = mergeTaskConfig(taskConfigs, { alias: { '@ice/runtime/router': '@ice/runtime/single-router', diff --git a/packages/ice/src/types/userConfig.ts b/packages/ice/src/types/userConfig.ts index dd0acca77a..8efced52f2 100644 --- a/packages/ice/src/types/userConfig.ts +++ b/packages/ice/src/types/userConfig.ts @@ -13,9 +13,15 @@ interface SyntaxFeatures { interface Optimization { /** - * Optimize code by remove react-router dependencies when set to true. + * Optimize code by remove react-router dependencies when set to true, + * it only works when route count is 1. */ router?: boolean; + /** + * @private + * Remove react-router dependencies by force, even if route count is greater than 1. + */ + disableRouter?: boolean; } interface MinifyOptions { diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index 4d2016a403..0052ce73d9 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -139,7 +139,7 @@ interface RenderOptions { async function render({ history, runtime, needHydrate }: RenderOptions) { const appContext = runtime.getAppContext(); - const { appConfig, loaderData, routes, basename } = appContext; + const { appConfig, loaderData, routes, basename, routePath } = appContext; const appRender = runtime.getRender(); const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment; const AppRouter = runtime.getAppRouter(); @@ -154,8 +154,9 @@ async function render({ history, runtime, needHydrate }: RenderOptions) { } const hydrationData = needHydrate ? { loaderData } : undefined; const routeModuleCache = {}; + const location = history.location ? history.location : { pathname: routePath || window.location.pathname }; if (needHydrate) { - const lazyMatches = matchRoutes(routes, history.location, basename).filter((m) => m.route.lazy); + const lazyMatches = matchRoutes(routes, location, basename).filter((m) => m.route.lazy); if (lazyMatches?.length > 0) { // Load the lazy matches and update the routes before creating your router // so we can hydrate the SSR-rendered content synchronously. @@ -182,8 +183,9 @@ async function render({ history, runtime, needHydrate }: RenderOptions) { let singleComponent = null; let routeData = null; if (process.env.ICE_CORE_ROUTER !== 'true') { - const { Component, loader } = await loadRouteModule(routes[0], routeModuleCache); - singleComponent = Component || routes[0].Component; + const singleRoute = matchRoutes(routes, location, basename)[0]; + const { Component, loader } = await loadRouteModule(singleRoute.route, routeModuleCache); + singleComponent = Component || singleRoute.route.Component; routeData = loader && await loader(); } const renderRoot = appRender( diff --git a/packages/runtime/src/singleRouter.tsx b/packages/runtime/src/singleRouter.tsx index ce20e796d1..46260345c5 100644 --- a/packages/runtime/src/singleRouter.tsx +++ b/packages/runtime/src/singleRouter.tsx @@ -42,15 +42,30 @@ export const createHistory = (): History => { }; }; -export const matchRoutes = (routes: any[]) => { - return routes.map(item => { - return { - params: {}, - pathname: '', - pathnameBase: '', - route: item, - }; +const stripString = (str: string) => { + const regexForSlash = /^\/|\/$/g; + return str.replace(regexForSlash, ''); +}; + +export const matchRoutes = (routes: any[], location: Partial | string, basename: string) => { + const stripedBasename = stripString(basename); + const pathname = typeof location === 'string' ? location : location.pathname; + let stripedPathname = stripString(pathname); + if (stripedBasename) { + stripedPathname = stripedPathname.replace(new RegExp(`^${stripedBasename}/`), ''); + } + const route = routes.length === 1 ? routes[0] : routes.find(item => { + return stripString(item.path || '') === stripedPathname; }); + if (!route) { + throw new Error(`No route matched pathname: ${pathname}`); + } + return [{ + route, + params: {}, + pathname, + pathnameBase: '', + }]; }; export const Link = () => null; diff --git a/packages/runtime/tests/singleRoute.test.tsx b/packages/runtime/tests/singleRoute.test.tsx index 7f5975b07a..9f6452d7ca 100644 --- a/packages/runtime/tests/singleRoute.test.tsx +++ b/packages/runtime/tests/singleRoute.test.tsx @@ -37,8 +37,54 @@ describe('single route api', () => { expect(createHistory().location).toBe(''); }); - it('matchRoutes', () => { - expect(matchRoutes([{}])[0].pathname).toBe(''); + it('matchRoutes - single route', () => { + const routes = [ + { + path: 'users', + element:
user
, + }, + ]; + const location = { + pathname: '/test', + }; + const matchedRoutes = matchRoutes(routes, location, '/'); + expect(matchedRoutes).toHaveLength(1); + expect(matchedRoutes[0].route.path).toBe('users'); + }); + + it('matchRoutes - mutiple route', () => { + const routes = [ + { + path: 'users', + element:
user
, + }, + { + path: 'posts', + element:
post
, + }, + ]; + const location = { + pathname: '/posts', + }; + const matchedRoutes = matchRoutes(routes, location, '/'); + expect(matchedRoutes).toHaveLength(1); + expect(matchedRoutes[0].route.path).toBe('posts'); + }); + + it('matchRoutes - basename', () => { + const routes = [ + { + path: 'users', + element:
user
, + }, + { + path: 'posts', + element:
post
, + }, + ]; + const matchedRoutes = matchRoutes(routes, '/basename/posts', '/basename'); + expect(matchedRoutes).toHaveLength(1); + expect(matchedRoutes[0].route.path).toBe('posts'); }); it('Link', () => {