From 4763bc7666d01087389d6607de968523204a621c Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 13 Apr 2023 12:01:16 +0800 Subject: [PATCH] Refactor: router (#6123) * chore: refactor unfinished * feat: support create router * refactor: render mode * fix: code splitting false * feat: add location for icestark * chore: remove console * test: examples * fix: dataloader is undefined * fix: test * fix: test case * fix: test case * fix: types * fix: test case * fix: lock * fix: update lock * fix: hydration * fix: router * fix: router * chore: log * fix: hmr * fix: test * fix: test * chore: optimize code * Update singleRoute.test.tsx * fix: types --- .../src/components/FrameworkLayout.tsx | 2 +- examples/icestark-layout/src/pages/about.tsx | 2 +- examples/multi-target/ice.config.mts | 1 - examples/with-ssg/src/pages/index.tsx | 2 +- examples/with-store/ice.config.mts | 1 + examples/with-store/src/app.tsx | 6 +- packages/ice/package.json | 2 +- packages/ice/src/constant.ts | 15 +- packages/ice/src/routes.ts | 24 +- packages/ice/src/utils/injectInitialEntry.ts | 2 +- .../ice/templates/core/entry.client.tsx.ejs | 4 +- .../ice/templates/core/entry.server.ts.ejs | 10 +- packages/ice/templates/core/routes.ts.ejs | 4 - packages/ice/templates/core/routes.tsx.ejs | 8 + packages/miniapp-runtime/src/app/connect.tsx | 2 +- packages/miniapp-runtime/src/app/index.ts | 3 +- .../miniapp-runtime/src/app/routeContext.ts | 26 ++ .../miniapp-runtime/src/app/runClientApp.tsx | 23 +- .../plugin-icestark/src/runtime/framework.tsx | 20 +- packages/plugin-store/src/runtime.tsx | 12 +- packages/runtime/package.json | 8 +- packages/runtime/src/App.tsx | 53 +-- packages/runtime/src/AppContext.tsx | 6 + packages/runtime/src/AppErrorBoundary.tsx | 2 +- packages/runtime/src/AppRouter.tsx | 34 -- packages/runtime/src/ClientRouter.tsx | 55 +++ packages/runtime/src/Document.tsx | 27 +- packages/runtime/src/RouteContext.ts | 22 +- packages/runtime/src/RouteWrapper.tsx | 18 +- packages/runtime/src/ServerRouter.tsx | 45 +++ .../runtime/src/{AppData.tsx => appData.ts} | 14 - packages/runtime/src/dataLoader.ts | 20 +- packages/runtime/src/history.ts | 4 +- packages/runtime/src/index.server.ts | 2 +- packages/runtime/src/index.ts | 24 +- packages/runtime/src/matchRoutes.ts | 8 +- packages/runtime/src/requestContext.ts | 4 +- packages/runtime/src/router.ts | 4 +- packages/runtime/src/routes.tsx | 209 ++++-------- packages/runtime/src/routesConfig.ts | 34 +- packages/runtime/src/runClientApp.tsx | 302 +++++------------ packages/runtime/src/runServerApp.tsx | 101 +++--- packages/runtime/src/runtime.tsx | 7 +- .../{single-router.tsx => singleRouter.tsx} | 20 +- packages/runtime/src/types.ts | 59 ++-- packages/runtime/tests/routes.test.tsx | 313 ++++++------------ packages/runtime/tests/routesConfig.test.ts | 7 +- packages/runtime/tests/runClientApp.test.tsx | 213 +++++------- packages/runtime/tests/runServerApp.test.tsx | 71 ++-- packages/runtime/tests/singleRoute.test.tsx | 4 +- pnpm-lock.yaml | 104 +++++- tests/integration/with-antd-mobile.test.ts | 3 +- 52 files changed, 889 insertions(+), 1077 deletions(-) delete mode 100644 packages/ice/templates/core/routes.ts.ejs create mode 100644 packages/ice/templates/core/routes.tsx.ejs create mode 100644 packages/miniapp-runtime/src/app/routeContext.ts delete mode 100644 packages/runtime/src/AppRouter.tsx create mode 100644 packages/runtime/src/ClientRouter.tsx create mode 100644 packages/runtime/src/ServerRouter.tsx rename packages/runtime/src/{AppData.tsx => appData.ts} (70%) rename packages/runtime/src/{single-router.tsx => singleRouter.tsx} (68%) diff --git a/examples/icestark-layout/src/components/FrameworkLayout.tsx b/examples/icestark-layout/src/components/FrameworkLayout.tsx index e8e34a3255..01a079d20d 100644 --- a/examples/icestark-layout/src/components/FrameworkLayout.tsx +++ b/examples/icestark-layout/src/components/FrameworkLayout.tsx @@ -13,4 +13,4 @@ export default function FrameworkLayout({ children }) { ); -} \ No newline at end of file +} diff --git a/examples/icestark-layout/src/pages/about.tsx b/examples/icestark-layout/src/pages/about.tsx index 78d823e59a..764f4847be 100644 --- a/examples/icestark-layout/src/pages/about.tsx +++ b/examples/icestark-layout/src/pages/about.tsx @@ -7,4 +7,4 @@ export default function About() { home ); -} \ No newline at end of file +} diff --git a/examples/multi-target/ice.config.mts b/examples/multi-target/ice.config.mts index d511ba9ed4..d86560cf30 100644 --- a/examples/multi-target/ice.config.mts +++ b/examples/multi-target/ice.config.mts @@ -2,7 +2,6 @@ import { defineConfig } from '@ice/app'; export default defineConfig(() => ({ ssr: true, - ssg: true, define: { 'process.env.NODE_ENV': JSON.stringify(true), }, diff --git a/examples/with-ssg/src/pages/index.tsx b/examples/with-ssg/src/pages/index.tsx index 5df7dc3e75..fd6781a8ca 100644 --- a/examples/with-ssg/src/pages/index.tsx +++ b/examples/with-ssg/src/pages/index.tsx @@ -39,4 +39,4 @@ export const staticDataLoader = defineStaticDataLoader(() => { return { price: '0.00', }; -}); \ No newline at end of file +}); diff --git a/examples/with-store/ice.config.mts b/examples/with-store/ice.config.mts index 8f105b7264..d9944d8bb5 100644 --- a/examples/with-store/ice.config.mts +++ b/examples/with-store/ice.config.mts @@ -2,6 +2,7 @@ import { defineConfig } from '@ice/app'; import store from '@ice/plugin-store'; export default defineConfig(() => ({ + ssg: false, plugins: [ store({ resetPageState: true, diff --git a/examples/with-store/src/app.tsx b/examples/with-store/src/app.tsx index 87805d7ca9..a41605439a 100644 --- a/examples/with-store/src/app.tsx +++ b/examples/with-store/src/app.tsx @@ -19,4 +19,8 @@ export const dataLoader = defineDataLoader(() => { }); }); -export default defineAppConfig(() => ({})); +export default defineAppConfig(() => ({ + router: { + type: 'hash', + }, +})); diff --git a/packages/ice/package.json b/packages/ice/package.json index 1038f262e2..73f3cee074 100644 --- a/packages/ice/package.json +++ b/packages/ice/package.json @@ -83,7 +83,7 @@ "esbuild": "^0.16.5", "jest": "^29.0.2", "react": "^18.2.0", - "react-router": "^6.8.2", + "react-router": "6.10.0", "unplugin": "^0.9.0", "webpack": "^5.76.2", "webpack-dev-server": "^4.7.4" diff --git a/packages/ice/src/constant.ts b/packages/ice/src/constant.ts index cf339bd8dc..caeb9b6b15 100644 --- a/packages/ice/src/constant.ts +++ b/packages/ice/src/constant.ts @@ -39,15 +39,22 @@ export const TARGETS = [ export const RUNTIME_EXPORTS = [ { - specifier: ['Link', 'Outlet', 'useParams', 'useSearchParams', 'useLocation', 'useNavigate'], + specifier: [ + 'Link', + 'Outlet', + 'useParams', + 'useSearchParams', + 'useLocation', + 'useData', + 'useConfig', + 'useNavigate', + ], source: '@ice/runtime/router', }, { specifier: [ 'defineAppConfig', 'useAppData', - 'useData', - 'useConfig', 'history', 'KeepAliveOutlet', 'useMounted', @@ -60,4 +67,4 @@ export const RUNTIME_EXPORTS = [ ], source: '@ice/runtime', }, -]; \ No newline at end of file +]; diff --git a/packages/ice/src/routes.ts b/packages/ice/src/routes.ts index a91eef510d..1f45ff1de6 100644 --- a/packages/ice/src/routes.ts +++ b/packages/ice/src/routes.ts @@ -66,15 +66,35 @@ export function getRoutesDefination(nestRouteManifest: NestedRouteManifest[], la routeImports.push(`import * as ${routeSpecifier} from '${formatPath(componentPath)}';`); loadStatement = routeSpecifier; } + const component = `Component: () => WrapRouteComponent({ + routeId: '${id}', + isLayout: ${layout}, + routeExports: ${lazy ? 'componentModule' : loadStatement}, + })`; + const loader = `loader: createRouteLoader({ + routeId: '${id}', + requestContext, + renderMode, + module: ${lazy ? 'componentModule' : loadStatement}, + })`; const routeProperties: string[] = [ `path: '${formatPath(routePath || '')}',`, - `load: () => ${loadStatement},`, + `async lazy() { + ${lazy ? `const componentModule = await ${loadStatement}` : ''}; + return { + ${lazy ? '...componentModule' : `...${loadStatement}`}, + ${component}, + ${loader}, + }; + },`, + // Empty errorElement to avoid default ui provided by react-router. + 'ErrorBoundary: RouteErrorComponent,', `componentName: '${componentName}',`, `index: ${index},`, `id: '${id}',`, 'exact: true,', `exports: ${JSON.stringify(exports)},`, - ]; + ].filter(Boolean); if (layout) { routeProperties.push('layout: true,'); diff --git a/packages/ice/src/utils/injectInitialEntry.ts b/packages/ice/src/utils/injectInitialEntry.ts index 9e17927918..a8375983b0 100644 --- a/packages/ice/src/utils/injectInitialEntry.ts +++ b/packages/ice/src/utils/injectInitialEntry.ts @@ -18,7 +18,7 @@ function injectInitialEntry(routeManifest: RouteManifest, outputDir: string) { const routePaths = routeManifest.getFlattenRoute(); const routeItems = routeManifest.getNestedRoute(); routePaths.forEach((routePath) => { - const routeAsset = getRouteAsset(routeItems, routePath); + const routeAsset = getRouteAsset(routeItems as unknown as RouteItem[], routePath); // Inject `initialPath` when router type is memory. const routeAssetPath = path.join(outputDir, 'js', routeAsset); if (fse.existsSync(routeAssetPath)) { diff --git a/packages/ice/templates/core/entry.client.tsx.ejs b/packages/ice/templates/core/entry.client.tsx.ejs index 24f76211ac..55d5071527 100644 --- a/packages/ice/templates/core/entry.client.tsx.ejs +++ b/packages/ice/templates/core/entry.client.tsx.ejs @@ -4,7 +4,7 @@ import { runClientApp, getAppConfig } from '<%- iceRuntimePath %>'; import { commons, statics } from './runtimeModules'; import * as app from '@/app'; <% if (enableRoutes) { -%> -import routes from './routes'; +import createRoutes from './routes'; <% } -%> <%- runtimeOptions.imports %> <% if(dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%> @@ -23,7 +23,7 @@ const render = (customOptions = {}) => { commons, statics, }, - <% if (enableRoutes) { %>routes,<% } %> + <% if (enableRoutes) { %>createRoutes,<% } %> basename: getRouterBasename(), hydrate: <%- hydrate %>, memoryRouter: <%- memoryRouter || false %>, diff --git a/packages/ice/templates/core/entry.server.ts.ejs b/packages/ice/templates/core/entry.server.ts.ejs index 9625ba7450..4ea5769093 100644 --- a/packages/ice/templates/core/entry.server.ts.ejs +++ b/packages/ice/templates/core/entry.server.ts.ejs @@ -8,7 +8,7 @@ import Document from '@/document'; import type { RenderMode, DistType } from '@ice/runtime'; // @ts-ignore import assetsManifest from 'virtual:assets-manifest.json'; -import routes from './routes'; +import createRoutes from './routes'; import routesConfig from './routes-config.bundle.mjs'; <% if(dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%> <%- runtimeOptions.imports %> @@ -47,7 +47,7 @@ interface RenderOptions { export async function renderToHTML(requestContext, options: RenderOptions = {}) { const { renderMode = 'SSR' } = options; setRuntimeEnv(renderMode); - + const mergedOptions = mergeOptions(options); return await runtime.renderToHTML(requestContext, mergedOptions); } @@ -55,7 +55,7 @@ export async function renderToHTML(requestContext, options: RenderOptions = {}) export async function renderToResponse(requestContext, options: RenderOptions = {}) { const { renderMode = 'SSR' } = options; setRuntimeEnv(renderMode); - + const mergedOptions = mergeOptions(options); return runtime.renderToResponse(requestContext, mergedOptions); } @@ -80,7 +80,7 @@ function mergeOptions(options) { return { app, assetsManifest, - routes, + createRoutes, runtimeModules, Document, serverOnlyBasename, @@ -98,4 +98,4 @@ function mergeOptions(options) { }, <% } -%> }; -} \ No newline at end of file +} diff --git a/packages/ice/templates/core/routes.ts.ejs b/packages/ice/templates/core/routes.ts.ejs deleted file mode 100644 index f15765f6e9..0000000000 --- a/packages/ice/templates/core/routes.ts.ejs +++ /dev/null @@ -1,4 +0,0 @@ -<%- routeImports.length ? routeImports.join('\n') + '\n\n' : ''; -%> -export default [ - <%- routeDefination %> -]; diff --git a/packages/ice/templates/core/routes.tsx.ejs b/packages/ice/templates/core/routes.tsx.ejs new file mode 100644 index 0000000000..16b78a77da --- /dev/null +++ b/packages/ice/templates/core/routes.tsx.ejs @@ -0,0 +1,8 @@ +import { createRouteLoader, WrapRouteComponent, RouteErrorComponent } from '@ice/runtime'; +<%- routeImports.length ? routeImports.join('\n') + '\n\n' : ''; -%> +export default ({ + requestContext, + renderMode, +}) => ([ + <%- routeDefination %> +]); diff --git a/packages/miniapp-runtime/src/app/connect.tsx b/packages/miniapp-runtime/src/app/connect.tsx index 34b950df4d..a80e91ea7d 100644 --- a/packages/miniapp-runtime/src/app/connect.tsx +++ b/packages/miniapp-runtime/src/app/connect.tsx @@ -1,7 +1,6 @@ import { EMPTY_OBJ, hooks } from '@ice/shared'; import React, { createElement } from 'react'; import * as ReactDOM from 'react-dom'; -import { ConfigProvider, DataProvider } from '@ice/runtime'; import type { MiniappAppConfig } from '../types.js'; import { Current, getPageInstance, incrementId, injectPageInstance, @@ -12,6 +11,7 @@ import type { PageLifeCycle, PageProps, ReactAppInstance, ReactPageComponent, } from '../index.js'; +import { ConfigProvider, DataProvider } from './routeContext.js'; import enableHtmlRuntime from './html/runtime.js'; import { reactMeta } from './react-meta.js'; import { ensureIsArray, HOOKS_APP_ID, isClassComponent, setDefaultDescriptor, setRouterParams } from './utils.js'; diff --git a/packages/miniapp-runtime/src/app/index.ts b/packages/miniapp-runtime/src/app/index.ts index e9771d6461..3d55c3be71 100644 --- a/packages/miniapp-runtime/src/app/index.ts +++ b/packages/miniapp-runtime/src/app/index.ts @@ -1,8 +1,9 @@ -import { getAppConfig, defineAppConfig, useAppData, useData, useConfig, defineDataLoader } from '@ice/runtime'; +import { getAppConfig, defineAppConfig, useAppData, defineDataLoader } from '@ice/runtime'; import runClientApp from './runClientApp.js'; import Link from './Link.js'; import useSearchParams from './useSearchParams.js'; import { routerHistory as history } from './history.js'; +import { useData, useConfig } from './routeContext.js'; export { runClientApp, diff --git a/packages/miniapp-runtime/src/app/routeContext.ts b/packages/miniapp-runtime/src/app/routeContext.ts new file mode 100644 index 0000000000..b39f1939bc --- /dev/null +++ b/packages/miniapp-runtime/src/app/routeContext.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; + +const DataContext = React.createContext(undefined); +DataContext.displayName = 'Data'; + +function useData(): T { + const value = React.useContext(DataContext); + return value; +} +const DataProvider = DataContext.Provider; + +const ConfigContext = React.createContext(undefined); +ConfigContext.displayName = 'Config'; + +function useConfig(): T { + const value = React.useContext(ConfigContext); + return value; +} +const ConfigProvider = ConfigContext.Provider; + +export { + useData, + DataProvider, + useConfig, + ConfigProvider, +}; diff --git a/packages/miniapp-runtime/src/app/runClientApp.tsx b/packages/miniapp-runtime/src/app/runClientApp.tsx index 4248e4eee2..9a0884039b 100644 --- a/packages/miniapp-runtime/src/app/runClientApp.tsx +++ b/packages/miniapp-runtime/src/app/runClientApp.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { AppContext, RunClientAppOptions, } from '@ice/runtime'; -import { AppContextProvider, AppDataProvider, getAppData, getAppConfig, Runtime } from '@ice/runtime'; +import { AppContextProvider, getAppData, getAppConfig, Runtime } from '@ice/runtime'; import { eventCenter } from '../emitter/emitter.js'; import { APP_READY } from '../constants/index.js'; import App from './App.js'; @@ -42,30 +42,15 @@ async function render( runtime: Runtime, ) { const appContext = runtime.getAppContext(); - const { appData } = appContext; const render = runtime.getRender(); const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment; render( document.getElementById('ice-container'), - + - + - , - ); -} - -interface BrowserEntryProps { - appContext: AppContext; -} - -function BrowserEntry({ - appContext, -}: BrowserEntryProps) { - return ( - - - + , ); } diff --git a/packages/plugin-icestark/src/runtime/framework.tsx b/packages/plugin-icestark/src/runtime/framework.tsx index c801e1bf6a..b83b81b1c9 100644 --- a/packages/plugin-icestark/src/runtime/framework.tsx +++ b/packages/plugin-icestark/src/runtime/framework.tsx @@ -4,10 +4,12 @@ import type { RuntimePlugin, AppRouterProps } from '@ice/runtime/types'; import type { RouteInfo, AppConfig } from '../types'; const { useState, useEffect } = React; + const runtime: RuntimePlugin = ({ getAppRouter, setAppRouter, appContext }) => { const { appExport, appData } = appContext; const OriginalRouter = getAppRouter(); const { layout, getApps, appRouter } = appExport?.icestark || {}; + if (getApps) { const FrameworkRouter = (props: AppRouterProps) => { const [routeInfo, setRouteInfo] = useState({}); @@ -62,7 +64,23 @@ const runtime: RuntimePlugin = ({ getAppRouter, setAppRouter, appContext }) => { path="/" location={props.location} render={() => { - return ; + const { routerContext } = props; + routerContext.routes = [ + ...routerContext.routes, + { + path: '*', + Component: () => ( + process.env.NODE_ENV === 'development' + ?
Add $.tsx to folder pages as a 404 component
+ : null + ), + }, + ]; + const routerProps = { + ...props, + routerContext, + }; + return ; }} /> diff --git a/packages/plugin-store/src/runtime.tsx b/packages/plugin-store/src/runtime.tsx index e375712827..cfb6fc9b80 100644 --- a/packages/plugin-store/src/runtime.tsx +++ b/packages/plugin-store/src/runtime.tsx @@ -5,7 +5,7 @@ import type { StoreConfig } from './types.js'; const EXPORT_CONFIG_NAME = 'storeConfig'; -const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider, useAppContext }, runtimeOptions) => { +const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider }, runtimeOptions) => { const { appExport, appData } = appContext; const exported = appExport[EXPORT_CONFIG_NAME]; const storeConfig: StoreConfig = (typeof exported === 'function' ? await exported(appData) : exported) || {}; @@ -27,12 +27,10 @@ const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider, use addProvider(StoreProvider); // Add page store . - const StoreProviderWrapper: RouteWrapper = ({ children, routeId }) => { - const { routeModules } = useAppContext(); - const routeModule = routeModules[routeId]; - if (routeModule?.[PAGE_STORE_PROVIDER]) { - const Provider = routeModule[PAGE_STORE_PROVIDER]; - const initialStates = routeModule[PAGE_STORE_INITIAL_STATES]; + const StoreProviderWrapper: RouteWrapper = ({ routeExports, children }) => { + if (routeExports?.[PAGE_STORE_PROVIDER]) { + const Provider = routeExports[PAGE_STORE_PROVIDER]; + const initialStates = routeExports[PAGE_STORE_INITIAL_STATES]; if (initialStates) { return {children}; } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 14f7c8db33..f818e5ed7d 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -13,7 +13,7 @@ "./jsx-dev-runtime": "./esm/jsx-dev-runtime.js", "./matchRoutes": "./esm/matchRoutes.js", "./router": "./esm/router.js", - "./single-router": "./esm/single-router.js", + "./single-router": "./esm/singleRouter.js", "./types": "./esm/types.js", "./package.json": "./package.json" }, @@ -37,16 +37,18 @@ "@types/react-dom": "^18.0.3", "react": "^18.0.0", "react-dom": "^18.0.0", - "regenerator-runtime": "^0.13.9" + "regenerator-runtime": "^0.13.9", + "@remix-run/web-fetch": "^4.3.3" }, "sideEffects": false, "dependencies": { "@ice/jsx-runtime": "^0.2.0", + "@remix-run/router": "1.5.0", "ejs": "^3.1.6", "fs-extra": "^10.0.0", "history": "^5.3.0", "htmlparser2": "^8.0.1", - "react-router-dom": "^6.8.2" + "react-router-dom": "6.10.0" }, "peerDependencies": { "react": "^18.1.0", diff --git a/packages/runtime/src/App.tsx b/packages/runtime/src/App.tsx index 897ca2b1ff..3dc53f7027 100644 --- a/packages/runtime/src/App.tsx +++ b/packages/runtime/src/App.tsx @@ -1,62 +1,17 @@ -import React, { useMemo } from 'react'; -import type { Action, Location } from 'history'; -import type { Navigator } from 'react-router-dom'; -import type { RouteWrapperConfig, AppRouterProps } from './types.js'; +import React from 'react'; import AppErrorBoundary from './AppErrorBoundary.js'; import { useAppContext } from './AppContext.js'; -import { createRouteElements } from './routes.js'; -interface Props { - action: Action; - location: Location; - navigator: Navigator; - static?: boolean; - RouteWrappers: RouteWrapperConfig[]; - AppRouter: React.ComponentType; -} - -export default function App(props: Props) { - const { - location, - action, - navigator, - static: staticProp = false, - AppRouter, - RouteWrappers, - } = props; - - const { appConfig, routes: originRoutes, basename } = useAppContext(); +export default function App({ children }) { + const { appConfig } = useAppContext(); const { strict, errorBoundary } = appConfig.app; const StrictMode = strict ? React.StrictMode : React.Fragment; - - if (!originRoutes || originRoutes.length === 0) { - throw new Error('Please add routes(like pages/index.tsx) to your app.'); - } - - const routes = useMemo( - () => createRouteElements(originRoutes, RouteWrappers), - // `originRoutes` and `RouteWrappers` will not be changed - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); - const ErrorBoundary = errorBoundary ? AppErrorBoundary : React.Fragment; - let element: React.ReactNode = ( - - ); - return ( - {element} + {children} ); diff --git a/packages/runtime/src/AppContext.tsx b/packages/runtime/src/AppContext.tsx index aa09b2537b..ff05e7fdbf 100644 --- a/packages/runtime/src/AppContext.tsx +++ b/packages/runtime/src/AppContext.tsx @@ -10,9 +10,15 @@ function useAppContext() { return value; } +function useAppData() { + const value = React.useContext(Context); + return value.appData; +} + const AppContextProvider = Context.Provider; export { useAppContext, + useAppData, AppContextProvider, }; diff --git a/packages/runtime/src/AppErrorBoundary.tsx b/packages/runtime/src/AppErrorBoundary.tsx index 886a1df2b2..08793e41b2 100644 --- a/packages/runtime/src/AppErrorBoundary.tsx +++ b/packages/runtime/src/AppErrorBoundary.tsx @@ -29,4 +29,4 @@ export default class AppErrorBoundary extends React.Component { return this.props.children; } -} \ No newline at end of file +} diff --git a/packages/runtime/src/AppRouter.tsx b/packages/runtime/src/AppRouter.tsx deleted file mode 100644 index 070009e457..0000000000 --- a/packages/runtime/src/AppRouter.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import type { RouteObject } from 'react-router-dom'; -import { Router, useRoutes } from 'react-router-dom'; -import type { AppRouterProps } from './types.js'; -import { Router as RouterSingle, useRoutes as useRoutesSingle } from './single-router.js'; - -const AppRouter: React.ComponentType = (props) => { - const { action, location, navigator, static: staticProps, routes, basename } = props; - const IceRouter = process.env.ICE_CORE_ROUTER === 'true' ? Router : RouterSingle; - - return ( - - - - ); -}; - -interface RoutesProps { - routes: RouteObject[]; -} - -function Routes({ routes }: RoutesProps) { - const useIceRoutes = process.env.ICE_CORE_ROUTER === 'true' ? useRoutes : useRoutesSingle; - const element = useIceRoutes(routes); - return element; -} - -export default AppRouter; diff --git a/packages/runtime/src/ClientRouter.tsx b/packages/runtime/src/ClientRouter.tsx new file mode 100644 index 0000000000..149f5f0b5c --- /dev/null +++ b/packages/runtime/src/ClientRouter.tsx @@ -0,0 +1,55 @@ +import React, { useEffect } from 'react'; +import { RouterProvider } from 'react-router-dom'; +import { createRouter } from '@remix-run/router'; +import type { AppRouterProps } from './types.js'; +import App from './App.js'; +import { DataContextProvider } from './singleRouter.js'; +import { useAppContext } from './AppContext.js'; + +let router: ReturnType = null; +function ClientRouter(props: AppRouterProps) { + const { Component, loaderData, routerContext } = props; + const { revalidate } = useAppContext(); + + function clearRouter() { + if (router) { + router.dispose(); + router = null; + } + } + // API createRouter only needs to be called once, and create before mount. + if (process.env.ICE_CORE_ROUTER === 'true') { + // Clear router before re-create in case of hot module replacement. + clearRouter(); + router = createRouter(routerContext).initialize(); + } + useEffect(() => { + if (revalidate) { + // Revalidate after render for SSG while staticDataLoader and dataLoader both defined. + router?.revalidate(); + } + return () => { + // In case of micro app, ClientRouter will be unmounted, + // duspose router before mount again. + clearRouter(); + }; + }, [revalidate]); + + let element: React.ReactNode; + if (process.env.ICE_CORE_ROUTER === 'true') { + element = ; + } else { + element = ( + + + + ); + } + return ( + + {element} + + ); +} + +export default ClientRouter; diff --git a/packages/runtime/src/Document.tsx b/packages/runtime/src/Document.tsx index 71c122bc67..299d219729 100644 --- a/packages/runtime/src/Document.tsx +++ b/packages/runtime/src/Document.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import type { WindowContext, RouteMatch, AssetsManifest } from './types.js'; -import { useAppContext } from './AppContext.js'; -import { useAppData } from './AppData.js'; +import { useAppContext, useAppData } from './AppContext.js'; import { getMeta, getTitle, getLinks, getScripts } from './routesConfig.js'; import getCurrentRoutePath from './utils/getCurrentRoutePath.js'; @@ -27,8 +26,8 @@ interface MetaProps extends React.HTMLAttributes{ export type MetaType = (props: MetaProps) => JSX.Element; export const Meta: MetaType = (props: MetaProps) => { - const { matches, routesConfig } = useAppContext(); - const meta = getMeta(matches, routesConfig); + const { matches, loaderData } = useAppContext(); + const meta = getMeta(matches, loaderData); const { MetaElement = 'meta', } = props; @@ -48,8 +47,8 @@ interface TitleProps extends React.HTMLAttributes{ export type TitleType = (props: TitleProps) => JSX.Element; export const Title: TitleType = (props: TitleProps) => { - const { matches, routesConfig } = useAppContext(); - const title = getTitle(matches, routesConfig); + const { matches, loaderData } = useAppContext(); + const title = getTitle(matches, loaderData); const { TitleElement = 'title', ...rest @@ -67,13 +66,13 @@ interface LinksProps extends React.LinkHTMLAttributes{ export type LinksType = (props: LinksProps) => JSX.Element; export const Links: LinksType = (props: LinksProps) => { - const { routesConfig, matches, assetsManifest } = useAppContext(); + const { loaderData, matches, assetsManifest } = useAppContext(); const { LinkElement = 'link', ...rest } = props; - const routeLinks = getLinks(matches, routesConfig); + const routeLinks = getLinks(matches, loaderData); const pageAssets = getPageAssets(matches, assetsManifest); const entryAssets = getEntryAssets(assetsManifest); const styles = entryAssets.concat(pageAssets).filter(path => path.indexOf('.css') > -1); @@ -97,18 +96,18 @@ interface ScriptsProps extends React.ScriptHTMLAttributes{ export type ScriptsType = (props: ScriptsProps) => JSX.Element; export const Scripts: ScriptsType = (props: ScriptsProps) => { - const { routesConfig, matches, assetsManifest } = useAppContext(); + const { loaderData, matches, assetsManifest } = useAppContext(); + const { ScriptElement = 'script', ...rest } = props; - const routeScripts = getScripts(matches, routesConfig); + const routeScripts = getScripts(matches, loaderData); const pageAssets = getPageAssets(matches, assetsManifest); const entryAssets = getEntryAssets(assetsManifest); // Page assets need to be load before entry assets, so when call dynamic import won't cause duplicate js chunk loaded. let scripts = pageAssets.concat(entryAssets).filter(path => path.indexOf('.js') > -1); - if (assetsManifest.dataLoader) { scripts.unshift(`${assetsManifest.publicPath}${assetsManifest.dataLoader}`); } @@ -146,7 +145,7 @@ export type DataType = (props: DataProps) => JSX.Element; // use app context separately export const Data: DataType = (props: DataProps) => { - const { routesData, documentOnly, matches, routesConfig, downgrade, renderMode, serverData } = useAppContext(); + const { documentOnly, matches, downgrade, renderMode, serverData, loaderData, revalidate } = useAppContext(); const appData = useAppData(); const { ScriptElement = 'script', @@ -156,14 +155,14 @@ export const Data: DataType = (props: DataProps) => { const routePath = getCurrentRoutePath(matches); const windowContext: WindowContext = { appData, - routesData, - routesConfig, + loaderData, routePath, downgrade, matchedIds, documentOnly, renderMode, serverData, + revalidate, }; return ( diff --git a/packages/runtime/src/RouteContext.ts b/packages/runtime/src/RouteContext.ts index 113b506001..9ddd8948f3 100644 --- a/packages/runtime/src/RouteContext.ts +++ b/packages/runtime/src/RouteContext.ts @@ -1,27 +1,17 @@ -import * as React from 'react'; -import type { RouteData, RouteConfig } from './types.js'; - -const DataContext = React.createContext(undefined); -DataContext.displayName = 'Data'; +import { useLoaderData } from 'react-router-dom'; +import type { RouteConfig } from './types.js'; function useData(): T { - const value = React.useContext(DataContext); - return value; + const data = useLoaderData(); + return (data as any).data; } -const DataProvider = DataContext.Provider; - -const ConfigContext = React.createContext | undefined>(undefined); -ConfigContext.displayName = 'Config'; function useConfig(): RouteConfig { - const value = React.useContext(ConfigContext); - return value; + const data = useLoaderData(); + return (data as any).pageConfig; } -const ConfigProvider = ConfigContext.Provider; export { useData, - DataProvider, useConfig, - ConfigProvider, }; diff --git a/packages/runtime/src/RouteWrapper.tsx b/packages/runtime/src/RouteWrapper.tsx index 04137aba61..adacd13289 100644 --- a/packages/runtime/src/RouteWrapper.tsx +++ b/packages/runtime/src/RouteWrapper.tsx @@ -1,18 +1,16 @@ import * as React from 'react'; -import type { RouteWrapperConfig } from './types.js'; -import { useAppContext } from './AppContext.js'; -import { DataProvider, ConfigProvider } from './RouteContext.js'; +import type { RouteWrapperConfig, ComponentModule } from './types.js'; interface Props { id: string; isLayout?: boolean; wrappers?: RouteWrapperConfig[]; children?: React.ReactNode; + routeExports: ComponentModule; } export default function RouteWrapper(props: Props) { - const { wrappers = [], id, isLayout } = props; - const { routesData, routesConfig } = useAppContext(); + const { wrappers = [], id, isLayout, routeExports } = props; // layout should only be wrapped by Wrapper with `layout: true` const filtered = isLayout ? wrappers.filter(wrapper => wrapper.layout === true) : wrappers; const RouteWrappers = filtered.map(item => item.Wrapper); @@ -21,7 +19,7 @@ export default function RouteWrapper(props: Props) { if (RouteWrappers.length) { element = RouteWrappers.reduce((preElement, CurrentWrapper) => ( - + {preElement} ), props.children); @@ -29,11 +27,5 @@ export default function RouteWrapper(props: Props) { element = props.children; } - return ( - - - {element} - - - ); + return element; } diff --git a/packages/runtime/src/ServerRouter.tsx b/packages/runtime/src/ServerRouter.tsx new file mode 100644 index 0000000000..a7fa4d43fd --- /dev/null +++ b/packages/runtime/src/ServerRouter.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { StaticRouterProvider, createStaticRouter } from 'react-router-dom/server.mjs'; +import type { RouteObject } from 'react-router-dom'; +import { RouteComponent } from './routes.js'; +import type { AppRouterProps } from './types.js'; +import App from './App.js'; + +function createServerRoutes(routes: RouteObject[]) { + return routes.map((route) => { + let dataRoute = { + // Static Router need element or Component when matched. + element: , + id: route.id, + index: route.index, + path: route.path, + children: null, + }; + + if (route?.children?.length > 0) { + let children = createServerRoutes( + route.children, + ); + dataRoute.children = children; + } + return dataRoute; + }); +} + +function ServerRouter(props: AppRouterProps) { + const { routerContext, routes } = props; + // Server router only be called once. + const router = createStaticRouter(createServerRoutes(routes), routerContext); + + return ( + + + + ); +} + +export default ServerRouter; diff --git a/packages/runtime/src/AppData.tsx b/packages/runtime/src/appData.ts similarity index 70% rename from packages/runtime/src/AppData.tsx rename to packages/runtime/src/appData.ts index b90fd84d68..351b3a6ade 100644 --- a/packages/runtime/src/AppData.tsx +++ b/packages/runtime/src/appData.ts @@ -1,18 +1,6 @@ -import * as React from 'react'; import type { AppExport, AppData, RequestContext } from './types.js'; import { callDataLoader } from './dataLoader.js'; -const Context = React.createContext(undefined); - -Context.displayName = 'AppDataContext'; - -function useAppData (): T { - const value = React.useContext(Context); - return value; -} - -const AppDataProvider = Context.Provider; - /** * Call the getData of app config. */ @@ -37,6 +25,4 @@ async function getAppData(appExport: AppExport, requestContext?: RequestContext) export { getAppData, - useAppData, - AppDataProvider, }; diff --git a/packages/runtime/src/dataLoader.ts b/packages/runtime/src/dataLoader.ts index d46ff6bb9e..dcb80936be 100644 --- a/packages/runtime/src/dataLoader.ts +++ b/packages/runtime/src/dataLoader.ts @@ -1,4 +1,4 @@ -import type { DataLoaderConfig, DataLoaderResult, RuntimeModules, AppExport, StaticRuntimePlugin, CommonJsRuntime, StaticDataLoader } from './types.js'; +import type { RequestContext, RenderMode, DataLoaderConfig, DataLoaderResult, RuntimeModules, AppExport, StaticRuntimePlugin, CommonJsRuntime, StaticDataLoader } from './types.js'; import getRequestContext from './requestContext.js'; interface Loaders { @@ -17,8 +17,7 @@ interface LoaderOptions { } export interface LoadRoutesDataOptions { - ssg?: boolean; - forceRequest?: boolean; + renderMode: RenderMode; } export function defineDataLoader(dataLoaderConfig: DataLoaderConfig): DataLoaderConfig { @@ -125,7 +124,7 @@ export function loadDataByCustomFetcher(config: StaticDataLoader) { /** * Handle for different dataLoader. */ -export function callDataLoader(dataLoader: DataLoaderConfig, requestContext): DataLoaderResult { +export function callDataLoader(dataLoader: DataLoaderConfig, requestContext: RequestContext): DataLoaderResult { if (Array.isArray(dataLoader)) { const loaders = dataLoader.map(loader => { return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : loader(requestContext); @@ -220,14 +219,13 @@ async function init(dataloaderConfig: Loaders, options: LoaderOptions) { // first render for ssg use data from build time. // second render for ssg will use data from data loader. - if (options?.ssg) { - result = cache.get(`${id}_ssg`); - } else { - result = cache.get(id); - } + const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`; + result = cache.get(cacheKey); + // Always fetch new data after cache is been used. + cache.delete(cacheKey); // Already send data request. - if (result && !options?.forceRequest) { + if (result) { const { status, value } = result; if (status === 'RESOLVED') { @@ -267,4 +265,4 @@ async function init(dataloaderConfig: Loaders, options: LoaderOptions) { export default { init, -}; \ No newline at end of file +}; diff --git a/packages/runtime/src/history.ts b/packages/runtime/src/history.ts index b9615c7458..3702d97f32 100644 --- a/packages/runtime/src/history.ts +++ b/packages/runtime/src/history.ts @@ -1,4 +1,4 @@ -import type { History } from 'history'; +import type { History } from '@remix-run/router'; // Value of history will be modified after render Router. let routerHistory: History | null = null; @@ -10,4 +10,4 @@ function setHistory(customHistory: History) { export { routerHistory, setHistory, -}; \ No newline at end of file +}; diff --git a/packages/runtime/src/index.server.ts b/packages/runtime/src/index.server.ts index 7a8dcf67bd..e1390daf38 100644 --- a/packages/runtime/src/index.server.ts +++ b/packages/runtime/src/index.server.ts @@ -1,2 +1,2 @@ export { renderToResponse, renderToHTML, renderToEntry } from './runServerApp.js'; -export * from './index.js'; \ No newline at end of file +export * from './index.js'; diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index cd654f60dc..879bdc5f72 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -20,12 +20,11 @@ import type { RouteWrapperConfig, } from './types.js'; import Runtime from './runtime.js'; -import App from './App.js'; import runClientApp from './runClientApp.js'; import type { RunClientAppOptions } from './runClientApp.js'; -import { useAppContext, AppContextProvider } from './AppContext.js'; -import { useAppData, AppDataProvider, getAppData } from './AppData.js'; -import { useData, useConfig, DataProvider, ConfigProvider } from './RouteContext.js'; +import { useAppContext, useAppData, AppContextProvider } from './AppContext.js'; +import { getAppData } from './appData.js'; +import { useData, useConfig } from './RouteContext.js'; import { Meta, Title, @@ -42,8 +41,8 @@ import type { DataType, MainType, } from './Document.js'; -import dataLoader, { defineDataLoader, defineServerDataLoader, defineStaticDataLoader } from './dataLoader.js'; -import AppRouter from './AppRouter.js'; +import dataLoader, { defineDataLoader, defineServerDataLoader, defineStaticDataLoader, callDataLoader } from './dataLoader.js'; +import getRequestContext from './requestContext.js'; import AppErrorBoundary from './AppErrorBoundary.js'; import getAppConfig, { defineAppConfig } from './appConfig.js'; import { routerHistory as history } from './history.js'; @@ -51,24 +50,21 @@ import KeepAliveOutlet from './KeepAliveOutlet.js'; import ClientOnly from './ClientOnly.js'; import useMounted from './useMounted.js'; import { withSuspense, useSuspenseData } from './Suspense.js'; +import { createRouteLoader, WrapRouteComponent, RouteErrorComponent } from './routes.js'; export { getAppConfig, defineAppConfig, Runtime, - App, runClientApp, AppContextProvider, useAppContext, - AppDataProvider, useAppData, useData, getAppData, defineDataLoader, defineServerDataLoader, defineStaticDataLoader, - DataProvider, - ConfigProvider, useConfig, Meta, Title, @@ -76,7 +72,10 @@ export { Scripts, Data, Main, + // API for data-loader. dataLoader, + callDataLoader, + getRequestContext, // react-router-dom API Link, Outlet, @@ -86,13 +85,16 @@ export { history, KeepAliveOutlet, - AppRouter, AppErrorBoundary, ClientOnly, useMounted, withSuspense, useSuspenseData, + + createRouteLoader, + WrapRouteComponent, + RouteErrorComponent, }; export type { diff --git a/packages/runtime/src/matchRoutes.ts b/packages/runtime/src/matchRoutes.ts index c9d6e4f632..f9d1830118 100644 --- a/packages/runtime/src/matchRoutes.ts +++ b/packages/runtime/src/matchRoutes.ts @@ -5,20 +5,20 @@ import type { Location } from 'history'; import type { RouteObject } from 'react-router-dom'; import { matchRoutes as originMatchRoutes } from 'react-router-dom'; import type { RouteItem, RouteMatch } from './types.js'; -import { matchRoutes as matchRoutesSingle } from './single-router.js'; +import { matchRoutes as matchRoutesSingle } from './singleRouter.js'; export default function matchRoutes( - routes: RouteItem[], + routes: unknown[], location: Partial | string, basename?: string, ): RouteMatch[] { const matchRoutesFn = process.env.ICE_CORE_ROUTER === 'true' ? originMatchRoutes : matchRoutesSingle; - let matches = matchRoutesFn(routes as unknown as RouteObject[], location, basename); + let matches = matchRoutesFn(routes as RouteObject[], location, basename); if (!matches) return []; return matches.map(({ params, pathname, pathnameBase, route }) => ({ params, pathname, - route: route as unknown as RouteItem, + route: route as RouteItem, pathnameBase, })); } diff --git a/packages/runtime/src/requestContext.ts b/packages/runtime/src/requestContext.ts index 81ccf643d1..04734b7b09 100644 --- a/packages/runtime/src/requestContext.ts +++ b/packages/runtime/src/requestContext.ts @@ -1,6 +1,6 @@ import type { ServerContext, RequestContext } from './types.js'; -interface Location { +export interface Location { pathname: string; search: string; } @@ -13,7 +13,7 @@ export default function getRequestContext(location: Location, serverContext: Ser const query = parseSearch(search); const requestContext: RequestContext = { - ...serverContext, + ...(serverContext || {}), pathname, query, }; diff --git a/packages/runtime/src/router.ts b/packages/runtime/src/router.ts index 5dc65e77c2..c86bfa1140 100644 --- a/packages/runtime/src/router.ts +++ b/packages/runtime/src/router.ts @@ -5,4 +5,6 @@ export { useSearchParams, useLocation, useNavigate, -} from 'react-router-dom'; \ No newline at end of file +} from 'react-router-dom'; + +export { useData, useConfig } from './RouteContext.js'; diff --git a/packages/runtime/src/routes.tsx b/packages/runtime/src/routes.tsx index fba5e0b00f..46db33fa50 100644 --- a/packages/runtime/src/routes.tsx +++ b/packages/runtime/src/routes.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import type { RouteObject } from 'react-router-dom'; -import { RouteComponent } from './types.js'; -import type { RouteItem, RouteModules, RouteWrapperConfig, RouteMatch, RequestContext, RoutesConfig, RoutesData, RenderMode } from './types.js'; +import { useRouteError } from 'react-router-dom'; +import type { RouteItem, RouteModules, RenderMode, DataLoaderConfig, RequestContext, ComponentModule } from './types.js'; import RouteWrapper from './RouteWrapper.js'; import { useAppContext } from './AppContext.js'; import { callDataLoader } from './dataLoader.js'; +import { updateRoutesConfig } from './routesConfig.js'; -type RouteModule = Pick; +type RouteModule = Pick; export function getRoutesPath(routes: RouteItem[], parentPath = ''): string[] { let paths = []; @@ -22,8 +22,8 @@ export function getRoutesPath(routes: RouteItem[], parentPath = ''): string[] { return paths.map(str => str.replace('//', '/')); } -export async function loadRouteModule(route: RouteModule, routeModulesCache: RouteModules) { - const { id, load } = route; +export async function loadRouteModule(route: RouteModule, routeModulesCache = {}) { + const { id, lazy } = route; if ( typeof window !== 'undefined' && // Don't use module cache and should load again in ssr. Ref: https://github.com/ice-lab/ice-next/issues/82 id in routeModulesCache @@ -32,7 +32,7 @@ export async function loadRouteModule(route: RouteModule, routeModulesCache: Rou } try { - const routeModule = await load(); + const routeModule = await lazy(); routeModulesCache[id] = routeModule; return routeModule; } catch (error) { @@ -50,125 +50,25 @@ export async function loadRouteModules(routes: RouteModule[], originRouteModules return routeModules; } -export interface LoadRoutesDataOptions { - renderMode?: RenderMode; - ssg?: boolean; - forceRequest?: boolean; -} - -/** -* get data for the matched routes. -*/ -export async function loadRoutesData( - matches: RouteMatch[], - requestContext: RequestContext, - routeModules: RouteModules, - options?: LoadRoutesDataOptions, -): Promise { - const { renderMode } = options || {}; - const routesData: RoutesData = {}; - - const hasGlobalLoader = typeof window !== 'undefined' && (window as any).__ICE_DATA_LOADER__; - const globalLoader = hasGlobalLoader ? (window as any).__ICE_DATA_LOADER__ : null; - - await Promise.all( - matches.map(async (match) => { - const { id } = match.route; - - if (globalLoader) { - routesData[id] = await globalLoader.getData(id, options); - return; - } - - const routeModule = routeModules[id]; - const { dataLoader, serverDataLoader, staticDataLoader } = routeModule ?? {}; - - let loader; - - // SSG -> getStaticData - // SSR -> getServerData || getData - // CSR -> getData - if (renderMode === 'SSG') { - loader = staticDataLoader; - } else if (renderMode === 'SSR') { - loader = serverDataLoader || dataLoader; - } else { - loader = dataLoader; - } - - if (loader) { - routesData[id] = await callDataLoader(loader, requestContext); - } - }), +// Wrap route component with runtime wrappers. +export function WrapRouteComponent(options: { + routeId: string; + isLayout?: boolean; + routeExports: ComponentModule; +}) { + const { routeId, isLayout, routeExports } = options; + const { RouteWrappers } = useAppContext(); + return ( + + + ); - - return routesData; -} - -/** - * Get page config for matched routes. - */ -export function getRoutesConfig( - matches: RouteMatch[], - routesData: RoutesData, - routeModules: RouteModules, -): RoutesConfig { - const routesConfig: RoutesConfig = {}; - - matches.forEach(async (match) => { - const { id } = match.route; - const routeModule = routeModules[id]; - - if (typeof routeModule === 'object') { - const { pageConfig } = routeModule; - const data = routesData[id]; - if (pageConfig) { - const value = pageConfig({ data }); - routesConfig[id] = value; - } - } else { - routesConfig[id] = {}; - } - }); - - return routesConfig; -} - -/** - * Create elements in routes which will be consumed by react-router-dom - */ -export function createRouteElements( - routes: RouteItem[], - RouteWrappers?: RouteWrapperConfig[], -) { - return routes.map((routeItem: RouteItem) => { - let { path, children, index, id, layout, element, ...rest } = routeItem; - element = ( - - - - ); - - const route: RouteObject = { - path, - element, - index, - id, - ...rest, - }; - - if (children) { - route.children = createRouteElements(children, RouteWrappers); - } - - return route; - }); } export function RouteComponent({ id }: { id: string }) { // get current route component from latest routeModules const { routeModules } = useAppContext(); - const { default: Component } = routeModules[id] || {}; + const { Component } = routeModules[id] || {}; if (process.env.NODE_ENV === 'development') { if (!Component) { throw new Error( @@ -180,29 +80,56 @@ export function RouteComponent({ id }: { id: string }) { return ; } +export function RouteErrorComponent() { + const error = useRouteError(); + if (error) { + // Re-throws the error so it can be caught by App Error Boundary. + throw error; + } + return <>; +} + /** - * filter matches is new or path changed. + * Create loader function for route module. */ -export function filterMatchesToLoad(prevMatches: RouteMatch[], currentMatches: RouteMatch[]): RouteMatch[] { - let isNew = (match: RouteMatch, index: number) => { - // [a] -> [a, b] - if (!prevMatches[index]) return true; - // [a, b] -> [a, c] - return match.route.id !== prevMatches[index].route.id; - }; +interface LoaderData { + data: any; + pageConfig: any; +} - let matchPathChanged = (match: RouteMatch, index: number) => { - return ( - // param change, /users/123 -> /users/456 - prevMatches[index].pathname !== match.pathname || - // splat param changed, which is not present in match.path - // e.g. /files/images/avatar.jpg -> files/finances.xls - (prevMatches[index].route.path?.endsWith('*') && - prevMatches[index].params['*'] !== match.params['*']) - ); - }; +export interface RouteLoaderOptions { + routeId: string; + requestContext?: RequestContext; + module: ComponentModule; + renderMode: RenderMode; +} - return currentMatches.filter((match, index) => { - return isNew(match, index) || matchPathChanged(match, index); - }); +export function createRouteLoader(options: RouteLoaderOptions): () => Promise { + return async () => { + const { dataLoader, pageConfig, staticDataLoader, serverDataLoader } = options.module; + const { requestContext, renderMode, routeId } = options; + const hasGlobalLoader = typeof window !== 'undefined' && (window as any).__ICE_DATA_LOADER__; + const globalLoader = hasGlobalLoader ? (window as any).__ICE_DATA_LOADER__ : null; + let routeData: any; + if (globalLoader) { + routeData = await globalLoader.getData(routeId, { renderMode }); + } else { + let loader: DataLoaderConfig; + if (renderMode === 'SSG') { + loader = staticDataLoader; + } else if (renderMode === 'SSR') { + loader = serverDataLoader || dataLoader; + } else { + loader = dataLoader; + } + routeData = loader && await callDataLoader(loader, requestContext); + } + const routeConfig = pageConfig ? pageConfig({ data: routeData }) : {}; + const loaderData = { data: routeData, pageConfig: routeConfig }; + // CSR and load next route data. + if (typeof window !== 'undefined') { + await updateRoutesConfig(loaderData); + } + return loaderData; + }; } diff --git a/packages/runtime/src/routesConfig.ts b/packages/runtime/src/routesConfig.ts index c9f55857a2..e377ec2cee 100644 --- a/packages/runtime/src/routesConfig.ts +++ b/packages/runtime/src/routesConfig.ts @@ -1,38 +1,38 @@ -import type { RouteMatch, RoutesConfig, RouteConfig } from './types.js'; +import type { RouteMatch, LoadersData, LoaderData, RouteConfig } from './types.js'; export function getMeta( matches: RouteMatch[], - routesConfig: RoutesConfig, + loadersData: LoadersData, ): React.MetaHTMLAttributes[] { - return getMergedValue('meta', matches, routesConfig) || []; + return getMergedValue('meta', matches, loadersData) || []; } export function getLinks( matches: RouteMatch[], - routesConfig: RoutesConfig, + loadersData: LoadersData, ): React.LinkHTMLAttributes[] { - return getMergedValue('links', matches, routesConfig) || []; + return getMergedValue('links', matches, loadersData) || []; } export function getScripts( matches: RouteMatch[], - routesConfig: RoutesConfig, + loadersData: LoadersData, ): React.ScriptHTMLAttributes[] { - return getMergedValue('scripts', matches, routesConfig) || []; + return getMergedValue('scripts', matches, loadersData) || []; } -export function getTitle(matches: RouteMatch[], routesConfig: RoutesConfig): string { - return getMergedValue('title', matches, routesConfig); +export function getTitle(matches: RouteMatch[], loadersData: LoadersData): string { + return getMergedValue('title', matches, loadersData); } /** * merge value for each matched route */ -function getMergedValue(key: string, matches: RouteMatch[], routesConfig: RoutesConfig) { +function getMergedValue(key: string, matches: RouteMatch[], loadersData: LoadersData) { let result; for (let match of matches) { const routeId = match.route.id; - const data = routesConfig[routeId]; + const data = loadersData[routeId]?.pageConfig; const value = data?.[key]; if (Array.isArray(value)) { @@ -50,15 +50,17 @@ function getMergedValue(key: string, matches: RouteMatch[], routesConfig: Routes /** * update routes config to document. */ -export async function updateRoutesConfig(matches: RouteMatch[], routesConfig: RoutesConfig) { - const title = getTitle(matches, routesConfig); +export async function updateRoutesConfig(loaderData: LoaderData) { + const routeConfig = loaderData?.pageConfig; + + const title = routeConfig?.title; if (title) { document.title = title; } - const meta = getMeta(matches, routesConfig) || []; - const links = getLinks(matches, routesConfig) || []; - const scripts = getScripts(matches, routesConfig) || []; + const meta = routeConfig?.meta || []; + const links = routeConfig?.links || []; + const scripts = routeConfig?.scripts || []; await Promise.all([ updateMeta(meta), diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index 2e21f39307..59446cc7df 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -1,30 +1,28 @@ -import React, { useLayoutEffect, useEffect, useState } from 'react'; +import React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { createHashHistory, createBrowserHistory, createMemoryHistory } from 'history'; -import type { HashHistory, BrowserHistory, Action, Location, MemoryHistory } from 'history'; +import { createHashHistory, createBrowserHistory, createMemoryHistory } from '@remix-run/router'; +import type { History } from '@remix-run/router'; import type { - AppContext, WindowContext, AppExport, RouteItem, AppRouterProps, RoutesData, RoutesConfig, - RouteWrapperConfig, RuntimeModules, RouteMatch, RouteModules, AppConfig, AssetsManifest, + AppContext, WindowContext, AppExport, RouteItem, RuntimeModules, AppConfig, AssetsManifest, } from './types.js'; -import { createHistory as createHistorySingle } from './single-router.js'; +import { createHistory as createHistorySingle } from './singleRouter.js'; import { setHistory } from './history.js'; import Runtime from './runtime.js'; -import App from './App.js'; -import { AppContextProvider } from './AppContext.js'; -import { AppDataProvider, getAppData } from './AppData.js'; -import { loadRouteModules, loadRoutesData, getRoutesConfig, filterMatchesToLoad, getRoutesPath } from './routes.js'; -import { updateRoutesConfig } from './routesConfig.js'; +import { getAppData } from './appData.js'; +import { getRoutesPath, loadRouteModule } from './routes.js'; +import type { RouteLoaderOptions } from './routes.js'; import getRequestContext from './requestContext.js'; import getAppConfig from './appConfig.js'; -import matchRoutes from './matchRoutes.js'; -import DefaultAppRouter from './AppRouter.js'; +import ClientRouter from './ClientRouter.js'; import { setFetcher } from './dataLoader.js'; import addLeadingSlash from './utils/addLeadingSlash.js'; +import { AppContextProvider } from './AppContext.js'; +import matchRoutes from './matchRoutes.js'; export interface RunClientAppOptions { app: AppExport; runtimeModules: RuntimeModules; - routes?: RouteItem[]; + createRoutes?: (options: Pick) => RouteItem[]; hydrate?: boolean; basename?: string; memoryRouter?: boolean; @@ -32,12 +30,10 @@ export interface RunClientAppOptions { dataLoaderFetcher?: Function; } -type History = BrowserHistory | HashHistory | MemoryHistory; - export default async function runClientApp(options: RunClientAppOptions) { const { app, - routes, + createRoutes, runtimeModules, basename, hydrate, @@ -50,17 +46,21 @@ export default async function runClientApp(options: RunClientAppOptions) { const assetsManifest: AssetsManifest = (window as any).__ICE_ASSETS_MANIFEST__ || {}; let { appData, - routesData, - routesConfig, + loaderData, routePath, downgrade, documentOnly, renderMode, serverData, + revalidate, } = windowContext; const formattedBasename = addLeadingSlash(basename); const requestContext = getRequestContext(window.location); const appConfig = getAppConfig(app); + const routes = createRoutes ? createRoutes({ + requestContext, + renderMode: 'CSR', + }) : []; const historyOptions = { memoryRouter, initialEntry: routePath, @@ -75,18 +75,18 @@ export default async function runClientApp(options: RunClientAppOptions) { routes, appConfig, appData, - routesData, - routesConfig, + loaderData, assetsManifest, basename: formattedBasename, routePath, renderMode, requestContext, serverData, + revalidate, }; const runtime = new Runtime(appContext, runtimeOptions); - runtime.setAppRouter(DefaultAppRouter); + runtime.setAppRouter(ClientRouter); // Load static module before getAppData, // so we can call request in in getAppData which provide by `plugin-request`. if (runtimeModules.statics) { @@ -99,51 +99,45 @@ export default async function runClientApp(options: RunClientAppOptions) { appData = await getAppData(app, requestContext); } - const matches = matchRoutes( - routes, - memoryRouter ? routePath : history.location, - formattedBasename, - ); - const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load }))); - if (Object.keys(routeModules).length === 0) { - // Log route info for debug. - console.warn('Routes:', routes, 'Basename:', formattedBasename); - } - if (!routesData) { - routesData = await loadRoutesData(matches, requestContext, routeModules, { - ssg: renderMode === 'SSG', - }); - } - - if (!routesConfig) { - routesConfig = getRoutesConfig(matches, routesData, routeModules); - } - - if (hydrate && !downgrade && !documentOnly) { + const needHydrate = hydrate && !downgrade && !documentOnly; + if (needHydrate) { + const defaultOnRecoverableError = typeof reportError === 'function' ? reportError + : function (error: unknown) { + console['error'](error); + }; runtime.setRender((container, element) => { - return ReactDOM.hydrateRoot(container, element); + const hydrateOptions = revalidate + ? { + onRecoverableError(error: unknown) { + // Ignore this error caused by router.revalidate + if ((error as Error)?.message?.indexOf('This Suspense boundary received an update before it finished hydrating.') == -1) { + defaultOnRecoverableError(error); + } + }, + } : {}; + return ReactDOM.hydrateRoot(container, element, hydrateOptions); }); } // Reset app context after app context is updated. - runtime.setAppContext({ ...appContext, matches, routeModules, routesData, routesConfig, appData }); + runtime.setAppContext({ ...appContext, appData }); if (runtimeModules.commons) { await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean)); } - return render({ runtime, history }); + return render({ runtime, history, needHydrate }); } interface RenderOptions { history: History; runtime: Runtime; + needHydrate: boolean; } -async function render({ history, runtime }: RenderOptions) { +async function render({ history, runtime, needHydrate }: RenderOptions) { const appContext = runtime.getAppContext(); - const { appConfig, appData } = appContext; + const { appConfig, loaderData, routes, basename } = appContext; const appRender = runtime.getRender(); const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment; - const RouteWrappers = runtime.getWrappers(); const AppRouter = runtime.getAppRouter(); const rootId = appConfig.app.rootId || 'app'; @@ -154,181 +148,51 @@ async function render({ history, runtime }: RenderOptions) { document.body.appendChild(root); console.warn(`Root node #${rootId} is not found, current root is automatically created by the framework.`); } - - return appRender( + const hydrationData = needHydrate ? { loaderData } : undefined; + if (needHydrate) { + const lazyMatches = matchRoutes(routes, history.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. + await Promise.all( + lazyMatches.map(async (m) => { + let routeModule = await m.route.lazy(); + Object.assign(m.route, { + ...routeModule, + lazy: undefined, + }); + }), + ); + } + } + const routerOptions = { + basename, + routes, + history, + hydrationData, + }; + let singleComponent = null; + let routeData = null; + if (process.env.ICE_CORE_ROUTER !== 'true') { + const { Component, loader } = await loadRouteModule(routes[0]); + singleComponent = Component || routes[0].Component; + routeData = loader && await loader(); + } + const renderRoot = appRender( root, - + - - , - ); -} - -interface BrowserEntryProps { - history: HashHistory | BrowserHistory | null; - appContext: AppContext; - RouteWrappers: RouteWrapperConfig[]; - AppRouter: React.ComponentType; -} - -interface HistoryState { - action: Action; - location: Location; -} - -interface RouteState { - routesData: RoutesData; - routesConfig: RoutesConfig; - matches: RouteMatch[]; - routeModules: RouteModules; -} - -function BrowserEntry({ - history, - appContext, - ...rest -}: BrowserEntryProps) { - const { - routes, - matches: originMatches, - routesData: initialRoutesData, - routesConfig: initialRoutesConfig, - routeModules: initialRouteModules, - basename, - renderMode, - } = appContext; - - const [historyState, setHistoryState] = useState({ - action: history.action, - location: history.location, - }); - const [routeState, setRouteState] = useState({ - routesData: initialRoutesData, - routesConfig: initialRoutesConfig, - matches: originMatches, - routeModules: initialRouteModules, - }); - - const { action, location } = historyState; - const { routesData, routesConfig, matches, routeModules } = routeState; - - // Listen the history change and update the state which including the latest action and location. - useLayoutEffect(() => { - if (history) { - const unlisten = history.listen(({ action, location }) => { - const currentMatches = matchRoutes(routes, location, basename); - if (!currentMatches.length) { - throw new Error(`Routes not found in location ${location.pathname}.`); - } - - loadNextPage( - currentMatches, - routeState, - ).then(({ routesData, routesConfig, routeModules }) => { - setRouteState({ - routesData, - routesConfig, - matches: currentMatches, - routeModules, - }); - setHistoryState({ - action, - location, - }); - }); - }); - - return () => unlisten(); - } - // Should add routeState to dependencies to ensure get the correct state in `history.listen`. - }, [routeState, history, basename, routes]); - - useEffect(() => { - // Rerender page use actual data for ssg. - if (renderMode === 'SSG') { - const initialContext = getRequestContext(window.location); - loadRoutesData(matches, initialContext, routeModules).then(data => { - setRouteState(r => { - return { - ...r, - routesData: data, - }; - }); - }); - } - // Trigger once after first render for SSG to update data. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // update app context for the current route. - const context = { - ...appContext, - matches, - routesData, - routesConfig, - routeModules, - }; - - return ( - - - + , ); -} - -/** - * Prepare for the next pages. - * Load modules、getPageData and preLoad the custom assets. - */ -export async function loadNextPage( - currentMatches: RouteMatch[], - preRouteState: RouteState, -) { - const { - matches: preMatches, - routesData: preRoutesData, - routeModules: preRouteModules, - } = preRouteState; - - const routeModules = await loadRouteModules( - currentMatches.map(({ route: { id, load } }) => ({ id, load })), - preRouteModules, - ); - - // load data for changed route. - const initialContext = getRequestContext(window.location); - const matchesToLoad = filterMatchesToLoad(preMatches, currentMatches); - // Navigate to other router should always fetch the latest data. - const data = await loadRoutesData(matchesToLoad, initialContext, routeModules, { - forceRequest: true, - }); - - const routesData: RoutesData = {}; - // merge page data. - currentMatches.forEach(({ route }) => { - const { id } = route; - routesData[id] = data[id] || preRoutesData[id]; - }); - - const routesConfig = getRoutesConfig(currentMatches, routesData, routeModules); - await updateRoutesConfig(currentMatches, routesConfig); - - return { - routesData, - routesConfig, - routeModules, - }; + return renderRoot; } interface HistoryOptions { diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx index 2af7b47c86..706406baf7 100644 --- a/packages/runtime/src/runServerApp.tsx +++ b/packages/runtime/src/runServerApp.tsx @@ -1,7 +1,7 @@ import type { ServerResponse } from 'http'; import * as React from 'react'; import * as ReactDOMServer from 'react-dom/server'; -import { Action, parsePath } from 'history'; +import { parsePath } from 'react-router-dom'; import type { Location } from 'history'; import type { AppContext, RouteItem, ServerContext, @@ -14,26 +14,25 @@ import type { AppData, } from './types.js'; import Runtime from './runtime.js'; -import App from './App.js'; import { AppContextProvider } from './AppContext.js'; -import { AppDataProvider, getAppData } from './AppData.js'; +import { getAppData } from './appData.js'; import getAppConfig from './appConfig.js'; import { DocumentContextProvider } from './Document.js'; -import { loadRouteModules, loadRoutesData, getRoutesConfig } from './routes.js'; +import { loadRouteModules } from './routes.js'; +import type { RouteLoaderOptions } from './routes.js'; import { pipeToString, renderToNodeStream } from './server/streamRender.js'; -import { createStaticNavigator } from './server/navigator.js'; import type { NodeWritablePiper } from './server/streamRender.js'; import getRequestContext from './requestContext.js'; import matchRoutes from './matchRoutes.js'; import getCurrentRoutePath from './utils/getCurrentRoutePath.js'; -import DefaultAppRouter from './AppRouter.js'; +import ServerRouter from './ServerRouter.js'; import { renderHTMLToJS } from './renderHTMLToJS.js'; import addLeadingSlash from './utils/addLeadingSlash.js'; interface RenderOptions { app: AppExport; assetsManifest: AssetsManifest; - routes: RouteItem[]; + createRoutes: (options: Pick) => RouteItem[]; runtimeModules: RuntimeModules; Document: DocumentComponent; documentOnly?: boolean; @@ -98,7 +97,6 @@ export async function renderToHTML( renderOptions: RenderOptions, ): Promise { const result = await doRender(requestContext, renderOptions); - const { value } = result; if (typeof value === 'string') { @@ -180,13 +178,17 @@ async function sendResult(res: ServerResponse, result: RenderResult) { res.end(result.value); } +function needRevalidate(matchedRoutes: RouteMatch[]) { + return matchedRoutes.some(({ route }) => route.exports.includes('dataLoader') && route.exports.includes('staticDataLoader')); +} + async function doRender(serverContext: ServerContext, renderOptions: RenderOptions): Promise { const { req } = serverContext; const { app, basename, serverOnlyBasename, - routes, + createRoutes, documentOnly, disableFallback, assetsManifest, @@ -200,15 +202,17 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio const requestContext = getRequestContext(location, serverContext); const appConfig = getAppConfig(app); - + const routes = createRoutes({ + requestContext, + renderMode, + }); let appData: AppData; const appContext: AppContext = { appExport: app, routes, appConfig, appData, - routesData: null, - routesConfig: null, + loaderData: {}, renderMode, assetsManifest, basename: finalBasename, @@ -217,7 +221,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio serverData, }; const runtime = new Runtime(appContext, runtimeOptions); - runtime.setAppRouter(DefaultAppRouter); + runtime.setAppRouter(ServerRouter); // Load static module before getAppData. if (runtimeModules.statics) { await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean)); @@ -233,22 +237,32 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio // HashRouter loads route modules by the CSR. if (appConfig?.router?.type === 'hash') { - return renderDocument({ matches: [], renderOptions }); + return renderDocument({ matches: [], routes, renderOptions }); } const matches = matchRoutes(routes, location, finalBasename); const routePath = getCurrentRoutePath(matches); if (documentOnly) { - return renderDocument({ matches, routePath, renderOptions }); + return renderDocument({ matches, routePath, routes, renderOptions }); } else if (!matches.length) { return render404(); } try { - const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load }))); - const routesData = await loadRoutesData(matches, requestContext, routeModules, { renderMode }); - const routesConfig = getRoutesConfig(matches, routesData, routeModules); - runtime.setAppContext({ ...appContext, routeModules, routesData, routesConfig, routePath, matches, appData }); + const routeModules = await loadRouteModules(matches.map(({ route: { id, lazy } }) => ({ id, lazy }))); + const loaderData = {}; + for (const routeId in routeModules) { + const { loader } = routeModules[routeId]; + if (loader) { + const { data, pageConfig } = await loader(); + loaderData[routeId] = { + data, + pageConfig, + }; + } + } + const revalidate = renderMode === 'SSG' && needRevalidate(matches); + runtime.setAppContext({ ...appContext, revalidate, routeModules, loaderData, routePath, matches, appData }); if (runtimeModules.commons) { await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean)); } @@ -264,7 +278,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio throw err; } console.error('Warning: render server entry error, downgrade to csr.', err); - return renderDocument({ matches, routePath, renderOptions, downgrade: true }); + return renderDocument({ matches, routePath, renderOptions, routes, downgrade: true }); } } @@ -282,7 +296,6 @@ interface RenderServerEntry { location: Location; renderOptions: RenderOptions; } - /** * Render App by SSR. */ @@ -296,39 +309,32 @@ async function renderServerEntry( ): Promise { const { Document } = renderOptions; const appContext = runtime.getAppContext(); - const { appData, routePath } = appContext; - const staticNavigator = createStaticNavigator(); + const { routes, routePath, loaderData, basename } = appContext; const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment; - const RouteWrappers = runtime.getWrappers(); const AppRouter = runtime.getAppRouter(); + const routerContext = { + matches, basename, loaderData, location, + }; const documentContext = { - main: , + main: ( + + ), }; - const element = ( - + - - - - - + + + - + ); const pipe = renderToNodeStream(element); const fallback = () => { - return renderDocument({ matches, routePath, renderOptions, downgrade: true }); + return renderDocument({ matches, routePath, renderOptions, routes, downgrade: true }); }; return { @@ -342,6 +348,7 @@ async function renderServerEntry( interface RenderDocumentOptions { matches: RouteMatch[]; renderOptions: RenderOptions; + routes: RouteItem[]; routePath?: string; downgrade?: boolean; } @@ -355,10 +362,10 @@ function renderDocument(options: RenderDocumentOptions): RenderResult { renderOptions, routePath, downgrade, + routes, }: RenderDocumentOptions = options; const { - routes, assetsManifest, app, Document, @@ -367,24 +374,24 @@ function renderDocument(options: RenderDocumentOptions): RenderResult { serverData, } = renderOptions; - const routesData = null; const appData = null; const appConfig = getAppConfig(app); - const matchedRoutesConfig = {}; + const loaderData = {}; matches.forEach(async (match) => { const { id } = match.route; const pageConfig = routesConfig[id]; - matchedRoutesConfig[id] = pageConfig ? pageConfig({}) : {}; + loaderData[id] = { + pageConfig: pageConfig ? pageConfig({}) : {}, + }; }); const appContext: AppContext = { assetsManifest, appConfig, appData, - routesData, - routesConfig: matchedRoutesConfig, + loaderData, matches, routes, documentOnly: true, diff --git a/packages/runtime/src/runtime.tsx b/packages/runtime/src/runtime.tsx index 95e1809691..a5b24aaeab 100644 --- a/packages/runtime/src/runtime.tsx +++ b/packages/runtime/src/runtime.tsx @@ -44,7 +44,12 @@ class Runtime { this.runtimeOptions = runtimeOptions; } - public getAppContext = () => this.appContext; + public getAppContext = () => { + return { + ...this.appContext, + RouteWrappers: this.RouteWrappers, + }; + }; public setAppContext = (appContext: AppContext) => { this.appContext = appContext; diff --git a/packages/runtime/src/single-router.tsx b/packages/runtime/src/singleRouter.tsx similarity index 68% rename from packages/runtime/src/single-router.tsx rename to packages/runtime/src/singleRouter.tsx index 97b5ab2af5..ce20e796d1 100644 --- a/packages/runtime/src/single-router.tsx +++ b/packages/runtime/src/singleRouter.tsx @@ -3,7 +3,25 @@ * if user config `optimize.router` false */ import * as React from 'react'; -import type { History } from 'history'; +import type { History } from '@remix-run/router'; +import type { LoaderData } from './types.js'; + +const Context = React.createContext(undefined); + +Context.displayName = 'DataContext'; + +export function useData(): T { + const value = React.useContext(Context); + return value.data; +} + +export function useConfig() { + const value = React.useContext(Context); + return value.pageConfig; +} + +export const DataContextProvider = Context.Provider; + export const useRoutes = (routes) => { return <>{routes[0].element}; diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 552e8ed91c..138fcd5468 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -1,8 +1,8 @@ import type { IncomingMessage, ServerResponse } from 'http'; -import type { Action, InitialEntry, Location } from 'history'; -import type { ComponentType, ReactNode, PropsWithChildren } from 'react'; +import type { InitialEntry, AgnosticRouteObject, Location } from '@remix-run/router'; +import type { ComponentType, PropsWithChildren } from 'react'; import type { HydrationOptions, Root } from 'react-dom/client'; -import type { Navigator, Params, RouteObject } from 'react-router-dom'; +import type { Params, RouteObject } from 'react-router-dom'; type UseConfig = () => RouteConfig>; type UseData = () => RouteData; @@ -71,15 +71,24 @@ export interface RoutesData { [routeId: string]: RouteData; } +export interface LoadersData { + [routeId: string]: LoaderData; +} + +export interface LoaderData { + data?: RouteData; + pageConfig?: RouteConfig; +} + // useAppContext export interface AppContext { appConfig: AppConfig; appData: any; serverData?: any; assetsManifest?: AssetsManifest; - routesData?: RoutesData; - routesConfig?: RoutesConfig; + loaderData?: LoadersData; routeModules?: RouteModules; + RouteWrappers?: RouteWrapperConfig[]; routePath?: string; matches?: RouteMatch[]; routes?: RouteItem[]; @@ -88,13 +97,14 @@ export interface AppContext { appExport?: AppExport; basename?: string; downgrade?: boolean; - renderMode?: string; + renderMode?: RenderMode; requestContext?: RequestContext; + revalidate?: boolean; } export type WindowContext = Pick< AppContext, - 'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' | 'renderMode' | 'serverData' + 'appData' | 'loaderData' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' | 'renderMode' | 'serverData' | 'revalidate' >; export type Renderer = ( @@ -113,27 +123,23 @@ export interface RequestContext extends ServerContext { query: Record; } -export interface RouteComponent { - default: ComponentType; +export type ComponentModule = { + default?: ComponentType; + Component?: ComponentType; staticDataLoader?: DataLoaderConfig; serverDataLoader?: DataLoaderConfig; dataLoader?: DataLoaderConfig; pageConfig?: PageConfig; [key: string]: any; -} +}; -export interface RouteItem { - id: string; - path: string; - element?: ReactNode; +export type RouteItem = AgnosticRouteObject & { componentName: string; - index?: boolean; - exact?: boolean; - strict?: boolean; - load?: () => Promise; - children?: RouteItem[]; + Component?: ComponentType; + exports?: string[]; layout?: boolean; -} + children?: RouteItem[]; +}; export type ComponentWithChildren

= ComponentType>; @@ -156,7 +162,7 @@ export type SetRender = (render: Renderer) => void; export type AddWrapper = (wrapper: RouteWrapper, forLayout?: boolean) => void; export interface RouteModules { - [routeId: string]: RouteComponent; + [routeId: string]: ComponentModule; } export interface AssetsManifest { @@ -215,12 +221,11 @@ export interface RuntimeModules { } export interface AppRouterProps { - action: Action; - location: Location; - navigator: Navigator; - routes: RouteObject[]; - static?: boolean; - basename?: string; + routes?: RouteObject[]; + routerContext?: any; + location?: Location; + Component?: ComponentType; + loaderData?: LoaderData; } export interface AppRouteProps { diff --git a/packages/runtime/tests/routes.test.tsx b/packages/runtime/tests/routes.test.tsx index ffd72c94a1..5012d87bcb 100644 --- a/packages/runtime/tests/routes.test.tsx +++ b/packages/runtime/tests/routes.test.tsx @@ -5,17 +5,13 @@ import React from 'react'; import { renderToString } from 'react-dom/server'; import { expect, it, describe, beforeEach, afterEach, vi } from 'vitest'; -import type { RouteComponent as IRouteComponent } from '../src/types'; -import RouteWrapper from '../src/RouteWrapper'; import { AppContextProvider } from '../src/AppContext'; import { - filterMatchesToLoad, - createRouteElements, RouteComponent, loadRouteModules, - loadRoutesData, - getRoutesConfig, + createRouteLoader, getRoutesPath, + WrapRouteComponent, } from '../src/routes.js'; describe('routes', () => { @@ -40,17 +36,33 @@ describe('routes', () => { default: () => <>, pageConfig: () => ({ title: 'about' }), }; + const homeLazyItem = { + Component: homeItem.default, + loader: createRouteLoader({ + routeId: 'home', + module: homeItem, + renderMode: 'CSR', + }), + }; + const aboutLazyItem = { + Component: aboutItem.default, + loader: createRouteLoader({ + routeId: 'about', + module: aboutItem, + renderMode: 'CSR', + }), + }; const routeModules = [ { id: 'home', - load: async () => { - return homeItem as IRouteComponent; + lazy: async () => { + return homeLazyItem; }, }, { id: 'about', - load: async () => { - return aboutItem as IRouteComponent; + lazy: async () => { + return aboutLazyItem; }, }, ]; @@ -61,7 +73,7 @@ describe('routes', () => {

home
, + Component: () =>
home
, }, }, }} @@ -84,12 +96,38 @@ describe('routes', () => { process.env.NODE_ENV = currentEnv; }); + it('route WrapRouteComponent', () => { + const domstring = renderToString( + +
wrapper{children}
, layout: false }] }} + > +
home
}} /> +
, + ); + expect(domstring).toBe('
wrapper
home
'); + }); + + it('route WrapRouteComponent match layout', () => { + const domstring = renderToString( + +
wrapper{children}
, layout: false }] }} + > +
home
}} /> +
, + ); + expect(domstring).toBe('
home
'); + }); + it('load route modules', async () => { windowSpy.mockImplementation(() => ({})); - const routeModule = await loadRouteModules(routeModules, { home: homeItem }); + const routeModule = await loadRouteModules(routeModules, {}); expect(routeModule).toStrictEqual({ - home: homeItem, - about: aboutItem, + home: homeLazyItem, + about: aboutLazyItem, }); }); @@ -97,7 +135,7 @@ describe('routes', () => { const routeModule = await loadRouteModules([{ id: 'error', // @ts-ignore - load: async () => { + lazy: async () => { throw new Error('err'); return {}; }, @@ -108,59 +146,53 @@ describe('routes', () => { }); it('load route data for SSG', async () => { - const routeModule = await loadRouteModules(routeModules); - const routesDataSSG = await loadRoutesData( - // @ts-ignore - [{ route: routeModules[0] }], - {}, - routeModule, - { - renderMode: 'SSG', - }, - ); + const routesDataSSG = await createRouteLoader({ + routeId: 'home', + module: homeItem, + renderMode: 'SSG', + })(); expect(routesDataSSG).toStrictEqual({ - home: { + data: { type: 'getStaticData', }, + pageConfig: { + title: 'home', + }, }); }); it('load route data for SSR', async () => { - const routeModule = await loadRouteModules(routeModules); - const routesDataSSR = await loadRoutesData( - // @ts-ignore - [{ route: routeModules[0] }], - {}, - routeModule, - { - renderMode: 'SSR', - }, - ); + const routesDataSSR = await createRouteLoader({ + routeId: 'home', + module: homeItem, + renderMode: 'SSR', + })(); expect(routesDataSSR).toStrictEqual({ - home: { + data: { type: 'getServerData', }, + pageConfig: { + title: 'home', + }, }); }); it('load route data for CSR', async () => { - const routeModule = await loadRouteModules(routeModules); - const routesDataCSR = await loadRoutesData( - // @ts-ignore - [{ route: routeModules[0] }], - {}, - routeModule, - { - renderMode: 'CSR', - }, - ); + const routesDataCSR = await createRouteLoader({ + routeId: 'home', + module: homeItem, + renderMode: 'CSR', + })(); expect(routesDataCSR).toStrictEqual({ - home: { + data: { type: 'getData', }, + pageConfig: { + title: 'home', + }, }); }); @@ -170,180 +202,35 @@ describe('routes', () => { getData: async (id) => ({ id: `${id}_data` }), }, })); - const routesData = await loadRoutesData( - // @ts-ignore - [{ route: routeModules[0] }], - {}, - {}, - { renderMode: 'SSG' }, - ); - expect(routesData).toStrictEqual({ - home: { + const routesDataCSR = await createRouteLoader({ + routeId: 'home', + module: homeItem, + renderMode: 'CSR', + })(); + + expect(routesDataCSR).toStrictEqual({ + data: { id: 'home_data', }, - }); - }); - - it('get routes config', async () => { - const routeModule = await loadRouteModules(routeModules); - const routesConfig = getRoutesConfig( - // @ts-ignore - [{ route: routeModules[0] }], - { home: {} }, - routeModule, - ); - expect(routesConfig).toStrictEqual({ - home: { + pageConfig: { title: 'home', }, }); }); - it('get routes config when failed get route module', async () => { - const routesConfig = getRoutesConfig( - // @ts-ignore - [{ route: routeModules[0] }], - { home: {} }, - {}, - ); - expect(routesConfig).toStrictEqual({ - home: {}, - }); - }); - - it('create route element', () => { - const routeElement = createRouteElements([{ - path: '/', - id: 'home', - componentName: 'home', - }]); - expect(routeElement).toEqual([{ - componentName: 'home', - element: ( - - - - ), - id: 'home', - path: '/', - }]); - }); - - it('create route with children', () => { - const routeElement = createRouteElements([{ - path: '/', - id: 'home', - componentName: 'home', - children: [{ - path: '/about', - id: 'about', - componentName: 'about', - }], - }]); - expect(routeElement).toEqual([{ - componentName: 'home', - element: ( - - - - ), - children: [{ - componentName: 'about', - element: ( - - - - ), - id: 'about', - path: '/about', - }], - id: 'home', - path: '/', - }]); - }); - - it('filter new matches', () => { - const oldMatches = [ - { - pathname: '/', - route: { - id: '/page/layout', - }, - }, - { - pathname: '/', - route: { - id: '/page/home', - }, - }, - ]; - - const newMatches = [ - { - pathname: '/', - route: { - id: '/page/layout', - }, - }, - { - pathname: '/about', - route: { - id: '/page/about', - }, - }, - ]; - - // @ts-ignore - const matches = filterMatchesToLoad(oldMatches, newMatches); - - expect( - matches, - ).toEqual([{ - pathname: '/about', - route: { - id: '/page/about', - }, - }]); - }); - - it('filter matches with path changed', () => { - const oldMatches = [ - { - pathname: '/users/123', - route: { - id: '/users/123', - }, - }, - ]; + it('get routes config without data', async () => { + const routesDataCSR = await createRouteLoader({ + routeId: 'about', + module: aboutItem, + renderMode: 'CSR', + })(); - const newMatches = [ - { - pathname: '/users/456', - route: { - id: '/users/456', - }, - }, - ]; - - // @ts-ignore - const matches = filterMatchesToLoad(oldMatches, newMatches); - - expect( - matches, - ).toEqual([ - { - pathname: '/users/456', - route: { - id: '/users/456', - }, + expect(routesDataCSR).toStrictEqual({ + data: undefined, + pageConfig: { + title: 'about', }, - ]); + }); }); it('get routes flatten path', () => { diff --git a/packages/runtime/tests/routesConfig.test.ts b/packages/runtime/tests/routesConfig.test.ts index 4344e1ac18..49c234e369 100644 --- a/packages/runtime/tests/routesConfig.test.ts +++ b/packages/runtime/tests/routesConfig.test.ts @@ -44,7 +44,7 @@ describe('routes config', () => { it('update routes config', async () => { const routesConfig = { - home: { + pageConfig: { title: 'home', meta: [ { @@ -61,12 +61,11 @@ describe('routes config', () => { }], }, }; - // @ts-ignore - await updateRoutesConfig([{ route: { id: 'home' } }], routesConfig); + await updateRoutesConfig(routesConfig); expect(insertTags.length).toBe(1); expect(insertTags[0]?.type).toBe('meta'); expect(appendTags.length).toBe(2); expect(appendTags[0]?.type).toBe('link'); expect(appendTags[1]?.type).toBe('script'); }); -}); \ No newline at end of file +}); diff --git a/packages/runtime/tests/runClientApp.test.tsx b/packages/runtime/tests/runClientApp.test.tsx index 04b68f002e..c2658b5471 100644 --- a/packages/runtime/tests/runClientApp.test.tsx +++ b/packages/runtime/tests/runClientApp.test.tsx @@ -5,19 +5,28 @@ import React from 'react'; import { renderToString } from 'react-dom/server'; import { expect, it, vi, describe, beforeEach, afterEach } from 'vitest'; -import runClientApp, { loadNextPage } from '../src/runClientApp'; -import { useAppData } from '../src/AppData'; -import { useConfig, useData } from '../src/RouteContext'; +import { fetch, Request, Response } from '@remix-run/web-fetch'; +import runClientApp from '../src/runClientApp'; +import { useAppData } from '../src/AppContext'; describe('run client app', () => { let windowSpy; let documentSpy; + if (!globalThis.fetch) { + // @ts-expect-error + globalThis.fetch = fetch; + // @ts-expect-error + globalThis.Request = Request; + // @ts-expect-error + globalThis.Response = Response; + } const mockData = { location: new URL('http://localhost:4000/'), history: { replaceState: vi.fn(), }, addEventListener: vi.fn(), + removeEventListener: vi.fn(), }; beforeEach(() => { process.env.ICE_CORE_ROUTER = 'true'; @@ -53,7 +62,9 @@ describe('run client app', () => { setRender((container, element) => { try { domstring = renderToString(element as any); - } catch (err) { } + } catch (err) { + domstring = ''; + } }); }; @@ -63,13 +74,6 @@ describe('run client app', () => { staticMsg = 'static'; }; - const wrapperRuntime = async ({ addWrapper }) => { - const RouteWrapper = ({ children }) => { - return
{children}
; - }; - addWrapper(RouteWrapper, true); - }; - const providerRuntmie = async ({ addProvider }) => { const Provider = ({ children }) => { return
{children}
; @@ -79,22 +83,27 @@ describe('run client app', () => { addProvider(Provider); }; + const homeItem = { + default: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const appData = useAppData(); + return ( +
home{appData?.msg || ''}
+ ); + }, + pageConfig: () => ({ title: 'home' }), + dataLoader: async () => ({ data: 'test' }), + }; const basicRoutes = [ { id: 'home', path: '/', componentName: 'Home', - load: async () => ({ - default: () => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const appData = useAppData(); - return ( -
home{appData?.msg || ''}
- ); - }, - pageConfig: () => ({ title: 'home' }), - dataLoader: async () => ({ data: 'test' }), - }), + lazy: () => { + return { + Component: homeItem.default, + }; + }, }, ]; @@ -105,9 +114,10 @@ describe('run client app', () => { return { msg: staticMsg }; }, }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime], statics: [staticRuntime] }, - hydrate: false, + hydrate: true, }); expect(domstring).toBe('
homestatic
'); }); @@ -117,10 +127,10 @@ describe('run client app', () => { ...mockData, location: new URL('http://localhost:4000/?test=1&runtime=true&baisc'), })); - await runClientApp({ app: {}, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); @@ -128,10 +138,23 @@ describe('run client app', () => { }); it('run client single-router', async () => { + const sigleRoutes = [ + { + id: 'home', + path: '/', + lazy: () => { + return { + Component: homeItem.default, + loader: () => {}, + }; + }, + }, + ]; process.env.ICE_CORE_ROUTER = ''; await runClientApp({ app: {}, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => sigleRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); @@ -139,48 +162,27 @@ describe('run client app', () => { expect(domstring).toBe('
home
'); }); - it('run client with wrapper', async () => { - await runClientApp({ - app: {}, - routes: basicRoutes, - runtimeModules: { commons: [serverRuntime, wrapperRuntime] }, - hydrate: true, - }); - expect(domstring).toBe('
home
'); - }); - it('run client with app provider', async () => { await runClientApp({ app: {}, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime, providerRuntmie] }, hydrate: true, }); expect(domstring).toBe('
home
'); }); - it('run client with empty route', async () => { - await runClientApp({ - app: {}, - routes: [], - runtimeModules: { commons: [serverRuntime] }, - hydrate: false, - }); - }); - it('run client with memory router', async () => { const routes = [...basicRoutes, { id: 'about', path: '/about', componentName: 'About', - load: async () => ({ - default: () => { - return ( -
about
- ); - }, - }), - }]; + Component: () => { + return ( +
about
+ ); + } }]; await runClientApp({ app: { default: { @@ -190,7 +192,8 @@ describe('run client app', () => { }, }, }, - routes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => routes, runtimeModules: { commons: [serverRuntime] }, hydrate: true, }); @@ -201,7 +204,8 @@ describe('run client app', () => { default: { }, }, - routes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: true, }); @@ -218,18 +222,16 @@ describe('run client app', () => { id: 'about', path: '/about', componentName: 'About', - load: async () => ({ - default: () => { - return ( -
about
- ); - }, - }), - }]; + Component: () => { + return ( +
about
+ ); + } }]; await runClientApp({ app: { }, - routes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => routes, runtimeModules: { commons: [serverRuntime] }, hydrate: true, memoryRouter: true, @@ -247,7 +249,8 @@ describe('run client app', () => { }, }, }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: true, }); @@ -263,7 +266,8 @@ describe('run client app', () => { return { msg: '-getAppData' }; }, }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); @@ -291,7 +295,8 @@ describe('run client app', () => { return { msg: 'app' }; }, }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); @@ -309,75 +314,11 @@ describe('run client app', () => { }, }, }, - routes: [{ - id: 'home', - path: '/', - componentName: 'Home', - load: async () => ({ - default: () => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const config = useConfig(); - // eslint-disable-next-line react-hooks/rules-of-hooks - const data = useData(); - return ( -
home{data?.data}{config.title}
- ); - }, - pageConfig: () => ({ title: 'home' }), - dataLoader: async () => ({ data: 'test' }), - }), - }], + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); - expect(domstring).toBe('
hometesthome
'); - }); - - it('load next page', async () => { - const indexPage = { - default: () => <>, - pageConfig: () => ({ title: 'index' }), - dataLoader: async () => ({ type: 'getDataIndex' }), - }; - const aboutPage = { - default: () => <>, - pageConfig: () => ({ title: 'about' }), - dataLoader: async () => ({ type: 'getDataAbout' }), - }; - const mockedModules = [ - { - id: 'index', - load: async () => { - return indexPage; - }, - }, - { - id: 'about', - load: async () => { - return aboutPage; - }, - }, - ]; - const { routesData, routesConfig, routeModules } = await loadNextPage( - // @ts-ignore - [{ route: mockedModules[0] }], - { - // @ts-ignore - matches: [{ route: mockedModules[1] }], - routesData: {}, - routeModules: {}, - }, - ); - expect(routesData).toStrictEqual({ - index: { type: 'getDataIndex' }, - }); - expect(routesConfig).toStrictEqual({ - index: { - title: 'index', - }, - }); - expect(routeModules).toStrictEqual({ - index: indexPage, - }); + expect(domstring).toBe('
home
'); }); }); diff --git a/packages/runtime/tests/runServerApp.test.tsx b/packages/runtime/tests/runServerApp.test.tsx index b5342d8f68..bfad032c4c 100644 --- a/packages/runtime/tests/runServerApp.test.tsx +++ b/packages/runtime/tests/runServerApp.test.tsx @@ -5,23 +5,32 @@ import React from 'react'; import { expect, it, describe } from 'vitest'; import { renderToHTML, renderToResponse } from '../src/runServerApp'; import { Meta, Title, Links, Main, Scripts } from '../src/Document'; +import { + createRouteLoader, +} from '../src/routes.js'; describe('run server app', () => { process.env.ICE_CORE_ROUTER = 'true'; + const homeItem = { + default: () =>
home
, + pageConfig: () => ({ title: 'home' }), + dataLoader: async () => ({ data: 'test' }), + }; const basicRoutes = [ { id: 'home', path: 'home', componentName: 'home', - load: async () => ({ - default: () => { - return ( -
home
- ); - }, - pageConfig: () => ({ title: 'home' }), - getData: async () => ({ data: 'test' }), - }), + lazy: () => { + return { + Component: homeItem.default, + loader: createRouteLoader({ + routeId: 'home', + module: homeItem, + renderMode: 'SSR', + }), + }; + }, }, ]; @@ -61,7 +70,8 @@ describe('run server app', () => { app: {}, assetsManifest, runtimeModules: { commons: [] }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, Document, renderMode: 'SSR', }); @@ -81,7 +91,8 @@ describe('run server app', () => { app: {}, assetsManifest, runtimeModules: { commons: [] }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, Document, renderMode: 'SSR', basename: '/ice', @@ -100,7 +111,8 @@ describe('run server app', () => { app: {}, assetsManifest, runtimeModules: { commons: [] }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, Document, renderMode: 'SSR', serverOnlyBasename: '/', @@ -120,7 +132,8 @@ describe('run server app', () => { app: {}, assetsManifest, runtimeModules: { commons: [] }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, Document, }); // @ts-ignore @@ -143,7 +156,8 @@ describe('run server app', () => { }, assetsManifest, runtimeModules: { commons: [] }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, Document, }); // @ts-ignore @@ -162,18 +176,24 @@ describe('run server app', () => { app: {}, assetsManifest, runtimeModules: { commons: [] }, - routes: [{ + // @ts-ignore don't need to pass params in test case. + createRoutes: () => [{ id: 'home', path: 'home', componentName: 'Home', - load: async () => ({ - default: () => { - throw new Error('err'); - return ( -
home
- ); - }, - }), + lazy: () => { + return { + Component: () => { + throw new Error('err'); + return ( +
home
+ ); + }, + loader: () => { + return { data: {}, pageConfig: {} }; + }, + }; + }, }], Document, }); @@ -201,7 +221,8 @@ describe('run server app', () => { app: {}, assetsManifest, runtimeModules: { commons: [] }, - routes: basicRoutes, + // @ts-ignore don't need to pass params in test case. + createRoutes: () => basicRoutes, Document, renderMode: 'SSR', routePath: '/', @@ -210,4 +231,4 @@ describe('run server app', () => { expect(!!htmlContent).toBe(true); expect(htmlContent.includes('
home { it('useRoutes', () => { @@ -64,4 +64,4 @@ describe('single route api', () => { it('useNavigate', () => { expect(useNavigate()).toStrictEqual({}); }); -}); \ No newline at end of file +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a623bad22..1dcaf5c4f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1021,7 +1021,7 @@ importers: open: ^8.4.0 path-to-regexp: ^6.2.0 react: ^18.2.0 - react-router: ^6.8.2 + react-router: 6.10.0 regenerator-runtime: ^0.13.0 resolve.exports: ^1.1.0 sass: ^1.50.0 @@ -1079,7 +1079,7 @@ importers: esbuild: 0.16.17 jest: 29.5.0 react: 18.2.0 - react-router: 6.8.2_react@18.2.0 + react-router: 6.10.0_react@18.2.0 sass: 1.58.3 unplugin: 0.9.6 webpack: 5.76.2_esbuild@0.16.17 @@ -1288,7 +1288,7 @@ importers: webpack: ^5.76.2 webpack-dev-server: ^4.9.2 dependencies: - '@remix-run/router': 1.3.3 + '@remix-run/router': 1.5.0 chalk: 4.1.2 consola: 2.15.3 htmlparser2: 8.0.1 @@ -1410,6 +1410,8 @@ importers: packages/runtime: specifiers: '@ice/jsx-runtime': ^0.2.0 + '@remix-run/router': 1.5.0 + '@remix-run/web-fetch': ^4.3.3 '@types/react': ^18.0.8 '@types/react-dom': ^18.0.3 ejs: ^3.1.6 @@ -1418,16 +1420,18 @@ importers: htmlparser2: ^8.0.1 react: ^18.0.0 react-dom: ^18.0.0 - react-router-dom: ^6.8.2 + react-router-dom: 6.10.0 regenerator-runtime: ^0.13.9 dependencies: '@ice/jsx-runtime': link:../jsx-runtime + '@remix-run/router': 1.5.0 ejs: 3.1.8 fs-extra: 10.1.0 history: 5.3.0 htmlparser2: 8.0.1 - react-router-dom: 6.8.2_biqbaboplfbrettd7655fr4n2y + react-router-dom: 6.10.0_biqbaboplfbrettd7655fr4n2y devDependencies: + '@remix-run/web-fetch': 4.3.3 '@types/react': 18.0.28 '@types/react-dom': 18.0.11 react: 18.2.0 @@ -6038,10 +6042,42 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false - /@remix-run/router/1.3.3: - resolution: {integrity: sha512-YRHie1yQEj0kqqCTCJEfHqYSSNlZQ696QJG+MMiW4mxSl9I0ojz/eRhJS4fs88Z5i6D1SmoF9d3K99/QOhI8/w==} + /@remix-run/router/1.5.0: + resolution: {integrity: sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==} engines: {node: '>=14'} + /@remix-run/web-blob/3.0.4: + resolution: {integrity: sha512-AfegzZvSSDc+LwnXV+SwROTrDtoLiPxeFW+jxgvtDAnkuCX1rrzmVJ6CzqZ1Ai0bVfmJadkG5GxtAfYclpPmgw==} + dependencies: + '@remix-run/web-stream': 1.0.3 + web-encoding: 1.1.5 + dev: true + + /@remix-run/web-fetch/4.3.3: + resolution: {integrity: sha512-DK9vA2tgsadcFPpxW4fvN198tiWpyPhwR0EYOuM4QjpDCz0G619c9RDMdyMy6a7Qb/jwiyx9SOPHWc65QAl+1g==} + engines: {node: ^10.17 || >=12.3} + dependencies: + '@remix-run/web-blob': 3.0.4 + '@remix-run/web-form-data': 3.0.4 + '@remix-run/web-stream': 1.0.3 + '@web3-storage/multipart-parser': 1.0.0 + abort-controller: 3.0.0 + data-uri-to-buffer: 3.0.1 + mrmime: 1.0.1 + dev: true + + /@remix-run/web-form-data/3.0.4: + resolution: {integrity: sha512-UMF1jg9Vu9CLOf8iHBdY74Mm3PUvMW8G/XZRJE56SxKaOFWGSWlfxfG+/a3boAgHFLTkP7K4H1PxlRugy1iQtw==} + dependencies: + web-encoding: 1.1.5 + dev: true + + /@remix-run/web-stream/1.0.3: + resolution: {integrity: sha512-wlezlJaA5NF6SsNMiwQnnAW6tnPzQ5I8qk0Y0pSohm0eHKa2FQ1QhEKLVVcDDu02TmkfHgnux0igNfeYhDOXiA==} + dependencies: + web-streams-polyfill: 3.2.1 + dev: true + /@rollup/plugin-alias/3.1.9_rollup@2.79.1: resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==} engines: {node: '>=8.0.0'} @@ -7254,6 +7290,10 @@ packages: - terser dev: true + /@web3-storage/multipart-parser/1.0.0: + resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + dev: true + /@webassemblyjs/ast/1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -7351,6 +7391,12 @@ packages: /@xtuc/long/4.2.2: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + /@zxing/text-encoding/0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: true + optional: true + /JSONStream/1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -7363,6 +7409,13 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true + /abort-controller/3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + /accepts/1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -9597,6 +9650,11 @@ packages: assert-plus: 1.0.0 dev: false + /data-uri-to-buffer/3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + dev: true + /data-urls/3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -11095,6 +11153,11 @@ packages: '@types/node': 17.0.45 require-like: 0.1.2 + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + /eventemitter3/4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -17775,17 +17838,17 @@ packages: tiny-invariant: 1.3.1 tiny-warning: 1.0.3 - /react-router-dom/6.8.2_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-N/oAF1Shd7g4tWy+75IIufCGsHBqT74tnzHQhbiUTYILYF0Blk65cg+HPZqwC+6SqEyx033nKqU7by38v3lBZg==} + /react-router-dom/6.10.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==} engines: {node: '>=14'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.3.3 + '@remix-run/router': 1.5.0 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - react-router: 6.8.2_react@18.2.0 + react-router: 6.10.0_react@18.2.0 dev: false /react-router/5.3.4_react@17.0.2: @@ -17804,13 +17867,13 @@ packages: tiny-invariant: 1.3.1 tiny-warning: 1.0.3 - /react-router/6.8.2_react@18.2.0: - resolution: {integrity: sha512-lF7S0UmXI5Pd8bmHvMdPKI4u4S5McxmHnzJhrYi9ZQ6wE+DA8JN5BzVC5EEBuduWWDaiJ8u6YhVOCmThBli+rw==} + /react-router/6.10.0_react@18.2.0: + resolution: {integrity: sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==} engines: {node: '>=14'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.3.3 + '@remix-run/router': 1.5.0 react: 18.2.0 /react-textarea-autosize/8.4.0_h7fc2el62uaa77gho3xhys6ola: @@ -20583,9 +20646,22 @@ packages: dependencies: defaults: 1.0.4 + /web-encoding/1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: true + /web-namespaces/1.1.4: resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} + /web-streams-polyfill/3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + dev: true + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} diff --git a/tests/integration/with-antd-mobile.test.ts b/tests/integration/with-antd-mobile.test.ts index 548f01c356..8c4d70e409 100644 --- a/tests/integration/with-antd-mobile.test.ts +++ b/tests/integration/with-antd-mobile.test.ts @@ -11,10 +11,9 @@ describe(`build ${example}`, () => { test('open /', async () => { await buildFixture(example); - const res = await setupBrowser({ example, disableJS: false }); + const res = await setupBrowser({ example }); page = res.page; browser = res.browser; - await page.waitForFunction('document.getElementsByTagName(\'h2\').length > 0'); expect(await page.$$text('h2')).toStrictEqual(['Counter']); });