Compare commits

...

12 Commits

Author SHA1 Message Date
ClarkXia 8ced7adf79
Merge 8bc00a2115 into 2742ac4678 2025-09-26 14:24:36 +08:00
水澜 2742ac4678
fix: duplicate css (#7137)
CI / build (16.x, ubuntu-latest) (push) Has been cancelled Details
CI / build (16.x, windows-latest) (push) Has been cancelled Details
CI / build (18.x, ubuntu-latest) (push) Has been cancelled Details
CI / build (18.x, windows-latest) (push) Has been cancelled Details
Coverage / coverage (16.x) (push) Has been cancelled Details
Release / Release (16) (push) Has been cancelled Details
* fix: duplicate css

* chore: add changelog

* fix: lint
2025-09-25 16:44:37 +08:00
AmAzing- fcc25dc3fd
docs: update contributors chart (#7139) 2025-09-11 12:16:55 +08:00
ClarkXia 8e27933423
feat: add SuspenseWrappers to Runtime (#7131) (#7134)
* feat: add SuspenseWrappers to Runtime (#7131)

* feat: add SuspenseWrappers to Runtime

* chore(runtime): format code

* fix(runtime): Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(runtime): remove redundant composeSuspenseWrappers function

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: update versions (#7133)

* chore: update versions

* Remove 3.6.6 entry from CHANGELOG

Removed version 3.6.6 entry from CHANGELOG.

* Update package.json

* Update CHANGELOG.md

* Downgrade version from 1.1.7 to 1.1.6

* Remove changelog entry for version 1.2.7

Removed version 1.2.7 entry from changelog.

* Update package.json

* Update CHANGELOG.md

* Update package.json

* Update package.json

* Update package.json

* Update pnpm-lock.yaml

---------

Co-authored-by: Mixiu <112144929+riopop@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-27 16:05:42 +08:00
ClarkXia 8bc00a2115 Merge branch 'release/next' into feat/dynamic-compile 2024-10-30 15:21:25 +08:00
ClarkXia b03194e046 fix: the value of commandArgs 2024-09-02 17:23:16 +08:00
ClarkXia 89fec6198d fix: add default request path 2024-09-02 16:02:39 +08:00
ClarkXia cb35145ff3 fix: only support lazycompile in dev mode 2024-09-02 15:30:19 +08:00
ClarkXia 3280dbf433 fix: optimize lazy compile 2024-09-02 15:10:40 +08:00
ClarkXia 7fdb3ae845 chore: changeset 2024-09-02 14:30:20 +08:00
ClarkXia f048b31327 fix: undefined config 2024-08-27 16:37:21 +08:00
ClarkXia a5d13dcfd3 feat: support lazy compile of routes 2024-08-27 16:10:33 +08:00
23 changed files with 221 additions and 23 deletions

View File

@ -0,0 +1,5 @@
---
'@ice/runtime': patch
---
fix: duplicate css

View File

@ -0,0 +1,5 @@
---
'@ice/app': patch
---
feat: support lazy compile of routes

View File

@ -48,7 +48,9 @@ Please see our [CONTRIBUTING.md](/.github/CONTRIBUTING.md)
Contributors can contact us to join the Contributor Group. Contributors can contact us to join the Contributor Group.
<a href="https://github.com/alibaba/ice/graphs/contributors"><img src="https://alibaba.github.io/ice/ice.png" /></a> <a href="https://openomy.com/alibaba/ice" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.com/svg?repo=alibaba/ice&chart=bubble&latestMonth=6" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
## Community ## Community

View File

@ -6,9 +6,11 @@ import type { Config } from '@ice/shared-config/types';
import createMockMiddleware from '../../middlewares/mock/createMiddleware.js'; import createMockMiddleware from '../../middlewares/mock/createMiddleware.js';
import createRenderMiddleware from '../../middlewares/renderMiddleware.js'; import createRenderMiddleware from '../../middlewares/renderMiddleware.js';
import createDataLoaderMiddleware from '../../middlewares/dataLoaderMiddleware.js'; import createDataLoaderMiddleware from '../../middlewares/dataLoaderMiddleware.js';
import createProxyModuleMiddleware from '../../middlewares/proxyModuleMiddleware.js';
import type { UserConfig } from '../../types/userConfig.js'; import type { UserConfig } from '../../types/userConfig.js';
import type RouteManifest from '../../utils/routeManifest.js'; import type RouteManifest from '../../utils/routeManifest.js';
import type { GetAppConfig } from '../../types/plugin.js'; import type { GetAppConfig } from '../../types/plugin.js';
import type Generator from '../../service/runtimeGenerator.js';
interface SetupOptions { interface SetupOptions {
userConfig: UserConfig; userConfig: UserConfig;
@ -18,7 +20,9 @@ interface SetupOptions {
excuteServerEntry: () => Promise<any>; excuteServerEntry: () => Promise<any>;
mock: boolean; mock: boolean;
rootDir: string; rootDir: string;
open?: boolean | string;
dataLoaderCompiler?: Compiler; dataLoaderCompiler?: Compiler;
generator?: Generator;
} }
function setupMiddlewares(middlewares: Parameters<DevServerConfiguration['setupMiddlewares']>[0], { function setupMiddlewares(middlewares: Parameters<DevServerConfiguration['setupMiddlewares']>[0], {
@ -30,8 +34,10 @@ function setupMiddlewares(middlewares: Parameters<DevServerConfiguration['setupM
mock, mock,
rootDir, rootDir,
dataLoaderCompiler, dataLoaderCompiler,
generator,
open,
}: SetupOptions) { }: SetupOptions) {
const { ssr, ssg } = userConfig; const { ssr, ssg, routes } = userConfig;
let renderMode: RenderMode; let renderMode: RenderMode;
// If ssr is set to true, use ssr for preview. // If ssr is set to true, use ssr for preview.
if (ssr) { if (ssr) {
@ -56,6 +62,21 @@ function setupMiddlewares(middlewares: Parameters<DevServerConfiguration['setupM
middlewares.unshift(dataLoaderMiddleware); middlewares.unshift(dataLoaderMiddleware);
} }
if (routes?.lazyCompile) {
const proxyModuleMiddleware = createProxyModuleMiddleware({
manifest: routeManifest.getNestedRoute(),
rootDir,
generator,
defaultPath: typeof open === 'string' ? open : '/',
});
// @ts-ignore property of name is exist.
const staticIndex = middlewares.findIndex(({ name }) => name === 'express-static');
middlewares.splice(
staticIndex, 0,
proxyModuleMiddleware,
);
}
// @ts-ignore property of name is exist. // @ts-ignore property of name is exist.
const insertIndex = middlewares.findIndex(({ name }) => name === 'serve-index'); const insertIndex = middlewares.findIndex(({ name }) => name === 'serve-index');
middlewares.splice( middlewares.splice(

View File

@ -18,6 +18,7 @@ async function bundler(
routeManifest, routeManifest,
appConfig, appConfig,
hasDataLoader, hasDataLoader,
generator,
} = options; } = options;
let compiler: MultiCompiler; let compiler: MultiCompiler;
let dataLoaderCompiler: Compiler; let dataLoaderCompiler: Compiler;
@ -63,6 +64,7 @@ async function bundler(
hooksAPI, hooksAPI,
taskConfigs, taskConfigs,
rspackConfigs, rspackConfigs,
generator,
}; };
if (command === 'start') { if (command === 'start') {
// @ts-expect-error dev-server has been pre-packed, so it will have different type. // @ts-expect-error dev-server has been pre-packed, so it will have different type.

View File

@ -16,6 +16,7 @@ const start = async ({
compiler, compiler,
appConfig, appConfig,
hooksAPI, hooksAPI,
generator,
}: BuildOptions, dataLoaderCompiler?: Compiler) => { }: BuildOptions, dataLoaderCompiler?: Compiler) => {
const { rootDir, applyHook, commandArgs, userConfig, extendsPluginAPI: { excuteServerEntry } } = context; const { rootDir, applyHook, commandArgs, userConfig, extendsPluginAPI: { excuteServerEntry } } = context;
const customMiddlewares = rspackConfigs[0].devServer?.setupMiddlewares; const customMiddlewares = rspackConfigs[0].devServer?.setupMiddlewares;
@ -32,8 +33,10 @@ const start = async ({
taskConfig: webTaskConfig, taskConfig: webTaskConfig,
excuteServerEntry, excuteServerEntry,
mock: commandArgs.mock, mock: commandArgs.mock,
open: commandArgs.open,
rootDir, rootDir,
dataLoaderCompiler, dataLoaderCompiler,
generator,
}); });
return customMiddlewares ? customMiddlewares(builtInMiddlewares, devServer) : builtInMiddlewares; return customMiddlewares ? customMiddlewares(builtInMiddlewares, devServer) : builtInMiddlewares;
}, },

View File

@ -8,6 +8,7 @@ import type { ServerCompiler, GetAppConfig, GetRoutesConfig, GetDataloaderConfig
import type { UserConfig } from '../types/userConfig.js'; import type { UserConfig } from '../types/userConfig.js';
import type RouteManifest from '../utils/routeManifest.js'; import type RouteManifest from '../utils/routeManifest.js';
import type ServerRunner from '../service/ServerRunner.js'; import type ServerRunner from '../service/ServerRunner.js';
import type Generator from '../service/runtimeGenerator.js';
export type Context = DefaultContext<Config, ExtendsPluginAPI>; export type Context = DefaultContext<Config, ExtendsPluginAPI>;
@ -19,9 +20,11 @@ export interface BuildOptions {
appConfig: BundlerOptions['appConfig']; appConfig: BundlerOptions['appConfig'];
hooksAPI: BundlerOptions['hooksAPI']; hooksAPI: BundlerOptions['hooksAPI'];
taskConfigs: BundlerOptions['taskConfigs']; taskConfigs: BundlerOptions['taskConfigs'];
generator: Generator;
} }
export interface BundlerOptions { export interface BundlerOptions {
generator: Generator;
taskConfigs: TaskConfig<Config>[]; taskConfigs: TaskConfig<Config>[];
spinner: ora.Ora; spinner: ora.Ora;
hooksAPI: { hooksAPI: {

View File

@ -27,6 +27,7 @@ export async function startDevServer(
routeManifest, routeManifest,
userConfig, userConfig,
appConfig, appConfig,
generator,
} = options; } = options;
const routePaths = routeManifest.getFlattenRoute().sort((a, b) => const routePaths = routeManifest.getFlattenRoute().sort((a, b) =>
// Sort by length, shortest path first. // Sort by length, shortest path first.
@ -40,12 +41,14 @@ export async function startDevServer(
...defaultDevServerConfig, ...defaultDevServerConfig,
setupMiddlewares: (middlewares, devServer) => { setupMiddlewares: (middlewares, devServer) => {
const builtInMiddlewares = getMiddlewares(middlewares, { const builtInMiddlewares = getMiddlewares(middlewares, {
generator,
userConfig, userConfig,
routeManifest, routeManifest,
getAppConfig: hooksAPI.getAppConfig, getAppConfig: hooksAPI.getAppConfig,
taskConfig: webTaskConfig, taskConfig: webTaskConfig,
excuteServerEntry, excuteServerEntry,
mock: commandArgs.mock, mock: commandArgs.mock,
open: commandArgs.open,
rootDir, rootDir,
}); });
return customMiddlewares ? customMiddlewares(builtInMiddlewares, devServer) : builtInMiddlewares; return customMiddlewares ? customMiddlewares(builtInMiddlewares, devServer) : builtInMiddlewares;

View File

@ -40,6 +40,7 @@ import rspackBundler from './bundler/rspack/index.js';
import getDefaultTaskConfig from './plugins/task.js'; import getDefaultTaskConfig from './plugins/task.js';
import { multipleServerEntry, renderMultiEntry } from './utils/multipleEntry.js'; import { multipleServerEntry, renderMultiEntry } from './utils/multipleEntry.js';
import hasDocument from './utils/hasDocument.js'; import hasDocument from './utils/hasDocument.js';
import { addLeadingSlash } from './utils/slash.js';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -66,6 +67,10 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
templates: [coreTemplate], templates: [coreTemplate],
}); });
if (commandArgs.open) {
commandArgs.open = typeof commandArgs.open === 'string' ? addLeadingSlash(commandArgs.open) : commandArgs.open;
}
const { addWatchEvent, removeWatchEvent } = createWatch({ const { addWatchEvent, removeWatchEvent } = createWatch({
watchDir: rootDir, watchDir: rootDir,
command, command,
@ -221,6 +226,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const { userConfig } = ctx; const { userConfig } = ctx;
const { routes: routesConfig, server, syntaxFeatures, polyfill } = userConfig; const { routes: routesConfig, server, syntaxFeatures, polyfill } = userConfig;
const coreEnvKeys = getCoreEnvKeys(); const coreEnvKeys = getCoreEnvKeys();
const routesInfo = await generateRoutesInfo(rootDir, routesConfig, routeManifest.getRoutesDefinitions()); const routesInfo = await generateRoutesInfo(rootDir, routesConfig, routeManifest.getRoutesDefinitions());
@ -252,6 +258,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const { routeImports, routeDefinition } = getRoutesDefinition({ const { routeImports, routeDefinition } = getRoutesDefinition({
manifest: routesInfo.routes, manifest: routesInfo.routes,
lazy, lazy,
compileRoutes: routesConfig?.lazyCompile && command === 'start' ? [commandArgs.open || '/'] : undefined,
}); });
const loaderExports = hasExportAppData || Boolean(routesInfo.loaders); const loaderExports = hasExportAppData || Boolean(routesInfo.loaders);
const hasDataLoader = Boolean(userConfig.dataLoader) && loaderExports; const hasDataLoader = Boolean(userConfig.dataLoader) && loaderExports;
@ -301,6 +308,9 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
generator.addRenderFile('core/entry.server.ts.ejs', FALLBACK_ENTRY, { hydrate: false }); generator.addRenderFile('core/entry.server.ts.ejs', FALLBACK_ENTRY, { hydrate: false });
} }
if (routesConfig?.lazyCompile && command === 'start') {
generator.addRenderFile('core/empty.tsx.ejs', 'empty.tsx');
}
if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) { if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) {
const { const {
packageName, packageName,
@ -401,6 +411,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
userConfig, userConfig,
configFile, configFile,
hasDataLoader, hasDataLoader,
generator,
}; };
try { try {
if (command === 'test') { if (command === 'test') {

View File

@ -0,0 +1,54 @@
import path from 'path';
import { fileURLToPath } from 'url';
import type { ExpressRequestHandler, Middleware } from 'webpack-dev-server';
import type { NestedRouteManifest } from '@ice/route-manifest';
import { getRoutesDefinition } from '../routes.js';
import { RUNTIME_TMP_DIR } from '../constant.js';
import type Generator from '../service/runtimeGenerator.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
interface Options {
manifest: NestedRouteManifest[];
generator: Generator;
rootDir: string;
defaultPath: string;
}
export default function createRenderMiddleware(options: Options): Middleware {
const {
manifest,
generator,
rootDir,
defaultPath,
} = options;
const accessedPath = new Set<string>(defaultPath);
const middleware: ExpressRequestHandler = async function (req, res, next) {
if (req.path === '/proxy-module') {
if (!accessedPath.has(req.query.pathname)) {
accessedPath.add(req.query.pathname);
const { routeImports, routeDefinition } = getRoutesDefinition({
manifest,
lazy: true,
compileRoutes: Array.from(accessedPath),
});
const templateDir = path.join(__dirname, '../../templates/core/');
generator.renderFile(
path.join(templateDir, 'routes.tsx.ejs'),
path.join(rootDir, RUNTIME_TMP_DIR, 'routes.tsx'),
{ routeImports, routeDefinition },
);
}
res.send('');
} else {
next();
}
};
return {
name: 'proxy-module',
middleware,
};
}

View File

@ -69,10 +69,11 @@ interface GetDefinationOptions {
lazy?: boolean; lazy?: boolean;
depth?: number; depth?: number;
matchRoute?: (route: NestedRouteManifest) => boolean; matchRoute?: (route: NestedRouteManifest) => boolean;
compileRoutes?: string[];
} }
export function getRoutesDefinition(options: GetDefinationOptions) { export function getRoutesDefinition(options: GetDefinationOptions) {
const { manifest, lazy = false, depth = 0, matchRoute = () => true } = options; const { manifest, lazy = false, depth = 0, matchRoute = () => true, compileRoutes } = options;
const routeImports: string[] = []; const routeImports: string[] = [];
const routeDefinition = manifest.reduce((prev, route) => { const routeDefinition = manifest.reduce((prev, route) => {
if (!matchRoute(route)) { if (!matchRoute(route)) {
@ -80,10 +81,13 @@ export function getRoutesDefinition(options: GetDefinationOptions) {
} }
const { children, path: routePath, index, componentName, file, id, layout, exports } = route; const { children, path: routePath, index, componentName, file, id, layout, exports } = route;
const componentPath = id.startsWith('__') ? file : getFilePath(file); const componentPath = id.startsWith('__') ? file : getFilePath(file);
const proxyModule = './empty';
let loadStatement = ''; let loadStatement = '';
if (lazy) { if (lazy) {
loadStatement = `import(/* webpackChunkName: "p_${componentName}" */ '${formatPath(componentPath)}')`; const filePath = compileRoutes && !layout
? (compileRoutes.includes(`/${routePath || ''}`) ? componentPath : proxyModule)
: componentPath;
loadStatement = `import(/* webpackChunkName: "p_${componentName}" */ '${formatPath(filePath)}')`;
} else { } else {
const routeSpecifier = formatRouteSpecifier(id); const routeSpecifier = formatRouteSpecifier(id);
routeImports.push(`import * as ${routeSpecifier} from '${formatPath(componentPath)}';`); routeImports.push(`import * as ${routeSpecifier} from '${formatPath(componentPath)}';`);
@ -128,6 +132,7 @@ export function getRoutesDefinition(options: GetDefinationOptions) {
lazy, lazy,
depth: depth + 1, depth: depth + 1,
matchRoute, matchRoute,
compileRoutes,
}); });
routeImports.push(...res.routeImports); routeImports.push(...res.routeImports);
routeProperties.push(`children: [${res.routeDefinition}]`); routeProperties.push(`children: [${res.routeDefinition}]`);

View File

@ -167,6 +167,10 @@ export interface UserConfig {
* inject initial route path for each route html. * inject initial route path for each route html.
*/ */
injectInitialEntry?: boolean; injectInitialEntry?: boolean;
/**
* Enable lazy compile for routes.
*/
lazyCompile?: boolean;
}; };
/** /**
* Add ice.js plugin to customize framework config. * Add ice.js plugin to customize framework config.

View File

@ -0,0 +1,3 @@
export const addLeadingSlash = (path: string) => {
return path.charAt(0) === '/' ? path : `/${path}`;
};

View File

@ -0,0 +1,12 @@
import { useEffect } from 'react';
import { useLocation } from 'ice';
const ProxyModule = () => {
const location = useLocation();
useEffect(() => {
fetch(`/proxy-module?pathname=${location.pathname}`);
}, []);
return <></>;
};
export default ProxyModule;

View File

@ -57,7 +57,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ice/app": "^3.6.4", "@ice/app": "^3.6.4",
"@ice/runtime": "^1.5.6" "@ice/runtime": "^1.5.7"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -51,7 +51,7 @@
}, },
"devDependencies": { "devDependencies": {
"@ice/app": "^3.6.4", "@ice/app": "^3.6.4",
"@ice/runtime": "^1.5.6", "@ice/runtime": "^1.5.7",
"webpack": "^5.88.0" "webpack": "^5.88.0"
}, },
"repository": { "repository": {

View File

@ -1,5 +1,11 @@
# @ice/runtime # @ice/runtime
## 1.5.7
### Patch Changes
- 4ff29969c: feat: add SuspenseWrappers to Runtime
## 1.5.6 ## 1.5.6
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@ice/runtime", "name": "@ice/runtime",
"version": "1.5.6", "version": "1.5.7",
"description": "Runtime module for ice.js", "description": "Runtime module for ice.js",
"type": "module", "type": "module",
"types": "./esm/index.d.ts", "types": "./esm/index.d.ts",

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import type { WindowContext, RouteMatch, AssetsManifest } from './types.js';
import { useAppContext, useAppData } from './AppContext.js'; import { useAppContext, useAppData } from './AppContext.js';
import { getMeta, getTitle, getLinks, getScripts } from './routesConfig.js'; import { getLinks, getMeta, getScripts, getTitle } from './routesConfig.js';
import type { AssetsManifest, RouteMatch, WindowContext } from './types.js';
import getCurrentRoutePath from './utils/getCurrentRoutePath.js'; import getCurrentRoutePath from './utils/getCurrentRoutePath.js';
interface DocumentContext { interface DocumentContext {
@ -81,7 +81,15 @@ export const Links: LinksType = (props: LinksProps) => {
const routeLinks = getLinks(matches, loaderData); const routeLinks = getLinks(matches, loaderData);
const pageAssets = getPageAssets(matches, assetsManifest); const pageAssets = getPageAssets(matches, assetsManifest);
const entryAssets = getEntryAssets(assetsManifest); const entryAssets = getEntryAssets(assetsManifest);
const styles = entryAssets.concat(pageAssets).filter(path => path.indexOf('.css') > -1); let styles = entryAssets.concat(pageAssets).filter(path => path.indexOf('.css') > -1);
// Unique styles for duplicate CSS files.
const cssSet = {};
styles = styles.filter((style) => {
if (cssSet[style]) return false;
cssSet[style] = true;
return true;
});
return ( return (
<> <>

View File

@ -135,7 +135,8 @@ export function withSuspense(Component) {
return (props: SuspenseProps) => { return (props: SuspenseProps) => {
const { fallback, id, ...componentProps } = props; const { fallback, id, ...componentProps } = props;
const [suspenseState, updateSuspenseData] = React.useState({
const [suspenseState, updateSuspenseData] = React.useState<SuspenseState>({
id: id, id: id,
data: null, data: null,
done: false, done: false,
@ -156,24 +157,47 @@ export function withSuspense(Component) {
updateSuspenseData(newState); updateSuspenseData(newState);
} }
return (
<React.Suspense fallback={fallback || null}> // Get SuspenseWrappers from app context
const { SuspenseWrappers = [] } = useAppContext();
// Compose SuspenseWrappers
const composeSuspenseWrappers = React.useCallback(
(children: React.ReactNode) => {
if (!SuspenseWrappers.length) return children;
return SuspenseWrappers.reduce((WrappedComponent, wrapperConfig) => {
const { Wrapper } = wrapperConfig;
return <Wrapper id={id}>{WrappedComponent}</Wrapper>;
}, children);
},
[SuspenseWrappers, id],
);
const wrappedComponent = (
<>
<InlineScript <InlineScript
id={`suspense-parse-start-${id}`} id={`suspense-parse-start-${id}`}
script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-start','${id}');`} script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-start','${id}');`}
/> />
<SuspenseContext.Provider value={suspenseState}> <Component {...componentProps} />
<Component {...componentProps} /> <InlineScript
<InlineScript id={`suspense-parse-data-${id}`}
id={`suspense-parse-data-${id}`} script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-data','${id}');`}
script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-data','${id}');`} />
/> <Data id={id} />
<Data id={id} />
</SuspenseContext.Provider>
<InlineScript <InlineScript
id={`suspense-parse-end-${id}`} id={`suspense-parse-end-${id}`}
script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-end','${id}');`} script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-end','${id}');`}
/> />
</>
);
return (
<React.Suspense fallback={fallback || null}>
<SuspenseContext.Provider value={suspenseState}>
{composeSuspenseWrappers(wrappedComponent)}
</SuspenseContext.Provider>
</React.Suspense> </React.Suspense>
); );
}; };

View File

@ -12,7 +12,9 @@ import type {
SetAppRouter, SetAppRouter,
AddProvider, AddProvider,
AddWrapper, AddWrapper,
AddSuspenseWrapper,
RouteWrapperConfig, RouteWrapperConfig,
SuspenseWrapperConfig,
SetRender, SetRender,
AppRouterProps, AppRouterProps,
ComponentWithChildren, ComponentWithChildren,
@ -33,6 +35,8 @@ class Runtime {
private RouteWrappers: RouteWrapperConfig[]; private RouteWrappers: RouteWrapperConfig[];
private SuspenseWrappers: SuspenseWrapperConfig[];
private render: Renderer; private render: Renderer;
private responseHandlers: ResponseHandler[]; private responseHandlers: ResponseHandler[];
@ -46,6 +50,7 @@ class Runtime {
return root; return root;
}; };
this.RouteWrappers = []; this.RouteWrappers = [];
this.SuspenseWrappers = [];
this.runtimeOptions = runtimeOptions; this.runtimeOptions = runtimeOptions;
this.responseHandlers = []; this.responseHandlers = [];
this.getAppRouter = this.getAppRouter.bind(this); this.getAppRouter = this.getAppRouter.bind(this);
@ -55,6 +60,7 @@ class Runtime {
return { return {
...this.appContext, ...this.appContext,
RouteWrappers: this.RouteWrappers, RouteWrappers: this.RouteWrappers,
SuspenseWrappers: this.SuspenseWrappers,
}; };
}; };
@ -72,6 +78,8 @@ class Runtime {
public getWrappers = () => this.RouteWrappers; public getWrappers = () => this.RouteWrappers;
public getSuspenseWrappers = () => this.SuspenseWrappers;
public loadModule(module: RuntimePlugin | StaticRuntimePlugin | CommonJsRuntime) { public loadModule(module: RuntimePlugin | StaticRuntimePlugin | CommonJsRuntime) {
let runtimeAPI: RuntimeAPI = { let runtimeAPI: RuntimeAPI = {
addProvider: this.addProvider, addProvider: this.addProvider,
@ -80,6 +88,7 @@ class Runtime {
getAppRouter: this.getAppRouter, getAppRouter: this.getAppRouter,
setRender: this.setRender, setRender: this.setRender,
addWrapper: this.addWrapper, addWrapper: this.addWrapper,
addSuspenseWrapper: this.addSuspenseWrapper,
appContext: this.appContext, appContext: this.appContext,
setAppRouter: this.setAppRouter, setAppRouter: this.setAppRouter,
useData: process.env.ICE_CORE_ROUTER === 'true' ? useData : useSingleRouterData, useData: process.env.ICE_CORE_ROUTER === 'true' ? useData : useSingleRouterData,
@ -122,6 +131,12 @@ class Runtime {
}); });
}; };
private addSuspenseWrapper: AddSuspenseWrapper = (Wrapper) => {
this.SuspenseWrappers.push({
Wrapper,
});
};
public setAppRouter: SetAppRouter = (AppRouter) => { public setAppRouter: SetAppRouter = (AppRouter) => {
this.AppRouter = AppRouter; this.AppRouter = AppRouter;
}; };

View File

@ -117,6 +117,7 @@ export interface AppContext {
loaderData?: LoadersData; loaderData?: LoadersData;
routeModules?: RouteModules; routeModules?: RouteModules;
RouteWrappers?: RouteWrapperConfig[]; RouteWrappers?: RouteWrapperConfig[];
SuspenseWrappers?: SuspenseWrapperConfig[];
routePath?: string; routePath?: string;
matches?: RouteMatch[]; matches?: RouteMatch[];
routes?: RouteItem[]; routes?: RouteItem[];
@ -187,16 +188,26 @@ export interface RouteWrapperConfig {
export type AppProvider = ComponentWithChildren<any>; export type AppProvider = ComponentWithChildren<any>;
export type RouteWrapper = ComponentType<any>; export type RouteWrapper = ComponentType<any>;
export type SuspenseWrapper = ComponentWithChildren<{
id: string;
}>;
export type ResponseHandler = ( export type ResponseHandler = (
req: IncomingMessage, req: IncomingMessage,
res: ServerResponse, res: ServerResponse,
) => any | Promise<any>; ) => any | Promise<any>;
export interface SuspenseWrapperConfig {
Wrapper: SuspenseWrapper;
}
export type SetAppRouter = <T>(AppRouter: ComponentType<T>) => void; export type SetAppRouter = <T>(AppRouter: ComponentType<T>) => void;
export type GetAppRouter = () => AppProvider; export type GetAppRouter = () => AppProvider;
export type AddProvider = (Provider: AppProvider) => void; export type AddProvider = (Provider: AppProvider) => void;
export type SetRender = (render: Renderer) => void; export type SetRender = (render: Renderer) => void;
export type AddWrapper = (wrapper: RouteWrapper, forLayout?: boolean) => void; export type AddWrapper = (wrapper: RouteWrapper, forLayout?: boolean) => void;
export type AddSuspenseWrapper = (wrapper: SuspenseWrapper) => void;
export type AddResponseHandler = (handler: ResponseHandler) => void; export type AddResponseHandler = (handler: ResponseHandler) => void;
export type GetResponseHandlers = () => ResponseHandler[]; export type GetResponseHandlers = () => ResponseHandler[];
@ -227,6 +238,7 @@ export interface RuntimeAPI {
getResponseHandlers: GetResponseHandlers; getResponseHandlers: GetResponseHandlers;
setRender: SetRender; setRender: SetRender;
addWrapper: AddWrapper; addWrapper: AddWrapper;
addSuspenseWrapper: AddSuspenseWrapper;
appContext: AppContext; appContext: AppContext;
useData: UseData; useData: UseData;
useConfig: UseConfig; useConfig: UseConfig;

View File

@ -2160,7 +2160,7 @@ importers:
specifier: ^3.6.4 specifier: ^3.6.4
version: link:../ice version: link:../ice
'@ice/runtime': '@ice/runtime':
specifier: ^1.5.6 specifier: ^1.5.7
version: link:../runtime version: link:../runtime
webpack: webpack:
specifier: ^5.88.0 specifier: ^5.88.0