mirror of https://github.com/alibaba/ice.git
Compare commits
12 Commits
393437a8a7
...
8ced7adf79
| Author | SHA1 | Date |
|---|---|---|
|
|
8ced7adf79 | |
|
|
2742ac4678 | |
|
|
fcc25dc3fd | |
|
|
8e27933423 | |
|
|
8bc00a2115 | |
|
|
b03194e046 | |
|
|
89fec6198d | |
|
|
cb35145ff3 | |
|
|
3280dbf433 | |
|
|
7fdb3ae845 | |
|
|
f048b31327 | |
|
|
a5d13dcfd3 |
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@ice/runtime': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: duplicate css
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@ice/app': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: support lazy compile of routes
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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}]`);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const addLeadingSlash = (path: string) => {
|
||||||
|
return path.charAt(0) === '/' ? path : `/${path}`;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue