From efe9a4a8e02dd4755ec9a9a26d9dfcef3bae1863 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Wed, 1 Sep 2021 18:44:53 +0800 Subject: [PATCH 1/6] chore: only insert runtime route --- .../src/master/masterRuntimePlugin.ts.tpl | 151 +++++++++++++++--- 1 file changed, 127 insertions(+), 24 deletions(-) diff --git a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl index 16fe26be..3d204718 100644 --- a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl +++ b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl @@ -14,6 +14,8 @@ import { getMasterOptions, setMasterOptions } from './masterOptions'; import { deferred } from './qiankunDefer.js'; import { App, HistoryType, MasterOptions, MicroAppRoute } from './types'; +let microAppRuntimeRoutes: MicroAppRoute[]; + async function getMasterRuntime() { const config = await plugin.applyPlugins({ key: 'qiankun', @@ -25,7 +27,129 @@ async function getMasterRuntime() { return master || config; } -let microAppRuntimeRoutes: MicroAppRoute[]; +// insert route with "insert" attribute into the route tree +function insertMicroAppRoute({ routes }) { + const insertedArray: IRouteProps[] = []; + + // count total number of current route tree + const countRoute = () => { + let count = 0; + const recursiveCount = (routes: IRouteProps[]) => { + for (let i = 0; i < routes.length; i++) { + count += 1; + if (routes[i].routes && routes[i].routes?.length) { + recursiveCount(routes[i].routes || []); + } + } + }; + recursiveCount(routes); + return count; + }; + + // traverse the route tree to splice all routes that need to be inserted + const traverse = (routes: IRouteProps[]) => { + if (routes.length) { + for (let i = 0; i < routes.length; i++) { + const route = routes[i]; + + if (route.insert && route.insert !== '/') { + const [toBeMovedRoute] = routes.splice(i, 1); + insertedArray.push(toBeMovedRoute); + i -= 1; + } + + if (route.routes?.length) { + traverse(route.routes); + } + } + } + }; + + const originalRouteCount = countRoute(); + + traverse(routes); + + // find parent node in route tree + const recursiveSearch = ( + routes: IRouteProps[], + path: string, + ): IRouteProps | null => { + for (let i = 0; i < routes.length; i++) { + if (routes[i].path === path) { + return routes[i]; + } + if (routes[i].routes && routes[i].routes?.length) { + const found = recursiveSearch(routes[i].routes || [], path); + if (found) { + return found; + } + } + } + return null; + }; + + insertedArray.forEach((item) => { + if(!item.path || !item.path.startsWith(item.insert)) { + logError(new Error(`[insert-routes]: path "${item.path}" need to starts with "${item.insert}"`)) + } + let found = recursiveSearch(routes, item.insert); + // possiablly a nested child of inserted item + if (!found) { + found = recursiveSearch(insertedArray, item.insert); + } + if (found) { + found.routes = found.routes || []; + found.routes.push(item); + found.exact = false; + } else { + logError( + new Error(`[insert-routes]: insert route not found for "${item.insert}"`), + ); + } + }); + + const patchedRouteCount = countRoute(); + + // check if any route lost while the insertion + if (patchedRouteCount < originalRouteCount) { + logError(new Error(`[insert-routes]: circular route insert detected`)); + } +} + +// modify route with "microApp" attribute to use real component +function patchMicroAppRouteComponent({ routes }) { + const getRootRoutes = (routes: IRouteProps[]) => { + const rootRoute = routes.find(route => route.path === '/'); + if (rootRoute) { + // 如果根路由是叶子节点,则直接返回其父节点 + if (!rootRoute.routes) { + return routes; + } + + return getRootRoutes(rootRoute.routes); + } + + return routes; + }; + + const rootRoutes = getRootRoutes(routes); + if (rootRoutes) { + const { routeBindingAlias, base, masterHistoryType } = getMasterOptions() as MasterOptions; + microAppRuntimeRoutes.reverse().forEach(microAppRoute => { + patchMicroAppRoute(microAppRoute, getMicroAppRouteComponent, { base, masterHistoryType, routeBindingAlias }); + rootRoutes.unshift(microAppRoute); + }); + } +} + +// throw in dev env, log on prod +function logError(error: Error){ + if(process.env.NODE_ENV === 'development') { + throw error; + } else { + console.error(error); + } +} export async function render(oldRender: typeof noop) { const runtimeOptions = await getMasterRuntime(); @@ -82,30 +206,9 @@ export async function render(oldRender: typeof noop) { export function patchRoutes(opts: { routes: IRouteProps[] }) { if (microAppRuntimeRoutes) { - const getRootRoutes = (routes: IRouteProps[]) => { - const rootRoute = routes.find(route => route.path === '/'); - if (rootRoute) { - // 如果根路由是叶子节点,则直接返回其父节点 - if (!rootRoute.routes) { - return routes; - } - - return getRootRoutes(rootRoute.routes); - } - - return routes; - }; - - const { routes } = opts; - const rootRoutes = getRootRoutes(routes); - if (rootRoutes) { - const { routeBindingAlias, base, masterHistoryType } = getMasterOptions() as MasterOptions; - microAppRuntimeRoutes.reverse().forEach(microAppRoute => { - patchMicroAppRoute(microAppRoute, getMicroAppRouteComponent, { base, masterHistoryType, routeBindingAlias }); - rootRoutes.unshift(microAppRoute); - }); - } + patchMicroAppRouteComponent(opts) } + insertMicroAppRoute(opts); } async function useLegacyRegisterMode( From 139ce74e221831065ac568965c51a41da3486b96 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Wed, 1 Sep 2021 19:41:06 +0800 Subject: [PATCH 2/6] test: add test case --- packages/plugin-qiankun/src/common.test.ts | 309 ++++++++++++------ packages/plugin-qiankun/src/common.ts | 105 ++++++ .../src/master/masterRuntimePlugin.ts.tpl | 100 +----- 3 files changed, 313 insertions(+), 201 deletions(-) diff --git a/packages/plugin-qiankun/src/common.test.ts b/packages/plugin-qiankun/src/common.test.ts index 79813eab..7d842c9a 100644 --- a/packages/plugin-qiankun/src/common.test.ts +++ b/packages/plugin-qiankun/src/common.test.ts @@ -2,107 +2,212 @@ * @author Kuitos * @since 2019-10-22 */ +import 'jest'; +import { testPathWithPrefix, insertMicroAppRoute } from './common'; -import { testPathWithPrefix } from './common'; - -test('testPathPrefix', () => { - // browser history - expect(testPathWithPrefix('/js', '/')).toBeFalsy(); - - expect(testPathWithPrefix('/js', '/js')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/jss')).toBeFalsy(); - expect(testPathWithPrefix('/js', '/js/')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/s')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js?')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/?')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/?a=b')).toBeTruthy(); - - // hash history - expect(testPathWithPrefix('#/js', '#/js')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/jss')).toBeFalsy(); - expect(testPathWithPrefix('#/js', '#/js/')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/s')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js?a=b')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js?')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/?')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/?a=b')).toBeTruthy(); - - // browser history with slash ending - expect(testPathWithPrefix('/js/', '/js')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/jss')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/js/')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/s')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js?a=b')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/js?')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/js/?')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/?a=b')).toBeTruthy(); - - // hash history with slash ending - expect(testPathWithPrefix('#/js/', '#/js')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/jss')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/js/')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/s')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js?a=b')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/js?')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/js/?')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/?a=b')).toBeTruthy(); - - // browser history with dynamic route - expect(testPathWithPrefix('/:abc', '/js')).toBeTruthy(); - expect(testPathWithPrefix('/:abc', '/123')).toBeTruthy(); - expect(testPathWithPrefix('/:abc/', '/js/')).toBeTruthy(); - expect(testPathWithPrefix('/:abc/js', '/js/js')).toBeTruthy(); - expect(testPathWithPrefix('/:abc/js', '/js/123')).toBeFalsy(); - expect(testPathWithPrefix('/js/:abc', '/js/123')).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/', '/js/123')).toBeFalsy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx')).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx/hello')).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123')).toBeFalsy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/css')).toBeFalsy(); - expect( - testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk'), - ).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsk')).toBeFalsy(); - expect( - testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk/hello'), - ).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc?', '/js')).toBeTruthy(); - expect(testPathWithPrefix('/js/*', '/js/245')).toBeTruthy(); - - // hash history with dynamic route - expect(testPathWithPrefix('#/:abc', '#/js')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc', '#/123')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc/', '#/js/')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc/js', '#/js/js')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc/js', '#/js/123')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc', '#/js/123')).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/', '#/js/123')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc/', '#/js/123/jsx')).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx')).toBeTruthy(); - expect( - testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello'), - ).toBeTruthy(); - expect( - testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello?test=1'), - ).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/css')).toBeFalsy(); - expect( - testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk'), - ).toBeTruthy(); - expect( - testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk/hello'), - ).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsk')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc?', '#/js')).toBeTruthy(); - expect(testPathWithPrefix('#/js/*', '#/js/245')).toBeTruthy(); +describe('testPathPrefix', () => { + test('testPathPrefix', () => { + // browser history + expect(testPathWithPrefix('/js', '/')).toBeFalsy(); + + expect(testPathWithPrefix('/js', '/js')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/jss')).toBeFalsy(); + expect(testPathWithPrefix('/js', '/js/')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/s')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js?a=b')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js?')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/?')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/?a=b')).toBeTruthy(); + + // hash history + expect(testPathWithPrefix('#/js', '#/js')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/jss')).toBeFalsy(); + expect(testPathWithPrefix('#/js', '#/js/')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/s')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js?a=b')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js?')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/?')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/?a=b')).toBeTruthy(); + + // browser history with slash ending + expect(testPathWithPrefix('/js/', '/js')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/jss')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/js/')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/s')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js?a=b')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/js?')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/js/?')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/?a=b')).toBeTruthy(); + + // hash history with slash ending + expect(testPathWithPrefix('#/js/', '#/js')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/jss')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/js/')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/s')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js?a=b')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/js?')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/js/?')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/?a=b')).toBeTruthy(); + + // browser history with dynamic route + expect(testPathWithPrefix('/:abc', '/js')).toBeTruthy(); + expect(testPathWithPrefix('/:abc', '/123')).toBeTruthy(); + expect(testPathWithPrefix('/:abc/', '/js/')).toBeTruthy(); + expect(testPathWithPrefix('/:abc/js', '/js/js')).toBeTruthy(); + expect(testPathWithPrefix('/:abc/js', '/js/123')).toBeFalsy(); + expect(testPathWithPrefix('/js/:abc', '/js/123')).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc/', '/js/123')).toBeFalsy(); + expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx')).toBeTruthy(); + expect( + testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx/hello'), + ).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc/jsx', '/js/123')).toBeFalsy(); + expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/css')).toBeFalsy(); + expect( + testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk'), + ).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsk')).toBeFalsy(); + expect( + testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk/hello'), + ).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc?', '/js')).toBeTruthy(); + expect(testPathWithPrefix('/js/*', '/js/245')).toBeTruthy(); + + // hash history with dynamic route + expect(testPathWithPrefix('#/:abc', '#/js')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc', '#/123')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc/', '#/js/')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc/js', '#/js/js')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc/js', '#/js/123')).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc', '#/js/123')).toBeTruthy(); + expect(testPathWithPrefix('#/js/:abc/', '#/js/123')).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc/', '#/js/123/jsx')).toBeTruthy(); + expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx')).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello'), + ).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello?test=1'), + ).toBeTruthy(); + expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123')).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/css')).toBeFalsy(); + expect( + testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk'), + ).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk/hello'), + ).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsk'), + ).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc?', '#/js')).toBeTruthy(); + expect(testPathWithPrefix('#/js/*', '#/js/245')).toBeTruthy(); + }); +}); + +describe('modifyRoutes', () => { + beforeAll(() => { + process.env.NODE_ENV = 'development'; + }); + + test('should work with insert', () => { + const mockRoutes = [{ path: '/a' }, { path: '/a/b', insert: '/a' }]; + insertMicroAppRoute({ routes: mockRoutes }); + expect(mockRoutes).toEqual([ + { path: '/a', exact: false, routes: [{ insert: '/a', path: '/a/b' }] }, + ]); + }); + + test('should work with nested insert', () => { + const mockRoutes = [ + { path: '/a' }, + { path: '/a/d/b', insert: '/a/d' }, + { path: '/a/d/b/c', insert: '/a/d/b' }, + { path: '/a/d', insert: '/a' }, + { + path: '/e', + exact: false, + routes: [{ path: '/e/f' }, { path: '/e/g' }], + }, + { path: '/e/f/h', insert: '/e/f' }, + ]; + insertMicroAppRoute({ routes: mockRoutes }); + console.log(JSON.stringify(mockRoutes)); + expect(mockRoutes).toEqual([ + { + path: '/a', + routes: [ + { + path: '/a/d', + insert: '/a', + routes: [ + { + path: '/a/d/b', + insert: '/a/d', + routes: [{ path: '/a/d/b/c', insert: '/a/d/b' }], + exact: false, + }, + ], + exact: false, + }, + ], + exact: false, + }, + { + path: '/e', + exact: false, + routes: [ + { + path: '/e/f', + routes: [{ path: '/e/f/h', insert: '/e/f' }], + exact: false, + }, + { path: '/e/g' }, + ], + }, + ]); + }); + + test('should detect loop', () => { + const mockRoutes = [ + { path: '/a/b', insert: '/a/b' }, + { path: '/a/b', insert: '/a/b' }, + ]; + + const fn = jest.fn(); + + try { + insertMicroAppRoute({ routes: mockRoutes }); + } catch (e) { + fn(); + expect(e.message).toEqual( + '[insert-routes]: circular route insert detected', + ); + } + + expect(fn).toBeCalled(); + }); + + test('insert route should exist', () => { + const mockRoutes = [{ path: '/a' }, { path: '/abc/b', insert: '/abc' }]; + const fn = jest.fn(); + + try { + insertMicroAppRoute({ routes: mockRoutes }); + } catch (e) { + fn(); + expect(e.message).toEqual( + '[insert-routes]: insert route not found for "/abc"', + ); + } + expect(fn).toBeCalled(); + }); }); diff --git a/packages/plugin-qiankun/src/common.ts b/packages/plugin-qiankun/src/common.ts index 14413857..895e9948 100644 --- a/packages/plugin-qiankun/src/common.ts +++ b/packages/plugin-qiankun/src/common.ts @@ -4,6 +4,7 @@ */ import { ReactComponentElement } from 'react'; +import type { IRouteProps } from '@umijs/types'; export const defaultMountContainerId = 'root-subapp'; @@ -11,6 +12,15 @@ export const defaultMountContainerId = 'root-subapp'; export const noop = () => {}; // @formatter:on +// throw in dev env, log on prod +function logError(error: Error) { + if (process.env.NODE_ENV === 'development') { + throw error; + } else { + console.error(error); + } +} + export function toArray(source: T | T[]): T[] { return Array.isArray(source) ? source : [source]; } @@ -88,3 +98,98 @@ export function patchMicroAppRoute( route.component = getMicroAppRouteComponent(opts); } } + +// insert route with "insert" attribute into the route tree +export function insertMicroAppRoute({ routes }: { routes: IRouteProps[] }) { + const insertedArray: IRouteProps[] = []; + + // count total number of current route tree + const countRoute = () => { + let count = 0; + const recursiveCount = (routes: IRouteProps[]) => { + for (let i = 0; i < routes.length; i++) { + count += 1; + if (routes[i].routes && routes[i].routes?.length) { + recursiveCount(routes[i].routes || []); + } + } + }; + recursiveCount(routes); + return count; + }; + + // traverse the route tree to splice all routes that need to be inserted + const traverse = (routes: IRouteProps[]) => { + if (routes.length) { + for (let i = 0; i < routes.length; i++) { + const route = routes[i]; + + if (route.insert && route.insert !== '/') { + const [toBeMovedRoute] = routes.splice(i, 1); + insertedArray.push(toBeMovedRoute); + i -= 1; + } + + if (route.routes?.length) { + traverse(route.routes); + } + } + } + }; + + const originalRouteCount = countRoute(); + + traverse(routes); + + // find parent node in route tree + const recursiveSearch = ( + routes: IRouteProps[], + path: string, + ): IRouteProps | null => { + for (let i = 0; i < routes.length; i++) { + if (routes[i].path === path) { + return routes[i]; + } + if (routes[i].routes && routes[i].routes?.length) { + const found = recursiveSearch(routes[i].routes || [], path); + if (found) { + return found; + } + } + } + return null; + }; + + insertedArray.forEach((item) => { + if (!item.path || !item.path.startsWith(item.insert)) { + logError( + new Error( + `[insert-routes]: path "${item.path}" need to starts with "${item.insert}"`, + ), + ); + } + let found = recursiveSearch(routes, item.insert); + // possiablly a nested child of inserted item + if (!found) { + found = recursiveSearch(insertedArray, item.insert); + } + if (found) { + found.routes = found.routes || []; + found.routes.push(item); + found.exact = false; + } else { + logError( + new Error( + `[insert-routes]: insert route not found for "${item.insert}"`, + ), + ); + } + }); + + const patchedRouteCount = countRoute(); + + // check if any route lost while the insertion + if (patchedRouteCount < originalRouteCount) { + logError(new Error(`[insert-routes]: circular route insert detected`)); + } +} diff --git a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl index 3d204718..47fcd68b 100644 --- a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl +++ b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl @@ -7,7 +7,7 @@ import { prefetchApps, registerMicroApps, start } from 'qiankun'; // @ts-ignore import { ApplyPluginsType, getMicroAppRouteComponent, plugin } from 'umi'; -import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray } from './common'; +import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray, insertMicroAppRoute } from './common'; import { defaultHistoryType } from './constants'; import { getMasterOptions, setMasterOptions } from './masterOptions'; // @ts-ignore @@ -27,95 +27,6 @@ async function getMasterRuntime() { return master || config; } -// insert route with "insert" attribute into the route tree -function insertMicroAppRoute({ routes }) { - const insertedArray: IRouteProps[] = []; - - // count total number of current route tree - const countRoute = () => { - let count = 0; - const recursiveCount = (routes: IRouteProps[]) => { - for (let i = 0; i < routes.length; i++) { - count += 1; - if (routes[i].routes && routes[i].routes?.length) { - recursiveCount(routes[i].routes || []); - } - } - }; - recursiveCount(routes); - return count; - }; - - // traverse the route tree to splice all routes that need to be inserted - const traverse = (routes: IRouteProps[]) => { - if (routes.length) { - for (let i = 0; i < routes.length; i++) { - const route = routes[i]; - - if (route.insert && route.insert !== '/') { - const [toBeMovedRoute] = routes.splice(i, 1); - insertedArray.push(toBeMovedRoute); - i -= 1; - } - - if (route.routes?.length) { - traverse(route.routes); - } - } - } - }; - - const originalRouteCount = countRoute(); - - traverse(routes); - - // find parent node in route tree - const recursiveSearch = ( - routes: IRouteProps[], - path: string, - ): IRouteProps | null => { - for (let i = 0; i < routes.length; i++) { - if (routes[i].path === path) { - return routes[i]; - } - if (routes[i].routes && routes[i].routes?.length) { - const found = recursiveSearch(routes[i].routes || [], path); - if (found) { - return found; - } - } - } - return null; - }; - - insertedArray.forEach((item) => { - if(!item.path || !item.path.startsWith(item.insert)) { - logError(new Error(`[insert-routes]: path "${item.path}" need to starts with "${item.insert}"`)) - } - let found = recursiveSearch(routes, item.insert); - // possiablly a nested child of inserted item - if (!found) { - found = recursiveSearch(insertedArray, item.insert); - } - if (found) { - found.routes = found.routes || []; - found.routes.push(item); - found.exact = false; - } else { - logError( - new Error(`[insert-routes]: insert route not found for "${item.insert}"`), - ); - } - }); - - const patchedRouteCount = countRoute(); - - // check if any route lost while the insertion - if (patchedRouteCount < originalRouteCount) { - logError(new Error(`[insert-routes]: circular route insert detected`)); - } -} - // modify route with "microApp" attribute to use real component function patchMicroAppRouteComponent({ routes }) { const getRootRoutes = (routes: IRouteProps[]) => { @@ -142,15 +53,6 @@ function patchMicroAppRouteComponent({ routes }) { } } -// throw in dev env, log on prod -function logError(error: Error){ - if(process.env.NODE_ENV === 'development') { - throw error; - } else { - console.error(error); - } -} - export async function render(oldRender: typeof noop) { const runtimeOptions = await getMasterRuntime(); let masterOptions: MasterOptions = { ...getMasterOptions(), ...runtimeOptions }; From afd365daa60b7027b70f4750b25ffdc3ce449691 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Wed, 1 Sep 2021 19:44:32 +0800 Subject: [PATCH 3/6] test: add test case for path not starts with insert --- packages/plugin-qiankun/src/common.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/plugin-qiankun/src/common.test.ts b/packages/plugin-qiankun/src/common.test.ts index 7d842c9a..5c5da869 100644 --- a/packages/plugin-qiankun/src/common.test.ts +++ b/packages/plugin-qiankun/src/common.test.ts @@ -176,6 +176,23 @@ describe('modifyRoutes', () => { ]); }); + test('should detect invalid children path', () => { + const mockRoutes = [{ path: '/a', insert: '/b' }, { path: '/b' }]; + + const fn = jest.fn(); + + try { + insertMicroAppRoute({ routes: mockRoutes }); + } catch (e) { + fn(); + expect(e.message).toEqual( + '[insert-routes]: path "/a" need to starts with "/b"', + ); + } + + expect(fn).toBeCalled(); + }); + test('should detect loop', () => { const mockRoutes = [ { path: '/a/b', insert: '/a/b' }, From 587a8e2979497ba8498276f5099c563f156d2cd1 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Wed, 1 Sep 2021 21:04:30 +0800 Subject: [PATCH 4/6] chore: only insert outmost route --- packages/plugin-qiankun/src/common.test.ts | 118 +----------------- packages/plugin-qiankun/src/common.ts | 96 -------------- .../src/master/masterRuntimePlugin.ts.tpl | 40 +++++- 3 files changed, 35 insertions(+), 219 deletions(-) diff --git a/packages/plugin-qiankun/src/common.test.ts b/packages/plugin-qiankun/src/common.test.ts index 5c5da869..654c230a 100644 --- a/packages/plugin-qiankun/src/common.test.ts +++ b/packages/plugin-qiankun/src/common.test.ts @@ -3,7 +3,7 @@ * @since 2019-10-22 */ import 'jest'; -import { testPathWithPrefix, insertMicroAppRoute } from './common'; +import { testPathWithPrefix } from './common'; describe('testPathPrefix', () => { test('testPathPrefix', () => { @@ -112,119 +112,3 @@ describe('testPathPrefix', () => { expect(testPathWithPrefix('#/js/*', '#/js/245')).toBeTruthy(); }); }); - -describe('modifyRoutes', () => { - beforeAll(() => { - process.env.NODE_ENV = 'development'; - }); - - test('should work with insert', () => { - const mockRoutes = [{ path: '/a' }, { path: '/a/b', insert: '/a' }]; - insertMicroAppRoute({ routes: mockRoutes }); - expect(mockRoutes).toEqual([ - { path: '/a', exact: false, routes: [{ insert: '/a', path: '/a/b' }] }, - ]); - }); - - test('should work with nested insert', () => { - const mockRoutes = [ - { path: '/a' }, - { path: '/a/d/b', insert: '/a/d' }, - { path: '/a/d/b/c', insert: '/a/d/b' }, - { path: '/a/d', insert: '/a' }, - { - path: '/e', - exact: false, - routes: [{ path: '/e/f' }, { path: '/e/g' }], - }, - { path: '/e/f/h', insert: '/e/f' }, - ]; - insertMicroAppRoute({ routes: mockRoutes }); - console.log(JSON.stringify(mockRoutes)); - expect(mockRoutes).toEqual([ - { - path: '/a', - routes: [ - { - path: '/a/d', - insert: '/a', - routes: [ - { - path: '/a/d/b', - insert: '/a/d', - routes: [{ path: '/a/d/b/c', insert: '/a/d/b' }], - exact: false, - }, - ], - exact: false, - }, - ], - exact: false, - }, - { - path: '/e', - exact: false, - routes: [ - { - path: '/e/f', - routes: [{ path: '/e/f/h', insert: '/e/f' }], - exact: false, - }, - { path: '/e/g' }, - ], - }, - ]); - }); - - test('should detect invalid children path', () => { - const mockRoutes = [{ path: '/a', insert: '/b' }, { path: '/b' }]; - - const fn = jest.fn(); - - try { - insertMicroAppRoute({ routes: mockRoutes }); - } catch (e) { - fn(); - expect(e.message).toEqual( - '[insert-routes]: path "/a" need to starts with "/b"', - ); - } - - expect(fn).toBeCalled(); - }); - - test('should detect loop', () => { - const mockRoutes = [ - { path: '/a/b', insert: '/a/b' }, - { path: '/a/b', insert: '/a/b' }, - ]; - - const fn = jest.fn(); - - try { - insertMicroAppRoute({ routes: mockRoutes }); - } catch (e) { - fn(); - expect(e.message).toEqual( - '[insert-routes]: circular route insert detected', - ); - } - - expect(fn).toBeCalled(); - }); - - test('insert route should exist', () => { - const mockRoutes = [{ path: '/a' }, { path: '/abc/b', insert: '/abc' }]; - const fn = jest.fn(); - - try { - insertMicroAppRoute({ routes: mockRoutes }); - } catch (e) { - fn(); - expect(e.message).toEqual( - '[insert-routes]: insert route not found for "/abc"', - ); - } - expect(fn).toBeCalled(); - }); -}); diff --git a/packages/plugin-qiankun/src/common.ts b/packages/plugin-qiankun/src/common.ts index 895e9948..66116f8c 100644 --- a/packages/plugin-qiankun/src/common.ts +++ b/packages/plugin-qiankun/src/common.ts @@ -4,7 +4,6 @@ */ import { ReactComponentElement } from 'react'; -import type { IRouteProps } from '@umijs/types'; export const defaultMountContainerId = 'root-subapp'; @@ -98,98 +97,3 @@ export function patchMicroAppRoute( route.component = getMicroAppRouteComponent(opts); } } - -// insert route with "insert" attribute into the route tree -export function insertMicroAppRoute({ routes }: { routes: IRouteProps[] }) { - const insertedArray: IRouteProps[] = []; - - // count total number of current route tree - const countRoute = () => { - let count = 0; - const recursiveCount = (routes: IRouteProps[]) => { - for (let i = 0; i < routes.length; i++) { - count += 1; - if (routes[i].routes && routes[i].routes?.length) { - recursiveCount(routes[i].routes || []); - } - } - }; - recursiveCount(routes); - return count; - }; - - // traverse the route tree to splice all routes that need to be inserted - const traverse = (routes: IRouteProps[]) => { - if (routes.length) { - for (let i = 0; i < routes.length; i++) { - const route = routes[i]; - - if (route.insert && route.insert !== '/') { - const [toBeMovedRoute] = routes.splice(i, 1); - insertedArray.push(toBeMovedRoute); - i -= 1; - } - - if (route.routes?.length) { - traverse(route.routes); - } - } - } - }; - - const originalRouteCount = countRoute(); - - traverse(routes); - - // find parent node in route tree - const recursiveSearch = ( - routes: IRouteProps[], - path: string, - ): IRouteProps | null => { - for (let i = 0; i < routes.length; i++) { - if (routes[i].path === path) { - return routes[i]; - } - if (routes[i].routes && routes[i].routes?.length) { - const found = recursiveSearch(routes[i].routes || [], path); - if (found) { - return found; - } - } - } - return null; - }; - - insertedArray.forEach((item) => { - if (!item.path || !item.path.startsWith(item.insert)) { - logError( - new Error( - `[insert-routes]: path "${item.path}" need to starts with "${item.insert}"`, - ), - ); - } - let found = recursiveSearch(routes, item.insert); - // possiablly a nested child of inserted item - if (!found) { - found = recursiveSearch(insertedArray, item.insert); - } - if (found) { - found.routes = found.routes || []; - found.routes.push(item); - found.exact = false; - } else { - logError( - new Error( - `[insert-routes]: insert route not found for "${item.insert}"`, - ), - ); - } - }); - - const patchedRouteCount = countRoute(); - - // check if any route lost while the insertion - if (patchedRouteCount < originalRouteCount) { - logError(new Error(`[insert-routes]: circular route insert detected`)); - } -} diff --git a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl index 47fcd68b..f0aaad16 100644 --- a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl +++ b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl @@ -7,7 +7,7 @@ import { prefetchApps, registerMicroApps, start } from 'qiankun'; // @ts-ignore import { ApplyPluginsType, getMicroAppRouteComponent, plugin } from 'umi'; -import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray, insertMicroAppRoute } from './common'; +import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray } from './common'; import { defaultHistoryType } from './constants'; import { getMasterOptions, setMasterOptions } from './masterOptions'; // @ts-ignore @@ -28,7 +28,7 @@ async function getMasterRuntime() { } // modify route with "microApp" attribute to use real component -function patchMicroAppRouteComponent({ routes }) { +function patchMicroAppRouteComponent(routes: IRouteProps[]) { const getRootRoutes = (routes: IRouteProps[]) => { const rootRoute = routes.find(route => route.path === '/'); if (rootRoute) { @@ -43,12 +43,41 @@ function patchMicroAppRouteComponent({ routes }) { return routes; }; + const recursiveSearch = ( + routes: IRouteProps[], + path: string, + ): IRouteProps | null => { + for (let i = 0; i < routes.length; i++) { + if (routes[i].path === path) { + return routes[i]; + } + if (routes[i].routes && routes[i].routes?.length) { + const found = recursiveSearch(routes[i].routes || [], path); + if (found) { + return found; + } + } + } + return null; + }; + const rootRoutes = getRootRoutes(routes); if (rootRoutes) { const { routeBindingAlias, base, masterHistoryType } = getMasterOptions() as MasterOptions; microAppRuntimeRoutes.reverse().forEach(microAppRoute => { patchMicroAppRoute(microAppRoute, getMicroAppRouteComponent, { base, masterHistoryType, routeBindingAlias }); - rootRoutes.unshift(microAppRoute); + if (microAppRoute.insert) { + const found = recursiveSearch(routes, microAppRoute.insert); + if (found) { + found.exact = false; + found.routes = found.routes || []; + found.routes.push(microAppRoute); + } else { + throw new Error(`[plugin-qiankun]: path "${microAppRoute.insert}" not found`) + } + } else { + rootRoutes.unshift(microAppRoute); + } }); } } @@ -106,11 +135,10 @@ export async function render(oldRender: typeof noop) { } } -export function patchRoutes(opts: { routes: IRouteProps[] }) { +export function patchRoutes({ routes }: { routes: IRouteProps[] }) { if (microAppRuntimeRoutes) { - patchMicroAppRouteComponent(opts) + patchMicroAppRouteComponent(routes) } - insertMicroAppRoute(opts); } async function useLegacyRegisterMode( From 9da0113db23783b93e74c0ef0009f68296d997c4 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Wed, 1 Sep 2021 23:00:57 +0800 Subject: [PATCH 5/6] chore: remove unused code --- packages/plugin-qiankun/src/common.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/plugin-qiankun/src/common.ts b/packages/plugin-qiankun/src/common.ts index 66116f8c..14413857 100644 --- a/packages/plugin-qiankun/src/common.ts +++ b/packages/plugin-qiankun/src/common.ts @@ -11,15 +11,6 @@ export const defaultMountContainerId = 'root-subapp'; export const noop = () => {}; // @formatter:on -// throw in dev env, log on prod -function logError(error: Error) { - if (process.env.NODE_ENV === 'development') { - throw error; - } else { - console.error(error); - } -} - export function toArray(source: T | T[]): T[] { return Array.isArray(source) ? source : [source]; } From ce8f647942ba96c35661e04047fe83be69d44e61 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Mon, 6 Sep 2021 16:02:51 +0800 Subject: [PATCH 6/6] test: add test case for insert route --- packages/plugin-qiankun/src/common.test.ts | 73 ++++++++++++++++++- packages/plugin-qiankun/src/common.ts | 41 +++++++++++ .../src/master/masterRuntimePlugin.ts.tpl | 29 +------- 3 files changed, 115 insertions(+), 28 deletions(-) diff --git a/packages/plugin-qiankun/src/common.test.ts b/packages/plugin-qiankun/src/common.test.ts index 654c230a..c6fe7966 100644 --- a/packages/plugin-qiankun/src/common.test.ts +++ b/packages/plugin-qiankun/src/common.test.ts @@ -3,7 +3,7 @@ * @since 2019-10-22 */ import 'jest'; -import { testPathWithPrefix } from './common'; +import { testPathWithPrefix, insertRoute } from './common'; describe('testPathPrefix', () => { test('testPathPrefix', () => { @@ -112,3 +112,74 @@ describe('testPathPrefix', () => { expect(testPathWithPrefix('#/js/*', '#/js/245')).toBeTruthy(); }); }); + +describe('test insert route', () => { + test('insert', () => { + const mockRoutes = [{ path: '/a' }]; + insertRoute(mockRoutes, { path: '/a/b', insert: '/a' }); + expect(mockRoutes).toEqual([ + { path: '/a', exact: false, routes: [{ insert: '/a', path: '/a/b' }] }, + ]); + }); + test('insert with children routes', () => { + const mockRoutes = [{ path: '/a' }]; + insertRoute(mockRoutes, { + path: '/a/b', + insert: '/a', + routes: [{ path: '/a/b/c' }], + }); + expect(mockRoutes).toEqual([ + { + path: '/a', + exact: false, + routes: [{ insert: '/a', path: '/a/b', routes: [{ path: '/a/b/c' }] }], + }, + ]); + }); + test('insert into children routes', () => { + const mockRoutes = [{ path: '/a', routes: [{ path: '/a/b' }] }]; + insertRoute(mockRoutes, { path: '/a/b/c', insert: '/a/b' }); + expect(mockRoutes).toEqual([ + { + path: '/a', + routes: [ + { + path: '/a/b', + exact: false, + routes: [{ path: '/a/b/c', insert: '/a/b' }], + }, + ], + }, + ]); + }); + + test('insert node does not exist', () => { + const mockRoutes = [{ path: '/a' }]; + const mockInsert = { path: '/a/b', insert: '/b' }; + const errorFn = jest.fn(); + try { + insertRoute(mockRoutes, mockInsert); + } catch (e) { + errorFn(); + expect(e.message).toEqual( + `[plugin-qiankun]: path "${mockInsert.insert}" not found`, + ); + } + expect(errorFn).toBeCalled(); + }); + + test('insert path does not follow hierarchy', () => { + const mockRoutes = [{ path: '/a' }]; + const mockInsert = { path: '/b', insert: '/a' }; + const errorFn = jest.fn(); + try { + insertRoute(mockRoutes, mockInsert); + } catch (e) { + errorFn(); + expect(e.message).toEqual( + `[plugin-qiankun]: path "${mockInsert.path}" need to starts with "${mockRoutes[0].path}"`, + ); + } + expect(errorFn).toBeCalled(); + }); +}); diff --git a/packages/plugin-qiankun/src/common.ts b/packages/plugin-qiankun/src/common.ts index 14413857..a06d0f1a 100644 --- a/packages/plugin-qiankun/src/common.ts +++ b/packages/plugin-qiankun/src/common.ts @@ -4,6 +4,7 @@ */ import { ReactComponentElement } from 'react'; +import type { IRouteProps } from 'umi'; export const defaultMountContainerId = 'root-subapp'; @@ -88,3 +89,43 @@ export function patchMicroAppRoute( route.component = getMicroAppRouteComponent(opts); } } + +const recursiveSearch = ( + routes: IRouteProps[], + path: string, +): IRouteProps | null => { + for (let i = 0; i < routes.length; i++) { + if (routes[i].path === path) { + return routes[i]; + } + if (routes[i].routes && routes[i].routes?.length) { + const found = recursiveSearch(routes[i].routes || [], path); + if (found) { + return found; + } + } + } + return null; +}; + +export function insertRoute(routes: IRouteProps[], microAppRoute: IRouteProps) { + const found = recursiveSearch(routes, microAppRoute.insert); + if (found) { + if ( + !microAppRoute.path || + !found.path || + !microAppRoute.path.startsWith(found.path) + ) { + throw new Error( + `[plugin-qiankun]: path "${microAppRoute.path}" need to starts with "${found.path}"`, + ); + } + found.exact = false; + found.routes = found.routes || []; + found.routes.push(microAppRoute); + } else { + throw new Error( + `[plugin-qiankun]: path "${microAppRoute.insert}" not found`, + ); + } +} diff --git a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl index f0aaad16..edc0cb72 100644 --- a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl +++ b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl @@ -7,7 +7,7 @@ import { prefetchApps, registerMicroApps, start } from 'qiankun'; // @ts-ignore import { ApplyPluginsType, getMicroAppRouteComponent, plugin } from 'umi'; -import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray } from './common'; +import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray, insertRoute } from './common'; import { defaultHistoryType } from './constants'; import { getMasterOptions, setMasterOptions } from './masterOptions'; // @ts-ignore @@ -43,38 +43,13 @@ function patchMicroAppRouteComponent(routes: IRouteProps[]) { return routes; }; - const recursiveSearch = ( - routes: IRouteProps[], - path: string, - ): IRouteProps | null => { - for (let i = 0; i < routes.length; i++) { - if (routes[i].path === path) { - return routes[i]; - } - if (routes[i].routes && routes[i].routes?.length) { - const found = recursiveSearch(routes[i].routes || [], path); - if (found) { - return found; - } - } - } - return null; - }; - const rootRoutes = getRootRoutes(routes); if (rootRoutes) { const { routeBindingAlias, base, masterHistoryType } = getMasterOptions() as MasterOptions; microAppRuntimeRoutes.reverse().forEach(microAppRoute => { patchMicroAppRoute(microAppRoute, getMicroAppRouteComponent, { base, masterHistoryType, routeBindingAlias }); if (microAppRoute.insert) { - const found = recursiveSearch(routes, microAppRoute.insert); - if (found) { - found.exact = false; - found.routes = found.routes || []; - found.routes.push(microAppRoute); - } else { - throw new Error(`[plugin-qiankun]: path "${microAppRoute.insert}" not found`) - } + insertRoute(routes, microAppRoute); } else { rootRoutes.unshift(microAppRoute); }