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.
<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

View File

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

View File

@ -18,6 +18,7 @@ async function bundler(
routeManifest,
appConfig,
hasDataLoader,
generator,
} = options;
let compiler: MultiCompiler;
let dataLoaderCompiler: Compiler;
@ -63,6 +64,7 @@ async function bundler(
hooksAPI,
taskConfigs,
rspackConfigs,
generator,
};
if (command === 'start') {
// @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,
appConfig,
hooksAPI,
generator,
}: BuildOptions, dataLoaderCompiler?: Compiler) => {
const { rootDir, applyHook, commandArgs, userConfig, extendsPluginAPI: { excuteServerEntry } } = context;
const customMiddlewares = rspackConfigs[0].devServer?.setupMiddlewares;
@ -32,8 +33,10 @@ const start = async ({
taskConfig: webTaskConfig,
excuteServerEntry,
mock: commandArgs.mock,
open: commandArgs.open,
rootDir,
dataLoaderCompiler,
generator,
});
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 RouteManifest from '../utils/routeManifest.js';
import type ServerRunner from '../service/ServerRunner.js';
import type Generator from '../service/runtimeGenerator.js';
export type Context = DefaultContext<Config, ExtendsPluginAPI>;
@ -19,9 +20,11 @@ export interface BuildOptions {
appConfig: BundlerOptions['appConfig'];
hooksAPI: BundlerOptions['hooksAPI'];
taskConfigs: BundlerOptions['taskConfigs'];
generator: Generator;
}
export interface BundlerOptions {
generator: Generator;
taskConfigs: TaskConfig<Config>[];
spinner: ora.Ora;
hooksAPI: {

View File

@ -27,6 +27,7 @@ export async function startDevServer(
routeManifest,
userConfig,
appConfig,
generator,
} = options;
const routePaths = routeManifest.getFlattenRoute().sort((a, b) =>
// Sort by length, shortest path first.
@ -40,12 +41,14 @@ export async function startDevServer(
...defaultDevServerConfig,
setupMiddlewares: (middlewares, devServer) => {
const builtInMiddlewares = getMiddlewares(middlewares, {
generator,
userConfig,
routeManifest,
getAppConfig: hooksAPI.getAppConfig,
taskConfig: webTaskConfig,
excuteServerEntry,
mock: commandArgs.mock,
open: commandArgs.open,
rootDir,
});
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 { multipleServerEntry, renderMultiEntry } from './utils/multipleEntry.js';
import hasDocument from './utils/hasDocument.js';
import { addLeadingSlash } from './utils/slash.js';
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -66,6 +67,10 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
templates: [coreTemplate],
});
if (commandArgs.open) {
commandArgs.open = typeof commandArgs.open === 'string' ? addLeadingSlash(commandArgs.open) : commandArgs.open;
}
const { addWatchEvent, removeWatchEvent } = createWatch({
watchDir: rootDir,
command,
@ -221,6 +226,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const { userConfig } = ctx;
const { routes: routesConfig, server, syntaxFeatures, polyfill } = userConfig;
const coreEnvKeys = getCoreEnvKeys();
const routesInfo = await generateRoutesInfo(rootDir, routesConfig, routeManifest.getRoutesDefinitions());
@ -252,6 +258,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const { routeImports, routeDefinition } = getRoutesDefinition({
manifest: routesInfo.routes,
lazy,
compileRoutes: routesConfig?.lazyCompile && command === 'start' ? [commandArgs.open || '/'] : undefined,
});
const loaderExports = hasExportAppData || Boolean(routesInfo.loaders);
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 });
}
if (routesConfig?.lazyCompile && command === 'start') {
generator.addRenderFile('core/empty.tsx.ejs', 'empty.tsx');
}
if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) {
const {
packageName,
@ -401,6 +411,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
userConfig,
configFile,
hasDataLoader,
generator,
};
try {
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;
depth?: number;
matchRoute?: (route: NestedRouteManifest) => boolean;
compileRoutes?: string[];
}
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 routeDefinition = manifest.reduce((prev, 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 componentPath = id.startsWith('__') ? file : getFilePath(file);
const proxyModule = './empty';
let loadStatement = '';
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 {
const routeSpecifier = formatRouteSpecifier(id);
routeImports.push(`import * as ${routeSpecifier} from '${formatPath(componentPath)}';`);
@ -128,6 +132,7 @@ export function getRoutesDefinition(options: GetDefinationOptions) {
lazy,
depth: depth + 1,
matchRoute,
compileRoutes,
});
routeImports.push(...res.routeImports);
routeProperties.push(`children: [${res.routeDefinition}]`);

View File

@ -167,6 +167,10 @@ export interface UserConfig {
* inject initial route path for each route html.
*/
injectInitialEntry?: boolean;
/**
* Enable lazy compile for routes.
*/
lazyCompile?: boolean;
};
/**
* 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": {
"@ice/app": "^3.6.4",
"@ice/runtime": "^1.5.6"
"@ice/runtime": "^1.5.7"
},
"publishConfig": {
"access": "public"

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import type { WindowContext, RouteMatch, AssetsManifest } from './types.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';
interface DocumentContext {
@ -81,7 +81,15 @@ export const Links: LinksType = (props: LinksProps) => {
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);
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 (
<>

View File

@ -135,7 +135,8 @@ export function withSuspense(Component) {
return (props: SuspenseProps) => {
const { fallback, id, ...componentProps } = props;
const [suspenseState, updateSuspenseData] = React.useState({
const [suspenseState, updateSuspenseData] = React.useState<SuspenseState>({
id: id,
data: null,
done: false,
@ -156,24 +157,47 @@ export function withSuspense(Component) {
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
id={`suspense-parse-start-${id}`}
script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-start','${id}');`}
/>
<SuspenseContext.Provider value={suspenseState}>
<Component {...componentProps} />
<InlineScript
id={`suspense-parse-data-${id}`}
script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-data','${id}');`}
/>
<Data id={id} />
</SuspenseContext.Provider>
<Component {...componentProps} />
<InlineScript
id={`suspense-parse-data-${id}`}
script={`(${DISPATCH_SUSPENSE_EVENT_STRING})('ice-suspense-parse-data','${id}');`}
/>
<Data id={id} />
<InlineScript
id={`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>
);
};

View File

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

View File

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

View File

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