From 30ef6a63c43d9364d6104e921da08ea6ea6ea419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=93=E9=99=8C=E5=90=8C=E5=AD=A6?= Date: Fri, 14 Oct 2022 14:45:34 +0800 Subject: [PATCH 01/27] feat: auto create root for pure js bundle (#592) * feat: auto create root for pure js bundle * chore: remove console and add warning * chore: modify warning * chore: add mock function for test --- packages/runtime/src/runClientApp.tsx | 11 ++++++++++- packages/runtime/tests/runClientApp.test.tsx | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index beef52eb9..401cf6c6f 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -112,8 +112,17 @@ async function render({ history, runtime }: RenderOptions) { const RouteWrappers = runtime.getWrappers(); const AppRouter = runtime.getAppRouter(); + const rootId = appConfig.app.rootId || 'app'; + let root = document.getElementById(rootId); + if (!root) { + root = document.createElement('div'); + root.id = rootId; + document.body.appendChild(root); + console.warn(`Root node #${rootId} is not found, current root is automatically created by the framework.`); + } + render( - document.getElementById(appConfig.app.rootId), + root, { content: '', }), }, + body: { + appendChild: () => null, + }, getElementById: () => null, + createElement: () => ({ + id: '', + }), querySelectorAll: () => [], })); }); @@ -42,7 +48,7 @@ describe('run client app', () => { setRender((container, element) => { try { domstring = renderToString(element as any); - } catch (err) {} + } catch (err) { } }); }; From 435868a7643ca6fa11520d27a0847c41f0ff2e1b Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Fri, 14 Oct 2022 17:18:31 +0800 Subject: [PATCH 02/27] docs: optimize development docs (#577) --- website/docs/guide/basic/development.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/website/docs/guide/basic/development.md b/website/docs/guide/basic/development.md index 1ef9506e3..81b776124 100644 --- a/website/docs/guide/basic/development.md +++ b/website/docs/guide/basic/development.md @@ -3,9 +3,11 @@ title: 开发环境 order: 1 --- +本文讲述在开发应用前如何安装最小开发环境。 + ## Node.js -开发前端应用前需要安装 [Node.js](https://nodejs.org),并确保 node 版本是 14.x 或以上。推荐使用 [nvm](https://github.com/nvm-sh/nvm) 或者 [fnm](https://github.com/Schniz/fnm) 来管理 node 版本,进行安装。下面以在 mac 下安装 nvm 为例: +开发前端应用前需要安装 [Node.js](https://nodejs.org),并确保 node 版本是 14.x 或以上。推荐使用 [nvm](https://github.com/nvm-sh/nvm)(Windows 下使用 [nvm-windows](https://github.com/coreybutler/nvm-windows)) 或者 [fnm](https://github.com/Schniz/fnm) 来管理 node 版本。下面以在 mac 下安装 nvm 为例: ```bash $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash @@ -18,15 +20,7 @@ $ node -v v14.19.3 ``` -在国内使用 npm 安装依赖可能会比较慢。建议使用国内镜像源进行加速: - -```bash -$ npm install -g cnpm --registry=https://registry.npm.taobao.org -# 验证 cnpm 安装是否成功 -$ cnpm -v -``` - -### 包管理工具 +## 包管理工具 安装 Node.js 后,默认会包含 npm。除此以外,还有其他的包管理工具: @@ -38,7 +32,7 @@ $ cnpm -v ```bash $ npm i pnpm -g --register=https://registry.npmmirror.com/ -# 验证 pnpm 安装是否成功 +# 验证 pnpm 是否安装成功 $ pnpm -v 7.1.7 ``` @@ -55,11 +49,11 @@ $ nrm ls nrm use taobao ``` -### IDE +## IDE 推荐使用 IDE 进行前端应用开发和调试,会有更好的调试体验。目前比较流行的 IDE 有: - [Visual Studio Code](https://code.visualstudio.com/)(推荐) -- [WebStorm](https://www.jetbrains.com/webstorm/)(推荐) +- [WebStorm](https://www.jetbrains.com/webstorm/)(推荐) - [Sublime Text](https://www.sublimetext.com/) - [Atom](https://atom.io/) From 952246803140ab8429cdc8c4f5c2ea1617cf230e Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Fri, 14 Oct 2022 17:19:57 +0800 Subject: [PATCH 03/27] chore: update test config (#584) --- packages/webpack-config/{test => tests}/fixtures/alias.js | 0 packages/webpack-config/{test => tests}/fixtures/aliasWithAs.js | 0 packages/webpack-config/{test => tests}/fixtures/as.js | 0 packages/webpack-config/{test => tests}/fixtures/basic.js | 0 packages/webpack-config/{test => tests}/fixtures/multiple.js | 0 packages/webpack-config/{test => tests}/redirectImport.test.ts | 0 vitest.config.ts | 1 + 7 files changed, 1 insertion(+) rename packages/webpack-config/{test => tests}/fixtures/alias.js (100%) rename packages/webpack-config/{test => tests}/fixtures/aliasWithAs.js (100%) rename packages/webpack-config/{test => tests}/fixtures/as.js (100%) rename packages/webpack-config/{test => tests}/fixtures/basic.js (100%) rename packages/webpack-config/{test => tests}/fixtures/multiple.js (100%) rename packages/webpack-config/{test => tests}/redirectImport.test.ts (100%) diff --git a/packages/webpack-config/test/fixtures/alias.js b/packages/webpack-config/tests/fixtures/alias.js similarity index 100% rename from packages/webpack-config/test/fixtures/alias.js rename to packages/webpack-config/tests/fixtures/alias.js diff --git a/packages/webpack-config/test/fixtures/aliasWithAs.js b/packages/webpack-config/tests/fixtures/aliasWithAs.js similarity index 100% rename from packages/webpack-config/test/fixtures/aliasWithAs.js rename to packages/webpack-config/tests/fixtures/aliasWithAs.js diff --git a/packages/webpack-config/test/fixtures/as.js b/packages/webpack-config/tests/fixtures/as.js similarity index 100% rename from packages/webpack-config/test/fixtures/as.js rename to packages/webpack-config/tests/fixtures/as.js diff --git a/packages/webpack-config/test/fixtures/basic.js b/packages/webpack-config/tests/fixtures/basic.js similarity index 100% rename from packages/webpack-config/test/fixtures/basic.js rename to packages/webpack-config/tests/fixtures/basic.js diff --git a/packages/webpack-config/test/fixtures/multiple.js b/packages/webpack-config/tests/fixtures/multiple.js similarity index 100% rename from packages/webpack-config/test/fixtures/multiple.js rename to packages/webpack-config/tests/fixtures/multiple.js diff --git a/packages/webpack-config/test/redirectImport.test.ts b/packages/webpack-config/tests/redirectImport.test.ts similarity index 100% rename from packages/webpack-config/test/redirectImport.test.ts rename to packages/webpack-config/tests/redirectImport.test.ts diff --git a/vitest.config.ts b/vitest.config.ts index c84a02b8f..6879a5fcc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ '**/bundles/compiled/**', // App runtime has been tested by unit test case '**/packages/runtime/esm/**', + '**/packages/route-manifest/esm/**', '**/packages/miniapp-runtime/esm/**', '**/tests/**', ], From 197cb6ac66bcf5859d9703a9b6f288f65da69e47 Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Fri, 14 Oct 2022 17:24:39 +0800 Subject: [PATCH 04/27] docs: cli (#576) * docs: cli * docs: typo --- website/docs/guide/basic/cli.md | 112 ++++++++++++++++++++++++++++++++ website/docs/guide/basic/env.md | 13 ++-- 2 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 website/docs/guide/basic/cli.md diff --git a/website/docs/guide/basic/cli.md b/website/docs/guide/basic/cli.md new file mode 100644 index 000000000..1d95f1ddf --- /dev/null +++ b/website/docs/guide/basic/cli.md @@ -0,0 +1,112 @@ +--- +title: 命令行 +order: 14 +--- + +:::tip + +指定命令行参数有两种方式: + +1. 在 `package.json` 中指定参数: + +```diff +{ + "scripts": { +- "start": "ice start" ++ "start": "ice start --https" + } +} +``` + +2. 在命令行中指定参数(需要多加 `--` 字符): + +```bash +$ npm start -- --https +``` + +::: + +## start + +启动本地开发服务器,用于调试项目。 + +```bash +Usage: ice-cli start [options] + +start server + +Options: + --platform 指定编译的 platform + --mode 指定 mode + --config 指定配置文件路径 + -h, --host 指定开发服务器主机名 + -p, --port 指定开发服务器端口 + --no-open 禁止默认打开浏览器预览行为 + --no-mock 禁用 mock 服务 + --rootDir 指定项目的根路径 + --analyzer 开启构建分析 + --https [https] 开启 https + --force 移除构建缓存 +``` + +## build + +构建项目,输出生产环境下的资源。 + +```bash +Usage: ice-cli build [options] + +build project + +Options: + --platform 指定编译的 platform + --mode 指定 mode + --analyzer 开启构建分析 + --config 指定配置文件路径 + --rootDir 指定项目的根路径 +``` + +## help + +查看帮助。 + +```bash +$ ice help +Usage: ice-cli [options] + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + build [options] build project + start [options] start server + help [command] display help for command +``` + +也可以查看指定命令的详细帮助信息。 + +```bash +$ ice help build +Usage: ice-cli build [options] + +build project + +Options: + --platform set platform + --mode set mode + --analyzer visualize size of output files + --config use custom config + --rootDir project root directory + -h, --help display help for command +``` + +## version + +查看 icejs 的版本。 + +```bash +$ ice --version + +3.0.0 +``` diff --git a/website/docs/guide/basic/env.md b/website/docs/guide/basic/env.md index a723badfb..7a0be09f3 100644 --- a/website/docs/guide/basic/env.md +++ b/website/docs/guide/basic/env.md @@ -1,6 +1,6 @@ --- title: 环境变量 -order: 13 +order: 15 --- ICE 内置通过环境变量实现给构建或运行时传递参数的功能。 @@ -41,11 +41,14 @@ DEV_PORT=9999 此外你也可以在 `.env.${mode}` 和 `.env.${mode}.local` 文件中指定不同模式下的环境变量。`${mode}` 的取值是 `development` 或 `production`。 需要注意的是: + 1. 这几个文件的优先级由低至高分别是 - - `.env` - - `.env.local` - - `.env.${mode}` - - `.env.${mode}.local` + +- `.env` +- `.env.local` +- `.env.${mode}` +- `.env.${mode}.local` + 2. 一般不建议将 `.local` 结尾的文件加入版本管理 (如 Git) 中。 ## 使用环境变量 From b4b4614908f379d5669042e30036d0ef297dfaa8 Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Mon, 17 Oct 2022 10:17:19 +0800 Subject: [PATCH 05/27] feat: optimize log (#588) * feat: optimize log * fix: object debug log * chore: fix log --- packages/ice/src/createService.ts | 5 +- .../ice/src/esbuild/removeTopLevelCode.ts | 19 +++-- packages/ice/src/esbuild/transformPipe.ts | 83 ++++++++++--------- packages/ice/src/plugins/web/index.ts | 2 +- packages/ice/src/service/analyze.ts | 7 +- packages/ice/src/service/config.ts | 16 +++- packages/ice/src/service/serverCompiler.ts | 6 +- packages/ice/src/service/webpackCompiler.ts | 4 +- packages/ice/src/webpack/DataLoaderPlugin.ts | 42 ++++++---- .../plugin-rax-compat/src/transform-styles.ts | 2 +- packages/types/src/plugin.ts | 17 +++- 11 files changed, 123 insertions(+), 80 deletions(-) diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 707f39b28..67245d40c 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -98,10 +98,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt const runtimeModules = getRuntimeModules(plugins); const { getAppConfig, init: initAppConfigCompiler } = getAppExportConfig(rootDir); - const { - getRoutesConfig, - init: initRouteConfigCompiler, - } = getRouteExportConfig(rootDir); + const { getRoutesConfig, init: initRouteConfigCompiler } = getRouteExportConfig(rootDir); // register config ['userConfig', 'cliOption'].forEach((configType) => { diff --git a/packages/ice/src/esbuild/removeTopLevelCode.ts b/packages/ice/src/esbuild/removeTopLevelCode.ts index d5524cd0c..d2a923a92 100644 --- a/packages/ice/src/esbuild/removeTopLevelCode.ts +++ b/packages/ice/src/esbuild/removeTopLevelCode.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import consola from 'consola'; import type { Plugin } from 'esbuild'; import { parse, type ParserOptions } from '@babel/parser'; import babelTraverse from '@babel/traverse'; @@ -34,13 +35,17 @@ const removeCodePlugin = (keepExports: string[], transformInclude: (id: string) isTS = true; parserOptions.plugins.push('typescript', 'decorators-legacy'); } - const ast = parse(source, parserOptions); - traverse(ast, removeTopLevelCode(keepExports)); - const contents = generate(ast).code; - return { - contents, - loader: isTS ? 'tsx' : 'jsx', - }; + try { + const ast = parse(source, parserOptions); + traverse(ast, removeTopLevelCode(keepExports)); + const contents = generate(ast).code; + return { + contents, + loader: isTS ? 'tsx' : 'jsx', + }; + } catch (error) { + consola.debug('Remove top level code error.', error.stack); + } }); }, }; diff --git a/packages/ice/src/esbuild/transformPipe.ts b/packages/ice/src/esbuild/transformPipe.ts index 56d787abc..163d6793b 100644 --- a/packages/ice/src/esbuild/transformPipe.ts +++ b/packages/ice/src/esbuild/transformPipe.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import * as path from 'path'; +import consola from 'consola'; import type { Plugin, PluginBuild, Loader } from 'esbuild'; import type { UnpluginOptions, UnpluginContext } from 'unplugin'; @@ -101,53 +102,57 @@ const transformPipe = (options: PluginOptions = {}): Plugin => { // it is required to forward `resolveDir` for esbuild to find dependencies. const resolveDir = path.dirname(args.path); const loader = guessLoader(id); - const transformedResult = await plugins.reduce(async (prevData, plugin) => { - const { contents } = await prevData; - const { transform, transformInclude, loadInclude } = plugin; - let sourceCode = contents; - let sourceMap = null; + try { + const transformedResult = await plugins.reduce(async (prevData, plugin) => { + const { contents } = await prevData; + const { transform, transformInclude, loadInclude } = plugin; + let sourceCode = contents; + let sourceMap = null; - if (plugin.load) { - if (!loadInclude || loadInclude?.(id)) { - const result = await plugin.load.call(pluginContext, id); - if (typeof result === 'string') { - sourceCode = result; - } else if (typeof result === 'object' && result !== null) { - sourceCode = result.code; - sourceMap = result.map; + if (plugin.load) { + if (!loadInclude || loadInclude?.(id)) { + const result = await plugin.load.call(pluginContext, id); + if (typeof result === 'string') { + sourceCode = result; + } else if (typeof result === 'object' && result !== null) { + sourceCode = result.code; + sourceMap = result.map; + } } } - } - if (!transformInclude || transformInclude?.(id)) { - if (!sourceCode) { - // Caution: 'utf8' assumes the input file is not in binary. - // If you want your plugin handle binary files, make sure to execute `plugin.load()` first. - sourceCode = await fs.promises.readFile(args.path, 'utf8'); - } - if (transform) { - const result = await transform.call(pluginContext, sourceCode, id); - if (typeof result === 'string') { - sourceCode = result; - } else if (typeof result === 'object' && result !== null) { - sourceCode = result.code; - sourceMap = result.map; + if (!transformInclude || transformInclude?.(id)) { + if (!sourceCode) { + // Caution: 'utf8' assumes the input file is not in binary. + // If you want your plugin handle binary files, make sure to execute `plugin.load()` first. + sourceCode = await fs.promises.readFile(args.path, 'utf8'); } - } - if (sourceMap) { - if (!sourceMap.sourcesContent || sourceMap.sourcesContent.length === 0) { - sourceMap.sourcesContent = [sourceCode]; + if (transform) { + const result = await transform.call(pluginContext, sourceCode, id); + if (typeof result === 'string') { + sourceCode = result; + } else if (typeof result === 'object' && result !== null) { + sourceCode = result.code; + sourceMap = result.map; + } + } + if (sourceMap) { + if (!sourceMap.sourcesContent || sourceMap.sourcesContent.length === 0) { + sourceMap.sourcesContent = [sourceCode]; + } + sourceMap = fixSourceMap(sourceMap); + sourceCode += `\n//# sourceMappingURL=${sourceMap.toUrl()}`; } - sourceMap = fixSourceMap(sourceMap); - sourceCode += `\n//# sourceMappingURL=${sourceMap.toUrl()}`; + return { contents: sourceCode, resolveDir, loader }; } - return { contents: sourceCode, resolveDir, loader }; + return { contents, resolveDir, loader }; + }, Promise.resolve({ contents: null, resolveDir, loader })); + // Make sure contents is not null when return. + if (transformedResult.contents) { + return transformedResult; } - return { contents, resolveDir, loader }; - }, Promise.resolve({ contents: null, resolveDir, loader })); - // Make sure contents is not null when return. - if (transformedResult.contents) { - return transformedResult; + } catch (error) { + consola.debug('Error occurs in esbuild-transform-pipe.', error.stack); } }); }, diff --git a/packages/ice/src/plugins/web/index.ts b/packages/ice/src/plugins/web/index.ts index bcd6eefdc..6a379564f 100644 --- a/packages/ice/src/plugins/web/index.ts +++ b/packages/ice/src/plugins/web/index.ts @@ -106,7 +106,7 @@ const plugin: Plugin = () => ({ } else { logoutMessage += `\n - Local : ${chalk.underline.white(`${urls.localUrlForBrowser}${hashChar}${devPath}`)} - - Network: ${chalk.underline.white(`${urls.lanUrlForTerminal}${hashChar}${devPath}`)}`; + - Network: ${chalk.underline.white(`${urls.lanUrlForTerminal}${hashChar}${devPath}`)}`; } consola.log(`${logoutMessage}\n`); diff --git a/packages/ice/src/service/analyze.ts b/packages/ice/src/service/analyze.ts index f4b7b03f8..b6c806602 100644 --- a/packages/ice/src/service/analyze.ts +++ b/packages/ice/src/service/analyze.ts @@ -194,8 +194,8 @@ export async function scanImports(entries: string[], options?: ScanOptions) { ); consola.debug(`Scan completed in ${(performance.now() - start).toFixed(2)}ms:`, deps); } catch (error) { - consola.error('Failed to scan imports.'); - consola.debug(error); + consola.error('Failed to scan module imports.'); + consola.debug(error.stack); } return orderedDependencies(deps); } @@ -250,8 +250,7 @@ export async function getFileExports(options: FileOptions): Promise { appExportConfig = { init(serverCompiler: ServerCompiler) { - config.setCompiler(serverCompiler); + try { + config.setCompiler(serverCompiler); + } catch (error) { + consola.error('Get app export config error.'); + console.debug(error.stack); + } }, getAppConfig, }; @@ -205,7 +212,12 @@ export const getRouteExportConfig = (rootDir: string) => { routeExportConfig = { init(serverCompiler: ServerCompiler) { config.clearTasks(); - config.setCompiler(serverCompiler); + try { + config.setCompiler(serverCompiler); + } catch (error) { + consola.error('Get route export config error.'); + console.debug(error.stack); + } }, getRoutesConfig, ensureRoutesConfig, diff --git a/packages/ice/src/service/serverCompiler.ts b/packages/ice/src/service/serverCompiler.ts index a677fbeb5..48a62cdc9 100644 --- a/packages/ice/src/service/serverCompiler.ts +++ b/packages/ice/src/service/serverCompiler.ts @@ -161,7 +161,7 @@ export function createServerCompiler(options: Options) { } const startTime = new Date().getTime(); - consola.debug('[esbuild]', `start compile for: ${buildOptions.entryPoints}`); + consola.debug('[esbuild]', `start compile for: ${JSON.stringify(buildOptions.entryPoints)}`); try { const esbuildResult = await esbuild.build(buildOptions); @@ -178,8 +178,8 @@ export function createServerCompiler(options: Options) { }; } catch (error) { consola.error('Server compile error.', `\nEntryPoints: ${JSON.stringify(buildOptions.entryPoints)}`); - consola.debug(buildOptions); - consola.debug(error); + consola.debug('Build options: ', buildOptions); + consola.debug(error.stack); return { error: error as Error, }; diff --git a/packages/ice/src/service/webpackCompiler.ts b/packages/ice/src/service/webpackCompiler.ts index 9f324a0bf..e1da54c8d 100644 --- a/packages/ice/src/service/webpackCompiler.ts +++ b/packages/ice/src/service/webpackCompiler.ts @@ -41,7 +41,7 @@ async function webpackCompiler(options: { // Add default plugins for spinner webpackConfigs[0].plugins.push((compiler: Compiler) => { compiler.hooks.beforeCompile.tap('spinner', () => { - spinner.text = 'compiling...'; + spinner.text = 'compiling...\n'; }); compiler.hooks.afterEmit.tap('spinner', () => { spinner.stop(); @@ -74,7 +74,7 @@ async function webpackCompiler(options: { if (messages.errors.length > 1) { messages.errors.length = 1; } - consola.error('Failed to compile.'); + consola.error('Compiled with errors.'); console.error(messages.errors.join('\n')); return; } else if (messages.warnings.length) { diff --git a/packages/ice/src/webpack/DataLoaderPlugin.ts b/packages/ice/src/webpack/DataLoaderPlugin.ts index b900e28f3..11fe0adba 100644 --- a/packages/ice/src/webpack/DataLoaderPlugin.ts +++ b/packages/ice/src/webpack/DataLoaderPlugin.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import fse from 'fs-extra'; +import consola from 'consola'; import type { ServerCompiler } from '@ice/types/esm/plugin.js'; import type { Compiler } from 'webpack'; import webpack from '@ice/bundles/compiled/webpack/index.js'; @@ -34,24 +35,33 @@ export default class DataLoaderPlugin { // Check file data-loader.ts if it is exists. const filePath = path.join(this.rootDir, RUNTIME_TMP_DIR, 'data-loader.ts'); if (fse.existsSync(filePath)) { - const { outputFiles } = await this.serverCompiler({ - // Code will be transformed by @swc/core reset target to esnext make modern js syntax do not transformed. - target: 'esnext', - entryPoints: [filePath], - write: false, - }, { - swc: { - keepExports: ['getData', 'getAppData'], - keepPlatform: 'web', - getRoutePaths: () => { - return getRoutePathsFromCache(this.dataCache); + const { outputFiles, error } = await this.serverCompiler( + { + // Code will be transformed by @swc/core reset target to esnext make modern js syntax do not transformed. + target: 'esnext', + entryPoints: [filePath], + write: false, + logLevel: 'silent', // The main server compile process will log it. + }, + { + swc: { + keepExports: ['getData', 'getAppData'], + keepPlatform: 'web', + getRoutePaths: () => { + return getRoutePathsFromCache(this.dataCache); + }, }, + preBundle: false, + externalDependencies: false, + transformEnv: false, }, - preBundle: false, - externalDependencies: false, - transformEnv: false, - }); - compilation.emitAsset('js/data-loader.js', new RawSource(new TextDecoder('utf-8').decode(outputFiles[0].contents))); + ); + if (error) { + consola.error('Server compile error in DataLoaderPlugin.'); + consola.debug(error.stack); + } else { + compilation.emitAsset('js/data-loader.js', new RawSource(new TextDecoder('utf-8').decode(outputFiles[0].contents))); + } } else { compilation.deleteAsset('js/data-loader.js'); } diff --git a/packages/plugin-rax-compat/src/transform-styles.ts b/packages/plugin-rax-compat/src/transform-styles.ts index 86372e336..e785b145e 100644 --- a/packages/plugin-rax-compat/src/transform-styles.ts +++ b/packages/plugin-rax-compat/src/transform-styles.ts @@ -32,7 +32,7 @@ async function styleSheetLoader(source, type = 'css') { const { stylesheet } = css.parse(newContent); if (stylesheet.parsingErrors.length) { - throw new Error('StyleSheet Parsing Error occured.'); + throw new Error('StyleSheet Parsing Error occurred.'); } // getOptions can return null if no query passed. diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index 68aa1b9a7..3f7fbde92 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -11,7 +11,22 @@ import type { AssetsManifest } from './runtime.js'; type AddExport = (exportData: ExportData) => void; type EventName = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'; -type ServerCompilerBuildOptions = Pick; +type ServerCompilerBuildOptions = Pick; + export type ServerCompiler = ( buildOptions: ServerCompilerBuildOptions, options?: { From f78b1a896d6670962b3ba03937747ad8951c472a Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:02:29 +0800 Subject: [PATCH 06/27] chore: remove some mock deps (#580) * chore: remove some mock deps * fix: lint warning --- packages/ice/package.json | 2 - .../src/middlewares/mock/createMiddleware.ts | 23 ++------- pnpm-lock.yaml | 47 ------------------- 3 files changed, 3 insertions(+), 69 deletions(-) diff --git a/packages/ice/package.json b/packages/ice/package.json index f98fc163f..89d13ee9a 100644 --- a/packages/ice/package.json +++ b/packages/ice/package.json @@ -39,7 +39,6 @@ "@ice/runtime": "^1.0.0", "@ice/webpack-config": "^1.0.0", "address": "^1.1.2", - "body-parser": "^1.20.0", "build-scripts": "^2.0.0-26", "chalk": "^4.0.0", "commander": "^9.0.0", @@ -56,7 +55,6 @@ "fs-extra": "^10.0.0", "micromatch": "^4.0.5", "mrmime": "^1.0.0", - "multer": "^1.4.5-lts.1", "open": "^8.4.0", "path-to-regexp": "^6.2.0", "regenerator-runtime": "^0.11.0", diff --git a/packages/ice/src/middlewares/mock/createMiddleware.ts b/packages/ice/src/middlewares/mock/createMiddleware.ts index e29b84d66..235fd3480 100644 --- a/packages/ice/src/middlewares/mock/createMiddleware.ts +++ b/packages/ice/src/middlewares/mock/createMiddleware.ts @@ -2,8 +2,6 @@ import * as path from 'path'; import type { ExpressRequestHandler, Request, Middleware } from 'webpack-dev-server'; import { pathToRegexp } from 'path-to-regexp'; import type { Key } from 'path-to-regexp'; -import bodyParser from 'body-parser'; -import multer from 'multer'; import createWatch from '../../service/watchSource.js'; import getConfigs, { MOCK_FILE_PATTERN } from './getConfigs.js'; import type { MockConfig } from './getConfigs.js'; @@ -28,7 +26,7 @@ export default function createMiddleware(options: MockOptions): Middleware { const matchResult = matchPath(req, mockConfigs); if (matchResult) { const { match, mockConfig, keys } = matchResult; - const { handler, method } = mockConfig; + const { handler } = mockConfig; if (typeof handler === 'function') { // params const params: Record = {}; @@ -41,23 +39,8 @@ export default function createMiddleware(options: MockOptions): Middleware { } } req.params = params; - // handler - if (method === 'GET') { - handler(req, res, next); - return; - } else { - // parses json and add to `req.body` - bodyParser.json({ limit: '5mb', strict: false })(req, res, () => { - // parses urlencoded bodies and add to `req.body` - bodyParser.urlencoded({ limit: '5mb', extended: true })(req, res, () => { - // handles `multipart/form-data` and add to `req.body` - multer().any()(req, res, () => { - handler(req, res, next); - return; - }); - }); - }); - } + handler(req, res, next); + return; } else { return res.status(200).json(handler); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62c960455..244951994 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -705,7 +705,6 @@ importers: '@types/sass': ^1.43.1 '@types/temp': ^0.9.1 address: ^1.1.2 - body-parser: ^1.20.0 build-scripts: ^2.0.0-26 chalk: ^4.0.0 chokidar: ^3.5.3 @@ -724,7 +723,6 @@ importers: jest: ^29.0.2 micromatch: ^4.0.5 mrmime: ^1.0.0 - multer: ^1.4.5-lts.1 open: ^8.4.0 path-to-regexp: ^6.2.0 react: ^18.2.0 @@ -746,7 +744,6 @@ importers: '@ice/runtime': link:../runtime '@ice/webpack-config': link:../webpack-config address: 1.2.1 - body-parser: 1.20.0 build-scripts: 2.0.0-26 chalk: 4.1.2 commander: 9.4.0 @@ -763,7 +760,6 @@ importers: fs-extra: 10.1.0 micromatch: 4.0.5 mrmime: 1.0.1 - multer: 1.4.5-lts.1 open: 8.4.0 path-to-regexp: 6.2.1 regenerator-runtime: 0.11.1 @@ -6758,10 +6754,6 @@ packages: normalize-path: 3.0.0 picomatch: 2.3.1 - /append-field/1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - dev: false - /aproba/1.2.0: resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} @@ -7433,13 +7425,6 @@ packages: engines: {node: '>=6'} dev: true - /busboy/1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - /bytes/3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -7931,16 +7916,6 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - /concat-stream/1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 2.3.7 - typedarray: 0.0.6 - dev: false - /concat-with-sourcemaps/1.1.0: resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} dependencies: @@ -13442,19 +13417,6 @@ packages: /ms/2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /multer/1.4.5-lts.1: - resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} - engines: {node: '>= 6.0.0'} - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 1.6.2 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 - dev: false - /multicast-dns/7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true @@ -17329,11 +17291,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /streamsearch/1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - /string-argv/0.3.1: resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} engines: {node: '>=0.6.19'} @@ -18222,10 +18179,6 @@ packages: dependencies: is-typedarray: 1.0.0 - /typedarray/0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - dev: false - /typescript/4.8.4: resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} engines: {node: '>=4.2.0'} From d985198eabb476aa23350592f8e5a5b1b5abad88 Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:26:37 +0800 Subject: [PATCH 07/27] feat: store initialStates (#435) * feat: store initialStates * chore: update README * chore: add file comment * chore: rename type * fix: peer dependencies * fix: defineStoreConfig not found * feat: support get the appData * fix: test case * fix: lock --- examples/with-store/package.json | 3 ++- examples/with-store/src/app.tsx | 20 ++++++++++++++++++++ examples/with-store/src/models/user.ts | 2 +- packages/plugin-store/README.md | 19 +++++++++++++++++++ packages/plugin-store/package.json | 14 +++++++++++++- packages/plugin-store/src/_store.ts | 3 +++ packages/plugin-store/src/runtime.tsx | 10 +++++++--- packages/plugin-store/src/types.ts | 13 +++++++++++++ pnpm-lock.yaml | 12 +++++++++--- tests/integration/with-store.test.ts | 2 +- 10 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 packages/plugin-store/src/types.ts diff --git a/examples/with-store/package.json b/examples/with-store/package.json index ff07de011..8f9903b27 100644 --- a/examples/with-store/package.json +++ b/examples/with-store/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@ice/plugin-store": "workspace:*", "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0" + "@types/react-dom": "^18.0.0", + "tslib": "^2.4.0" } } \ No newline at end of file diff --git a/examples/with-store/src/app.tsx b/examples/with-store/src/app.tsx index c1664902e..1ba6c4eab 100644 --- a/examples/with-store/src/app.tsx +++ b/examples/with-store/src/app.tsx @@ -1,3 +1,23 @@ +import type { GetAppData } from 'ice'; import { defineAppConfig } from 'ice'; +import { defineStoreConfig } from '@ice/plugin-store/esm/types'; + +export const store = defineStoreConfig(async (appData) => { + return { + initialStates: { + ...appData, + }, + }; +}); + +export const getAppData: GetAppData = () => { + return new Promise((resolve) => { + resolve({ + user: { + name: 'icejs', + }, + }); + }); +}; export default defineAppConfig({}); diff --git a/examples/with-store/src/models/user.ts b/examples/with-store/src/models/user.ts index 041b699b2..90fef0c89 100644 --- a/examples/with-store/src/models/user.ts +++ b/examples/with-store/src/models/user.ts @@ -2,6 +2,6 @@ import { createModel } from 'ice'; export default createModel({ state: { - name: 'ICE 3', + name: '', }, }); diff --git a/packages/plugin-store/README.md b/packages/plugin-store/README.md index e69de29bb..cc9e45c35 100644 --- a/packages/plugin-store/README.md +++ b/packages/plugin-store/README.md @@ -0,0 +1,19 @@ +# @ice/plugin-store + +A plugin of state management base on Redux and React Redux used in framework `ICE`. + +## Usage + +```ts +import { defineConfig } from '@ice/app'; +import store from '@ice/plugin-store'; +export default defineConfig({ + plugins: [ + store(), + ], +}); +``` + +## Options + +- disableResetPageState: 默认值是 `false`。开启后,切换页面再次进入原页面后不会重新初始化页面状态 diff --git a/packages/plugin-store/package.json b/packages/plugin-store/package.json index 3d8a82d63..07b006b37 100644 --- a/packages/plugin-store/package.json +++ b/packages/plugin-store/package.json @@ -24,6 +24,16 @@ "types": "./esm/api.d.ts", "import": "./esm/api.js", "default": "./esm/api.js" + }, + "./types": { + "types": "./esm/types.d.ts", + "import": "./esm/types.js", + "default": "./esm/types.js" + }, + "./esm/types": { + "types": "./esm/types.d.ts", + "import": "./esm/types.js", + "default": "./esm/types.js" } }, "main": "./esm/index.js", @@ -33,12 +43,14 @@ "!esm/**/*.map" ], "dependencies": { - "@ice/store": "^2.0.1", + "@ice/store": "^2.0.3", "fast-glob": "^3.2.11", "micromatch": "^4.0.5" }, "devDependencies": { "@ice/types": "^1.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/micromatch": "^4.0.2", diff --git a/packages/plugin-store/src/_store.ts b/packages/plugin-store/src/_store.ts index ff1ba0c93..3381e8273 100644 --- a/packages/plugin-store/src/_store.ts +++ b/packages/plugin-store/src/_store.ts @@ -1,3 +1,6 @@ +/** + * This file which is imported by the runtime.tsx, is to avoid TS error. + */ import type { IcestoreDispatch, IcestoreRootState } from '@ice/store'; import { createStore } from '@ice/store'; diff --git a/packages/plugin-store/src/runtime.tsx b/packages/plugin-store/src/runtime.tsx index e70dfb596..c2bda6965 100644 --- a/packages/plugin-store/src/runtime.tsx +++ b/packages/plugin-store/src/runtime.tsx @@ -1,15 +1,19 @@ import * as React from 'react'; import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/types'; import { PAGE_STORE_INITIAL_STATES, PAGE_STORE_PROVIDER } from './constants.js'; +import type { StoreConfig } from './types.js'; import appStore from '$store'; -const runtime: RuntimePlugin = async ({ addWrapper, addProvider, useAppContext }) => { +const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider, useAppContext }) => { + const { appExport, appData } = appContext; + const storeConfig: StoreConfig = (typeof appExport.store === 'function' + ? (await appExport.store(appData)) : appExport.store) || {}; + const { initialStates } = storeConfig; if (appStore && Object.prototype.hasOwnProperty.call(appStore, 'Provider')) { // Add app store Provider const StoreProvider: AppProvider = ({ children }) => { return ( - // TODO: support initialStates: https://github.com/ice-lab/ice-next/issues/395#issuecomment-1210552931 - + {children} ); diff --git a/packages/plugin-store/src/types.ts b/packages/plugin-store/src/types.ts new file mode 100644 index 000000000..dad4c6ca6 --- /dev/null +++ b/packages/plugin-store/src/types.ts @@ -0,0 +1,13 @@ +export interface StoreConfig { + initialStates: Record; +} + +type Store = ((data?: any) => Promise) | StoreConfig; + +function defineStoreConfig(fn: Store) { + return fn; +} + +export { + defineStoreConfig, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 244951994..77a8860e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -483,6 +483,7 @@ importers: '@ice/runtime': workspace:* '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 + tslib: ^2.4.0 dependencies: '@ice/app': link:../../packages/ice '@ice/runtime': link:../../packages/runtime @@ -490,6 +491,7 @@ importers: '@ice/plugin-store': link:../../packages/plugin-store '@types/react': 18.0.21 '@types/react-dom': 18.0.6 + tslib: 2.4.0 examples/with-vitest: specifiers: @@ -1007,13 +1009,15 @@ importers: packages/plugin-store: specifiers: - '@ice/store': ^2.0.1 + '@ice/store': ^2.0.3 '@ice/types': ^1.0.0 '@types/micromatch': ^4.0.2 '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 fast-glob: ^3.2.11 micromatch: ^4.0.5 + react: ^18.2.0 + react-dom: ^18.2.0 regenerator-runtime: ^0.13.9 dependencies: '@ice/store': 2.0.3 @@ -1024,6 +1028,8 @@ importers: '@types/micromatch': 4.0.2 '@types/react': 18.0.21 '@types/react-dom': 18.0.6 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 regenerator-runtime: 0.13.9 packages/rax-compat: @@ -16142,6 +16148,8 @@ packages: hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 react-is: 17.0.2 dev: false @@ -17752,7 +17760,6 @@ packages: serialize-javascript: 6.0.0 terser: 5.14.2 webpack: 5.74.0_esbuild@0.14.54 - dev: true /terser-webpack-plugin/5.3.5_webpack@5.74.0: resolution: {integrity: sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA==} @@ -19232,7 +19239,6 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: true /webpackbar/5.0.2_webpack@5.74.0: resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==} diff --git a/tests/integration/with-store.test.ts b/tests/integration/with-store.test.ts index 0235f86dc..ac8fba64a 100644 --- a/tests/integration/with-store.test.ts +++ b/tests/integration/with-store.test.ts @@ -15,7 +15,7 @@ describe(`build ${example}`, () => { page = res.page; browser = res.browser; await page.waitForFunction('document.getElementsByTagName(\'button\').length > 0'); - expect(await page.$$text('#username')).toStrictEqual(['name: ICE 3']); + expect(await page.$$text('#username')).toStrictEqual(['name: icejs']); expect(await page.$$text('#count')).toStrictEqual(['0']); }, 120000); From 18f54ec392844105a9e5ad1b93b820999835e9ee Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Tue, 18 Oct 2022 13:54:20 +0800 Subject: [PATCH 08/27] feat: ClientOnly component (#542) * feat: BrowserOnly Component * docs: BrowserOnly component docs * docs: optimize * feat: export useIsBrowser Hook * feat: useIsBrowser example * docs: useIsBrowser * docs: typo * chore: clientOnly * feat: ClientOnly and useMounted * feat: add ClientOnly test * docs: update docs * feat: use hooks instead of Context * chore: import name --- .../basic-project/src/components/PageUrl.tsx | 3 + .../basic-project/src/pages/client-only.tsx | 22 ++++++ examples/basic-project/src/pages/index.tsx | 16 +++- packages/ice/src/plugins/web/index.ts | 2 + packages/runtime/src/ClientOnly.tsx | 23 ++++++ packages/runtime/src/index.ts | 5 ++ packages/runtime/src/useMounted.tsx | 10 +++ tests/integration/basic-project.test.ts | 17 ++++- website/docs/guide/basic/api.md | 74 +++++++++++++++++++ website/docs/guide/basic/config.md | 2 +- 10 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 examples/basic-project/src/components/PageUrl.tsx create mode 100644 examples/basic-project/src/pages/client-only.tsx create mode 100644 packages/runtime/src/ClientOnly.tsx create mode 100644 packages/runtime/src/useMounted.tsx create mode 100644 website/docs/guide/basic/api.md diff --git a/examples/basic-project/src/components/PageUrl.tsx b/examples/basic-project/src/components/PageUrl.tsx new file mode 100644 index 000000000..8207d3a3e --- /dev/null +++ b/examples/basic-project/src/components/PageUrl.tsx @@ -0,0 +1,3 @@ +export default function PageUrl() { + return page url is {window.location.href}; +} diff --git a/examples/basic-project/src/pages/client-only.tsx b/examples/basic-project/src/pages/client-only.tsx new file mode 100644 index 000000000..82328ca35 --- /dev/null +++ b/examples/basic-project/src/pages/client-only.tsx @@ -0,0 +1,22 @@ +import { lazy, Suspense } from 'react'; +import { ClientOnly as ClientOnlyComponent, useMounted } from 'ice'; + +export default function ClientOnly() { + const mounted = useMounted(); + + return ( + <> +
{mounted ? 'Client' : 'Server'}
+ + {() => { + const PageUrl = lazy(() => import('@/components/PageUrl')); + return ( + + + + ); + }} + + + ); +} diff --git a/examples/basic-project/src/pages/index.tsx b/examples/basic-project/src/pages/index.tsx index 0f35f9bff..23dd5623f 100644 --- a/examples/basic-project/src/pages/index.tsx +++ b/examples/basic-project/src/pages/index.tsx @@ -1,5 +1,5 @@ import { Suspense, lazy } from 'react'; -import { Link, useData, useAppData, useConfig } from 'ice'; +import { Link, useData, useAppData, useConfig, ClientOnly, useMounted } from 'ice'; // not recommended but works import { useAppContext } from '@ice/runtime'; import { useRequest } from 'ahooks'; @@ -16,6 +16,7 @@ export default function Home(props) { const appData = useAppData(); const data = useData(); const config = useConfig(); + const mounted = useMounted(); if (typeof window !== 'undefined') { console.log('render Home', props); @@ -41,6 +42,19 @@ export default function Home(props) {
userInfo: {JSON.stringify(userInfo)}
data from: {data.from}
+

+

{mounted ? 'Client' : 'Server'}
+ + {() => { + const PageUrl = lazy(() => import('@/components/PageUrl')); + return ( + + + + ); + }} + +

); } diff --git a/packages/ice/src/plugins/web/index.ts b/packages/ice/src/plugins/web/index.ts index 6a379564f..8122ad29d 100644 --- a/packages/ice/src/plugins/web/index.ts +++ b/packages/ice/src/plugins/web/index.ts @@ -37,6 +37,8 @@ const plugin: Plugin = () => ({ 'Data', 'Main', 'history', + 'useMounted', + 'ClientOnly', ], source: '@ice/runtime', }); diff --git a/packages/runtime/src/ClientOnly.tsx b/packages/runtime/src/ClientOnly.tsx new file mode 100644 index 000000000..295308d55 --- /dev/null +++ b/packages/runtime/src/ClientOnly.tsx @@ -0,0 +1,23 @@ +import React, { isValidElement } from 'react'; +import type { ComponentWithChildren } from '@ice/types'; +import useMounted from './useMounted.js'; + +const ClientOnly: ComponentWithChildren<{ fallback: React.ReactNode }> = ({ children, fallback }) => { + const mounted = useMounted(); + + // Ref https://github.com/facebook/docusaurus/blob/v2.1.0/packages/docusaurus/src/client/exports/BrowserOnly.tsx + if (mounted) { + if ( + typeof children !== 'function' && + process.env.NODE_ENV === 'development' + ) { + throw new Error(`Error: The children of must be a "render function", e.g. {() => {window.location.href}}. +Current type: ${isValidElement(children) ? 'React element' : typeof children}`); + } + return <>{children?.()}; + } + + return fallback ?? null; +}; + +export default ClientOnly; diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index bcbf01e79..9f036880a 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -34,6 +34,8 @@ import { import dataLoader from './dataLoader.js'; import getAppConfig, { defineAppConfig } from './appConfig.js'; import { routerHistory as history } from './history.js'; +import ClientOnly from './ClientOnly.js'; +import useMounted from './useMounted.js'; export { getAppConfig, @@ -59,6 +61,9 @@ export { useSearchParams, useLocation, history, + + ClientOnly, + useMounted, }; export type { diff --git a/packages/runtime/src/useMounted.tsx b/packages/runtime/src/useMounted.tsx new file mode 100644 index 000000000..0b38ec91f --- /dev/null +++ b/packages/runtime/src/useMounted.tsx @@ -0,0 +1,10 @@ +import { useState, useEffect } from 'react'; + +export default function useMounted() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + return mounted; +} diff --git a/tests/integration/basic-project.test.ts b/tests/integration/basic-project.test.ts index 3a1ee0d8c..867c852b4 100644 --- a/tests/integration/basic-project.test.ts +++ b/tests/integration/basic-project.test.ts @@ -43,6 +43,12 @@ describe(`build ${example}`, () => { expect(stats.size).toBeLessThan(1024 * 14); }, 120000); + test('ClientOnly Component', async () => { + await page.push('/client-only.html'); + expect(await page.$$text('#mounted')).toStrictEqual(['Server']); + expect(await page.$$text('#page-url')).toStrictEqual([]); + }); + test('disable splitChunks', async () => { await buildFixture(example, { config: 'splitChunks.config.mts', @@ -52,7 +58,7 @@ describe(`build ${example}`, () => { browser = res.browser; const files = fs.readdirSync(path.join(__dirname, `../../examples/${example}/build/js`), 'utf-8'); - expect(files.length).toBe(8); + expect(files.length).toBe(10); }, 120000); test('render route config when downgrade to CSR.', async () => { @@ -93,7 +99,7 @@ describe(`start ${example}`, () => { const routeManifest = fs.readFileSync(path.join(rootDir, '.ice/route-manifest.json'), 'utf-8'); fs.writeFileSync(targetPath, routeContent); await page.reload(); - expect(JSON.parse(routeManifest)[0].children.length).toBe(4); + expect(JSON.parse(routeManifest)[0].children.length).toBe(5); }, 120000); test('update watched file: global.css', () => { @@ -139,6 +145,13 @@ describe(`start ${example}`, () => { ).toBe(1); }, 120000); + test('ClientOnly Component', async () => { + await page.push('/client-only'); + expect(await page.$$text('#mounted')).toStrictEqual(['Client']); + const pageUrlText = await page.$$text('#page-url'); + expect((pageUrlText as string[])[0].endsWith('/client-only')).toBeTruthy(); + }); + afterAll(async () => { await browser.close(); }); diff --git a/website/docs/guide/basic/api.md b/website/docs/guide/basic/api.md new file mode 100644 index 000000000..536bdfb2e --- /dev/null +++ b/website/docs/guide/basic/api.md @@ -0,0 +1,74 @@ +--- +title: API +order: 15 +--- + + +## Hooks + +### `useMounted` + +该方法会在 React Hydrate 完成后返回 `true`,一般在开启 SSR/SSG 的应用中,用于控制在不同端中渲染不同的组件。 + +:::caution + +使用此 `useMounted` 而不是 `typeof windows !== 'undefined'` 来判断当前是否在 Client 端中渲染。 + +因为第一次 Client 端渲染必须与 Server 端渲染的接口一致,如果不使用此 Hook 判断的话,在 Hydrate 时可能出现节点不匹配的情况。 +::: + +使用示例: + +```tsx +import { useMounted } from 'ice'; + +const Home = () => { + const mounted = useMounted(); + return
{mounted ? 'Client' : 'Server'}
; +}; +``` + +## 组件 + +### `` + +`` 组件只允许在 React Hydrate 完成后在 Client 端中渲染组件。 + +:::tip + +用 `` 组件包裹不能在 Node.js 中运行的组件,比如如果组件要访问 `window` 或 `document` 对象。 +::: + +**Props** + +- `children`: 一个函数,且返回仅在浏览器中渲染的组件。该函数不会在 Server 端中执行 +- `fallback`(可选): 在 React Hydrate 完成之前渲染的组件 + +使用示例: + +```tsx +import { ClientOnly } from 'ice'; + +export function Home () { + return ( + loading...}> + {() => page url is {window.location.href}} + + ); +}; +``` + +引入一个组件: + +```tsx +import { ClientOnly } from 'ice'; +import MyComponent from './MyComponent'; + +export function Home () { + return ( + loading...}> + {() => } + + ); +}; +``` diff --git a/website/docs/guide/basic/config.md b/website/docs/guide/basic/config.md index 67b79debf..463938732 100644 --- a/website/docs/guide/basic/config.md +++ b/website/docs/guide/basic/config.md @@ -1,6 +1,6 @@ --- title: 构建配置 -order: 13 +order: 14 --- ICE 支持常用的构建配置项,所有的配置项在 `ice.config.mts` 中设置。 From 1c09a29fdef3be1046d99c7032a600876c654c51 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Wed, 19 Oct 2022 11:13:25 +0800 Subject: [PATCH 09/27] refactor: miniapp runtime (#597) --- examples/miniapp-project/package.json | 1 + packages/ice/package.json | 2 +- packages/ice/src/createService.ts | 4 - packages/ice/src/tasks/web/index.ts | 4 + .../ice/templates/core/entry.client.ts.ejs | 5 +- packages/miniapp-runtime/package.json | 13 ++- .../src/app}/App.tsx | 3 +- .../src/app}/Link.tsx | 6 +- .../src/app}/connect.tsx | 18 ++-- .../src/app}/history.ts | 0 .../src/app}/hooks.ts | 15 ++- .../src/app}/html/constant.ts | 0 .../src/app}/html/runtime.ts | 2 +- .../src/app}/html/utils.ts | 0 .../src/app}/index.ts | 4 +- .../src/app}/react-meta.ts | 0 .../src/app}/runClientApp.tsx | 15 +-- .../src/app}/useSearchParams.ts | 2 +- .../src/app}/utils.ts | 4 +- packages/plugin-miniapp/package.json | 3 +- packages/plugin-miniapp/src/index.ts | 4 +- packages/plugin-miniapp/src/miniapp/index.ts | 3 +- packages/runtime/package.json | 6 +- packages/runtime/src/index.ts | 18 +++- packages/runtime/src/miniapp/runtime.tsx | 92 ------------------- packages/runtime/src/runClientApp.tsx | 8 +- packages/runtime/src/runServerApp.tsx | 2 + packages/runtime/src/runtime.tsx | 3 - pnpm-lock.yaml | 38 +++++--- 29 files changed, 100 insertions(+), 175 deletions(-) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/App.tsx (85%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/Link.tsx (83%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/connect.tsx (98%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/history.ts (100%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/hooks.ts (93%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/html/constant.ts (100%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/html/runtime.ts (98%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/html/utils.ts (100%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/index.ts (66%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/react-meta.ts (100%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/runClientApp.tsx (83%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/useSearchParams.ts (84%) rename packages/{runtime/src/miniapp => miniapp-runtime/src/app}/utils.ts (95%) delete mode 100644 packages/runtime/src/miniapp/runtime.tsx diff --git a/examples/miniapp-project/package.json b/examples/miniapp-project/package.json index 35e383f5a..1d65529c9 100644 --- a/examples/miniapp-project/package.json +++ b/examples/miniapp-project/package.json @@ -15,6 +15,7 @@ "@ice/app": "workspace:*", "@ice/runtime": "workspace:*", "@ice/plugin-miniapp": "workspace:*", + "@ice/miniapp-runtime": "workspace:*", "ahooks": "^3.3.8", "react": "^18.0.0", "react-dom": "^18.0.0" diff --git a/packages/ice/package.json b/packages/ice/package.json index 89d13ee9a..af5652512 100644 --- a/packages/ice/package.json +++ b/packages/ice/package.json @@ -57,7 +57,7 @@ "mrmime": "^1.0.0", "open": "^8.4.0", "path-to-regexp": "^6.2.0", - "regenerator-runtime": "^0.11.0", + "regenerator-runtime": "^0.13.0", "resolve.exports": "^1.1.0", "semver": "^7.3.5", "temp": "^0.9.4" diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 67245d40c..436bdfdfa 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -137,10 +137,6 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt // merge task config with built-in config taskConfigs = mergeTaskConfig(taskConfigs, { port: commandArgs.port, - alias: { - // Get absolute path of `regenerator-runtime`, so it's unnecessary to add it to project dependencies - 'regenerator-runtime': require.resolve('regenerator-runtime'), - }, }); // Get first task config as default platform config. diff --git a/packages/ice/src/tasks/web/index.ts b/packages/ice/src/tasks/web/index.ts index ea3ca7310..7b99dec1b 100644 --- a/packages/ice/src/tasks/web/index.ts +++ b/packages/ice/src/tasks/web/index.ts @@ -1,8 +1,10 @@ import * as path from 'path'; +import { createRequire } from 'module'; import type { Config } from '@ice/types'; import { CACHE_DIR, RUNTIME_TMP_DIR } from '../../constant.js'; import { getRoutePathsFromCache } from '../../utils/getRoutePaths.js'; +const require = createRequire(import.meta.url); const getWebTask = ({ rootDir, command, dataCache }): Config => { // basic task config of web task const defaultLogging = command === 'start' ? 'summary' : 'summary assets'; @@ -15,6 +17,8 @@ const getWebTask = ({ rootDir, command, dataCache }): Config => { '@': path.join(rootDir, 'src'), // set alias for webpack/hot while webpack has been prepacked 'webpack/hot': '@ice/bundles/compiled/webpack/hot', + // Get absolute path of `regenerator-runtime`, so it's unnecessary to add it to project dependencies + 'regenerator-runtime': require.resolve('regenerator-runtime'), }, swcOptions: { // getData is built by data-loader diff --git a/packages/ice/templates/core/entry.client.ts.ejs b/packages/ice/templates/core/entry.client.ts.ejs index e40dd5331..b42de4432 100644 --- a/packages/ice/templates/core/entry.client.ts.ejs +++ b/packages/ice/templates/core/entry.client.ts.ejs @@ -1,7 +1,6 @@ import { runClientApp, getAppConfig } from '<%- iceRuntimePath %>'; import * as app from '@/app'; import runtimeModules from './runtimeModules'; - <% if (enableRoutes) { %> import routes from './routes'; <% } %> @@ -13,9 +12,7 @@ const getRouterBasename = () => { runClientApp({ app, runtimeModules, - <% if (enableRoutes) { %> - routes, - <% } %> + <% if (enableRoutes) { %>routes,<% } %> basename: getRouterBasename(), hydrate: <%- hydrate %>, memoryRouter: <%- memoryRouter || false %>, diff --git a/packages/miniapp-runtime/package.json b/packages/miniapp-runtime/package.json index 01782203b..0d322fba3 100644 --- a/packages/miniapp-runtime/package.json +++ b/packages/miniapp-runtime/package.json @@ -6,7 +6,8 @@ "types": "./esm/index.d.ts", "main": "./esm/index.js", "exports": { - ".": "./esm/index.js" + ".": "./esm/index.js", + "./app": "./esm/app/index.js" }, "files": [ "esm", @@ -24,6 +25,14 @@ "sideEffects": false, "dependencies": { "@ice/shared": "^1.0.0", - "@ice/types": "^1.0.0" + "@ice/types": "^1.0.0", + "@ice/runtime": "^1.0.0", + "miniapp-history": "^0.1.7" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "history": "^5.3.0", + "react": "^18.1.0", + "react-dom": "^18.1.0" } } diff --git a/packages/runtime/src/miniapp/App.tsx b/packages/miniapp-runtime/src/app/App.tsx similarity index 85% rename from packages/runtime/src/miniapp/App.tsx rename to packages/miniapp-runtime/src/app/App.tsx index a552bc7e1..63f170453 100644 --- a/packages/runtime/src/miniapp/App.tsx +++ b/packages/miniapp-runtime/src/app/App.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import AppErrorBoundary from '../AppErrorBoundary.js'; -import { useAppContext } from '../AppContext.js'; +import { AppErrorBoundary, useAppContext } from '@ice/runtime'; import { AppWrapper } from './connect.js'; interface Props { diff --git a/packages/runtime/src/miniapp/Link.tsx b/packages/miniapp-runtime/src/app/Link.tsx similarity index 83% rename from packages/runtime/src/miniapp/Link.tsx rename to packages/miniapp-runtime/src/app/Link.tsx index 40941510e..5623b6768 100644 --- a/packages/runtime/src/miniapp/Link.tsx +++ b/packages/miniapp-runtime/src/app/Link.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Current } from '@ice/miniapp-runtime'; +import { Current } from '../current.js'; -interface ILinkProps extends React.ComponentProps { +interface LinkProps extends React.ComponentProps { to: string; } @@ -25,7 +25,7 @@ function matchRoute(url: string, routes: Array): string | undefined { return query ? `${matchedRoute}?${query}` : matchedRoute; } -export default function Link(props: ILinkProps) { +export default function Link(props: LinkProps) { const { routes } = Current.app.config; const url = matchRoute(props.to, routes); // @ts-ignore diff --git a/packages/runtime/src/miniapp/connect.tsx b/packages/miniapp-runtime/src/app/connect.tsx similarity index 98% rename from packages/runtime/src/miniapp/connect.tsx rename to packages/miniapp-runtime/src/app/connect.tsx index 9d250608b..9bc0689f9 100644 --- a/packages/runtime/src/miniapp/connect.tsx +++ b/packages/miniapp-runtime/src/app/connect.tsx @@ -1,17 +1,17 @@ +import { EMPTY_OBJ, hooks } from '@ice/shared'; +import type { MiniappAppConfig } from '@ice/types'; +import React, { createElement } from 'react'; +import * as ReactDOM from 'react-dom'; +import { ConfigProvider, DataProvider } from '@ice/runtime'; +import { Current, getPageInstance, + incrementId, injectPageInstance, +} from '../index.js'; import type { MountOptions, AppInstance, Instance, PageLifeCycle, PageProps, ReactAppInstance, ReactPageComponent, -} from '@ice/miniapp-runtime'; -import { Current, getPageInstance, - incrementId, injectPageInstance, -} from '@ice/miniapp-runtime'; -import { EMPTY_OBJ, hooks } from '@ice/shared'; -import type { MiniappAppConfig } from '@ice/types'; -import React, { createElement } from 'react'; -import * as ReactDOM from 'react-dom'; -import { ConfigProvider, DataProvider } from '../RouteContext.js'; +} from '../index.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/runtime/src/miniapp/history.ts b/packages/miniapp-runtime/src/app/history.ts similarity index 100% rename from packages/runtime/src/miniapp/history.ts rename to packages/miniapp-runtime/src/app/history.ts diff --git a/packages/runtime/src/miniapp/hooks.ts b/packages/miniapp-runtime/src/app/hooks.ts similarity index 93% rename from packages/runtime/src/miniapp/hooks.ts rename to packages/miniapp-runtime/src/app/hooks.ts index 06b23a548..c257dc0bb 100644 --- a/packages/runtime/src/miniapp/hooks.ts +++ b/packages/miniapp-runtime/src/app/hooks.ts @@ -1,12 +1,11 @@ -import type { - AppInstance, - Func, - PageLifeCycle, -} from '@ice/miniapp-runtime'; -import { Current, getPageInstance, - injectPageInstance, -} from '@ice/miniapp-runtime'; import { isArray, isFunction } from '@ice/shared'; +import type { AppInstance, PageLifeCycle } from '../dsl/instance.js'; +import type { Func } from '../interface/index.js'; +import { Current } from '../current.js'; +import { + getPageInstance, + injectPageInstance, +} from '../dsl/common.js'; import { reactMeta } from './react-meta.js'; import { HOOKS_APP_ID } from './utils.js'; diff --git a/packages/runtime/src/miniapp/html/constant.ts b/packages/miniapp-runtime/src/app/html/constant.ts similarity index 100% rename from packages/runtime/src/miniapp/html/constant.ts rename to packages/miniapp-runtime/src/app/html/constant.ts diff --git a/packages/runtime/src/miniapp/html/runtime.ts b/packages/miniapp-runtime/src/app/html/runtime.ts similarity index 98% rename from packages/runtime/src/miniapp/html/runtime.ts rename to packages/miniapp-runtime/src/app/html/runtime.ts index 119e3aade..3814db607 100644 --- a/packages/runtime/src/miniapp/html/runtime.ts +++ b/packages/miniapp-runtime/src/app/html/runtime.ts @@ -1,5 +1,5 @@ -import type { Element } from '@ice/miniapp-runtime'; import { hooks, Shortcuts, warn } from '@ice/shared'; +import type { Element } from '../../dom/element.js'; import { defineMappedProp, diff --git a/packages/runtime/src/miniapp/html/utils.ts b/packages/miniapp-runtime/src/app/html/utils.ts similarity index 100% rename from packages/runtime/src/miniapp/html/utils.ts rename to packages/miniapp-runtime/src/app/html/utils.ts diff --git a/packages/runtime/src/miniapp/index.ts b/packages/miniapp-runtime/src/app/index.ts similarity index 66% rename from packages/runtime/src/miniapp/index.ts rename to packages/miniapp-runtime/src/app/index.ts index 9064ff60b..153c5c4c9 100644 --- a/packages/runtime/src/miniapp/index.ts +++ b/packages/miniapp-runtime/src/app/index.ts @@ -1,6 +1,4 @@ -import getAppConfig, { defineAppConfig } from '../appConfig.js'; -import { useAppData } from '../AppData.js'; -import { useData, useConfig } from '../RouteContext.js'; +import { getAppConfig, defineAppConfig, useAppData, useData, useConfig } from '@ice/runtime'; import runClientApp from './runClientApp.js'; import Link from './Link.js'; import useSearchParams from './useSearchParams.js'; diff --git a/packages/runtime/src/miniapp/react-meta.ts b/packages/miniapp-runtime/src/app/react-meta.ts similarity index 100% rename from packages/runtime/src/miniapp/react-meta.ts rename to packages/miniapp-runtime/src/app/react-meta.ts diff --git a/packages/runtime/src/miniapp/runClientApp.tsx b/packages/miniapp-runtime/src/app/runClientApp.tsx similarity index 83% rename from packages/runtime/src/miniapp/runClientApp.tsx rename to packages/miniapp-runtime/src/app/runClientApp.tsx index bbb7bd7a8..179cf4e6a 100644 --- a/packages/runtime/src/miniapp/runClientApp.tsx +++ b/packages/miniapp-runtime/src/app/runClientApp.tsx @@ -1,22 +1,13 @@ import React from 'react'; - import type { - AppContext, AppExport, RouteWrapperConfig, RuntimeModules, + AppContext, RouteWrapperConfig, } from '@ice/types'; -import { AppContextProvider } from '../AppContext.js'; -import { AppDataProvider, getAppData } from '../AppData.js'; - -import getAppConfig from '../appConfig.js'; -import Runtime from './runtime.js'; +import { AppContextProvider, AppDataProvider, getAppData, getAppConfig, Runtime } from '@ice/runtime'; +import type { RunClientAppOptions } from '@ice/runtime'; import App from './App.js'; import { createMiniApp } from './connect.js'; import { setHistory } from './history.js'; -interface RunClientAppOptions { - app: AppExport; - runtimeModules: RuntimeModules; -} - export default async function runClientApp(options: RunClientAppOptions) { const { app, runtimeModules } = options; const appData = await getAppData(app); diff --git a/packages/runtime/src/miniapp/useSearchParams.ts b/packages/miniapp-runtime/src/app/useSearchParams.ts similarity index 84% rename from packages/runtime/src/miniapp/useSearchParams.ts rename to packages/miniapp-runtime/src/app/useSearchParams.ts index 231e9379a..7cf2044c7 100644 --- a/packages/runtime/src/miniapp/useSearchParams.ts +++ b/packages/miniapp-runtime/src/app/useSearchParams.ts @@ -1,4 +1,4 @@ -import { Current } from '@ice/miniapp-runtime'; +import { Current } from '../current.js'; export default function useSearchParams() { const searchParams = Current.router.params; diff --git a/packages/runtime/src/miniapp/utils.ts b/packages/miniapp-runtime/src/app/utils.ts similarity index 95% rename from packages/runtime/src/miniapp/utils.ts rename to packages/miniapp-runtime/src/app/utils.ts index 6c0ddafa2..71f9d5d3b 100644 --- a/packages/runtime/src/miniapp/utils.ts +++ b/packages/miniapp-runtime/src/app/utils.ts @@ -1,6 +1,6 @@ -import { Current } from '@ice/miniapp-runtime'; -import { isFunction } from '@ice/shared'; import type React from 'react'; +import { isFunction } from '@ice/shared'; +import { Current } from '../current.js'; export const HOOKS_APP_ID = 'ice-miniapp'; diff --git a/packages/plugin-miniapp/package.json b/packages/plugin-miniapp/package.json index 68f0c7ac1..ddcfb46b5 100644 --- a/packages/plugin-miniapp/package.json +++ b/packages/plugin-miniapp/package.json @@ -26,7 +26,8 @@ "@ice/miniapp-runtime": "^1.0.0", "@ice/miniapp-react-dom": "^1.0.0", "@ice/miniapp-loader": "^1.0.0", - "@ice/bundles": "^0.1.0" + "@ice/bundles": "^0.1.0", + "regenerator-runtime": "^0.11.0" }, "devDependencies": { "@ice/types": "^1.0.0", diff --git a/packages/plugin-miniapp/src/index.ts b/packages/plugin-miniapp/src/index.ts index bb3c119e4..8946548d2 100644 --- a/packages/plugin-miniapp/src/index.ts +++ b/packages/plugin-miniapp/src/index.ts @@ -20,7 +20,9 @@ const plugin: Plugin = () => ({ getAppConfig: async () => ({}), getRoutesConfig: async () => ({}), }; - const miniappRuntime = '@ice/runtime/miniapp'; + // Recommand add @ice/miniapp-runtime in dependencies when use pnpm. + // Use `@ice/miniapp-runtime/esm/app` for vscode type hint. + const miniappRuntime = '@ice/miniapp-runtime/esm/app'; generator.addExport({ specifier: [ 'defineAppConfig', diff --git a/packages/plugin-miniapp/src/miniapp/index.ts b/packages/plugin-miniapp/src/miniapp/index.ts index df076ef7f..ce74fe234 100644 --- a/packages/plugin-miniapp/src/miniapp/index.ts +++ b/packages/plugin-miniapp/src/miniapp/index.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License * https://github.com/NervJS/taro/blob/next/LICENSE * */ - import * as path from 'path'; import { createRequire } from 'node:module'; import fg from 'fast-glob'; @@ -41,7 +40,6 @@ const getMiniappTask = ({ const entry = getEntry(rootDir, runtimeDir); const mode = command === 'start' ? 'development' : 'production'; const { template, globalObject, fileType } = getMiniappPlatformConfig(platform); - const { plugins, module } = getMiniappWebpackConfig({ rootDir, template, @@ -67,6 +65,7 @@ const getMiniappTask = ({ '@': path.join(rootDir, 'src'), // 小程序使用 regenerator-runtime@0.11 'regenerator-runtime': require.resolve('regenerator-runtime'), + '@ice/miniapp-runtime/esm/app': require.resolve('@ice/miniapp-runtime/app'), // 开发组件库时 link 到本地调试,runtime 包需要指向本地 node_modules 顶层的 runtime,保证闭包值 Current 一致,shared 也一样 '@ice/miniapp-runtime': require.resolve('@ice/miniapp-runtime'), '@ice/shared': require.resolve('@ice/shared'), diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a4875595d..4a45bebc7 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -13,8 +13,7 @@ "./jsx-dev-runtime": "./esm/jsx-dev-runtime.js", "./matchRoutes": "./esm/matchRoutes.js", "./router": "./esm/router.js", - "./single-router": "./esm/single-router.js", - "./miniapp": "./esm/miniapp/index.js" + "./single-router": "./esm/single-router.js" }, "files": [ "esm", @@ -40,11 +39,8 @@ "sideEffects": false, "dependencies": { "@ice/jsx-runtime": "^0.1.0", - "@ice/miniapp-runtime": "^1.0.0", - "@ice/shared": "^1.0.0", "@ice/types": "^1.0.0", "history": "^5.3.0", - "miniapp-history": "^0.1.7", "react-router-dom": "^6.2.2" }, "peerDependencies": { diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 9f036880a..69c57c415 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -20,9 +20,10 @@ import type { import Runtime from './runtime.js'; import App from './App.js'; import runClientApp from './runClientApp.js'; -import { useAppContext } from './AppContext.js'; -import { useAppData } from './AppData.js'; -import { useData, useConfig } from './RouteContext.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 { Meta, Title, @@ -32,6 +33,8 @@ import { Data, } from './Document.js'; import dataLoader from './dataLoader.js'; +import AppRouter from './AppRouter.js'; +import AppErrorBoundary from './AppErrorBoundary.js'; import getAppConfig, { defineAppConfig } from './appConfig.js'; import { routerHistory as history } from './history.js'; import ClientOnly from './ClientOnly.js'; @@ -43,9 +46,14 @@ export { Runtime, App, runClientApp, + AppContextProvider, useAppContext, + AppDataProvider, useAppData, useData, + getAppData, + DataProvider, + ConfigProvider, useConfig, Meta, Title, @@ -61,7 +69,8 @@ export { useSearchParams, useLocation, history, - + AppRouter, + AppErrorBoundary, ClientOnly, useMounted, }; @@ -77,4 +86,5 @@ export type { RouteWrapper, RenderMode, GetAppData, + RunClientAppOptions, }; diff --git a/packages/runtime/src/miniapp/runtime.tsx b/packages/runtime/src/miniapp/runtime.tsx deleted file mode 100644 index 704f4d827..000000000 --- a/packages/runtime/src/miniapp/runtime.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import type { - Renderer, - AppContext, - RuntimePlugin, - CommonJsRuntime, - RuntimeAPI, - AddProvider, - AddWrapper, - RouteWrapperConfig, - SetRender, - ComponentWithChildren, -} from '@ice/types'; -import { useData, useConfig } from '../RouteContext.js'; -import { useAppContext } from '../AppContext.js'; - -class Runtime { - private appContext: AppContext; - - private AppProvider: ComponentWithChildren[]; - - private RouteWrappers: RouteWrapperConfig[]; - - private render: Renderer; - - public constructor(appContext: AppContext) { - this.AppProvider = []; - this.appContext = appContext; - this.render = (container, element) => { - const root = ReactDOM.createRoot(container); - root.render(element); - }; - this.RouteWrappers = []; - } - - public getAppContext = () => this.appContext; - - public getRender = () => { - return this.render; - }; - - - public getWrappers = () => this.RouteWrappers; - - public async loadModule(module: RuntimePlugin | CommonJsRuntime) { - let runtimeAPI: RuntimeAPI = { - addProvider: this.addProvider, - setRender: this.setRender, - addWrapper: this.addWrapper, - appContext: this.appContext, - useData, - useConfig, - useAppContext, - }; - - const runtimeModule = (module as CommonJsRuntime).default || module as RuntimePlugin; - if (module) { - return await runtimeModule(runtimeAPI); - } - } - - public composeAppProvider() { - if (!this.AppProvider.length) return null; - return this.AppProvider.reduce((ProviderComponent, CurrentProvider) => { - return ({ children, ...rest }) => { - const element = CurrentProvider - ? {children} - : children; - return {element}; - }; - }); - } - - public addProvider: AddProvider = (Provider) => { - // must promise user's providers are wrapped by the plugins' providers - this.AppProvider.unshift(Provider); - }; - - public setRender: SetRender = (render) => { - this.render = render; - }; - - private addWrapper: AddWrapper = (Wrapper, forLayout?: boolean) => { - this.RouteWrappers.push({ - Wrapper, - layout: forLayout, - }); - }; -} - -export default Runtime; diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index 401cf6c6f..7f313ccba 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -17,12 +17,13 @@ import { updateRoutesConfig } from './routesConfig.js'; import getRequestContext from './requestContext.js'; import getAppConfig from './appConfig.js'; import matchRoutes from './matchRoutes.js'; +import DefaultAppRouter from './AppRouter.js'; -interface RunClientAppOptions { +export interface RunClientAppOptions { app: AppExport; - routes: RouteItem[]; runtimeModules: RuntimeModules; - hydrate: boolean; + routes?: RouteItem[]; + hydrate?: boolean; basename?: string; memoryRouter?: boolean; } @@ -88,6 +89,7 @@ export default async function runClientApp(options: RunClientAppOptions) { }; const runtime = new Runtime(appContext); + runtime.setAppRouter(DefaultAppRouter); if (hydrate && !downgrade) { runtime.setRender((container, element) => { diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx index 8a81178cb..4b61f17ea 100644 --- a/packages/runtime/src/runServerApp.tsx +++ b/packages/runtime/src/runServerApp.tsx @@ -28,6 +28,7 @@ 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'; interface RenderOptions { app: AppExport; @@ -257,6 +258,7 @@ async function renderServerEntry( }; const runtime = new Runtime(appContext); + runtime.setAppRouter(DefaultAppRouter); await Promise.all(runtimeModules.map(m => runtime.loadModule(m)).filter(Boolean)); const staticNavigator = createStaticNavigator(); diff --git a/packages/runtime/src/runtime.tsx b/packages/runtime/src/runtime.tsx index 3199e8c59..1392ba548 100644 --- a/packages/runtime/src/runtime.tsx +++ b/packages/runtime/src/runtime.tsx @@ -15,7 +15,6 @@ import type { AppRouterProps, ComponentWithChildren, } from '@ice/types'; -import DefaultAppRouter from './AppRouter.js'; import { useData, useConfig } from './RouteContext.js'; import { useAppContext } from './AppContext.js'; @@ -37,7 +36,6 @@ class Runtime { const root = ReactDOM.createRoot(container); root.render(element); }; - this.AppRouter = DefaultAppRouter; this.RouteWrappers = []; } @@ -97,7 +95,6 @@ class Runtime { }); }; - // for plugin-icestark public setAppRouter: SetAppRouter = (AppRouter) => { this.AppRouter = AppRouter; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77a8860e2..18cfa05bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,7 @@ importers: examples/miniapp-project: specifiers: '@ice/app': workspace:* + '@ice/miniapp-runtime': workspace:* '@ice/plugin-miniapp': workspace:* '@ice/runtime': workspace:* '@types/react': ^18.0.0 @@ -222,6 +223,7 @@ importers: webpack: ^5.73.0 dependencies: '@ice/app': link:../../packages/ice + '@ice/miniapp-runtime': link:../../packages/miniapp-runtime '@ice/plugin-miniapp': link:../../packages/plugin-miniapp '@ice/runtime': link:../../packages/runtime ahooks: 3.7.1_react@18.2.0 @@ -729,7 +731,7 @@ importers: path-to-regexp: ^6.2.0 react: ^18.2.0 react-router: ^6.3.0 - regenerator-runtime: ^0.11.0 + regenerator-runtime: ^0.13.0 resolve.exports: ^1.1.0 semver: ^7.3.5 temp: ^0.9.4 @@ -764,7 +766,7 @@ importers: mrmime: 1.0.1 open: 8.4.0 path-to-regexp: 6.2.1 - regenerator-runtime: 0.11.1 + regenerator-runtime: 0.13.9 resolve.exports: 1.1.0 semver: 7.3.7 temp: 0.9.4 @@ -831,11 +833,24 @@ importers: packages/miniapp-runtime: specifiers: + '@ice/runtime': ^1.0.0 '@ice/shared': ^1.0.0 '@ice/types': ^1.0.0 + '@types/react': ^18.0.0 + history: ^5.3.0 + miniapp-history: ^0.1.7 + react: ^18.1.0 + react-dom: ^18.1.0 dependencies: + '@ice/runtime': link:../runtime '@ice/shared': link:../shared '@ice/types': link:../types + miniapp-history: 0.1.7 + devDependencies: + '@types/react': 18.0.21 + history: 5.3.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 packages/plugin-antd: specifiers: @@ -918,6 +933,7 @@ importers: consola: ^2.15.3 fast-glob: ^3.2.11 html-minifier: ^4.0.0 + regenerator-runtime: ^0.11.0 sax: ^1.2.4 webpack: ^5.73.0 dependencies: @@ -931,6 +947,7 @@ importers: consola: 2.15.3 fast-glob: 3.2.12 html-minifier: 4.0.0 + regenerator-runtime: 0.11.1 sax: 1.2.4 devDependencies: '@ice/types': link:../types @@ -1020,7 +1037,7 @@ importers: react-dom: ^18.2.0 regenerator-runtime: ^0.13.9 dependencies: - '@ice/store': 2.0.3 + '@ice/store': 2.0.3_biqbaboplfbrettd7655fr4n2y fast-glob: 3.2.12 micromatch: 4.0.5 devDependencies: @@ -1066,24 +1083,18 @@ importers: packages/runtime: specifiers: '@ice/jsx-runtime': ^0.1.0 - '@ice/miniapp-runtime': ^1.0.0 - '@ice/shared': ^1.0.0 '@ice/types': ^1.0.0 '@types/react': ^18.0.8 '@types/react-dom': ^18.0.3 history: ^5.3.0 - miniapp-history: ^0.1.7 react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.2.2 regenerator-runtime: ^0.13.9 dependencies: '@ice/jsx-runtime': link:../jsx-runtime - '@ice/miniapp-runtime': link:../miniapp-runtime - '@ice/shared': link:../shared '@ice/types': link:../types history: 5.3.0 - miniapp-history: 0.1.7 react-router-dom: 6.4.1_biqbaboplfbrettd7655fr4n2y devDependencies: '@types/react': 18.0.21 @@ -4236,14 +4247,15 @@ packages: - ts-node dev: true - /@ice/store/2.0.3: + /@ice/store/2.0.3_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-U1YcY380bejqc3+WtkEqIwE6HnjBjSKd4IWFyq8gakPeAvA6fEJ58Qx9hzscYxlogWbiCb0Wm9kqkcDU+njx7g==} peerDependencies: react: ^16.8 || ^17 || ^18 dependencies: immer: 9.0.15 lodash.isfunction: 3.0.9 - react-redux: 7.2.9 + react: 18.2.0 + react-redux: 7.2.9_biqbaboplfbrettd7655fr4n2y redux: 4.2.0 redux-thunk: 2.4.1_redux@4.2.0 transitivePeerDependencies: @@ -16131,7 +16143,7 @@ packages: scheduler: 0.21.0 dev: false - /react-redux/7.2.9: + /react-redux/7.2.9_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: react: ^16.8.3 || ^17 || ^18 @@ -17760,6 +17772,7 @@ packages: serialize-javascript: 6.0.0 terser: 5.14.2 webpack: 5.74.0_esbuild@0.14.54 + dev: true /terser-webpack-plugin/5.3.5_webpack@5.74.0: resolution: {integrity: sha512-AOEDLDxD2zylUGf/wxHxklEkOe2/r+seuyOWujejFrIxHf11brA1/dWQNIgXa1c6/Wkxgu7zvv0JhOWfc2ELEA==} @@ -19239,6 +19252,7 @@ packages: - '@swc/core' - esbuild - uglify-js + dev: true /webpackbar/5.0.2_webpack@5.74.0: resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==} From 539bb92c2df420017fbd02597ff577665f3e41de Mon Sep 17 00:00:00 2001 From: ZeroLing Date: Wed, 19 Oct 2022 15:20:02 +0800 Subject: [PATCH 10/27] Docs/update (#593) * docs: add docs for plugin list (#589) * docs: add docs for plugin list * docs: update store * docs: update to ice.js3 * docs: update docs * docs: update * docs: update directory (#590) * docs: ICE to ice.js * docs: typos Co-authored-by: ClarkXia --- website/docs/guide/about.md | 6 +- website/docs/guide/advanced/auth.md | 210 ++++++++++++++++- .../{plugins => advanced}/css-assets-local.md | 0 website/docs/guide/advanced/deploy.md | 3 +- website/docs/guide/advanced/faas.md | 1 - website/docs/guide/advanced/i18n.md | 1 - .../docs/guide/advanced/integrate-from-rax.md | 19 +- .../docs/guide/advanced/micro-frontends.md | 1 - .../docs/guide/{plugins => advanced}/store.md | 0 website/docs/guide/advanced/unit-test.md | 1 - website/docs/guide/basic/assets.md | 4 +- website/docs/guide/basic/config.md | 14 +- website/docs/guide/basic/router.md | 2 +- website/docs/guide/basic/style.md | 12 +- website/docs/guide/plugins/auth.md | 211 ------------------ website/docs/guide/plugins/plugin-dev.md | 8 +- website/docs/guide/plugins/plugin-list.md | 6 +- 17 files changed, 245 insertions(+), 254 deletions(-) rename website/docs/guide/{plugins => advanced}/css-assets-local.md (100%) rename website/docs/guide/{plugins => advanced}/store.md (100%) delete mode 100644 website/docs/guide/plugins/auth.md diff --git a/website/docs/guide/about.md b/website/docs/guide/about.md index 5b300bb94..3e2ca56a6 100644 --- a/website/docs/guide/about.md +++ b/website/docs/guide/about.md @@ -17,7 +17,7 @@ order: 1 在应用框架之上,我们还提供了 NPM 包开发工具 [ICE PKG](https://pkg.ice.work): -- 提供 React 组件开发、Node.js 模块开发、前端通用库等[多场景需求](https://pkg.ice.work/scenarios/component) +- 提供 React 组件开发、Node.js 模块开发、前端通用库等[多场景需求](https://pkg.ice.work/scenarios/react) - 组件开发提供基础研发范式,提供组件文档、示例、预览等功能,[查看文档](https://pkg.ice.work/guide/preview) - 更多场景可以通过插件的方式完成定制,查看[插件开发](https://pkg.ice.work/reference/plugins-development) @@ -41,7 +41,7 @@ Webpack 只提供了基础的构建能力,ice.js 在此基础上扩展了很 ### 我正在使用 ice.js 2,需要升级到 ice.js 3 吗? -ice.js 3 相比之前的版本,增加了更多对移动端能力的优化和适配,同时提升了页面性能体验。对于新项目推荐 ice.js 3 进行开发,对于历史项目原先的 ice.js 2 依然是可用的,并且我们仍会持续修复已知的问题。 +ice.js 3 相比之前的版本,增加了更多对移动端能力的优化和适配,同时提升了页面性能体验。对于新项目推荐 ice.js 3 进行开发,对于历史项目原先的 ice.js 2.x 依然是可用的,并且我们仍会持续修复已知的问题。 如果你的页面会同时运行在移动端和桌面端,使用 ice.js 3 可能会是更好的选择,亦或者是你对 ice.js 3 提供的更新的构建工具链、更优更多的解决方案感兴趣,你都可以选择升级到 ice.js 3。 @@ -57,7 +57,7 @@ ice.js 3 相比之前的版本,增加了更多对移动端能力的优化和 应用框架 ice.js 默认使用的是 React 18,你可以查看 React 18 官方说明[对 JavaScript 环境的要求](https://zh-hans.reactjs.org/docs/javascript-environment-requirements.html)。如果你支持旧的浏览器和设备,可能需要引入对应的 Polyfill。 -此外,飞冰官方 React 物料默认使用 React 16+ 进行开发,所以通常情况下这些物料在 ICE 中是可以正常运行的,如果你遇到任何问题,也可以通过 [Issue](https://github.com/alibaba/ice/issues) 或其它方式反馈给我们。 +此外,飞冰官方 React 物料默认使用 React 16+ 进行开发,所以通常情况下这些物料在 ice.js 中是可以正常运行的,如果你遇到任何问题,也可以通过 [Issue](https://github.com/alibaba/ice/issues) 或其它方式反馈给我们。 ### 飞冰可以使用哪些 UI 组件? diff --git a/website/docs/guide/advanced/auth.md b/website/docs/guide/advanced/auth.md index 03360a6f5..c1b136629 100644 --- a/website/docs/guide/advanced/auth.md +++ b/website/docs/guide/advanced/auth.md @@ -1,7 +1,211 @@ --- title: 权限管理 -order: 7 -hide: true --- -@TODO +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +
+ 示例 + +
+ +对于一个 Web 应用,权限管理是经常会涉及的需求之一,通常包含以下几种常见的权限管理类型: + +- 页面权限:当用户访问某个没有权限的页面时跳转到无权限页面 +- 操作权限:页面中的某些按钮或组件针对无权限的用户直接隐藏 +- 接口权限:当用户通过操作调用没有权限的接口时跳转到无权限页面 + +ice.js 提供 `@ice/plugin-auth` 插件,帮助用户更简单管理前两种类型的权限。接口权限管理请见数据请求文档。 + +## 安装插件 + +安装插件: + +```bash +$ npm i @ice/plugin-auth -D +``` + +在 `ice.config.mts` 中添加插件: + +```ts title="ice.config.mts" +import { defineConfig } from '@ice/app'; +import auth from '@ice/plugin-auth'; + +export default defineConfig({ + plugins: [ + auth(), + ], +}); +``` + +## 初始化权限数据 + +大多数情况下权限管理通常需要从服务端获取权限数据,然后在前端通过权限对比以此控制页面、操作等等权限行为。约定在 `src/app.ts` 中导出 `auth` 对象,该对象包含从服务端异步获取初始化的权限数据,并且约定最终返回格式为 `{ initialAuth: { [key: string]: boolean } }`。 + +```ts title="src/app.ts" +import { defineAuthConfig } from '@ice/plugin-auth/esm/types'; + +export const auth = defineAuthConfig(async () => { + // 模拟请求权限数据 + // const data = (await fetch('/api/auth')).json(); + return { + initialAuth: { + admin: true, + guest: false, + }, + }; +}); +``` + +## 页面权限 + +如需对某些页面进行权限控制,只需在页面组件的 `getConfig` 中配置准入权限即可。 + + + + +```tsx +export default function Home() { + return
Home
+} + +export function getConfig() { + return { + // 当前用户是 admin 时,有权限访问该页面 + auth: ['admin'], + }; +} +``` + +
+ + +```tsx +export default function About() { + return
About
+} + +export function getConfig() { + return { + // 当前用户是 admin 时,无权限访问该页面 + auth: ['guest'], + }; +} +``` + +
+
+ +## 操作权限 + +在某些场景下,如某个组件中要根据角色判断是否有操作权限,我们可以通过 useAuth Hooks 在组件中获取权限数据,同时也可以更新初始的权限数据。 + +### 获取权限数据 + +```tsx +import React from 'react'; +import { useAuth } from 'ice'; + +function Foo() { + const [auth] = useAuth(); + return ( + <> + 当前用户权限数据: + {JSON.stringify(auth)} + + ); +} +``` + +### 设置权限数据 + +```tsx +import React from 'react'; +import { useAuth } from 'ice'; + +function Home() { + const [auth, setAuth] = useAuth(); + + // 更新权限,与默认的 auth 数据进行合并 + function updateAuth() { + setAuth({ admin: false, guest: true }); + } + + return ( + <> + 当前用户角色: + {JSON.stringify(auth)} + + + ); +} +``` + +### 自定义权限组件 + +对于操作类权限,通常我们可以自定义封装权限组件,以便更细粒度的控制权限和复用。 + +```tsx +import React from 'react'; +import { useAuth } from 'ice'; +import NoAuth from '@/components/NoAuth'; + +function Auth({ children, authKey, fallback }) { + const [auth] = useAuth(); + // 判断是否有权限 + const hasAuth = auth[authKey]; + + // 有权限时直接渲染内容 + if (hasAuth) { + return children; + } else { + // 无权限时显示指定 UI + return fallback || NoAuth; + } +} + +export default Auth; +``` + +使用如下: + +```tsx +function Foo() { + return ( + + + + ); +} +``` + +## 自定义 Fallback + +支持自定义无权限时的展示组件,默认为 `<>No Auth` + +```diff title="src/app.tsx" +import { defineAuthConfig } from '@ice/plugin-auth/esm/types'; + +export const auth = defineAuthConfig(async () => { + return { + initialAuth: { + admin: true, + }, ++ NoAuthFallback: (routeConfig) => { ++ console.log(routeConfig); // 当前页面的配置 ++ return ( ++
没有权限
++ ) ++ }, ++ }; +}); +``` diff --git a/website/docs/guide/plugins/css-assets-local.md b/website/docs/guide/advanced/css-assets-local.md similarity index 100% rename from website/docs/guide/plugins/css-assets-local.md rename to website/docs/guide/advanced/css-assets-local.md diff --git a/website/docs/guide/advanced/deploy.md b/website/docs/guide/advanced/deploy.md index dfa4b3588..386c21bc1 100644 --- a/website/docs/guide/advanced/deploy.md +++ b/website/docs/guide/advanced/deploy.md @@ -1,6 +1,5 @@ --- title: 部署 -order: 10 --- 前端代码开发完成后,我们会执行 `npm build` 命令进行项目构建。构建完成后,我们需要把 js/css/html 等静态资源部署到服务器或者发布到 CDN 上。 @@ -37,7 +36,7 @@ npm install --global surge #### 运行 surge -以 `ice-demo` 项目名,ice 项目默认构建目录 `build` 为例: +以 `ice-demo` 项目名,ice.js 项目默认构建目录 `build` 为例: ```bash $ cd ice-demo/build diff --git a/website/docs/guide/advanced/faas.md b/website/docs/guide/advanced/faas.md index c758f0e53..90b75b5d4 100644 --- a/website/docs/guide/advanced/faas.md +++ b/website/docs/guide/advanced/faas.md @@ -1,6 +1,5 @@ --- title: 一体化 -order: 8 hide: true --- diff --git a/website/docs/guide/advanced/i18n.md b/website/docs/guide/advanced/i18n.md index ec9c0b601..3f5573ea1 100644 --- a/website/docs/guide/advanced/i18n.md +++ b/website/docs/guide/advanced/i18n.md @@ -1,6 +1,5 @@ --- title: 国际化 -order: 6 hide: true --- diff --git a/website/docs/guide/advanced/integrate-from-rax.md b/website/docs/guide/advanced/integrate-from-rax.md index ff63184ed..8d09d9a72 100644 --- a/website/docs/guide/advanced/integrate-from-rax.md +++ b/website/docs/guide/advanced/integrate-from-rax.md @@ -1,19 +1,18 @@ --- title: 从 Rax 迁移 -order: 20 --- -本文档面向的是使用 Rax App 的开发者,提供迁移到 ICE 的方式。React 的社区生态显著优于 Rax,切换到 React 之后可以享受到更多的 React 生态,复用复杂场景(富文本、脑图等)社区生态可以大幅度降低成本。 +本文档面向的是使用 Rax App 的开发者,提供迁移到 ice.js 的方式。React 的社区生态显著优于 Rax,切换到 React 之后可以享受到更多的 React 生态,复用复杂场景(富文本、脑图等)社区生态可以大幅度降低成本。 ## 如何迁移 -ICE 提供了 [rax-compat](https://github.com/ice-lab/ice-next/tree/master/packages/rax-compat) 以支持 [Rax](https://github.com/alibaba/rax) 到 React 运行时的切换。 +ice.js 提供了 [rax-compat](https://github.com/ice-lab/ice-next/tree/master/packages/rax-compat) 以支持 [Rax](https://github.com/alibaba/rax) 到 React 运行时的切换。 `rax-compat` 通过对 React 的能力的封装,在内部抹平了 Rax 与 React 使用上的一些差异,同时导出了与 Rax 一致的 API 等能力,通过 alias 来将源码中的 rax 用 rax-compat 来替换,即可桥接上 React 的运行时能力。 ## 安装与使用 -用户可以直接通过引入插件 [@ice/plugin-rax-compat](https://www.npmjs.com/package/@ice/plugin-rax-compat) 来完成在 ICE 中运行 Rax 应用。 +用户可以直接通过引入插件 [@ice/plugin-rax-compat](https://www.npmjs.com/package/@ice/plugin-rax-compat) 来完成在 ice.js 中运行 Rax 应用。 ```bash $ npm i @ice/plugin-rax-compat --save-dev @@ -26,9 +25,7 @@ export default defineConfig({ // ... plugins: [ // ... - compatRax({ - inlineStyle: true, - }), + compatRax({ inlineStyle: true }), ], }); ``` @@ -66,12 +63,10 @@ function App { ### 样式的处理 -rpx 是什么?rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素,1rpx = 0.5px = 1物理像素。 - -当打开 @ice/plugin-rax-compat 插件的 `inlineStyle` 时,以 `.module.css` 结尾的文件会默认走 CSS Module 的模式。此外,当 `width` 等属性没有单位如 `width: 300`,该模式下会自动补齐 `rpx` 单位并最终转化成 `vw`,同理,写了 `rpx` 单位的值也一样会被转化成 `vw`。这块逻辑与之前 Rax Driver 中处理的逻辑是一致的,Rax DSL 用户无需做任何修改。 +当打开 `@ice/plugin-rax-compat` 插件的 `inlineStyle` 参数时,以 `.module.css` 结尾的文件会默认走 CSS Module 的模式。此外,当 `width` 等属性没有单位如 `width: 300`,该模式下会自动补齐 `rpx` 单位并最终转化成 `vw`,同理,写了 `rpx` 单位的值也一样会被转化成 `vw`。这块逻辑与之前 Rax Driver 中处理的逻辑是一致的,Rax DSL 用户无需做任何修改。 ### 其他差异 -`DOM attributes 处理`: +- Attributes: -在 React 中,Element 的 props 会存在白名单,而 Rax 中没有该判断。这差异导致使用非 `data-*` 的自定义属性在 React Runtime 中会被忽略(会有 warning),如果用户通过不合法的自定义属性存储在 attributes 中,在 React Runtime 中会无法从真实 Element 中通过 `getAttribute` 获取。如果用了这些非法自定义属性,推荐使用 `data-*` 来标识自定义属性。 +在 React 中,原生标签的 props 是存在白名单的,而 Rax 中没有该判断。这差异导致使用非 `data-*` 的自定义属性在 React Runtime 中会被忽略(会有 warning),如果用户通过非标的自定义属性存储在 attributes 中,在 React Runtime 中会无法从真实节点的 ref 中通过 `getAttribute` 获取。如果用了这些非标自定义属性,推荐使用 `data-*` 来标识自定义属性。 diff --git a/website/docs/guide/advanced/micro-frontends.md b/website/docs/guide/advanced/micro-frontends.md index 74652a7c1..8ca57aa2c 100644 --- a/website/docs/guide/advanced/micro-frontends.md +++ b/website/docs/guide/advanced/micro-frontends.md @@ -1,6 +1,5 @@ --- title: 微前端 -order: 9 hide: true --- diff --git a/website/docs/guide/plugins/store.md b/website/docs/guide/advanced/store.md similarity index 100% rename from website/docs/guide/plugins/store.md rename to website/docs/guide/advanced/store.md diff --git a/website/docs/guide/advanced/unit-test.md b/website/docs/guide/advanced/unit-test.md index db549f291..9ec9a3e67 100644 --- a/website/docs/guide/advanced/unit-test.md +++ b/website/docs/guide/advanced/unit-test.md @@ -1,6 +1,5 @@ --- title: 单元测试 -order: 8 ---
diff --git a/website/docs/guide/basic/assets.md b/website/docs/guide/basic/assets.md index a3a15ebcf..3aa35f44a 100644 --- a/website/docs/guide/basic/assets.md +++ b/website/docs/guide/basic/assets.md @@ -3,14 +3,14 @@ title: 静态资源 order: 7 --- -ICE 内置了大量规则处理静态资源,一般情况下开发者无需设置资源的处理方式,而对于一些特殊的处理规则框架同样给出了便捷方式和指引 +ice.js 内置了大量规则处理静态资源,一般情况下开发者无需设置资源的处理方式,而对于一些特殊的处理规则框架同样给出了便捷方式和指引 # 基础规则 框架内置了针对以下资源的处理: - 字体文件:`.woff`、`.woff2`、`.ttf`、`.eot` -- sgv 文件 `.svg` +- svg 文件 `.svg` - 图片资源 `.png`、`.jpg`、`.webp`、`.jpeg`、`.gif` 上述资源默认通过资源地址加载,推荐将这些资源放在 `src/assets` 目录下: diff --git a/website/docs/guide/basic/config.md b/website/docs/guide/basic/config.md index 9d4e0e464..083358606 100644 --- a/website/docs/guide/basic/config.md +++ b/website/docs/guide/basic/config.md @@ -3,13 +3,13 @@ title: 构建配置 order: 14 --- -ICE 支持常用的构建配置项,所有的配置项在 `ice.config.mts` 中设置。 +ice.js 支持常用的构建配置项,所有的配置项在 `ice.config.mts` 中设置。 ## 配置文件 ### 构建配置文件 -为了获取良好的类型提示,ICE 推荐以 `ice.config.mts` 作为配置文件: +为了获取良好的类型提示,ice.js 推荐以 `ice.config.mts` 作为配置文件: ```js import { defineConfig } from '@ice/app'; @@ -80,7 +80,7 @@ console.log(ASSETS_VERSION); console.log(process.env.TEST); ``` -对于运行时变量,ICE 更加推荐通过[环境变量](./env.md)的方式注入。 +对于运行时变量,ice.js 更加推荐通过[环境变量](./env.md)的方式注入。 #### dataLoader @@ -248,7 +248,7 @@ export default defineConfig({ }); ``` -> ICE 内置通过 `swc` 提升编译体验,如果在 `transform` 配置上过多依赖 babel 等工具将可以能造成编译性能瓶颈 +> ice.js 内置通过 `swc` 提升编译体验,如果在 `transform` 配置上过多依赖 babel 等工具将可以能造成编译性能瓶颈 ### ssr @@ -345,7 +345,7 @@ export default defineConfig({ - 类型:`{ exportDefaultFrom: boolean; functionBind: boolean; }` - 默认值:`undefined` -ICE 内置了大量 ES 语法,便于开发者进行编码。对于 [proposal-export-default-from](https://github.com/tc39/proposal-export-default-from) 和 [proposal-bind-operator](https://github.com/tc39/proposal-bind-operator) 由于其提案进度较慢,我们并不推荐使用。如果希望支持该语法,可以主动配置 `syntaxFeatures` 进行启用。 +ice.js 内置了大量 ES 语法支持,便于开发者进行编码。对于 [proposal-export-default-from](https://github.com/tc39/proposal-export-default-from) 和 [proposal-bind-operator](https://github.com/tc39/proposal-bind-operator) 由于其提案进度较慢,我们并不推荐使用。如果希望支持该语法,可以主动配置 `syntaxFeatures` 进行启用。 ### tsChecker @@ -408,7 +408,7 @@ export default defineConfig({ - 类型:`(config: WebpackConfig, taskConfig: TaskConfig) => WebpackConfig` - 默认值:`true` -ICE 默认基于 webpack 进行构建,在上述提供的构建配置无法满足的情况下,用户可以定制 webpack 配置: +ice.js 默认基于 webpack 5 进行构建,在上述提供的构建配置无法满足的情况下,用户可以定制 webpack 配置: ```js import { defineConfig } from '@ice/app'; @@ -425,5 +425,5 @@ export default defineConfig({ }); ``` -> ICE 对 webpack 构建配置进行了定制,并借助 esbuild 等工具提升用户开发体验,直接修改 webpack 配置的方式并不推荐。 +> ice.js 对 webpack 构建配置进行了定制,并借助 esbuild 等工具提升用户开发体验,直接修改 webpack 配置的方式并不推荐。 > 如有定制需求欢迎👏 PR 或反馈: diff --git a/website/docs/guide/basic/router.md b/website/docs/guide/basic/router.md index b1f39607c..8dbe09d2b 100644 --- a/website/docs/guide/basic/router.md +++ b/website/docs/guide/basic/router.md @@ -45,7 +45,7 @@ export default function Home() { ## 路由跳转 -ICE 通过 `Link` 组件,来提供路由间的跳转能力。基于 `Link` 组件,可以只加载下一个页面相比于当前页面差异化的 Bundle 进行渲染,以达到更好的性能体验。 +ice.js 通过 `Link` 组件,来提供路由间的跳转能力。基于 `Link` 组件,可以只加载下一个页面相比于当前页面差异化的 Bundle 进行渲染,以达到更好的性能体验。 ```jsx // src/pages/index.tsx diff --git a/website/docs/guide/basic/style.md b/website/docs/guide/basic/style.md index 581ed7e53..a9d455443 100644 --- a/website/docs/guide/basic/style.md +++ b/website/docs/guide/basic/style.md @@ -5,7 +5,7 @@ order: 5 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -ICE 推荐使用原生 CSS + PostCSS 的方案编写样式,不建议使用 `less/sass` 之类的预编译方案,CSS 写法目前扩展支持了 `@import` 以及嵌套写法。 +ice.js 推荐使用原生 CSS + PostCSS 的方案编写样式,不建议使用 `less/sass` 之类的预编译方案,CSS 写法目前扩展支持了 `@import` 以及嵌套写法。 @@ -40,7 +40,7 @@ function Home() { -> ICE 同时支持 `less/scss` 预编译器,只要保证文件后缀匹配即可。 +> ice.js 同时支持 `less/scss` 预编译器,只要保证文件后缀匹配即可。 ## 全局样式 @@ -126,6 +126,14 @@ export default function () { ## 常见问题 +### ice.js 支持 rpx 吗 + +ice.js 原生支持 `rpx` 单位。在无线端中,阿里巴巴集团标准统一使用 `rpx` 作为响应式长度单位。你可以直接在样式文件中使用 rpx,不需要担心转换的问题。 + +> rpx(responsive pixel),可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。以 iPhone6 为例,屏幕宽度为 375px,共有 750 个物理像素,则 750rpx = 375px = 750 物理像素,1rpx = 0.5px = 1物理像素。 + +在浏览器中,ice.js 会将 rpx 会转换为 vw 进行渲染,其转换关系为:750rpx = 100vw,即 1rpx = 1/7.5vw,保留 5 位小数。 + ### 如何覆盖全局基础组件(next/antd)样式 推荐通过 `src/global.css` 覆盖全局样式: diff --git a/website/docs/guide/plugins/auth.md b/website/docs/guide/plugins/auth.md deleted file mode 100644 index c1b136629..000000000 --- a/website/docs/guide/plugins/auth.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: 权限管理 ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -
- 示例 - -
- -对于一个 Web 应用,权限管理是经常会涉及的需求之一,通常包含以下几种常见的权限管理类型: - -- 页面权限:当用户访问某个没有权限的页面时跳转到无权限页面 -- 操作权限:页面中的某些按钮或组件针对无权限的用户直接隐藏 -- 接口权限:当用户通过操作调用没有权限的接口时跳转到无权限页面 - -ice.js 提供 `@ice/plugin-auth` 插件,帮助用户更简单管理前两种类型的权限。接口权限管理请见数据请求文档。 - -## 安装插件 - -安装插件: - -```bash -$ npm i @ice/plugin-auth -D -``` - -在 `ice.config.mts` 中添加插件: - -```ts title="ice.config.mts" -import { defineConfig } from '@ice/app'; -import auth from '@ice/plugin-auth'; - -export default defineConfig({ - plugins: [ - auth(), - ], -}); -``` - -## 初始化权限数据 - -大多数情况下权限管理通常需要从服务端获取权限数据,然后在前端通过权限对比以此控制页面、操作等等权限行为。约定在 `src/app.ts` 中导出 `auth` 对象,该对象包含从服务端异步获取初始化的权限数据,并且约定最终返回格式为 `{ initialAuth: { [key: string]: boolean } }`。 - -```ts title="src/app.ts" -import { defineAuthConfig } from '@ice/plugin-auth/esm/types'; - -export const auth = defineAuthConfig(async () => { - // 模拟请求权限数据 - // const data = (await fetch('/api/auth')).json(); - return { - initialAuth: { - admin: true, - guest: false, - }, - }; -}); -``` - -## 页面权限 - -如需对某些页面进行权限控制,只需在页面组件的 `getConfig` 中配置准入权限即可。 - - - - -```tsx -export default function Home() { - return
Home
-} - -export function getConfig() { - return { - // 当前用户是 admin 时,有权限访问该页面 - auth: ['admin'], - }; -} -``` - -
- - -```tsx -export default function About() { - return
About
-} - -export function getConfig() { - return { - // 当前用户是 admin 时,无权限访问该页面 - auth: ['guest'], - }; -} -``` - -
-
- -## 操作权限 - -在某些场景下,如某个组件中要根据角色判断是否有操作权限,我们可以通过 useAuth Hooks 在组件中获取权限数据,同时也可以更新初始的权限数据。 - -### 获取权限数据 - -```tsx -import React from 'react'; -import { useAuth } from 'ice'; - -function Foo() { - const [auth] = useAuth(); - return ( - <> - 当前用户权限数据: - {JSON.stringify(auth)} - - ); -} -``` - -### 设置权限数据 - -```tsx -import React from 'react'; -import { useAuth } from 'ice'; - -function Home() { - const [auth, setAuth] = useAuth(); - - // 更新权限,与默认的 auth 数据进行合并 - function updateAuth() { - setAuth({ admin: false, guest: true }); - } - - return ( - <> - 当前用户角色: - {JSON.stringify(auth)} - - - ); -} -``` - -### 自定义权限组件 - -对于操作类权限,通常我们可以自定义封装权限组件,以便更细粒度的控制权限和复用。 - -```tsx -import React from 'react'; -import { useAuth } from 'ice'; -import NoAuth from '@/components/NoAuth'; - -function Auth({ children, authKey, fallback }) { - const [auth] = useAuth(); - // 判断是否有权限 - const hasAuth = auth[authKey]; - - // 有权限时直接渲染内容 - if (hasAuth) { - return children; - } else { - // 无权限时显示指定 UI - return fallback || NoAuth; - } -} - -export default Auth; -``` - -使用如下: - -```tsx -function Foo() { - return ( - - - - ); -} -``` - -## 自定义 Fallback - -支持自定义无权限时的展示组件,默认为 `<>No Auth` - -```diff title="src/app.tsx" -import { defineAuthConfig } from '@ice/plugin-auth/esm/types'; - -export const auth = defineAuthConfig(async () => { - return { - initialAuth: { - admin: true, - }, -+ NoAuthFallback: (routeConfig) => { -+ console.log(routeConfig); // 当前页面的配置 -+ return ( -+
没有权限
-+ ) -+ }, -+ }; -}); -``` diff --git a/website/docs/guide/plugins/plugin-dev.md b/website/docs/guide/plugins/plugin-dev.md index b14e17c25..2dce02d8b 100644 --- a/website/docs/guide/plugins/plugin-dev.md +++ b/website/docs/guide/plugins/plugin-dev.md @@ -6,7 +6,7 @@ order: 1 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -ICE 提供了插件机制,在提供丰富的框架能力的基础上也可以让开发者可以在框架能力不满足诉求的情况下进行定制: +ice.js 提供了插件机制,在提供丰富的框架能力的基础上也可以让开发者可以在框架能力不满足诉求的情况下进行定制: - 定制修改框架构建配置 - 支持在整个构建生命周期定制行为,比如项目启动前拉取某些资源 @@ -14,7 +14,7 @@ ICE 提供了插件机制,在提供丰富的框架能力的基础上也可以 ## 插件规范 -ICE 插件本质是一个 JS 模块,官方推荐以 TS 进行开发以获得良好的类型提示: +ice.js 插件本质是一个 JS 模块,官方推荐以 TS 进行开发以获得良好的类型提示: ```ts import type { Plugin } from '@ice/types'; @@ -377,7 +377,7 @@ export default () => ({ #### `addExport` -向 ice 里注册模块,实现 `import { request } from 'ice';` 的能力: +向 ice.js 里注册模块,实现 `import { request } from 'ice';` 的能力: ```ts export default () => ({ @@ -393,7 +393,7 @@ export default () => ({ #### `addExportTypes` -向 ice 里注册类型,实现 `import type { Request } from 'ice';` 的能力: +向 ice.js 里注册类型,实现 `import type { Request } from 'ice';` 的能力: ```ts export default () => ({ diff --git a/website/docs/guide/plugins/plugin-list.md b/website/docs/guide/plugins/plugin-list.md index e5d71e1b8..eae89cbf4 100644 --- a/website/docs/guide/plugins/plugin-list.md +++ b/website/docs/guide/plugins/plugin-list.md @@ -3,14 +3,14 @@ title: 插件列表 order: 2 --- -## [@ice/plugin-auth](./auth) +## [@ice/plugin-auth](../advanced/auth) 提供权限管理方案。 -## [@ice/plugin-store](./store) +## [@ice/plugin-store](../advanced/store) 提供基于单向数据流的数据管理方案。 -## [@ice/plugin-css-assets-local](./css-assets-local) +## [@ice/plugin-css-assets-local](../advanced/css-assets-local) 提供将 CSS 中的网络资源本地化的能力。 From 7bd90f1d1dd1a3347be679f962ec013a42c9f396 Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Fri, 21 Oct 2022 11:21:19 +0800 Subject: [PATCH 11/27] feat: keep-alive (#556) * feat: init keep-alive plugin * chore: update lock * feat: add api * chore: remove plugin * chore: not modify code * fix: lock * feat: support keep-alive * chore: lock * chore: update component name * chore: add ref * chore: import name * docs: keep-alive * chore: line break * chore: remove unstable prefix --- .gitignore | 4 + examples/with-keep-alive/README.md | 23 ++++++ examples/with-keep-alive/ice.config.mts | 3 + examples/with-keep-alive/package.json | 18 +++++ examples/with-keep-alive/src/app.ts | 3 + .../src/components/Counter.tsx | 12 +++ examples/with-keep-alive/src/document.tsx | 22 ++++++ .../with-keep-alive/src/pages/about/index.tsx | 20 +++++ .../src/pages/about/layout.tsx | 10 +++ .../with-keep-alive/src/pages/about/me.tsx | 18 +++++ examples/with-keep-alive/src/pages/index.tsx | 18 +++++ examples/with-keep-alive/src/pages/layout.tsx | 10 +++ examples/with-keep-alive/tsconfig.json | 32 ++++++++ packages/ice/src/plugins/web/index.ts | 1 + packages/runtime/src/KeepAliveOutlet.tsx | 43 ++++++++++ packages/runtime/src/index.ts | 3 + pnpm-lock.yaml | 2 +- pnpm-workspace.yaml | 3 +- website/docs/guide/advanced/keep-alive.md | 78 +++++++++++++++++++ website/docs/guide/basic/api.md | 4 + 20 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 examples/with-keep-alive/README.md create mode 100644 examples/with-keep-alive/ice.config.mts create mode 100644 examples/with-keep-alive/package.json create mode 100644 examples/with-keep-alive/src/app.ts create mode 100644 examples/with-keep-alive/src/components/Counter.tsx create mode 100644 examples/with-keep-alive/src/document.tsx create mode 100644 examples/with-keep-alive/src/pages/about/index.tsx create mode 100644 examples/with-keep-alive/src/pages/about/layout.tsx create mode 100644 examples/with-keep-alive/src/pages/about/me.tsx create mode 100644 examples/with-keep-alive/src/pages/index.tsx create mode 100644 examples/with-keep-alive/src/pages/layout.tsx create mode 100644 examples/with-keep-alive/tsconfig.json create mode 100644 packages/runtime/src/KeepAliveOutlet.tsx create mode 100644 website/docs/guide/advanced/keep-alive.md diff --git a/.gitignore b/.gitignore index 719610ec1..b2492fde4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,10 @@ coverage *.temp.json *.swc +# yalc +.yalc +yalc.lock + # Packages packages/*/lib/ packages/*/esm/ diff --git a/examples/with-keep-alive/README.md b/examples/with-keep-alive/README.md new file mode 100644 index 000000000..55dab7d82 --- /dev/null +++ b/examples/with-keep-alive/README.md @@ -0,0 +1,23 @@ +# with-keep-alive + +Experimental keep-alive with React 18 ``. + +## How to debug + +First of all, publish the package to the yalc repo. + +```bash +$ cd packages/ice && yalc publish --push + +$ cd packages/runtime && yalc publish --push +``` + +Then, install the example dependencies. + +```bash +$ cd examples/with-keep-alive && yarn install + +$ yalc add @ice/app @ice/runtime + +$ npm run start +``` diff --git a/examples/with-keep-alive/ice.config.mts b/examples/with-keep-alive/ice.config.mts new file mode 100644 index 000000000..129fcfa67 --- /dev/null +++ b/examples/with-keep-alive/ice.config.mts @@ -0,0 +1,3 @@ +import { defineConfig } from '@ice/app'; + +export default defineConfig({}); diff --git a/examples/with-keep-alive/package.json b/examples/with-keep-alive/package.json new file mode 100644 index 000000000..46dce86a5 --- /dev/null +++ b/examples/with-keep-alive/package.json @@ -0,0 +1,18 @@ +{ + "name": "with-keep-alive", + "version": "1.0.0", + "scripts": { + "start": "ice start", + "build": "ice build" + }, + "dependencies": { + "@ice/runtime": "alpha", + "react": "experimental", + "react-dom": "experimental" + }, + "devDependencies": { + "@ice/app": "alpha", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.2" + } +} diff --git a/examples/with-keep-alive/src/app.ts b/examples/with-keep-alive/src/app.ts new file mode 100644 index 000000000..c1664902e --- /dev/null +++ b/examples/with-keep-alive/src/app.ts @@ -0,0 +1,3 @@ +import { defineAppConfig } from 'ice'; + +export default defineAppConfig({}); diff --git a/examples/with-keep-alive/src/components/Counter.tsx b/examples/with-keep-alive/src/components/Counter.tsx new file mode 100644 index 000000000..ed3f87b77 --- /dev/null +++ b/examples/with-keep-alive/src/components/Counter.tsx @@ -0,0 +1,12 @@ +import { useState } from 'react'; + +export default function Counter() { + const [count, setCount] = useState(0); + + return ( +
+ count: {count} + +
+ ); +} diff --git a/examples/with-keep-alive/src/document.tsx b/examples/with-keep-alive/src/document.tsx new file mode 100644 index 000000000..961b46c16 --- /dev/null +++ b/examples/with-keep-alive/src/document.tsx @@ -0,0 +1,22 @@ +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +function Document() { + return ( + + + + + + + + <Links /> + </head> + <body> + <Main /> + <Scripts /> + </body> + </html> + ); +} + +export default Document; diff --git a/examples/with-keep-alive/src/pages/about/index.tsx b/examples/with-keep-alive/src/pages/about/index.tsx new file mode 100644 index 000000000..86432e454 --- /dev/null +++ b/examples/with-keep-alive/src/pages/about/index.tsx @@ -0,0 +1,20 @@ +import { Link } from 'ice'; +import Counter from '@/components/Counter'; + +export default function About() { + return ( + <> + <h3>About</h3> + <Counter /> + <Link to="/">Home</Link> + <br /> + <Link to="/about/me">About Me</Link> + </> + ); +} + +export function getConfig() { + return { + title: 'About', + }; +} diff --git a/examples/with-keep-alive/src/pages/about/layout.tsx b/examples/with-keep-alive/src/pages/about/layout.tsx new file mode 100644 index 000000000..79fe76a82 --- /dev/null +++ b/examples/with-keep-alive/src/pages/about/layout.tsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; + +export default function AboutLayout() { + return ( + <> + <h2>About Layout </h2> + <Outlet /> + </> + ); +} diff --git a/examples/with-keep-alive/src/pages/about/me.tsx b/examples/with-keep-alive/src/pages/about/me.tsx new file mode 100644 index 000000000..590bef400 --- /dev/null +++ b/examples/with-keep-alive/src/pages/about/me.tsx @@ -0,0 +1,18 @@ +import { Link } from 'ice'; + +export default function About() { + return ( + <> + <h3>About Me</h3> + <input /> + <br /> + <Link to="/about">About</Link> + </> + ); +} + +export function getConfig() { + return { + title: 'About Me', + }; +} diff --git a/examples/with-keep-alive/src/pages/index.tsx b/examples/with-keep-alive/src/pages/index.tsx new file mode 100644 index 000000000..c9c131fcc --- /dev/null +++ b/examples/with-keep-alive/src/pages/index.tsx @@ -0,0 +1,18 @@ +import { Link } from 'ice'; +import Counter from '@/components/Counter'; + +export default function Home() { + return ( + <main> + <h2>Home</h2> + <Counter /> + <Link to="/about">About</Link> + </main> + ); +} + +export function getConfig() { + return { + title: 'Home', + }; +} diff --git a/examples/with-keep-alive/src/pages/layout.tsx b/examples/with-keep-alive/src/pages/layout.tsx new file mode 100644 index 000000000..c78efc3dd --- /dev/null +++ b/examples/with-keep-alive/src/pages/layout.tsx @@ -0,0 +1,10 @@ +import { KeepAliveOutlet } from 'ice'; + +export default function Layout() { + return ( + <> + <h1>I'm Keep Alive</h1> + <KeepAliveOutlet /> + </> + ); +} diff --git a/examples/with-keep-alive/tsconfig.json b/examples/with-keep-alive/tsconfig.json new file mode 100644 index 000000000..26fd9ec79 --- /dev/null +++ b/examples/with-keep-alive/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compileOnSave": false, + "buildOnSave": false, + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "module": "esnext", + "target": "es6", + "jsx": "react-jsx", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "rootDir": "./", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": false, + "importHelpers": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"], + "ice": [".ice"] + } + }, + "include": ["src", ".ice", "ice.config.*"], + "exclude": ["build", "public"] +} \ No newline at end of file diff --git a/packages/ice/src/plugins/web/index.ts b/packages/ice/src/plugins/web/index.ts index 8122ad29d..feb9f3c5c 100644 --- a/packages/ice/src/plugins/web/index.ts +++ b/packages/ice/src/plugins/web/index.ts @@ -37,6 +37,7 @@ const plugin: Plugin = () => ({ 'Data', 'Main', 'history', + 'KeepAliveOutlet', 'useMounted', 'ClientOnly', ], diff --git a/packages/runtime/src/KeepAliveOutlet.tsx b/packages/runtime/src/KeepAliveOutlet.tsx new file mode 100644 index 000000000..f8a7a43ee --- /dev/null +++ b/packages/runtime/src/KeepAliveOutlet.tsx @@ -0,0 +1,43 @@ +import React, { useState, useEffect } from 'react'; +import { useOutlet, useLocation } from 'react-router-dom'; + +// @ts-ignore +const Offscreen = React.unstable_Offscreen; + +// ref: https://leomyili.github.io/react-stillness-component/docs/examples/react-router/v6 +export default function KeepAliveOutlet() { + if (!Offscreen) { + throw new Error('`<KeepAliveOutlet />` now requires react experimental version. Please install it first.'); + } + const [outlets, setOutlets] = useState([]); + const location = useLocation(); + const outlet = useOutlet(); + + useEffect(() => { + const result = outlets.some(o => o.pathname === location.pathname); + if (!result) { + setOutlets([ + ...outlets, + { + key: location.key, + pathname: location.pathname, + outlet, + }, + ]); + } + }, [location.pathname, location.key, outlet, outlets]); + + return ( + <> + { + outlets.map((o) => { + return ( + <Offscreen key={o.key} mode={location.pathname === o.pathname ? 'visible' : 'hidden'}> + {o.outlet} + </Offscreen> + ); + }) + } + </> + ); +} diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 69c57c415..45d3eb3df 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -37,6 +37,7 @@ import AppRouter from './AppRouter.js'; import AppErrorBoundary from './AppErrorBoundary.js'; import getAppConfig, { defineAppConfig } from './appConfig.js'; import { routerHistory as history } from './history.js'; +import KeepAliveOutlet from './KeepAliveOutlet.js'; import ClientOnly from './ClientOnly.js'; import useMounted from './useMounted.js'; @@ -69,6 +70,8 @@ export { useSearchParams, useLocation, history, + + KeepAliveOutlet, AppRouter, AppErrorBoundary, ClientOnly, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18cfa05bf..50a87d67e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19545,4 +19545,4 @@ packages: engines: {node: '>=10'} /zwitch/1.0.5: - resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} + resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} \ No newline at end of file diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0a0b6ea22..bba0d4d4c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: - 'packages/*' - 'examples/*' - - 'website' \ No newline at end of file + - 'website' + - '!examples/with-keep-alive/' \ No newline at end of file diff --git a/website/docs/guide/advanced/keep-alive.md b/website/docs/guide/advanced/keep-alive.md new file mode 100644 index 000000000..84d147877 --- /dev/null +++ b/website/docs/guide/advanced/keep-alive.md @@ -0,0 +1,78 @@ +--- +title: Keep Alive +--- + +<details open> + <summary>示例</summary> + <ul> + <li> + <a href="https://github.com/ice-lab/ice-next/tree/master/examples/with-keep-alive" target="_blank" rel="noopener noreferrer"> + with-keep-alive + </a> + </li> + </ul> +</details> + +ice.js 提供 Keep Alive 能力,支持在组件间进行切换时缓存被移除的组件实例。 + +使用 Keep Alive 能力需要安装 react 和 react-dom 的 experimental 版本: + +```bash +$ npm i react@experimental react-dom@experimental -S +``` + +## 缓存路由组件 + +ice.js 提供 `<KeepAliveOutlet />` 组件,用于在路由切换时缓存被移除的组件状态。 + +:::caution + +`<KeepAliveOutlet />` 目前是实验性的组件,可能会存在不稳定性。 +::: + +在 `src/pages/layout.tsx` 文件中引入 `<KeepAliveOutlet />` 组件后,即可缓存所有的路由组件: + +```tsx title="src/pages/layout.tsx" +import { KeepAliveOutlet } from 'ice'; + +export default function Layout() { + return ( + <> + <h1>I'm Keep Alive</h1> + <KeepAliveOutlet /> + </> + ); +} +``` + +## 缓存其他组件 + +除了缓存路由组件,还可以直接使用 React 18 提供的实验特性 `<Offscreen />` 组件,进一步缓存更细粒度的组件。 + +```tsx +import React from 'react'; + +// @ts-ignore +const Offscreen = React.unstable_Offscreen; + +export default function Home() { + const [auth, setAuth] = React.useState('admin'); + + return ( + <> + <div> + <button onClick={() => setAuth('admin')}>Set Admin</button> + <button onClick={() => setAuth('user')}>Set User</button> + </div> + <> + <Offscreen mode={auth === 'admin' ? 'visible' : 'hidden'}> + Admin Name: <input /> + </Offscreen> + <Offscreen mode={auth === 'user' ? 'visible' : 'hidden'}> + User Name: <input /> + </Offscreen> + </> + </> + ) +} +``` diff --git a/website/docs/guide/basic/api.md b/website/docs/guide/basic/api.md index 536bdfb2e..e49f61d1d 100644 --- a/website/docs/guide/basic/api.md +++ b/website/docs/guide/basic/api.md @@ -72,3 +72,7 @@ export function Home () { ); }; ``` + +### `<KeepAliveOutlet />` + +缓存所有路由组件的状态。详细使用方式参考 [Keep Alive 文档](../advanced/keep-alive/#缓存路由组件)。 From 503546a11aeae0b9c31a2e7ab99b5d67dfc537f3 Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Fri, 21 Oct 2022 15:18:33 +0800 Subject: [PATCH 12/27] feat: support route config type (#595) * feat: support route config type * fix: type * fix: optimize code --- examples/basic-project/src/pages/about.tsx | 5 ++-- examples/basic-project/src/pages/blog.tsx | 6 ++--- examples/with-pha/src/pages/home.tsx | 6 +++-- packages/ice/src/createService.ts | 3 +++ packages/ice/src/service/runtimeGenerator.ts | 14 ++++++---- packages/ice/templates/core/env.server.ts.ejs | 3 +-- packages/ice/templates/core/index.ts.ejs | 12 ++++++--- packages/ice/templates/core/types.ts.ejs | 24 +++++++---------- packages/plugin-auth/src/index.ts | 6 +++++ packages/plugin-auth/src/types.ts | 4 +++ packages/plugin-pha/src/index.ts | 7 ++++- packages/runtime/src/RouteContext.ts | 7 +++-- packages/runtime/src/routesConfig.ts | 2 +- packages/types/src/generator.ts | 7 ++++- packages/types/src/plugin.ts | 1 + packages/types/src/runtime.ts | 27 +++++++++---------- 16 files changed, 80 insertions(+), 54 deletions(-) diff --git a/examples/basic-project/src/pages/about.tsx b/examples/basic-project/src/pages/about.tsx index 935e30549..5afe5522a 100644 --- a/examples/basic-project/src/pages/about.tsx +++ b/examples/basic-project/src/pages/about.tsx @@ -1,4 +1,5 @@ import { Link, useData, useConfig, history } from 'ice'; +import type { RouteConfig } from 'ice'; import { isWeb } from '@uni/env'; import url from './ice.png'; @@ -8,7 +9,7 @@ interface Data { export default function About() { const data = useData<Data>(); - const config = useConfig(); + const config = useConfig<RouteConfig>(); console.log('render About', 'data', data, 'config', config); console.log('history in component', history); @@ -24,7 +25,7 @@ export default function About() { ); } -export function getConfig() { +export function getConfig(): RouteConfig { return { title: 'About', meta: [ diff --git a/examples/basic-project/src/pages/blog.tsx b/examples/basic-project/src/pages/blog.tsx index e92e441ed..ad00c943e 100644 --- a/examples/basic-project/src/pages/blog.tsx +++ b/examples/basic-project/src/pages/blog.tsx @@ -1,4 +1,4 @@ -import { Link, useData, useConfig } from 'ice'; +import { Link, useData, useConfig, defineGetConfig } from 'ice'; export default function Blog() { const data = useData(); @@ -14,9 +14,9 @@ export default function Blog() { ); } -export function getConfig() { +export const getConfig = defineGetConfig(() => { return { title: 'Blog', auth: ['guest'], }; -} \ No newline at end of file +}); \ No newline at end of file diff --git a/examples/with-pha/src/pages/home.tsx b/examples/with-pha/src/pages/home.tsx index 473b28797..0c396120b 100644 --- a/examples/with-pha/src/pages/home.tsx +++ b/examples/with-pha/src/pages/home.tsx @@ -1,3 +1,5 @@ +import { defineGetConfig } from 'ice'; + export default function Home() { return ( <> @@ -6,7 +8,7 @@ export default function Home() { ); } -export function getConfig() { +export const getConfig = defineGetConfig(() => { return { queryParamsPassKeys: [ 'questionId', @@ -15,4 +17,4 @@ export function getConfig() { ], title: 'Home', }; -} +}); diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 436bdfdfa..a9d4b7b82 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -62,6 +62,9 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt addExportTypes: (exportData: ExportData) => { generator.addExport('frameworkTypes', exportData); }, + addRouteTypes: (exportData: ExportData) => { + generator.addExport('routeConfigTypes', exportData); + }, addRenderFile: generator.addRenderFile, addRenderTemplate: generator.addTemplateFiles, modifyRenderData: generator.modifyRenderData, diff --git a/packages/ice/src/service/runtimeGenerator.ts b/packages/ice/src/service/runtimeGenerator.ts index 7100dcf9c..2d221269a 100644 --- a/packages/ice/src/service/runtimeGenerator.ts +++ b/packages/ice/src/service/runtimeGenerator.ts @@ -8,7 +8,7 @@ import type { AddExport, RemoveExport, AddContent, - GetExportStr, + GetExportData, ParseRenderData, Render, RenderFile, @@ -38,6 +38,7 @@ interface Options { export function generateExports(exportList: ExportData[]) { const importStatements = []; let exportStatements = []; + let exportNames: string[] = []; exportList.forEach(data => { const { specifier, source, exportAlias, type } = data; const isDefaultImport = !Array.isArray(specifier); @@ -50,6 +51,7 @@ export function generateExports(exportList: ExportData[]) { } else { exportStatements.push(`${specifierStr}${symbol}`); } + exportNames.push(specifierStr); }); }); return { @@ -62,6 +64,7 @@ export function generateExports(exportList: ExportData[]) { }; */ exportStr: exportStatements.join('\n '), + exportNames, }; } @@ -118,7 +121,7 @@ export default class Generator { this.rerender = false; this.renderTemplates = []; this.renderDataRegistration = []; - this.contentTypes = ['framework', 'frameworkTypes', 'configTypes']; + this.contentTypes = ['framework', 'frameworkTypes', 'routeConfigTypes']; // empty .ice before render fse.emptyDirSync(path.join(rootDir, targetDir)); // add initial templates @@ -166,13 +169,14 @@ export default class Generator { this.contentRegistration[registerKey].push(...content); }; - private getExportStr: GetExportStr = (registerKey, dataKeys) => { + private getExportData: GetExportData = (registerKey, dataKeys) => { const exportList = this.contentRegistration[registerKey] || []; - const { importStr, exportStr } = generateExports(exportList); + const { importStr, exportStr, exportNames } = generateExports(exportList); const [importStrKey, exportStrKey] = dataKeys; return { [importStrKey]: importStr, [exportStrKey]: exportStr, + exportNames, }; }; @@ -181,7 +185,7 @@ export default class Generator { const globalStyles = fg.sync([getGlobalStyleGlobPattern()], { cwd: this.rootDir }); let exportsData = {}; this.contentTypes.forEach(item => { - const data = this.getExportStr(item, ['imports', 'exports']); + const data = this.getExportData(item, ['imports', 'exports']); exportsData = Object.assign({}, exportsData, { [`${item}`]: data, }); diff --git a/packages/ice/templates/core/env.server.ts.ejs b/packages/ice/templates/core/env.server.ts.ejs index 2c2774eff..43d4cee05 100644 --- a/packages/ice/templates/core/env.server.ts.ejs +++ b/packages/ice/templates/core/env.server.ts.ejs @@ -1,5 +1,4 @@ // Define process.env in top make it possible to use ICE_CORE_* in @ice/runtime, esbuild define options doesn't have the ability // The runtime value such as __process.env.ICE_CORE_*__ will be replaced by esbuild define, so the value is real-time <% coreEnvKeys.forEach((key) => { %> -process.env.<%= key %> = __process.env.<%= key %>__; -<% }) %> \ No newline at end of file +process.env.<%= key %> = __process.env.<%= key %>__;<% }) %> \ No newline at end of file diff --git a/packages/ice/templates/core/index.ts.ejs b/packages/ice/templates/core/index.ts.ejs index be7f90421..ac170aeb2 100644 --- a/packages/ice/templates/core/index.ts.ejs +++ b/packages/ice/templates/core/index.ts.ejs @@ -1,13 +1,17 @@ <% if (globalStyle) {-%> import '<%= globalStyle %>' <% } -%> - +import type { RouteConfig } from './types'; <%- framework.imports %> -<% if (framework.exports) { -%> +type GetConfig = () => RouteConfig; +function defineGetConfig(getConfig: GetConfig): GetConfig { + return getConfig; +} + export { - <%- framework.exports %> + defineGetConfig, + <% if (framework.exports) { -%><%- framework.exports %><% } -%> }; -<% } -%> export * from './types'; diff --git a/packages/ice/templates/core/types.ts.ejs b/packages/ice/templates/core/types.ts.ejs index 787cdc31c..5da9dd23c 100644 --- a/packages/ice/templates/core/types.ts.ejs +++ b/packages/ice/templates/core/types.ts.ejs @@ -1,20 +1,16 @@ -import type { AppConfig as DefaultAppConfig, GetAppData, AppData } from '@ice/runtime'; +import type { AppConfig, GetAppData, RouteConfig as DefaultRouteConfig } from '@ice/runtime'; +<%- routeConfigTypes.imports -%> -<%- configTypes.imports -%> -<% if (configTypes.imports) {-%> -interface ExtendsAppConfig extends DefaultAppConfig { - <% if (configTypes.imports) { %> - <%- configTypes.exports %> - <% } %> -}; - -export type AppConfig = ExtendsAppConfig; +<% if (routeConfigTypes.imports) {-%> +type ExtendsRouteConfig = <% if (routeConfigTypes.imports) { %><%- routeConfigTypes.exportNames.join(' & ') %><% } %>; <% } else { -%> -export type AppConfig = DefaultAppConfig; +type ExtendsRouteConfig = {}; <% } -%> +type RouteConfig = DefaultRouteConfig<ExtendsRouteConfig>; -export { +export type { + AppConfig, GetAppData, - AppData -} \ No newline at end of file + RouteConfig, +}; \ No newline at end of file diff --git a/packages/plugin-auth/src/index.ts b/packages/plugin-auth/src/index.ts index 00f9da773..b83f9200e 100644 --- a/packages/plugin-auth/src/index.ts +++ b/packages/plugin-auth/src/index.ts @@ -10,6 +10,12 @@ const plugin: Plugin = () => ({ specifier: ['withAuth', 'useAuth'], source: '@ice/plugin-auth/runtime/Auth', }); + + generator.addRouteTypes({ + specifier: ['ConfigAuth'], + type: true, + source: '@ice/plugin-auth/esm/types', + }); }, runtime: `${PLUGIN_NAME}/esm/runtime`, }); diff --git a/packages/plugin-auth/src/types.ts b/packages/plugin-auth/src/types.ts index 77e134834..e8711d5bb 100644 --- a/packages/plugin-auth/src/types.ts +++ b/packages/plugin-auth/src/types.ts @@ -14,3 +14,7 @@ export type Auth = (data?: any) => Promise<AuthConfig> | AuthConfig; export function defineAuthConfig(fn: Auth) { return fn; } + +export interface ConfigAuth { + auth?: string[]; +} diff --git a/packages/plugin-pha/src/index.ts b/packages/plugin-pha/src/index.ts index 49f9c5141..be3d3fb18 100644 --- a/packages/plugin-pha/src/index.ts +++ b/packages/plugin-pha/src/index.ts @@ -24,7 +24,7 @@ function getDevPath(url: string): string { const plugin: Plugin<PluginOptions> = (options) => ({ name: '@ice/plugin-pha', - setup: ({ onGetConfig, onHook, context, serverCompileTask }) => { + setup: ({ onGetConfig, onHook, context, serverCompileTask, generator }) => { const { template } = options || {}; const { command, rootDir } = context; @@ -36,6 +36,11 @@ const plugin: Plugin<PluginOptions> = (options) => ({ let getAppConfig: GetAppConfig; let getRoutesConfig: GetRoutesConfig; + generator.addRouteTypes({ + specifier: ['PageConfig'], + type: true, + source: '@ice/plugin-pha/esm/types', + }); // Get server compiler by hooks onHook(`before.${command as 'start' | 'build'}.run`, async ({ serverCompiler, taskConfigs, urls, ...restAPI }) => { const taskConfig = taskConfigs.find(({ name }) => name === 'web').config; diff --git a/packages/runtime/src/RouteContext.ts b/packages/runtime/src/RouteContext.ts index d2de8c174..e8cdabfec 100644 --- a/packages/runtime/src/RouteContext.ts +++ b/packages/runtime/src/RouteContext.ts @@ -4,16 +4,16 @@ import type { RouteData, RouteConfig } from '@ice/types'; const DataContext = React.createContext<RouteData | undefined>(undefined); DataContext.displayName = 'Data'; -function useData(): RouteData { +function useData<T = any>(): T { const value = React.useContext(DataContext); return value; } const DataProvider = DataContext.Provider; -const ConfigContext = React.createContext<RouteConfig | undefined>(undefined); +const ConfigContext = React.createContext<RouteConfig<any> | undefined>(undefined); ConfigContext.displayName = 'Config'; -function useConfig(): RouteConfig { +function useConfig<T = {}>(): RouteConfig<T> { const value = React.useContext(ConfigContext); return value; } @@ -22,7 +22,6 @@ const ConfigProvider = ConfigContext.Provider; export { useData, DataProvider, - useConfig, ConfigProvider, }; diff --git a/packages/runtime/src/routesConfig.ts b/packages/runtime/src/routesConfig.ts index 106cd11cc..712c914e7 100644 --- a/packages/runtime/src/routesConfig.ts +++ b/packages/runtime/src/routesConfig.ts @@ -110,7 +110,7 @@ const DOMAttributeNames: Record<string, string> = { noModule: 'noModule', }; -type ElementProps = RouteConfig['meta'] | RouteConfig['links'] | RouteConfig['scripts']; +type ElementProps = RouteConfig['meta'][0] | RouteConfig['links'][0] | RouteConfig['scripts'][0]; /** * map element props to dom diff --git a/packages/types/src/generator.ts b/packages/types/src/generator.ts index 354e604ca..5006268f5 100644 --- a/packages/types/src/generator.ts +++ b/packages/types/src/generator.ts @@ -24,7 +24,12 @@ export type SetPlugins = (plugins: any) => void; export type AddExport = (registerKey: string, exportData: ExportData | ExportData[]) => void; export type RemoveExport = (registerKey: string, removeSource: string | string[]) => void; export type AddContent = (apiName: string, ...args: any) => void; -export type GetExportStr = (registerKey: string, dataKeys: string[]) => { [x: string]: string }; +export type GetExportData = (registerKey: string, dataKeys: string[]) => { + imports?: string; + exports?: string; + exportNames?: string[]; + [x: string]: any; +}; export type ParseRenderData = () => Record<string, unknown>; export type Render = () => void; export type ModifyRenderData = (registration: RenderDataRegistration) => void; diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index 3f7fbde92..c12e42508 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -104,6 +104,7 @@ export interface ExtendsPluginAPI { generator: { addExport: AddExport; addExportTypes: AddExport; + addRouteTypes: AddExport; addRenderFile: AddRenderFile; addRenderTemplate: AddTemplateFiles; modifyRenderData: ModifyRenderData; diff --git a/packages/types/src/runtime.ts b/packages/types/src/runtime.ts index 9c201c84d..4a56a79e3 100644 --- a/packages/types/src/runtime.ts +++ b/packages/types/src/runtime.ts @@ -4,9 +4,9 @@ import type { ComponentType, ReactNode, PropsWithChildren } from 'react'; import type { HydrationOptions } from 'react-dom/client'; import type { Navigator, Params } from 'react-router-dom'; -type useConfig = () => RouteConfig; -type useData = () => RouteData; -type useAppContext = () => AppContext; +type UseConfig = () => RouteConfig<Record<string, any>>; +type UseData = () => RouteData; +type UseAppContext = () => AppContext; type VoidFunction = () => void; type AppLifecycle = 'onShow' | 'onHide' | 'onPageNotFound' | 'onShareAppMessage' | 'onUnhandledRejection' | 'onLaunch' | 'onError' | 'onTabItemClick'; @@ -20,16 +20,13 @@ export type AppData = any; export type RouteData = any; // route.getConfig return value -export interface RouteConfig { +export type RouteConfig<T = {}> = T & { + // Support for extends config. title?: string; - // TODO: fix type - meta?: any[]; - links?: any[]; - scripts?: any[]; - - // plugin extends - auth?: string[]; -} + meta?: React.MetaHTMLAttributes<HTMLMetaElement>[]; + links?: React.LinkHTMLAttributes<HTMLLinkElement>[]; + scripts?: React.ScriptHTMLAttributes<HTMLScriptElement>[]; +}; export interface AppExport { default?: AppConfig; @@ -167,9 +164,9 @@ export interface RuntimeAPI { setRender: SetRender; addWrapper: AddWrapper; appContext: AppContext; - useData: useData; - useConfig: useConfig; - useAppContext: useAppContext; + useData: UseData; + useConfig: UseConfig; + useAppContext: UseAppContext; } export interface RuntimePlugin { From 708f4a8faa0c098e7305be7dd04fb76325381f19 Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Fri, 21 Oct 2022 15:19:09 +0800 Subject: [PATCH 13/27] fix: error occurs when remove unused code (#608) * fix: ensure specifiers in parent node * test: test case --- packages/ice/src/utils/babelPluginRemoveCode.ts | 2 +- .../ice/tests/fixtures/removeCode/mixed-import.tsx | 11 +++++++++++ packages/ice/tests/removeTopLevelCode.test.ts | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/ice/tests/fixtures/removeCode/mixed-import.tsx diff --git a/packages/ice/src/utils/babelPluginRemoveCode.ts b/packages/ice/src/utils/babelPluginRemoveCode.ts index e928a4665..4f9b78c63 100644 --- a/packages/ice/src/utils/babelPluginRemoveCode.ts +++ b/packages/ice/src/utils/babelPluginRemoveCode.ts @@ -10,7 +10,7 @@ const removeUnreferencedCode = (nodePath: NodePath<t.Program>) => { if (!binding.referenced && binding.path.node) { const nodeType = binding.path.node.type; if (['VariableDeclarator', 'ImportSpecifier', 'FunctionDeclaration'].includes(nodeType)) { - if (nodeType === 'ImportSpecifier' && (binding.path.parentPath.node as t.ImportDeclaration).specifiers.length === 1) { + if (nodeType === 'ImportSpecifier' && (binding.path.parentPath.node as t.ImportDeclaration)?.specifiers.length === 1) { binding.path.parentPath.remove(); } else if (nodeType === 'VariableDeclarator') { if (binding.identifier === binding.path.node.id) { diff --git a/packages/ice/tests/fixtures/removeCode/mixed-import.tsx b/packages/ice/tests/fixtures/removeCode/mixed-import.tsx new file mode 100644 index 000000000..dd3810c41 --- /dev/null +++ b/packages/ice/tests/fixtures/removeCode/mixed-import.tsx @@ -0,0 +1,11 @@ +import React, { useState, useEffect } from 'react'; + +export default function Bar() { + const [str] = useState(''); + useEffect(() => {}, []); + return <React.Fragment>{str}</React.Fragment>; +} + +export function getConfig() { + return { a: 1 }; +} \ No newline at end of file diff --git a/packages/ice/tests/removeTopLevelCode.test.ts b/packages/ice/tests/removeTopLevelCode.test.ts index ab8619258..944aa913c 100644 --- a/packages/ice/tests/removeTopLevelCode.test.ts +++ b/packages/ice/tests/removeTopLevelCode.test.ts @@ -53,6 +53,12 @@ describe('remove top level code', () => { const content = generate(ast).code; expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function getConfig() { return { a: 1 };}'); }); + it('remove mixed import statement', () => { + const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/mixed-import.tsx'), 'utf-8'), parserOptions); + traverse(ast, removeTopLevelCodePlugin(['getConfig'])); + const content = generate(ast).code; + expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function getConfig() { return { a: 1 };}'); + }); it('remove IIFE code', () => { const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/iife.ts'), 'utf-8'), parserOptions); traverse(ast, removeTopLevelCodePlugin(['getConfig'])); From 7e64434194d072d86a9f1b936b9cd2004fc4978d Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Fri, 21 Oct 2022 16:00:47 +0800 Subject: [PATCH 14/27] fix: log error stack when transform error (#618) * fix: log error stack when transform error * fix: compat with unplugin and webpack transform error --- .../webpack-config/src/unPlugins/compilation.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/webpack-config/src/unPlugins/compilation.ts b/packages/webpack-config/src/unPlugins/compilation.ts index 1facad823..4459b144d 100644 --- a/packages/webpack-config/src/unPlugins/compilation.ts +++ b/packages/webpack-config/src/unPlugins/compilation.ts @@ -136,11 +136,16 @@ const compilationPlugin = (options: Options): UnpluginOptions => { const { code } = output; let { map } = output; return { code, map }; - } catch (e) { + } catch (error) { // Catch error for unhandled promise rejection. - // In some cases, this referred to undefined. - if (this) this.error(e); - return { code: null, map: null }; + if (this) { + // Handled by unplugin. + this.error(error); + return { code: null, map: null }; + } else { + // Handled by webpack. + throw error; + } } }, }; From 0a919168c0f989d0b4e059a235d271977eaa12c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=B4=E6=BE=9C?= <shuilan.cj@taobao.com> Date: Fri, 21 Oct 2022 16:01:20 +0800 Subject: [PATCH 15/27] fix: server format config (#614) --- packages/ice/src/plugins/web/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ice/src/plugins/web/index.ts b/packages/ice/src/plugins/web/index.ts index feb9f3c5c..b2c8a1e6f 100644 --- a/packages/ice/src/plugins/web/index.ts +++ b/packages/ice/src/plugins/web/index.ts @@ -15,7 +15,7 @@ const plugin: Plugin = () => ({ name: 'plugin-web', setup: ({ registerTask, onHook, context, generator, serverCompileTask, dataCache }) => { const { rootDir, commandArgs, command, userConfig } = context; - const { ssg, server: { format } } = userConfig; + const { ssg } = userConfig; registerTask(WEB, getWebTask({ rootDir, command, dataCache })); @@ -49,7 +49,7 @@ const plugin: Plugin = () => ({ // Compile server entry after the webpack compilation. const { reCompile: reCompileRouteConfig, ensureRoutesConfig } = getRouteExportConfig(rootDir); const outputDir = webpackConfigs[0].output.path; - serverOutfile = path.join(outputDir, SERVER_OUTPUT_DIR, `index${format === 'esm' ? '.mjs' : '.cjs'}`); + serverOutfile = path.join(outputDir, SERVER_OUTPUT_DIR, `index${userConfig?.server?.format === 'esm' ? '.mjs' : '.cjs'}`); webpackConfigs[0].plugins.push( // Add webpack plugin of data-loader in web task new DataLoaderPlugin({ serverCompiler, rootDir, dataCache }), From faa54b80e866b4fe72c141c9ea7843520a795b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=93=E9=99=8C=E5=90=8C=E5=AD=A6?= <answershuto@gmail.com> Date: Fri, 21 Oct 2022 16:02:03 +0800 Subject: [PATCH 16/27] Feat/static prefetch (#611) * feat: add matchedIds * feat: add matchedIds * feat: transfrom object of getData * feat: add transformLoaders * feat: add fetcher to ejs * feat: add dataLoaderExport * feat: modify export to import * chore: modify comment * feat: add default fetcher * feat: modify transformLoaders --- packages/ice/src/createService.ts | 3 ++ packages/ice/src/service/runtimeGenerator.ts | 4 +-- .../ice/templates/exports/data-loader.ts.ejs | 10 ++++++- packages/runtime/src/Document.tsx | 2 ++ packages/runtime/src/dataLoader.ts | 28 +++++++++++++++++-- packages/types/src/runtime.ts | 12 ++++---- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index a9d4b7b82..3c9ade65e 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -68,6 +68,9 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt addRenderFile: generator.addRenderFile, addRenderTemplate: generator.addTemplateFiles, modifyRenderData: generator.modifyRenderData, + addDataLoaderImport: (exportData: ExportData) => { + generator.addExport('dataLoaderImport', exportData); + }, }; const serverCompileTask = new ServerCompileTask(); diff --git a/packages/ice/src/service/runtimeGenerator.ts b/packages/ice/src/service/runtimeGenerator.ts index 2d221269a..3b1341792 100644 --- a/packages/ice/src/service/runtimeGenerator.ts +++ b/packages/ice/src/service/runtimeGenerator.ts @@ -44,7 +44,7 @@ export function generateExports(exportList: ExportData[]) { const isDefaultImport = !Array.isArray(specifier); const specifiers = isDefaultImport ? [specifier] : specifier; const symbol = type ? ';' : ','; - importStatements.push(`import ${type ? 'type ' : ''}${isDefaultImport ? specifier : `{ ${specifier.join(', ')} }`} from '${source}';`); + importStatements.push(`import ${type ? 'type ' : ''}${isDefaultImport ? specifier : `{ ${specifiers.map(specifierStr => ((exportAlias && exportAlias[specifierStr]) ? `${specifierStr} as ${exportAlias[specifierStr]}` : specifierStr)).join(', ')} }`} from '${source}';`); specifiers.forEach((specifierStr) => { if (exportAlias && exportAlias[specifierStr]) { exportStatements.push(`${exportAlias[specifierStr]}: ${specifierStr}${symbol}`); @@ -121,7 +121,7 @@ export default class Generator { this.rerender = false; this.renderTemplates = []; this.renderDataRegistration = []; - this.contentTypes = ['framework', 'frameworkTypes', 'routeConfigTypes']; + this.contentTypes = ['framework', 'frameworkTypes', 'routeConfigTypes', 'dataLoaderImport']; // empty .ice before render fse.emptyDirSync(path.join(rootDir, targetDir)); // add initial templates diff --git a/packages/ice/templates/exports/data-loader.ts.ejs b/packages/ice/templates/exports/data-loader.ts.ejs index 3a64cd860..5cd8b8e00 100644 --- a/packages/ice/templates/exports/data-loader.ts.ejs +++ b/packages/ice/templates/exports/data-loader.ts.ejs @@ -1,7 +1,15 @@ import { dataLoader } from '@ice/runtime'; <% if(hasExportAppData) {-%>import { getAppData } from '@/app';<% } -%> +<% if(dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%> + <%- loaders %> <% if(hasExportAppData) {-%>loaders['__app'] = getAppData;<% } -%> -dataLoader.init(loaders); +<% if(!dataLoaderImport.imports) {-%> +let fetcher = (options) => { + window.fetch(options.url, options); +} +<% } -%> + +dataLoader.init(loaders, fetcher); diff --git a/packages/runtime/src/Document.tsx b/packages/runtime/src/Document.tsx index 413fe3143..98c3abc0e 100644 --- a/packages/runtime/src/Document.tsx +++ b/packages/runtime/src/Document.tsx @@ -76,6 +76,7 @@ export function Scripts(props: React.ScriptHTMLAttributes<HTMLScriptElement>) { scripts.unshift(`${assetsManifest.publicPath}${assetsManifest.dataLoader}`); } + const matchedIds = matches.map(match => match.route.id); const routePath = getCurrentRoutePath(matches); const windowContext: WindowContext = { appData, @@ -83,6 +84,7 @@ export function Scripts(props: React.ScriptHTMLAttributes<HTMLScriptElement>) { routesConfig, routePath, downgrade, + matchedIds, }; return ( diff --git a/packages/runtime/src/dataLoader.ts b/packages/runtime/src/dataLoader.ts index 7e33b10cf..654024aea 100644 --- a/packages/runtime/src/dataLoader.ts +++ b/packages/runtime/src/dataLoader.ts @@ -1,10 +1,14 @@ -import type { GetData } from '@ice/types'; +import type { GetData, GetDataConfig } from '@ice/types'; import getRequestContext from './requestContext.js'; interface Loaders { [routeId: string]: GetData; } +interface LoadersConfig { + [routeId: string]: GetDataConfig; +} + interface Result { value: any; status: string; @@ -88,11 +92,31 @@ async function load(id: string, loader: GetData) { } } +/** + * Get loaders by config of loaders. + */ +function getLoaders(loaders: LoadersConfig, fetcher: Function): Loaders { + const context = (window as any).__ICE_APP_CONTEXT__ || {}; + const matchedIds = context.matchedIds || []; + + const transformedLoaders: Loaders = {}; + matchedIds.forEach(id => { + // If getData is an object, it is wrapped with a function. + transformedLoaders[id] = typeof loaders[id] === 'function' ? loaders[id] : () => { + return fetcher(loaders[id]); + }; + }); + + return transformedLoaders; +} + /** * Load initial data and register global loader. * In order to load data, JavaScript modules, CSS and other assets in parallel. */ -function init(loaders: Loaders) { +function init(loadersConfig: LoadersConfig, fetcher: Function) { + const loaders = getLoaders(loadersConfig, fetcher); + try { loadInitialData(loaders); } catch (error) { diff --git a/packages/types/src/runtime.ts b/packages/types/src/runtime.ts index 4a56a79e3..fa3c3fe35 100644 --- a/packages/types/src/runtime.ts +++ b/packages/types/src/runtime.ts @@ -34,12 +34,14 @@ export interface AppExport { getAppData?: GetAppData; } -export type GetAppData = (ctx: RequestContext) => Promise<AppData> | AppData; +export type GetAppData = (ctx: RequestContext) => (Promise<AppData> | AppData); + +export type GetDataConfig = (ctx: RequestContext) => (Promise<RouteData> | RouteData) | RouteData; // app.getData & route.getData -export type GetData = (ctx: RequestContext) => Promise<RouteData> | RouteData; -export type GetServerData = (ctx: RequestContext) => Promise<RouteData> | RouteData; -export type GetStaticData = (ctx: RequestContext) => Promise<RouteData> | RouteData; +export type GetData = (ctx: RequestContext) => (Promise<RouteData> | RouteData); +export type GetServerData = (ctx: RequestContext) => (Promise<RouteData> | RouteData); +export type GetStaticData = (ctx: RequestContext) => (Promise<RouteData> | RouteData); // route.getConfig export type GetConfig = (args: { data?: RouteData }) => RouteConfig; @@ -80,7 +82,7 @@ export interface AppContext { export type WindowContext = Pick< AppContext, - 'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' + 'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' | 'matchedIds' >; export type Renderer = ( From b71963a55ae1888b4ecfdd0b62db6f260f7cc9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=93=E9=99=8C=E5=90=8C=E5=AD=A6?= <answershuto@gmail.com> Date: Fri, 21 Oct 2022 17:58:57 +0800 Subject: [PATCH 17/27] chore: modify name of loaders (#621) --- packages/runtime/src/dataLoader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/dataLoader.ts b/packages/runtime/src/dataLoader.ts index 654024aea..382e5dfaa 100644 --- a/packages/runtime/src/dataLoader.ts +++ b/packages/runtime/src/dataLoader.ts @@ -95,19 +95,19 @@ async function load(id: string, loader: GetData) { /** * Get loaders by config of loaders. */ -function getLoaders(loaders: LoadersConfig, fetcher: Function): Loaders { +function getLoaders(loadersConfig: LoadersConfig, fetcher: Function): Loaders { const context = (window as any).__ICE_APP_CONTEXT__ || {}; const matchedIds = context.matchedIds || []; - const transformedLoaders: Loaders = {}; + const loaders: Loaders = {}; matchedIds.forEach(id => { // If getData is an object, it is wrapped with a function. - transformedLoaders[id] = typeof loaders[id] === 'function' ? loaders[id] : () => { - return fetcher(loaders[id]); + loaders[id] = typeof loadersConfig[id] === 'function' ? loadersConfig[id] : () => { + return fetcher(loadersConfig[id]); }; }); - return transformedLoaders; + return loaders; } /** From c0981492095af1b41441acc4d17923850195e028 Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Mon, 24 Oct 2022 15:17:16 +0800 Subject: [PATCH 18/27] feat: support static runtime (#609) * feat: support static runtime * fix: optimize code * fix: optimize code * fix: update app data * fix: optimize code and add test case --- examples/with-plugin-request/ice.config.mts | 1 + examples/with-plugin-request/src/app.tsx | 10 ++ .../ice/templates/core/entry.client.ts.ejs | 7 +- .../ice/templates/core/entry.server.ts.ejs | 4 +- .../ice/templates/core/runtimeModules.ts.ejs | 20 ++- .../miniapp-runtime/src/app/runClientApp.tsx | 16 ++- packages/plugin-request/src/index.ts | 1 + packages/runtime/src/routesConfig.ts | 2 - packages/runtime/src/runClientApp.tsx | 51 +++---- packages/runtime/src/runServerApp.tsx | 124 ++++++++---------- packages/runtime/src/runtime.tsx | 4 + packages/runtime/tests/runClientApp.test.tsx | 44 +++++-- packages/runtime/tests/runServerApp.test.tsx | 14 +- packages/types/src/runtime.ts | 5 +- 14 files changed, 176 insertions(+), 127 deletions(-) diff --git a/examples/with-plugin-request/ice.config.mts b/examples/with-plugin-request/ice.config.mts index b0076d018..5685132f3 100644 --- a/examples/with-plugin-request/ice.config.mts +++ b/examples/with-plugin-request/ice.config.mts @@ -2,6 +2,7 @@ import { defineConfig } from '@ice/app'; import request from '@ice/plugin-request'; export default defineConfig({ + dataLoader: false, plugins: [ request(), ], diff --git a/examples/with-plugin-request/src/app.tsx b/examples/with-plugin-request/src/app.tsx index fe245be42..2203173ac 100644 --- a/examples/with-plugin-request/src/app.tsx +++ b/examples/with-plugin-request/src/app.tsx @@ -1,4 +1,6 @@ +import { request as requestAPI } from 'ice'; import { defineRequestConfig } from '@ice/plugin-request/esm/types'; + const requestConfig = { // 可选的,全局设置 request 是否返回 response 对象,默认为 false withFullResponse: false, @@ -50,6 +52,14 @@ const requestConfig = { }, }; +export async function getAppData() { + try { + return await requestAPI('/user'); + } catch (err) { + console.log('request error', err); + } +} + export default { app: { rootId: 'app', diff --git a/packages/ice/templates/core/entry.client.ts.ejs b/packages/ice/templates/core/entry.client.ts.ejs index b42de4432..731679e38 100644 --- a/packages/ice/templates/core/entry.client.ts.ejs +++ b/packages/ice/templates/core/entry.client.ts.ejs @@ -1,6 +1,6 @@ import { runClientApp, getAppConfig } from '<%- iceRuntimePath %>'; +import { commons, statics } from './runtimeModules'; import * as app from '@/app'; -import runtimeModules from './runtimeModules'; <% if (enableRoutes) { %> import routes from './routes'; <% } %> @@ -11,7 +11,10 @@ const getRouterBasename = () => { runClientApp({ app, - runtimeModules, + runtimeModules: { + commons, + statics, + }, <% if (enableRoutes) { %>routes,<% } %> basename: getRouterBasename(), hydrate: <%- hydrate %>, diff --git a/packages/ice/templates/core/entry.server.ts.ejs b/packages/ice/templates/core/entry.server.ts.ejs index 0b5fc3d5e..9af719038 100644 --- a/packages/ice/templates/core/entry.server.ts.ejs +++ b/packages/ice/templates/core/entry.server.ts.ejs @@ -1,7 +1,7 @@ import './env.server'; import * as runtime from '@ice/runtime/server'; +import { commons, statics } from './runtimeModules'; import * as app from '@/app'; -import runtimeModules from './runtimeModules'; import Document from '@/document'; import type { RenderMode } from '@ice/runtime'; // @ts-ignore @@ -9,6 +9,8 @@ import assetsManifest from 'virtual:assets-manifest.json'; import routes from './routes'; import routesConfig from './routes-config.bundle.mjs'; +const runtimeModules = { commons, statics }; + const getRouterBasename = () => { const appConfig = runtime.getAppConfig(app); return appConfig?.router?.basename ?? '<%- basename %>' ?? ''; diff --git a/packages/ice/templates/core/runtimeModules.ts.ejs b/packages/ice/templates/core/runtimeModules.ts.ejs index 4251b0996..81e7aef66 100644 --- a/packages/ice/templates/core/runtimeModules.ts.ejs +++ b/packages/ice/templates/core/runtimeModules.ts.ejs @@ -1,15 +1,25 @@ <% const moduleNames = []; -%> -<% if (runtimeModules.length) {-%> - <% runtimeModules.filter((moduleInfo) => !moduleInfo.staticModule).forEach((runtimeModule, index) => { -%> - <% moduleNames.push('module' + index) %> +<% const staticModuleNames = []; -%> +<% if (runtimeModules.length) { -%> +<% runtimeModules.forEach((runtimeModule, index) => { -%> import module<%= index %> from '<%= runtimeModule.path %>'; + <% if (runtimeModule.staticRuntime) { -%> + <% staticModuleNames.push('module' + index) -%> + <% } else { -%> + <% moduleNames.push('module' + index) -%> + <% } -%> <% }) -%> <% } -%> -const modules = [ +export const statics = [ +<% staticModuleNames.forEach((moduleName, index) => { -%> + <%= moduleName %>, +<% }) -%> +]; +export const commons = [ <% moduleNames.forEach((moduleName, index) => { -%> <%= moduleName %>, <% }) -%> ]; -export default modules; + diff --git a/packages/miniapp-runtime/src/app/runClientApp.tsx b/packages/miniapp-runtime/src/app/runClientApp.tsx index 179cf4e6a..f15a019e8 100644 --- a/packages/miniapp-runtime/src/app/runClientApp.tsx +++ b/packages/miniapp-runtime/src/app/runClientApp.tsx @@ -10,19 +10,25 @@ import { setHistory } from './history.js'; export default async function runClientApp(options: RunClientAppOptions) { const { app, runtimeModules } = options; - const appData = await getAppData(app); - const { miniappManifest } = app; const appConfig = getAppConfig(app); - setHistory(miniappManifest.routes); const appContext: AppContext = { appExport: app, appConfig, - appData, + appData: null, }; const runtime = new Runtime(appContext); + if (runtimeModules.statics) { + await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean)); + } + const appData = await getAppData(app); + const { miniappManifest } = app; + setHistory(miniappManifest.routes); + runtime.setAppContext({ ...appContext, appData }); // TODO: to be tested - await Promise.all(runtimeModules.map(m => runtime.loadModule(m)).filter(Boolean)); + if (runtimeModules.commons) { + await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean)); + } render(runtime); // TODO: transform routes to pages in miniappManifest createMiniApp(miniappManifest); diff --git a/packages/plugin-request/src/index.ts b/packages/plugin-request/src/index.ts index 57d349ed7..dbfdea59d 100644 --- a/packages/plugin-request/src/index.ts +++ b/packages/plugin-request/src/index.ts @@ -25,6 +25,7 @@ const plugin: Plugin<PluginRequestOptions | void> = () => ({ }); }, runtime: `${PLUGIN_NAME}/esm/runtime`, + staticRuntime: true, }); export type { diff --git a/packages/runtime/src/routesConfig.ts b/packages/runtime/src/routesConfig.ts index 712c914e7..15ad90b4b 100644 --- a/packages/runtime/src/routesConfig.ts +++ b/packages/runtime/src/routesConfig.ts @@ -30,7 +30,6 @@ export function getTitle(matches: RouteMatch[], routesConfig: RoutesConfig): str */ function getMergedValue(key: string, matches: RouteMatch[], routesConfig: RoutesConfig) { let result; - for (let match of matches) { const routeId = match.route.id; const data = routesConfig[routeId]; @@ -57,7 +56,6 @@ export async function updateRoutesConfig(matches: RouteMatch[], routesConfig: Ro if (title) { document.title = title; } - const meta = getMeta(matches, routesConfig) || []; const links = getLinks(matches, routesConfig) || []; const scripts = getScripts(matches, routesConfig) || []; diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index 7f313ccba..c34173044 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -50,30 +50,11 @@ export default async function runClientApp(options: RunClientAppOptions) { } = windowContext; const requestContext = getRequestContext(window.location); - - if (!appData) { - appData = await getAppData(app, requestContext); - } - const appConfig = getAppConfig(app); const history = createHistory(appConfig, { memoryRouter, routePath }); // Set history for import it from ice. setHistory(history); - const matches = matchRoutes( - routes, - memoryRouter ? routePath : history.location, - basename, - ); - const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load }))); - - if (!routesData) { - routesData = await loadRoutesData(matches, requestContext, routeModules); - } - if (!routesConfig) { - routesConfig = getRoutesConfig(matches, routesData, routeModules); - } - const appContext: AppContext = { appExport: app, routes, @@ -82,22 +63,46 @@ export default async function runClientApp(options: RunClientAppOptions) { routesData, routesConfig, assetsManifest, - matches, - routeModules, basename, routePath, }; const runtime = new Runtime(appContext); runtime.setAppRouter(DefaultAppRouter); + // Load static module before getAppData, + // so we can call request in in getAppData which provide by `plugin-request`. + if (runtimeModules.statics) { + await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean)); + } + + if (!appData) { + appData = await getAppData(app, requestContext); + } + + const matches = matchRoutes( + routes, + memoryRouter ? routePath : history.location, + basename, + ); + const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load }))); + + if (!routesData) { + routesData = await loadRoutesData(matches, requestContext, routeModules); + } + if (!routesConfig) { + routesConfig = getRoutesConfig(matches, routesData, routeModules); + } if (hydrate && !downgrade) { runtime.setRender((container, element) => { ReactDOM.hydrateRoot(container, element); }); } - - await Promise.all(runtimeModules.map(m => runtime.loadModule(m)).filter(Boolean)); + // Reset app context after app context is updated. + runtime.setAppContext({ ...appContext, matches, routeModules, routesData, routesConfig, appData }); + if (runtimeModules.commons) { + await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean)); + } render({ runtime, history }); } diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx index 4b61f17ea..1d2f4a0a7 100644 --- a/packages/runtime/src/runServerApp.tsx +++ b/packages/runtime/src/runServerApp.tsx @@ -5,15 +5,12 @@ import { Action, parsePath } from 'history'; import type { Location } from 'history'; import type { AppContext, RouteItem, ServerContext, - AppData, - AppExport, RuntimePlugin, CommonJsRuntime, AssetsManifest, + AppExport, AssetsManifest, RouteMatch, - RequestContext, - AppConfig, GetConfig, - RouteModules, RenderMode, DocumentComponent, + RuntimeModules, } from '@ice/types'; import Runtime from './runtime.js'; import App from './App.js'; @@ -34,7 +31,7 @@ interface RenderOptions { app: AppExport; assetsManifest: AssetsManifest; routes: RouteItem[]; - runtimeModules: (RuntimePlugin | CommonJsRuntime)[]; + runtimeModules: RuntimeModules; Document: DocumentComponent; documentOnly?: boolean; renderMode?: RenderMode; @@ -141,19 +138,48 @@ function pipeToResponse(res: ServerResponse, pipe: NodeWritablePiper) { async function doRender(serverContext: ServerContext, renderOptions: RenderOptions): Promise<RenderResult> { const { req } = serverContext; - const { routes, documentOnly, app, basename, serverOnlyBasename, disableFallback } = renderOptions; + const { + app, + basename, + serverOnlyBasename, + routes, + documentOnly, + disableFallback, + assetsManifest, + runtimeModules, + renderMode, + } = renderOptions; const location = getLocation(req.url); const requestContext = getRequestContext(location, serverContext); - - let appData; + const appConfig = getAppConfig(app); + let appData: any; + const appContext: AppContext = { + appExport: app, + routes, + appConfig, + appData, + routesData: null, + routesConfig: null, + assetsManifest, + basename, + matches: [], + }; + const runtime = new Runtime(appContext); + runtime.setAppRouter(DefaultAppRouter); + // Load static module before getAppData. + if (runtimeModules.statics) { + await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean)); + } // don't need to execute getAppData in CSR if (!documentOnly) { - appData = await getAppData(app, requestContext); + try { + appData = await getAppData(app, requestContext); + } catch (err) { + console.error('Error: get app data error when SSR.', err); + } } - - const appConfig = getAppConfig(app); // HashRouter loads route modules by the CSR. if (appConfig?.router?.type === 'hash') { return renderDocument({ matches: [], renderOptions }); @@ -169,21 +195,19 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio if (documentOnly) { return renderDocument({ matches, routePath, renderOptions }); } - - const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load }))); - 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 }); + if (runtimeModules.commons) { + await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean)); + } return await renderServerEntry({ - appExport: app, - requestContext, - renderOptions, + runtime, matches, location, - appConfig, - appData, - routeModules, - basename: serverOnlyBasename || basename, - routePath, + renderOptions, }); } catch (err) { if (disableFallback) { @@ -202,17 +226,11 @@ function render404(): RenderResult { }; } -interface renderServerEntry { - appExport: AppExport; - requestContext: RequestContext; - renderOptions: RenderOptions; +interface RenderServerEntry { + runtime: Runtime; matches: RouteMatch[]; location: Location; - appConfig: AppConfig; - appData: AppData; - routeModules: RouteModules; - routePath?: string; - basename?: string; + renderOptions: RenderOptions; } /** @@ -220,47 +238,15 @@ interface renderServerEntry { */ async function renderServerEntry( { - appExport, - requestContext, + runtime, matches, location, - appConfig, - appData, renderOptions, - routeModules, - basename, - routePath, - }: renderServerEntry, + }: RenderServerEntry, ): Promise<RenderResult> { - const { - assetsManifest, - runtimeModules, - routes, - renderMode, - Document, - } = renderOptions; - - const routesData = await loadRoutesData(matches, requestContext, routeModules, renderMode); - const routesConfig = getRoutesConfig(matches, routesData, routeModules); - - const appContext: AppContext = { - appExport, - assetsManifest, - appConfig, - appData, - routesData, - routesConfig, - matches, - routes, - routeModules, - basename, - routePath, - }; - - const runtime = new Runtime(appContext); - runtime.setAppRouter(DefaultAppRouter); - await Promise.all(runtimeModules.map(m => runtime.loadModule(m)).filter(Boolean)); - + const { Document } = renderOptions; + const appContext = runtime.getAppContext(); + const { appData, routePath } = appContext; const staticNavigator = createStaticNavigator(); const AppProvider = runtime.composeAppProvider() || React.Fragment; const RouteWrappers = runtime.getWrappers(); diff --git a/packages/runtime/src/runtime.tsx b/packages/runtime/src/runtime.tsx index 1392ba548..05d6c3e91 100644 --- a/packages/runtime/src/runtime.tsx +++ b/packages/runtime/src/runtime.tsx @@ -41,6 +41,10 @@ class Runtime { public getAppContext = () => this.appContext; + public setAppContext = (appContext: AppContext) => { + this.appContext = appContext; + }; + public getRender = () => { return this.render; }; diff --git a/packages/runtime/tests/runClientApp.test.tsx b/packages/runtime/tests/runClientApp.test.tsx index b7102a72d..8d29a7b9b 100644 --- a/packages/runtime/tests/runClientApp.test.tsx +++ b/packages/runtime/tests/runClientApp.test.tsx @@ -52,6 +52,12 @@ describe('run client app', () => { }); }; + let staticMsg = ''; + + const staticRuntime = async () => { + staticMsg = 'static'; + }; + const wrapperRuntime = async ({ addWrapper }) => { const RouteWrapper = ({ children }) => { return <div>{children}</div>; @@ -87,6 +93,20 @@ describe('run client app', () => { }, ]; + it('run with static runtime', async () => { + await runClientApp({ + app: { + getAppData: async () => { + return { msg: staticMsg }; + }, + }, + routes: basicRoutes, + runtimeModules: { commons: [serverRuntime], statics: [staticRuntime] }, + hydrate: false, + }); + expect(domstring).toBe('<div>home<!-- -->static</div>'); + }); + it('run client basic', async () => { windowSpy.mockImplementation(() => ({ ...mockData, @@ -96,7 +116,7 @@ describe('run client app', () => { await runClientApp({ app: {}, routes: basicRoutes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); expect(domstring).toBe('<div>home</div>'); @@ -107,7 +127,7 @@ describe('run client app', () => { await runClientApp({ app: {}, routes: basicRoutes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); process.env.ICE_CORE_ROUTER = 'true'; @@ -118,7 +138,7 @@ describe('run client app', () => { await runClientApp({ app: {}, routes: basicRoutes, - runtimeModules: [serverRuntime, wrapperRuntime], + runtimeModules: { commons: [serverRuntime, wrapperRuntime] }, hydrate: true, }); expect(domstring).toBe('<div><div>home</div></div>'); @@ -128,7 +148,7 @@ describe('run client app', () => { await runClientApp({ app: {}, routes: basicRoutes, - runtimeModules: [serverRuntime, providerRuntmie], + runtimeModules: { commons: [serverRuntime, providerRuntmie] }, hydrate: true, }); expect(domstring).toBe('<div><div><div>home</div></div></div>'); @@ -138,7 +158,7 @@ describe('run client app', () => { await runClientApp({ app: {}, routes: [], - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); }); @@ -166,7 +186,7 @@ describe('run client app', () => { }, }, routes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: true, }); @@ -177,7 +197,7 @@ describe('run client app', () => { }, }, routes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: true, }); }); @@ -205,7 +225,7 @@ describe('run client app', () => { app: { }, routes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: true, memoryRouter: true, }); @@ -223,7 +243,7 @@ describe('run client app', () => { }, }, routes: basicRoutes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: true, }); expect(domstring).toBe('<div>home</div>'); @@ -239,7 +259,7 @@ describe('run client app', () => { }, }, routes: basicRoutes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); expect(domstring).toBe('<div>home<!-- -->-getAppData</div>'); @@ -265,7 +285,7 @@ describe('run client app', () => { }, }, routes: basicRoutes, - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); expect(executed).toBe(false); @@ -300,7 +320,7 @@ describe('run client app', () => { getData: async () => ({ data: 'test' }), }), }], - runtimeModules: [serverRuntime], + runtimeModules: { commons: [serverRuntime] }, hydrate: false, }); expect(domstring).toBe('<div>home<!-- -->test<!-- -->home</div>'); diff --git a/packages/runtime/tests/runServerApp.test.tsx b/packages/runtime/tests/runServerApp.test.tsx index 9794eb3d8..b3dd92da3 100644 --- a/packages/runtime/tests/runServerApp.test.tsx +++ b/packages/runtime/tests/runServerApp.test.tsx @@ -57,7 +57,7 @@ describe('run server app', () => { }, { app: {}, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: basicRoutes, Document, renderMode: 'SSR', @@ -77,7 +77,7 @@ describe('run server app', () => { }, { app: {}, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: basicRoutes, Document, renderMode: 'SSR', @@ -96,7 +96,7 @@ describe('run server app', () => { }, { app: {}, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: basicRoutes, Document, renderMode: 'SSR', @@ -116,7 +116,7 @@ describe('run server app', () => { }, { app: {}, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: basicRoutes, Document, }); @@ -139,7 +139,7 @@ describe('run server app', () => { }, }, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: basicRoutes, Document, }); @@ -158,7 +158,7 @@ describe('run server app', () => { }, { app: {}, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: [{ id: 'home', path: 'home', @@ -197,7 +197,7 @@ describe('run server app', () => { }, { app: {}, assetsManifest, - runtimeModules: [], + runtimeModules: { commons: [] }, routes: basicRoutes, Document, renderMode: 'SSR', diff --git a/packages/types/src/runtime.ts b/packages/types/src/runtime.ts index fa3c3fe35..6ca766c4d 100644 --- a/packages/types/src/runtime.ts +++ b/packages/types/src/runtime.ts @@ -181,7 +181,10 @@ export interface CommonJsRuntime { default: RuntimePlugin; } -export type RuntimeModules = (RuntimePlugin | CommonJsRuntime)[]; +export interface RuntimeModules { + statics?: (RuntimePlugin | CommonJsRuntime)[]; + commons?: (RuntimePlugin | CommonJsRuntime)[]; +} export interface AppRouterProps { action: Action; From 8b8461eb1a23bf95610d0d2626b7b040e3d15ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=B4=E6=BE=9C?= <shuilan.cj@taobao.com> Date: Tue, 25 Oct 2022 14:13:08 +0800 Subject: [PATCH 19/27] fix: should disable hydrate when document only (#625) * fix: should disable hydrate when document only * fix: disable hydrate when document only --- packages/runtime/src/Document.tsx | 1 + packages/runtime/src/dataLoader.ts | 11 +++++++---- packages/runtime/src/runClientApp.tsx | 3 ++- packages/types/src/runtime.ts | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/Document.tsx b/packages/runtime/src/Document.tsx index 98c3abc0e..c1ec7246e 100644 --- a/packages/runtime/src/Document.tsx +++ b/packages/runtime/src/Document.tsx @@ -85,6 +85,7 @@ export function Scripts(props: React.ScriptHTMLAttributes<HTMLScriptElement>) { routePath, downgrade, matchedIds, + documentOnly, }; return ( diff --git a/packages/runtime/src/dataLoader.ts b/packages/runtime/src/dataLoader.ts index 382e5dfaa..c50110bd3 100644 --- a/packages/runtime/src/dataLoader.ts +++ b/packages/runtime/src/dataLoader.ts @@ -101,10 +101,13 @@ function getLoaders(loadersConfig: LoadersConfig, fetcher: Function): Loaders { const loaders: Loaders = {}; matchedIds.forEach(id => { - // If getData is an object, it is wrapped with a function. - loaders[id] = typeof loadersConfig[id] === 'function' ? loadersConfig[id] : () => { - return fetcher(loadersConfig[id]); - }; + const loaderConfig = loadersConfig[id]; + if (loaderConfig) { + // If getData is an object, it is wrapped with a function. + loaders[id] = typeof loaderConfig === 'function' ? loadersConfig[id] : () => { + return fetcher(loaderConfig); + }; + } }); return loaders; diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index c34173044..edec592e3 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -47,6 +47,7 @@ export default async function runClientApp(options: RunClientAppOptions) { routesConfig, routePath, downgrade, + documentOnly, } = windowContext; const requestContext = getRequestContext(window.location); @@ -93,7 +94,7 @@ export default async function runClientApp(options: RunClientAppOptions) { routesConfig = getRoutesConfig(matches, routesData, routeModules); } - if (hydrate && !downgrade) { + if (hydrate && !downgrade && !documentOnly) { runtime.setRender((container, element) => { ReactDOM.hydrateRoot(container, element); }); diff --git a/packages/types/src/runtime.ts b/packages/types/src/runtime.ts index 6ca766c4d..64c2e7a84 100644 --- a/packages/types/src/runtime.ts +++ b/packages/types/src/runtime.ts @@ -82,7 +82,7 @@ export interface AppContext { export type WindowContext = Pick< AppContext, - 'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' | 'matchedIds' + 'appData' | 'routesData' | 'routesConfig' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' >; export type Renderer = ( From 9f2bbfbc7198a2ff66622b4a8e4df0ab40682c85 Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Tue, 25 Oct 2022 15:15:25 +0800 Subject: [PATCH 20/27] docs: api (#628) * docs: add api title * docs: add api * docs: api link --- website/docs/guide/basic/api.md | 263 +++++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 4 deletions(-) diff --git a/website/docs/guide/basic/api.md b/website/docs/guide/basic/api.md index e49f61d1d..8bc5f762a 100644 --- a/website/docs/guide/basic/api.md +++ b/website/docs/guide/basic/api.md @@ -3,10 +3,199 @@ title: API order: 15 --- +### defineAppConfig -## Hooks +该方法用于获取框架配置的类型提示。 -### `useMounted` +```ts title="src/app.ts" +import { defineAppConfig } from 'ice'; + +export default defineAppConfig(() => ({ + app: { + rootId: 'ice-container', + } +})); +``` + +### defineGetConfig + +该方法用于获取路由组件支持的配置类型,支持的配置可以被插件动态扩展。 + +```tsx title="src/pages/home.tsx" +import { defineGetConfig } from 'ice'; + +export const getConfig = defineGetConfig(() => ({ + title: 'About', + meta: [ + { + name: 'theme-color', + content: '#eee', + }, + ], +})); +``` + +### history + +应用的 history,用于获取路由信息、执行跳转等。 + +```ts + +import { history } from 'ice'; + +export function historyPush (link: string) { + history.push(link); +} +``` + +:::caution + +在应用入口 `src/app.ts` 导入使用时,由于 history 还未完成初始化创建,不能以立即执行的方式使用。推荐以上述方式封装后在必要的时候进行调用。 +::: + +### useParams + +useParams 函数返回动态路由的匹配参数信息。 + +```tsx +import { useParams } from 'ice'; + +// 路由规则为 home/:uid/repo/:repoid +// 当前路径 home/clark/repo/1234 +export default function Home() { + const params = useParams(); + // params 输出内容为 { uid: 'clark', repoid: '1234'} + return ( + <> + <h2>Home Page</h2> + </> + ); +} +``` + +### useSearchParams + +useSearchParams 用于读取和修改当前 URL 的 query string。 + +```tsx +import { useParams } from 'ice'; + +// 路由规则 home?uid=1234 +export default function Home() { + const [searchParams, setSearchParams] = useSearchParams(); + // searchParams 输出内容为 { uid: '1234'} + + const changeSearch = () => { + // 通过 setSearchParams 可以修改对应 query string + setSearchParams({ uid: '4321'}); + } + return ( + <> + <h2>Home Page</h2> + </> + ); +} +``` + +### useNavigate + +useNavigate 函数返回一个可以控制跳转的函数,用于组件内部控制路径跳转 + +```tsx +import { useNavigate } from 'ice'; + +export default function Home() { + const navigate = useNavigate(); + useEffect(() => { + navigate('/logout', { replace: true }); + }, []); + + return ( + <> + <h2>Home Page</h2> + </> + ); +} +``` + +### useLocation + +useLocation 返回当前 location 信息。 + +```tsx +import { useLocation } from 'ice'; + +function Home() { + const location = useLocation(); + useEffect(() => { + // send pv info + }, [location]); + return ( + <> + <h2>Home Page</h2> + </> + ); +} +``` + +### useAppData + +useAppData 返回应用全局数据,需要搭配 `src/app.ts` 中导出的 getAppData 使用: + +```ts title="src/app.ts" +export async function getAppData() { + return await fetch('/api/user'); +} +``` + +在任意组件内进行消费: + +```tsx +import { useAppData } from 'ice'; + +function Home() { + const data = useAppData(); + // data 内容为 /api/user 接口返回数据 + return ( + <> + <h2>Home Page</h2> + </> + ); +} +``` + +### useData + +useData 返回路由组件数据,需要搭配在路由组件中定义数据获取方法进行使用。参考[页面数据请求文档](./request) + +### useConfig + +useConfig 返回路由组件配置,搭配 [defineGetConfig](./api#definegetconfig)。 + +```tsx title="src/pages/home.tsx" +import { defineGetConfig, useConfig } from 'ice'; + +export default function Home() { + const config = useConfig(); + return ( + <> + <h2>Home Page</h2> + </> + ); +} + +export const getConfig = defineGetConfig(() => ({ + title: 'About', + meta: [ + { + name: 'theme-color', + content: '#eee', + }, + ], +})); +``` + +### useMounted 该方法会在 React Hydrate 完成后返回 `true`,一般在开启 SSR/SSG 的应用中,用于控制在不同端中渲染不同的组件。 @@ -28,8 +217,6 @@ const Home = () => { }; ``` -## 组件 - ### `<ClientOnly />` `<ClientOnly />` 组件只允许在 React Hydrate 完成后在 Client 端中渲染组件。 @@ -76,3 +263,71 @@ export function Home () { ### `<KeepAliveOutlet />` 缓存所有路由组件的状态。详细使用方式参考 [Keep Alive 文档](../advanced/keep-alive/#缓存路由组件)。 + +### `<Link />` + +`<Link>` 是 React 组件,用于渲染带路由跳转功能的 `<a>` 元素。 + +```tsx +import { Link } from 'ice'; + +function Home() { + const data = useAppData(); + // data 内容为 /api/user 接口返回数据 + return ( + <> + <h2>Home Page</h2> + <Link to="/user">user</Link> + </> + ); +} +``` + +### `<Outlet />` + +`<Outlet>` 用于渲染父路由中渲染子路由,通常出现在 `layout.tsx` Layout 组件中。 + +```tsx title="src/layout.tsx" + +import { Outlet } from 'ice'; + +export default function Layout() { + return ( + <div> + <h1>title</h1> + <Outlet /> + </div> + ); +} +``` + +### AppConfig + +AppConfig 是 TS 类型定义,用于获取框架配置类型。 + +```ts +import type { AppConfig } from 'ice'; +``` + +:::caution + +推荐通过 [defineAppConfig](./api#defineappconfig) 的方式在入口定义应用类型,如果涉及到类型拓展和泛型的应用可以通过上述方式导入该类型。 +::: + +### RouteConfig + +RouteConfig 是 TS 类型定义,用于获取路由配置类型。 + +```ts +import type { RouteConfig } from 'ice'; +``` + +:::caution + +推荐通过 [defineGetConfig](./api#definegetconfig) 的方式在路由组件中定义类型,如果涉及到类型拓展和泛型的应用可以通过上述方式导入该类型。 +::: + +### Document 组件 + +`Meta`、`Title`、`Links`、`Scripts` 和 `Main` 组件仅支持在 `src/document.tsx` 中使用,使用场景参考 Document 文档](./document) + From 8b4fdfc207d13b5330b90b83d216df186bbefaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=93=E9=99=8C=E5=90=8C=E5=AD=A6?= <answershuto@gmail.com> Date: Tue, 25 Oct 2022 15:43:26 +0800 Subject: [PATCH 21/27] Chore/modify export (#623) * chore: modify ExportData to IdentifierData * chore: add type for addDataLoaderImport * chore: modify to removeIdentifierData * chore: add AddDataLoaderImport * chore: modify exportAlias to alias * feat: modify generateIdentifier * chore: modify transformIdentifierToDeclaration * chore: modify to declaration * chore: modify addDeclaration * chore: modify removeDeclaration --- packages/ice/src/createService.ts | 18 +++--- packages/ice/src/service/runtimeGenerator.ts | 58 ++++++++++--------- packages/ice/tests/generator.test.ts | 22 +++---- packages/plugin-request/src/index.ts | 2 +- packages/types/src/generator.ts | 11 ++-- packages/types/src/plugin.ts | 5 +- .../src/unPlugins/redirectImport.ts | 11 ++-- .../tests/redirectImport.test.ts | 2 +- 8 files changed, 67 insertions(+), 62 deletions(-) diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 3c9ade65e..65a5f849f 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -5,7 +5,7 @@ import { Context } from 'build-scripts'; import consola from 'consola'; import type { CommandArgs, CommandName } from 'build-scripts'; import type { AppConfig, Config, PluginData } from '@ice/types'; -import type { ExportData } from '@ice/types/esm/generator.js'; +import type { DeclarationData } from '@ice/types/esm/generator.js'; import type { ExtendsPluginAPI } from '@ice/types/esm/plugin.js'; import webpack from '@ice/bundles/compiled/webpack/index.js'; import Generator from './service/runtimeGenerator.js'; @@ -56,20 +56,20 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt }); const generatorAPI = { - addExport: (exportData: ExportData) => { - generator.addExport('framework', exportData); + addExport: (declarationData: DeclarationData) => { + generator.addDeclaration('framework', declarationData); }, - addExportTypes: (exportData: ExportData) => { - generator.addExport('frameworkTypes', exportData); + addExportTypes: (declarationData: DeclarationData) => { + generator.addDeclaration('frameworkTypes', declarationData); }, - addRouteTypes: (exportData: ExportData) => { - generator.addExport('routeConfigTypes', exportData); + addRouteTypes: (declarationData: DeclarationData) => { + generator.addDeclaration('routeConfigTypes', declarationData); }, addRenderFile: generator.addRenderFile, addRenderTemplate: generator.addTemplateFiles, modifyRenderData: generator.modifyRenderData, - addDataLoaderImport: (exportData: ExportData) => { - generator.addExport('dataLoaderImport', exportData); + addDataLoaderImport: (declarationData: DeclarationData) => { + generator.addDeclaration('dataLoaderImport', declarationData); }, }; diff --git a/packages/ice/src/service/runtimeGenerator.ts b/packages/ice/src/service/runtimeGenerator.ts index 3b1341792..1826925a3 100644 --- a/packages/ice/src/service/runtimeGenerator.ts +++ b/packages/ice/src/service/runtimeGenerator.ts @@ -5,10 +5,10 @@ import fg from 'fast-glob'; import ejs from 'ejs'; import lodash from '@ice/bundles/compiled/lodash/index.js'; import type { - AddExport, - RemoveExport, + AddDeclaration, + RemoveDeclaration, AddContent, - GetExportData, + GetDeclarations, ParseRenderData, Render, RenderFile, @@ -18,7 +18,7 @@ import type { RenderDataRegistration, RenderTemplate, RenderData, - ExportData, + DeclarationData, Registration, TemplateOptions, } from '@ice/types/esm/generator.js'; @@ -35,27 +35,27 @@ interface Options { templates?: (string | TemplateOptions)[]; } -export function generateExports(exportList: ExportData[]) { - const importStatements = []; - let exportStatements = []; +export function generateDeclaration(exportList: DeclarationData[]) { + const importDeclarations = []; + let exportDeclarations = []; let exportNames: string[] = []; exportList.forEach(data => { - const { specifier, source, exportAlias, type } = data; + const { specifier, source, alias, type } = data; const isDefaultImport = !Array.isArray(specifier); const specifiers = isDefaultImport ? [specifier] : specifier; const symbol = type ? ';' : ','; - importStatements.push(`import ${type ? 'type ' : ''}${isDefaultImport ? specifier : `{ ${specifiers.map(specifierStr => ((exportAlias && exportAlias[specifierStr]) ? `${specifierStr} as ${exportAlias[specifierStr]}` : specifierStr)).join(', ')} }`} from '${source}';`); + importDeclarations.push(`import ${type ? 'type ' : ''}${isDefaultImport ? specifier : `{ ${specifiers.map(specifierStr => ((alias && alias[specifierStr]) ? `${specifierStr} as ${alias[specifierStr]}` : specifierStr)).join(', ')} }`} from '${source}';`); specifiers.forEach((specifierStr) => { - if (exportAlias && exportAlias[specifierStr]) { - exportStatements.push(`${exportAlias[specifierStr]}: ${specifierStr}${symbol}`); + if (alias && alias[specifierStr]) { + exportDeclarations.push(`${alias[specifierStr]}: ${specifierStr}${symbol}`); } else { - exportStatements.push(`${specifierStr}${symbol}`); + exportDeclarations.push(`${specifierStr}${symbol}`); } exportNames.push(specifierStr); }); }); return { - importStr: importStatements.join('\n'), + importStr: importDeclarations.join('\n'), /** * Add two whitespace character in order to get the formatted code. For example: * export { @@ -63,23 +63,27 @@ export function generateExports(exportList: ExportData[]) { useAuth, }; */ - exportStr: exportStatements.join('\n '), + exportStr: exportDeclarations.join('\n '), exportNames, }; } -export function checkExportData(currentList: ExportData[], exportData: ExportData | ExportData[], apiName: string) { +export function checkExportData( + currentList: DeclarationData[], + exportData: DeclarationData | DeclarationData[], + apiName: string, +) { (Array.isArray(exportData) ? exportData : [exportData]).forEach((data) => { const exportNames = (Array.isArray(data.specifier) ? data.specifier : [data.specifier]).map((specifierStr) => { - return data?.exportAlias?.[specifierStr] || specifierStr; + return data?.alias?.[specifierStr] || specifierStr; }); - currentList.forEach(({ specifier, exportAlias }) => { + currentList.forEach(({ specifier, alias }) => { // check exportName and specifier const currentExportNames = (Array.isArray(specifier) ? specifier : [specifier]).map((specifierStr) => { - return exportAlias?.[specifierStr] || specifierStr; + return alias?.[specifierStr] || specifierStr; }); if (currentExportNames.some((name) => exportNames.includes(name))) { - consola.error('specifier:', specifier, 'exportAlias:', exportAlias); + consola.error('specifier:', specifier, 'alias:', alias); consola.error('duplicate with', data); throw new Error(`duplicate export data added by ${apiName}`); } @@ -87,7 +91,7 @@ export function checkExportData(currentList: ExportData[], exportData: ExportDat }); } -export function removeExportData(exportList: ExportData[], removeSource: string | string[]) { +export function removeDeclarations(exportList: DeclarationData[], removeSource: string | string[]) { const removeSourceNames = Array.isArray(removeSource) ? removeSource : [removeSource]; return exportList.filter(({ source }) => { const needRemove = removeSourceNames.includes(source); @@ -138,19 +142,19 @@ export default class Generator { this.render(); }, RENDER_WAIT); - public addExport: AddExport = (registerKey, exportData) => { + public addDeclaration: AddDeclaration = (registerKey, exportData) => { const exportList = this.contentRegistration[registerKey] || []; checkExportData(exportList, exportData, registerKey); // remove export before add - this.removeExport( + this.removeDeclaration( registerKey, Array.isArray(exportData) ? exportData.map((data) => data.source) : exportData.source); this.addContent(registerKey, exportData); }; - public removeExport: RemoveExport = (registerKey, removeSource) => { + public removeDeclaration: RemoveDeclaration = (registerKey, removeSource) => { const exportList = this.contentRegistration[registerKey] || []; - this.contentRegistration[registerKey] = removeExportData(exportList, removeSource); + this.contentRegistration[registerKey] = removeDeclarations(exportList, removeSource); }; public addContent: AddContent = (apiName, ...args) => { @@ -169,9 +173,9 @@ export default class Generator { this.contentRegistration[registerKey].push(...content); }; - private getExportData: GetExportData = (registerKey, dataKeys) => { + private getDeclarations: GetDeclarations = (registerKey, dataKeys) => { const exportList = this.contentRegistration[registerKey] || []; - const { importStr, exportStr, exportNames } = generateExports(exportList); + const { importStr, exportStr, exportNames } = generateDeclaration(exportList); const [importStrKey, exportStrKey] = dataKeys; return { [importStrKey]: importStr, @@ -185,7 +189,7 @@ export default class Generator { const globalStyles = fg.sync([getGlobalStyleGlobPattern()], { cwd: this.rootDir }); let exportsData = {}; this.contentTypes.forEach(item => { - const data = this.getExportData(item, ['imports', 'exports']); + const data = this.getDeclarations(item, ['imports', 'exports']); exportsData = Object.assign({}, exportsData, { [`${item}`]: data, }); diff --git a/packages/ice/tests/generator.test.ts b/packages/ice/tests/generator.test.ts index cf33efca7..8c35227db 100644 --- a/packages/ice/tests/generator.test.ts +++ b/packages/ice/tests/generator.test.ts @@ -1,9 +1,9 @@ import { expect, it, describe } from 'vitest'; -import { generateExports, checkExportData, removeExportData } from '../src/service/runtimeGenerator'; +import { generateDeclaration, checkExportData, removeDeclarations } from '../src/service/runtimeGenerator'; -describe('generateExports', () => { +describe('generateDeclaration', () => { it('basic usage', () => { - const { importStr, exportStr } = generateExports([{ + const { importStr, exportStr } = generateDeclaration([{ source: 'react-router', specifier: 'Router', type: false, @@ -12,7 +12,7 @@ describe('generateExports', () => { expect(exportStr).toBe('Router,'); }); it('type export', () => { - const { importStr, exportStr } = generateExports([{ + const { importStr, exportStr } = generateDeclaration([{ source: 'react-router', specifier: 'Router', type: true, @@ -21,7 +21,7 @@ describe('generateExports', () => { expect(exportStr).toBe('Router;'); }); it('named exports', () => { - const { importStr, exportStr } = generateExports([{ + const { importStr, exportStr } = generateDeclaration([{ source: 'react-router', specifier: ['Switch', 'Route'], }]); @@ -30,10 +30,10 @@ describe('generateExports', () => { }); it('aliased exports', () => { - const { importStr, exportStr } = generateExports([{ + const { importStr, exportStr } = generateDeclaration([{ source: 'react-helmet', specifier: 'Helmet', - exportAlias: { + alias: { Helmet: 'Head', }, }]); @@ -68,18 +68,18 @@ describe('checkExportData', () => { }); }); -describe('removeExportData', () => { +describe('removeDeclarations', () => { it('basic usage', () => { - const removed = removeExportData(defaultExportData, 'react-router'); + const removed = removeDeclarations(defaultExportData, 'react-router'); expect(removed.length).toBe(1); expect(removed[0].source).toBe('react-helmet'); }); it('fail to remove', () => { - const removed = removeExportData(defaultExportData, ['react-dom']); + const removed = removeDeclarations(defaultExportData, ['react-dom']); expect(removed.length).toBe(2); }); it('remove exports', () => { - const removed = removeExportData(defaultExportData, ['react-router', 'react-helmet']); + const removed = removeDeclarations(defaultExportData, ['react-router', 'react-helmet']); expect(removed.length).toBe(0); }); }); \ No newline at end of file diff --git a/packages/plugin-request/src/index.ts b/packages/plugin-request/src/index.ts index dbfdea59d..1398d5012 100644 --- a/packages/plugin-request/src/index.ts +++ b/packages/plugin-request/src/index.ts @@ -3,7 +3,7 @@ import type { Request, Interceptors, InterceptorRequest, InterceptorResponse } f // @ts-ignore // eslint-disable-next-line -interface PluginRequestOptions {} +interface PluginRequestOptions { } const PLUGIN_NAME = '@ice/plugin-request'; diff --git a/packages/types/src/generator.ts b/packages/types/src/generator.ts index 5006268f5..1090fa549 100644 --- a/packages/types/src/generator.ts +++ b/packages/types/src/generator.ts @@ -1,8 +1,8 @@ -export interface ExportData { +export interface DeclarationData { specifier: string | string[]; source: string; type?: boolean; - exportAlias?: Record<string, string>; + alias?: Record<string, string>; } export type RenderData = Record<string, unknown>; @@ -21,10 +21,10 @@ export interface Registration { } export type SetPlugins = (plugins: any) => void; -export type AddExport = (registerKey: string, exportData: ExportData | ExportData[]) => void; -export type RemoveExport = (registerKey: string, removeSource: string | string[]) => void; +export type AddDeclaration = (registerKey: string, declarationData: DeclarationData | DeclarationData[]) => void; +export type RemoveDeclaration = (registerKey: string, removeSource: string | string[]) => void; export type AddContent = (apiName: string, ...args: any) => void; -export type GetExportData = (registerKey: string, dataKeys: string[]) => { +export type GetDeclarations = (registerKey: string, dataKeys: string[]) => { imports?: string; exports?: string; exportNames?: string[]; @@ -33,6 +33,7 @@ export type GetExportData = (registerKey: string, dataKeys: string[]) => { export type ParseRenderData = () => Record<string, unknown>; export type Render = () => void; export type ModifyRenderData = (registration: RenderDataRegistration) => void; +export type AddDataLoaderImport = (declarationData: DeclarationData) => void; export type AddRenderFile = (templatePath: string, targetPath: string, extraData?: ExtraData) => void; export type AddTemplateFiles = (templateOptions: string | TemplateOptions, extraData?: ExtraData) => void; export type RenderFile = (templatePath: string, targetPath: string, extraData?: ExtraData) => void; diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index c12e42508..25faff3d4 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -5,10 +5,10 @@ import type WebpackDevServer from 'webpack-dev-server'; import type { BuildOptions, BuildResult } from 'esbuild'; import type { NestedRouteManifest } from '@ice/route-manifest'; import type { Config } from './config.js'; -import type { ExportData, AddRenderFile, AddTemplateFiles, ModifyRenderData } from './generator.js'; +import type { DeclarationData, AddRenderFile, AddTemplateFiles, ModifyRenderData, AddDataLoaderImport } from './generator.js'; import type { AssetsManifest } from './runtime.js'; -type AddExport = (exportData: ExportData) => void; +type AddExport = (exportData: DeclarationData) => void; type EventName = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'; type ServerCompilerBuildOptions = Pick<BuildOptions, 'write' | @@ -108,6 +108,7 @@ export interface ExtendsPluginAPI { addRenderFile: AddRenderFile; addRenderTemplate: AddTemplateFiles; modifyRenderData: ModifyRenderData; + addDataLoaderImport: AddDataLoaderImport; }; watch: { addEvent?: (watchEvent: WatchEvent) => void; diff --git a/packages/webpack-config/src/unPlugins/redirectImport.ts b/packages/webpack-config/src/unPlugins/redirectImport.ts index 9341314ac..dd61570a3 100644 --- a/packages/webpack-config/src/unPlugins/redirectImport.ts +++ b/packages/webpack-config/src/unPlugins/redirectImport.ts @@ -4,16 +4,16 @@ import type { UnpluginOptions } from '@ice/bundles/compiled/unplugin/index.js'; import consola from 'consola'; import MagicString from '@ice/bundles/compiled/magic-string/index.js'; import { createFilter } from '@rollup/pluginutils'; -import type { ExportData } from '@ice/types/esm/generator.js'; +import type { DeclarationData } from '@ice/types/esm/generator.js'; import type { Config } from '@ice/types'; interface Options { - exportData: ExportData[]; + exportData: DeclarationData[]; targetSource: string; } interface PluginOptions { - exportData: ExportData[]; + exportData: DeclarationData[]; sourceMap?: Config['sourceMap']; } @@ -40,11 +40,10 @@ const { init, parse } = moduleLexer; const AS_ALIAS_REG_EXP = /^(\w+)\s+as\s+(\w+)/; const ICE_REG_EXP = /import\s?(?:type)?\s?\{([\w*\s{},]*)\}\s+from\s+['"](.*)['"]/; -export function parseRedirectData(data: ExportData[]): RedirectData { +export function parseRedirectData(data: DeclarationData[]): RedirectData { const redirectData: RedirectData = {}; - data.forEach(({ specifier, exportAlias, type, source }) => { + data.forEach(({ specifier, alias = {}, type, source }) => { const isDefault = typeof specifier === 'string'; - const alias = exportAlias || {}; (isDefault ? [specifier] : specifier).forEach((str) => { redirectData[str] = { type, diff --git a/packages/webpack-config/tests/redirectImport.test.ts b/packages/webpack-config/tests/redirectImport.test.ts index a17780925..d1ad7b87c 100644 --- a/packages/webpack-config/tests/redirectImport.test.ts +++ b/packages/webpack-config/tests/redirectImport.test.ts @@ -13,7 +13,7 @@ describe('redirect import', () => { }, { specifier: 'Head', source: 'react-helmet', - exportAlias: { + alias: { Head: 'Helmet', }, }, { From f2073e02bb63eaf226aaf4788e821b794d4bcf5b Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Tue, 25 Oct 2022 20:18:43 +0800 Subject: [PATCH 22/27] docs: plugin antd and fusion (#617) --- website/docs/guide/advanced/antd.md | 84 +++++++++++++++++++++ website/docs/guide/advanced/fusion.md | 89 +++++++++++++++++++++++ website/docs/guide/plugins/plugin-list.md | 8 ++ 3 files changed, 181 insertions(+) create mode 100644 website/docs/guide/advanced/antd.md create mode 100644 website/docs/guide/advanced/fusion.md diff --git a/website/docs/guide/advanced/antd.md b/website/docs/guide/advanced/antd.md new file mode 100644 index 000000000..d055b1f62 --- /dev/null +++ b/website/docs/guide/advanced/antd.md @@ -0,0 +1,84 @@ +--- +title: 使用 antd 组件 +--- + +icejs 项目中可以直接使用 antd 组件,关于 antd 组件按需引入的问题说明: +- 脚本代码按需引入:不推荐使用 babel-plugin-import,社区主流工具 Webpack/Vite 等都已支持 tree-shaking,构建时默认都会做按需的引入 +- 样式代码按需引入:结合社区讨论 [issue](https://github.com/ant-design/ant-design/issues/16600#issuecomment-492572520),大多数场景下样式按需引入并无太大意义,反而会引入其他工程问题,因此推荐组件样式在项目级全量引入 + +综上所述,如果不存在主题定制以及样式大小极致的要求,项目中并不需要使用 antd 插件,通过在 `src/global.css` 中全量引入样式即可: + +```css title="src/global.css" +@import 'antd/dist/antd.css'; + +body {} +``` + +## 开启插件 + +安装插件: + +```bash +$ npm i -D @ice/plugin-antd +``` + +在 `ice.config.mts` 中添加插件: + +```ts title="ice.config.mts" +import { defineConfig } from '@ice/app'; +import antd from '@ice/plugin-antd'; + +export default defineConfig({ + plugins: [ + antd({ + importStyle: true, + }), + ], +}); +``` + +## 配置 + +### importStyle + +- 类型: `boolean` +- 默认值: `false` + +为 antd 组件按需加载样式。 + +### dark + +- 类型: `boolean` +- 默认值: `false` + +开启暗色主题。 + +### compact + +- 类型: `boolean` +- 默认值: `false` + +开启紧凑主题。 + +### theme + +- 类型: `Record<string, string>` +- 默认值: `{}` + +配置 antd 的 theme 主题,配置形式如下: + +```ts title="ice.config.mts" +import { defineConfig } from '@ice/app'; +import antd from '@ice/plugin-antd'; + +export default defineConfig({ + plugins: [ + antd({ + theme: { + // primary-color 为 antd 的 theme token + 'primary-color': '#1DA57A', + }, + }), + ], +}); +``` \ No newline at end of file diff --git a/website/docs/guide/advanced/fusion.md b/website/docs/guide/advanced/fusion.md new file mode 100644 index 000000000..d5c957738 --- /dev/null +++ b/website/docs/guide/advanced/fusion.md @@ -0,0 +1,89 @@ +--- +title: 使用 fusion 组件 +--- + +icejs 项目中可以直接使用 fusion 组件,关于 fusion 组件按需引入的问题说明: +- 脚本代码按需引入:不推荐使用 babel-plugin-import,社区主流工具 Webpack/Vite 等都已支持 tree-shaking,构建时默认都会做按需的引入 +- 样式代码按需引入:结合社区讨论 [issue](https://github.com/ant-design/ant-design/issues/16600#issuecomment-492572520),大多数场景下样式按需引入并无太大意义,反而会引入其他工程问题,因此推荐组件样式在项目级全量引入 + +综上所述,如果不存在主题定制以及样式大小极致的要求,项目中并不需要使用 fusion 插件,通过在 `src/global.css` 中全量引入样式即可: + +```css title="src/global.css" +@import '@alifd/next/dist/next.var.css'; + +body {} +``` + +## 开启插件 + +安装插件: + +```bash +$ npm i -D @ice/plugin-fusion +``` + +在 `ice.config.mts` 中添加插件: + +```ts title="ice.config.mts" +import { defineConfig } from '@ice/app'; +import fusion from '@ice/plugin-fusion'; + +export default defineConfig({ + plugins: [ + fusion({ + importStyle: true, + }), + ], +}); +``` + +## 配置 + +### importStyle + +- 类型: `boolean|'sass'` +- 默认值: `false` + +为 fusion 组件按需加载样式,目前 fusion 组件提供两种类型样式,默认加载 `css` 样式,如果希望加载 `sass` 样式可以将 `importStyle` 配置为 `sass`。 + +### themePackage + +- 类型: `string` +- 默认值: `''` + +为 fusion 组件配置主题包,比如: + +```ts title="ice.config.mts" +import { defineConfig } from '@ice/app'; +import fusion from '@ice/plugin-fusion'; + +export default defineConfig({ + plugins: [ + fusion({ + themePackage: '@alifd/theme-design-pro', + }), + ], +}); +``` + +### theme + +- 类型: `Record<string, string>` +- 默认值: `{}` + +配置 antd 的 theme 主题,配置形式如下: + +```ts title="ice.config.mts" +import { defineConfig } from '@ice/app'; +import fusion from '@ice/plugin-fusion'; + +export default defineConfig({ + plugins: [ + fusion({ + theme: { + 'css-prefix': 'next-icestark-', + }, + }), + ], +}); +``` \ No newline at end of file diff --git a/website/docs/guide/plugins/plugin-list.md b/website/docs/guide/plugins/plugin-list.md index eae89cbf4..1df60dde5 100644 --- a/website/docs/guide/plugins/plugin-list.md +++ b/website/docs/guide/plugins/plugin-list.md @@ -14,3 +14,11 @@ order: 2 ## [@ice/plugin-css-assets-local](../advanced/css-assets-local) 提供将 CSS 中的网络资源本地化的能力。 + +## [@ice/plugin-antd](../advanced/antd) + +提供 antd 组件样式按需加载及主题配置能力。 + +## [@ice/plugin-fusion](../advanced/fusion) + +提供 fusion 组件样式按需加载及主题配置能力。 \ No newline at end of file From efad74b6fa39ee9a91a6a90b184084c4f2189fb6 Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Wed, 26 Oct 2022 15:46:42 +0800 Subject: [PATCH 23/27] fix: add transform include for non-js files (#599) * fix: add transform include for non-js files * fix: optimize code --- packages/style-import/src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/style-import/src/index.ts b/packages/style-import/src/index.ts index a1e2447d5..b7946c422 100644 --- a/packages/style-import/src/index.ts +++ b/packages/style-import/src/index.ts @@ -81,9 +81,12 @@ export default function importStylePlugin(options: TransformOptions) { name: 'transform-import-style', // Add plugin as a post plugin, so we do not need to deal with ts language. enforce: 'post', - async transform(code: string, id: string, transformOption: { isServer: Boolean }) { + transformInclude(id: string) { // Only transform source code. - if (transformOption.isServer || !code || !id.match(/\.(js|jsx|ts|tsx)$/) || id.match(/node_modules/)) { + return id.match(/\.(js|jsx|ts|tsx)$/) && !id.match(/node_modules/); + }, + async transform(code: string, id: string, transformOption: { isServer: Boolean }) { + if (transformOption.isServer || !code) { return null; } return await importStyle(code, options); From f606569ed10f6d49c65b36da98242032d7efb135 Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Wed, 26 Oct 2022 16:14:55 +0800 Subject: [PATCH 24/27] fix: add assets manifest only in js file (#637) --- .../webpack-config/src/webpackPlugins/AssetsManifestPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webpack-config/src/webpackPlugins/AssetsManifestPlugin.ts b/packages/webpack-config/src/webpackPlugins/AssetsManifestPlugin.ts index 0ff7fb3fe..2d20b7763 100644 --- a/packages/webpack-config/src/webpackPlugins/AssetsManifestPlugin.ts +++ b/packages/webpack-config/src/webpackPlugins/AssetsManifestPlugin.ts @@ -48,7 +48,8 @@ export default class AssetsManifestPlugin { const entryName = entrypoint.name; const mainFiles = filterAssets(entrypoint); entries[entryName] = mainFiles; - entryFiles.push(mainFiles[0]); + const jsMainFiles = mainFiles.filter((file) => file.endsWith('.js')); + entryFiles.push(jsMainFiles[0]); const chunks = entrypoint?.getChildren(); chunks.forEach((chunk) => { const chunkName = chunk.name; From 3e491e8a7bbee5b4af39e6bafda574595da5028b Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Thu, 27 Oct 2022 13:53:55 +0800 Subject: [PATCH 25/27] docs: mock use middleware (#612) * docs: mock use middleware * fix: comment * docs: add types --- website/docs/guide/basic/mock.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/website/docs/guide/basic/mock.md b/website/docs/guide/basic/mock.md index 79a5b96c3..d065e6927 100644 --- a/website/docs/guide/basic/mock.md +++ b/website/docs/guide/basic/mock.md @@ -132,3 +132,22 @@ export default { ``` 完整的语法请参考 [Mock.js 文档](http://mockjs.com/examples.html)。 + +## 处理请求数据 + +如果用户希望使用一些中间件来处理请求的数据(`req` 对象),可以参考以下的示例代码: + +```ts +import bodyParser from 'body-parser'; +import type { Request, Response } from 'express'; + +export default { + 'POST /api/login': (req: Request, res: Response) => { + bodyParser.json({ limit: '5mb', strict: false })(req, res, () => { + console.log(req.body); + + res.send({}); + }) + }, +} +``` From e8fe21a00b2c9bc96b1f25af27d040e559d51e6e Mon Sep 17 00:00:00 2001 From: ClarkXia <xiawenwu41@gmail.com> Date: Thu, 27 Oct 2022 14:18:47 +0800 Subject: [PATCH 26/27] fix: regex rule for different package manager (#635) * fix: regex rule for different package manager * chore: remove log --- packages/runtime/package.json | 3 +- .../webpack-config/src/config/splitChunks.ts | 44 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 4a45bebc7..019979f6e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -13,7 +13,8 @@ "./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/single-router.js", + "./package.json": "./package.json" }, "files": [ "esm", diff --git a/packages/webpack-config/src/config/splitChunks.ts b/packages/webpack-config/src/config/splitChunks.ts index 04c1361b7..127ba5483 100644 --- a/packages/webpack-config/src/config/splitChunks.ts +++ b/packages/webpack-config/src/config/splitChunks.ts @@ -1,3 +1,5 @@ +import * as path from 'path'; +import { createRequire } from 'module'; import crypto from 'crypto'; import type webpack from 'webpack'; @@ -10,10 +12,11 @@ interface NameModule { type: string; updateHash: (hash: crypto.Hash) => void; } +const require = createRequire(import.meta.url); export const FRAMEWORK_BUNDLES = [ // runtime dependencies - 'react', 'react-dom', '@ice/runtime', 'react-router', 'react-router-dom', + 'react', 'react-dom', 'react-router', 'react-router-dom', ]; @@ -26,7 +29,33 @@ const isModuleCSS = (module: { type: string }): boolean => { ); }; const getSplitChunksConfig = (rootDir: string): webpack.Configuration['optimization']['splitChunks'] => { - const frameworkRegex = new RegExp(`[\\\\/]node_modules[\\\\/](${FRAMEWORK_BUNDLES.join('|')})[\\\\/]`); + const frameworkPaths: string[] = []; + const visitedFramework = new Set<string>(); + + function addPackagePath(packageName: string, dir: string) { + try { + if (visitedFramework.has(packageName)) { + return; + } + visitedFramework.add(packageName); + const packageJsonPath = require.resolve(`${packageName}/package.json`, { + paths: [dir], + }); + const packageDir = path.join(packageJsonPath, '../'); + if (frameworkPaths.includes(packageDir)) return; + frameworkPaths.push(packageDir); + const dependencies = require(packageJsonPath).dependencies || {}; + for (const name of Object.keys(dependencies)) { + addPackagePath(name, packageDir); + } + } catch (_) { + // Do not error on resolve framework package + } + } + + FRAMEWORK_BUNDLES.forEach((packageName) => { + addPackagePath(packageName, rootDir); + }); return { chunks: 'all', cacheGroups: { @@ -34,15 +63,13 @@ const getSplitChunksConfig = (rootDir: string): webpack.Configuration['optimizat chunks: 'all', name: 'framework', test(module: TestModule) { - const resource = module.nameForCondition && module.nameForCondition(); - if (!resource) { - return false; - } - return frameworkRegex.test(resource); + const resource = module.nameForCondition?.(); + return resource ? frameworkPaths.some((pkgPath) => resource.startsWith(pkgPath)) : false; }, priority: 40, enforce: true, }, + // Fork from https://github.com/vercel/next.js/blob/1b2636763c39433dcc52756d158b4a444abc85cb/packages/next/build/webpack-config.ts#L1463-L1494 lib: { test(module: TestModule) { return module.size() > 160000 && /node_modules[/\\]/.test(module.nameForCondition() || ''); @@ -61,6 +88,9 @@ const getSplitChunksConfig = (rootDir: string): webpack.Configuration['optimizat } return hash.digest('hex').substring(0, 8); }, + priority: 30, + minChunks: 1, + reuseExistingChunk: true, }, }, maxInitialRequests: 25, From cfad5dfa5e9022bf6bfe4f68e727fec126cf52ab Mon Sep 17 00:00:00 2001 From: luhc228 <44047106+luhc228@users.noreply.github.com> Date: Thu, 27 Oct 2022 15:26:45 +0800 Subject: [PATCH 27/27] refactor: import app store (#596) * fix: throw error when no app store * fix: ts type * refactor: import app store * refactor: change extraContext to runtimeOptions * feat: call render in plugin * refactor: optimize code * chore: remove log --- packages/ice/src/createService.ts | 7 ++++ packages/ice/src/service/runtimeGenerator.ts | 2 +- .../ice/templates/core/entry.client.ts.ejs | 11 +++++- .../ice/templates/core/entry.server.ts.ejs | 11 ++++++ packages/plugin-store/package.json | 5 --- packages/plugin-store/src/_store.ts | 13 ------- packages/plugin-store/src/api.ts | 1 - packages/plugin-store/src/constants.ts | 2 +- packages/plugin-store/src/index.ts | 37 ++++++++++++++----- packages/plugin-store/src/runtime.tsx | 28 ++++++++------ packages/plugin-store/tsconfig.json | 5 +-- packages/runtime/src/runClientApp.tsx | 4 +- packages/runtime/src/runServerApp.tsx | 4 +- packages/runtime/src/runtime.tsx | 7 +++- packages/types/src/plugin.ts | 6 ++- packages/types/src/runtime.ts | 3 +- 16 files changed, 92 insertions(+), 54 deletions(-) delete mode 100644 packages/plugin-store/src/_store.ts delete mode 100644 packages/plugin-store/src/api.ts diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 65a5f849f..cb0ac8a47 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -62,6 +62,12 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt addExportTypes: (declarationData: DeclarationData) => { generator.addDeclaration('frameworkTypes', declarationData); }, + addRuntimeOptions: (declarationData: DeclarationData) => { + generator.addDeclaration('runtimeOptions', declarationData); + }, + removeRuntimeOptions: (removeSource: string | string[]) => { + generator.removeDeclaration('runtimeOptions', removeSource); + }, addRouteTypes: (declarationData: DeclarationData) => { generator.addDeclaration('routeConfigTypes', declarationData); }, @@ -71,6 +77,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt addDataLoaderImport: (declarationData: DeclarationData) => { generator.addDeclaration('dataLoaderImport', declarationData); }, + render: generator.render, }; const serverCompileTask = new ServerCompileTask(); diff --git a/packages/ice/src/service/runtimeGenerator.ts b/packages/ice/src/service/runtimeGenerator.ts index 1826925a3..2eedc0e72 100644 --- a/packages/ice/src/service/runtimeGenerator.ts +++ b/packages/ice/src/service/runtimeGenerator.ts @@ -125,7 +125,7 @@ export default class Generator { this.rerender = false; this.renderTemplates = []; this.renderDataRegistration = []; - this.contentTypes = ['framework', 'frameworkTypes', 'routeConfigTypes', 'dataLoaderImport']; + this.contentTypes = ['framework', 'frameworkTypes', 'routeConfigTypes', 'dataLoaderImport', 'runtimeOptions']; // empty .ice before render fse.emptyDirSync(path.join(rootDir, targetDir)); // add initial templates diff --git a/packages/ice/templates/core/entry.client.ts.ejs b/packages/ice/templates/core/entry.client.ts.ejs index 731679e38..bfee27acc 100644 --- a/packages/ice/templates/core/entry.client.ts.ejs +++ b/packages/ice/templates/core/entry.client.ts.ejs @@ -1,9 +1,11 @@ import { runClientApp, getAppConfig } from '<%- iceRuntimePath %>'; import { commons, statics } from './runtimeModules'; import * as app from '@/app'; -<% if (enableRoutes) { %> +<% if (enableRoutes) { -%> import routes from './routes'; -<% } %> +<% } -%> +<%- runtimeOptions.imports %> + const getRouterBasename = () => { const appConfig = getAppConfig(app); return appConfig?.router?.basename ?? '<%- basename %>' ?? ''; @@ -16,6 +18,11 @@ runClientApp({ statics, }, <% if (enableRoutes) { %>routes,<% } %> +<% if (runtimeOptions.exports) { -%> + runtimeOptions: { + <%- runtimeOptions.exports %> + }, +<% } -%> 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 9af719038..11d9409b9 100644 --- a/packages/ice/templates/core/entry.server.ts.ejs +++ b/packages/ice/templates/core/entry.server.ts.ejs @@ -8,6 +8,7 @@ import type { RenderMode } from '@ice/runtime'; import assetsManifest from 'virtual:assets-manifest.json'; import routes from './routes'; import routesConfig from './routes-config.bundle.mjs'; +<%- runtimeOptions.imports %> const runtimeModules = { commons, statics }; @@ -50,6 +51,11 @@ export async function renderToHTML(requestContext, options: RenderOptions = {}) routePath, disableFallback, routesConfig, +<% if (runtimeOptions.exports) { -%> + runtimeOptions: { + <%- runtimeOptions.exports %> + }, +<% } -%> }); } @@ -69,5 +75,10 @@ export async function renderToResponse(requestContext, options: RenderOptions = renderMode, disableFallback, routesConfig, +<% if (runtimeOptions.exports) { -%> + runtimeOptions: { + <%- runtimeOptions.exports %> + }, +<% } -%> }); } diff --git a/packages/plugin-store/package.json b/packages/plugin-store/package.json index 07b006b37..6934ba259 100644 --- a/packages/plugin-store/package.json +++ b/packages/plugin-store/package.json @@ -20,11 +20,6 @@ "import": "./esm/runtime.js", "default": "./esm/runtime.js" }, - "./api": { - "types": "./esm/api.d.ts", - "import": "./esm/api.js", - "default": "./esm/api.js" - }, "./types": { "types": "./esm/types.d.ts", "import": "./esm/types.js", diff --git a/packages/plugin-store/src/_store.ts b/packages/plugin-store/src/_store.ts deleted file mode 100644 index 3381e8273..000000000 --- a/packages/plugin-store/src/_store.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * This file which is imported by the runtime.tsx, is to avoid TS error. - */ -import type { IcestoreDispatch, IcestoreRootState } from '@ice/store'; -import { createStore } from '@ice/store'; - -const models = {}; - -const store = createStore(models); - -export default store; -export type IRootDispatch = IcestoreDispatch<typeof models>; -export type IRootState = IcestoreRootState<typeof models>; diff --git a/packages/plugin-store/src/api.ts b/packages/plugin-store/src/api.ts deleted file mode 100644 index 215dfc296..000000000 --- a/packages/plugin-store/src/api.ts +++ /dev/null @@ -1 +0,0 @@ -export { createStore, createModel } from '@ice/store'; diff --git a/packages/plugin-store/src/constants.ts b/packages/plugin-store/src/constants.ts index 95fa1cbbe..18067fa1e 100644 --- a/packages/plugin-store/src/constants.ts +++ b/packages/plugin-store/src/constants.ts @@ -1,3 +1,3 @@ export const PAGE_STORE_MODULE = '__PAGE_STORE__'; export const PAGE_STORE_PROVIDER = '__PAGE_STORE_PROVIDER__'; -export const PAGE_STORE_INITIAL_STATES = '__PAGE_STORE_INITIAL_STATES__'; \ No newline at end of file +export const PAGE_STORE_INITIAL_STATES = '__PAGE_STORE_INITIAL_STATES__'; diff --git a/packages/plugin-store/src/index.ts b/packages/plugin-store/src/index.ts index 53ba2bcc7..f32497ce4 100644 --- a/packages/plugin-store/src/index.ts +++ b/packages/plugin-store/src/index.ts @@ -14,7 +14,7 @@ const ignoreStoreFilePatterns = ['**/models/**', storeFilePattern]; const plugin: Plugin<Options> = (options) => ({ name: PLUGIN_NAME, - setup: ({ onGetConfig, modifyUserConfig, generator, context: { rootDir, userConfig } }) => { + setup: ({ onGetConfig, modifyUserConfig, generator, context: { rootDir, userConfig }, watch }) => { const { resetPageState = false } = options || {}; const srcDir = path.join(rootDir, 'src'); const pageDir = path.join(srcDir, 'pages'); @@ -24,15 +24,32 @@ const plugin: Plugin<Options> = (options) => ({ ignoreFiles: [...(userConfig?.routes?.ignoreFiles || []), ...ignoreStoreFilePatterns], }); + if (getAppStorePath(srcDir)) { + generator.addRuntimeOptions({ + source: '@/store', + specifier: 'appStore', + }); + } + + watch.addEvent([ + /src\/store.(js|ts)$/, + (event) => { + if (event === 'unlink') { + generator.removeRuntimeOptions('@/store'); + } + if (event === 'add') { + generator.addRuntimeOptions({ + source: '@/store', + specifier: 'appStore', + }); + } + if (['add', 'unlink'].includes(event)) { + generator.render(); + } + }, + ]); + onGetConfig(config => { - // Add app store provider. - const appStorePath = getAppStorePath(srcDir); - if (appStorePath) { - config.alias = { - ...config.alias || {}, - $store: appStorePath, - }; - } config.transformPlugins = [ ...(config.transformPlugins || []), exportStoreProviderPlugin({ pageDir, resetPageState }), @@ -43,7 +60,7 @@ const plugin: Plugin<Options> = (options) => ({ // Export store api: createStore, createModel from `.ice/index.ts`. generator.addExport({ specifier: ['createStore', 'createModel'], - source: '@ice/plugin-store/api', + source: '@ice/plugin-store/esm/runtime', type: false, }); }, diff --git a/packages/plugin-store/src/runtime.tsx b/packages/plugin-store/src/runtime.tsx index c2bda6965..0181045b9 100644 --- a/packages/plugin-store/src/runtime.tsx +++ b/packages/plugin-store/src/runtime.tsx @@ -2,25 +2,29 @@ import * as React from 'react'; import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/types'; import { PAGE_STORE_INITIAL_STATES, PAGE_STORE_PROVIDER } from './constants.js'; import type { StoreConfig } from './types.js'; -import appStore from '$store'; -const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider, useAppContext }) => { +const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider, useAppContext }, runtimeOptions) => { const { appExport, appData } = appContext; const storeConfig: StoreConfig = (typeof appExport.store === 'function' ? (await appExport.store(appData)) : appExport.store) || {}; const { initialStates } = storeConfig; - if (appStore && Object.prototype.hasOwnProperty.call(appStore, 'Provider')) { - // Add app store Provider - const StoreProvider: AppProvider = ({ children }) => { + + // Add app store <Provider />. + const StoreProvider: AppProvider = ({ children }) => { + if (runtimeOptions?.appStore?.Provider) { + const { Provider } = runtimeOptions.appStore; return ( - <appStore.Provider initialStates={initialStates}> + <Provider initialStates={initialStates}> {children} - </appStore.Provider> + </Provider> ); - }; - addProvider(StoreProvider); - } - // page store + } + return <>{children}</>; + }; + + addProvider(StoreProvider); + + // Add page store <Provider />. const StoreProviderWrapper: RouteWrapper = ({ children, routeId }) => { const { routeModules } = useAppContext(); const routeModule = routeModules[routeId]; @@ -39,3 +43,5 @@ const runtime: RuntimePlugin = async ({ appContext, addWrapper, addProvider, use }; export default runtime; + +export { createModel, createStore } from '@ice/store'; diff --git a/packages/plugin-store/tsconfig.json b/packages/plugin-store/tsconfig.json index c98231291..972f3542f 100644 --- a/packages/plugin-store/tsconfig.json +++ b/packages/plugin-store/tsconfig.json @@ -4,10 +4,7 @@ "baseUrl": "./", "rootDir": "src", "outDir": "esm", - "jsx": "react", - "paths": { - "$store": ["./src/_store"] - } + "jsx": "react" }, "include": ["src"] } \ No newline at end of file diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index edec592e3..7b093a515 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -26,6 +26,7 @@ export interface RunClientAppOptions { hydrate?: boolean; basename?: string; memoryRouter?: boolean; + runtimeOptions?: Record<string, any>; } type History = BrowserHistory | HashHistory | MemoryHistory; @@ -38,6 +39,7 @@ export default async function runClientApp(options: RunClientAppOptions) { basename, hydrate, memoryRouter, + runtimeOptions, } = options; const windowContext: WindowContext = (window as any).__ICE_APP_CONTEXT__ || {}; const assetsManifest: AssetsManifest = (window as any).__ICE_ASSETS_MANIFEST__ || {}; @@ -68,7 +70,7 @@ export default async function runClientApp(options: RunClientAppOptions) { routePath, }; - const runtime = new Runtime(appContext); + const runtime = new Runtime(appContext, runtimeOptions); runtime.setAppRouter(DefaultAppRouter); // Load static module before getAppData, // so we can call request in in getAppData which provide by `plugin-request`. diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx index 1d2f4a0a7..c79cf2801 100644 --- a/packages/runtime/src/runServerApp.tsx +++ b/packages/runtime/src/runServerApp.tsx @@ -44,6 +44,7 @@ interface RenderOptions { routesConfig: { [key: string]: GetConfig; }; + runtimeOptions?: Record<string, any>; } interface Piper { @@ -148,6 +149,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio assetsManifest, runtimeModules, renderMode, + runtimeOptions, } = renderOptions; const location = getLocation(req.url); @@ -166,7 +168,7 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio basename, matches: [], }; - const runtime = new Runtime(appContext); + const runtime = new Runtime(appContext, runtimeOptions); runtime.setAppRouter(DefaultAppRouter); // Load static module before getAppData. if (runtimeModules.statics) { diff --git a/packages/runtime/src/runtime.tsx b/packages/runtime/src/runtime.tsx index 05d6c3e91..83066cd9b 100644 --- a/packages/runtime/src/runtime.tsx +++ b/packages/runtime/src/runtime.tsx @@ -21,6 +21,8 @@ import { useAppContext } from './AppContext.js'; class Runtime { private appContext: AppContext; + private runtimeOptions?: Record<string, any>; + private AppRouter: ComponentType<AppRouterProps>; private AppProvider: ComponentWithChildren[]; @@ -29,7 +31,7 @@ class Runtime { private render: Renderer; - public constructor(appContext: AppContext) { + public constructor(appContext: AppContext, runtimeOptions?: Record<string, any>) { this.AppProvider = []; this.appContext = appContext; this.render = (container, element) => { @@ -37,6 +39,7 @@ class Runtime { root.render(element); }; this.RouteWrappers = []; + this.runtimeOptions = runtimeOptions; } public getAppContext = () => this.appContext; @@ -67,7 +70,7 @@ class Runtime { const runtimeModule = (module as CommonJsRuntime).default || module as RuntimePlugin; if (module) { - return await runtimeModule(runtimeAPI); + return await runtimeModule(runtimeAPI, this.runtimeOptions); } } diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index 25faff3d4..29a9a0779 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -5,10 +5,11 @@ import type WebpackDevServer from 'webpack-dev-server'; import type { BuildOptions, BuildResult } from 'esbuild'; import type { NestedRouteManifest } from '@ice/route-manifest'; import type { Config } from './config.js'; -import type { DeclarationData, AddRenderFile, AddTemplateFiles, ModifyRenderData, AddDataLoaderImport } from './generator.js'; +import type { DeclarationData, AddRenderFile, AddTemplateFiles, ModifyRenderData, AddDataLoaderImport, Render } from './generator.js'; import type { AssetsManifest } from './runtime.js'; type AddExport = (exportData: DeclarationData) => void; +type RemoveExport = (removeSource: string | string[]) => void; type EventName = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'; type ServerCompilerBuildOptions = Pick<BuildOptions, 'write' | @@ -104,10 +105,13 @@ export interface ExtendsPluginAPI { generator: { addExport: AddExport; addExportTypes: AddExport; + addRuntimeOptions: AddExport; + removeRuntimeOptions: RemoveExport; addRouteTypes: AddExport; addRenderFile: AddRenderFile; addRenderTemplate: AddTemplateFiles; modifyRenderData: ModifyRenderData; + render: Render; addDataLoaderImport: AddDataLoaderImport; }; watch: { diff --git a/packages/types/src/runtime.ts b/packages/types/src/runtime.ts index 64c2e7a84..c7145d4c1 100644 --- a/packages/types/src/runtime.ts +++ b/packages/types/src/runtime.ts @@ -173,7 +173,8 @@ export interface RuntimeAPI { export interface RuntimePlugin { ( - apis: RuntimeAPI + apis: RuntimeAPI, + runtimeOptions?: Record<string, any>, ): Promise<void> | void; }