From 93b280e00bc380a69b5a0c18086f31d1e0e09304 Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Mon, 1 Jan 2024 20:37:14 +0800 Subject: [PATCH] [gem-book] Refactor live update --- .prettierrc.json | 12 +- .../docs/en/02-elements/coach-mark.md | 5 +- .../duoyun-ui/docs/en/02-elements/popover.md | 4 +- .../docs/zh/02-elements/coach-mark.md | 5 +- .../duoyun-ui/docs/zh/02-elements/popover.md | 4 +- packages/gem-book/docs/.gitignore | 1 - .../docs/en/002-guide/004-metadata.md | 2 +- .../docs/en/002-guide/007-extension.md | 6 +- .../gem-book/docs/zh/002-guide/001-sort.md | 3 + .../gem-book/docs/zh/002-guide/003-cli.md | 41 ++-- .../docs/zh/002-guide/004-metadata.md | 3 +- .../docs/zh/002-guide/007-extension.md | 39 +--- packages/gem-book/docs/zh/002-guide/README.md | 6 +- packages/gem-book/docs/zh/003-plugins.md | 20 +- packages/gem-book/docs/zh/README.md | 14 +- packages/gem-book/gem-book.cli.json | 3 +- packages/gem-book/package.json | 8 +- packages/gem-book/public/custom-ws-client.js | 45 ++++ packages/gem-book/schema.json | 213 ++++++++++++++++++ packages/gem-book/src/bin/builder.ts | 38 ++-- packages/gem-book/src/bin/index.ts | 189 +++++++++++----- packages/gem-book/src/bin/utils.ts | 60 +++-- packages/gem-book/src/common/config.ts | 6 +- packages/gem-book/src/common/constant.ts | 5 +- .../gem-book/src/element/elements/footer.ts | 2 +- .../gem-book/src/element/elements/main.ts | 9 +- .../gem-book/src/element/elements/sidebar.ts | 2 - packages/gem-book/src/element/helper/i18n.ts | 4 +- packages/gem-book/src/element/index.ts | 25 +- packages/gem-book/src/element/lib/fetch.ts | 16 -- packages/gem-book/src/element/store.ts | 50 ++-- packages/gem-book/src/plugins/import.ts | 9 +- packages/gem-book/src/plugins/raw.ts | 4 +- packages/gem-book/src/website/index.ts | 55 ++--- .../001-basic/001-reactive-element.md | 16 +- packages/gem/docs/en/README.md | 34 ++- .../001-basic/001-reactive-element.md | 16 +- packages/gem/docs/zh/README.md | 34 ++- packages/gem/src/elements/base/route.ts | 54 ++--- 39 files changed, 757 insertions(+), 305 deletions(-) delete mode 100644 packages/gem-book/docs/.gitignore create mode 100644 packages/gem-book/public/custom-ws-client.js create mode 100644 packages/gem-book/schema.json delete mode 100644 packages/gem-book/src/element/lib/fetch.ts diff --git a/.prettierrc.json b/.prettierrc.json index 5dbcd03c..52fd429e 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,13 @@ "trailingComma": "all", "singleQuote": true, "printWidth": 120, - "endOfLine": "lf" -} \ No newline at end of file + "endOfLine": "lf", + "overrides": [ + { + "files": ["*.md"], + "options": { + "printWidth": 80 + } + } + ] +} diff --git a/packages/duoyun-ui/docs/en/02-elements/coach-mark.md b/packages/duoyun-ui/docs/en/02-elements/coach-mark.md index 3f3234a4..3cb02158 100644 --- a/packages/duoyun-ui/docs/en/02-elements/coach-mark.md +++ b/packages/duoyun-ui/docs/en/02-elements/coach-mark.md @@ -45,7 +45,10 @@ const items = [ ]; render( - html``, + html``, document.getElementById('root'), ); ``` diff --git a/packages/duoyun-ui/docs/en/02-elements/popover.md b/packages/duoyun-ui/docs/en/02-elements/popover.md index 74c1f237..f84ab167 100644 --- a/packages/duoyun-ui/docs/en/02-elements/popover.md +++ b/packages/duoyun-ui/docs/en/02-elements/popover.md @@ -15,7 +15,9 @@ render( +
${new Date().toLocaleString()}
`} diff --git a/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md b/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md index 3f3234a4..3cb02158 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md +++ b/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md @@ -45,7 +45,10 @@ const items = [ ]; render( - html``, + html``, document.getElementById('root'), ); ``` diff --git a/packages/duoyun-ui/docs/zh/02-elements/popover.md b/packages/duoyun-ui/docs/zh/02-elements/popover.md index dfae86b6..ffd27d03 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/popover.md +++ b/packages/duoyun-ui/docs/zh/02-elements/popover.md @@ -15,7 +15,9 @@ render( +
${new Date().toLocaleString()}
`} diff --git a/packages/gem-book/docs/.gitignore b/packages/gem-book/docs/.gitignore deleted file mode 100644 index f5aa9435..00000000 --- a/packages/gem-book/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -gem-book.json \ No newline at end of file diff --git a/packages/gem-book/docs/en/002-guide/004-metadata.md b/packages/gem-book/docs/en/002-guide/004-metadata.md index 9e80156d..bd5eec8f 100644 --- a/packages/gem-book/docs/en/002-guide/004-metadata.md +++ b/packages/gem-book/docs/en/002-guide/004-metadata.md @@ -34,7 +34,7 @@ redirect: ./new.md > [!WARNING] > Specifying redirection for folders is not supported -## Folder +## Folder {#dir} Folders sometimes need to specify a name (e.g: [i18n](./002-i18n.md)) and a display method in the sidebar/navigation bar. You can add `config.yml` to the folder, for example: diff --git a/packages/gem-book/docs/en/002-guide/007-extension.md b/packages/gem-book/docs/en/002-guide/007-extension.md index a0a2979b..d00e2de3 100644 --- a/packages/gem-book/docs/en/002-guide/007-extension.md +++ b/packages/gem-book/docs/en/002-guide/007-extension.md @@ -119,7 +119,11 @@ Some plugin need to be used with slots, such as the built-in plugin ` - + ``` diff --git a/packages/gem-book/docs/zh/002-guide/001-sort.md b/packages/gem-book/docs/zh/002-guide/001-sort.md index 6baa8b91..11f33770 100644 --- a/packages/gem-book/docs/zh/002-guide/001-sort.md +++ b/packages/gem-book/docs/zh/002-guide/001-sort.md @@ -22,4 +22,7 @@ src/docs/ └── ``` +> [!TIP] +> 用文件名进行排序的好处是你能在 IDE 中预览排序后的结果,可以使用 [`--reverse`](./004-metadata.md#dir) 进行降序显示侧边栏 + 默认情况下,这个数字不会显示在 URL 中,如果你的文件名本来就有相同格式的前缀,可以使用 [`displayRank`](./003-cli.md#--display-rank) 选项显示它们。 diff --git a/packages/gem-book/docs/zh/002-guide/003-cli.md b/packages/gem-book/docs/zh/002-guide/003-cli.md index da56dd18..5d0dfa85 100644 --- a/packages/gem-book/docs/zh/002-guide/003-cli.md +++ b/packages/gem-book/docs/zh/002-guide/003-cli.md @@ -6,7 +6,7 @@ npx gem-book -h ## 配置文件 -`gem-book` 命令会自动从项目根目录查找配置文件 `gem-book.cli.json`,支持大部分命令行选项(同时提供时覆盖命令行选项),例如: +`gem-book` 命令会自动从项目根目录查找配置文件 `gem-book.cli.json`,支持大部分命令行选项(同时提供时合并命令行选项),例如: @@ -22,31 +22,28 @@ npx gem-book -h #### `-o, --output ` -指定输出文件目录,默认为指定的文档目录;如果是 `json` 文件路径则作为 `gem-book.json` 的输出路径。 +指定输出文件目录,默认为指定的文档目录;如果是 `json` 文件路径则只输出 [`json`](#--json) 文件。 -#### `-d, --source-dir ` +#### `--github ` -指定文档目录在 GitHub 中的位置,最终生成前往 GitHub 的链接,默认使用当前命令指定目录。 +指定 GitHub 地址,会渲染到导航栏中,默认会从 `package.json` 和本地 `.git` 目录中读取。 #### `-b, --source-branch ` -指定文档的分支,最终生成前往 GitHub 的链接。 +指定文档的分支,最终生成前往 GitHub 的链接,默认使用 `main`。 #### `--base ` -指定项目的基础目录,默认会读取 `package.json` 的 `repository.directory` 字段。 - -#### `--github ` - -指定 GitHub 地址,会渲染到导航栏中,默认会从 `package.json` 和本地 `.git` 目录中读取。 +指定项目的基础目录,默认会读取 `package.json` 的 `repository.directory` 字段,这在 monorepo 项目中很有用。 -#### `--footer ` +#### `-d, --source-dir ` -使用 Markdown 自定义渲染页脚。 +指定文档目录在 GitHub 中的位置,最终生成前往 GitHub 的链接,默认使用当前命令指定目录, +只有当命令执行位置在项目子目录时才会用到。 -#### `--display-rank` +#### `--build` -在 URL 中显示用于排序的值。 +输出所有前端资源。 #### `--home-mode` @@ -57,7 +54,9 @@ npx gem-book -h 指定导航栏外部链接,例如: ```bash -npx gem-book docs --nav Example,https://example.com --nav MyWebsite,https://my.website +npx gem-book docs \ + --nav Example,https://example.com \ + --nav MyWebsite,https://my.website ``` #### `--ga ` @@ -78,11 +77,15 @@ npx gem-book docs --nav Example,https://example.com --nav MyWebsite,https://my.w #### `--theme ` -使用内置主题或者自定义主题。 +使用内置主题或者自定义主题,支持有默认导出的模块。 -#### `--build` +#### `--footer ` -输出所有前端资源和 `` 渲染数据。 +使用 Markdown 自定义渲染页脚。 + +#### `--display-rank` + +在 URL 中显示用于排序的值。 #### `--json` @@ -94,4 +97,4 @@ npx gem-book docs --nav Example,https://example.com --nav MyWebsite,https://my.w #### `--debug` -输出调试信息:`gem-book.json`, `stats.json`, `source-map`。 +输出调试信息:应用的命令行选项, `stats.json`, `source-map`。 diff --git a/packages/gem-book/docs/zh/002-guide/004-metadata.md b/packages/gem-book/docs/zh/002-guide/004-metadata.md index 6c026e2f..9ccdf250 100644 --- a/packages/gem-book/docs/zh/002-guide/004-metadata.md +++ b/packages/gem-book/docs/zh/002-guide/004-metadata.md @@ -7,6 +7,7 @@ title: 侧边栏标题 isNav: true navTitle: 导航栏标题 +navOrder: 2 # 导航栏位置 sidebarIgnore: true --- @@ -32,7 +33,7 @@ redirect: ./new.md > [!WARNING] > 不支持为文件夹指定重定向 -## 文件夹配置 +## 文件夹配置 {#dir} 文件夹有时候也需要指定名称(例如[多语言情况下的标题](./002-i18n.md))和在侧边栏/导航栏中的显示方式,可以在该文件夹中添加 `config.yml` 来完成,例如: diff --git a/packages/gem-book/docs/zh/002-guide/007-extension.md b/packages/gem-book/docs/zh/002-guide/007-extension.md index bbab1409..ac5b91e9 100644 --- a/packages/gem-book/docs/zh/002-guide/007-extension.md +++ b/packages/gem-book/docs/zh/002-guide/007-extension.md @@ -1,6 +1,6 @@ # 扩展 -`` 渲染 Markdown,同时也扩展了 Markdown 语法。另外还提供一些方法让用户自定义 ``。 +GemBook 使用 [`marked`](https://github.com/markedjs/marked) 渲染 Markdown,默认支持 [CommonMark](http://spec.commonmark.org/0.30/) 和 [GitHub Flavored Markdown](https://github.github.com/gfm/),GemBook 扩展了 Markdown 语法,另外还提供一些方法让用户自定义 GemBook。 ## Markdown 增强 @@ -12,37 +12,18 @@ ``` ```` -下面是在 `` [插件](#plugins)中写代码块的例子: +例如使用高亮: - +````md 1 +# 代码块信息 -```js index.js -import { render } from '@mantou/gem'; +```md 1 +# 代码块信息 -render('这是一个 `` 例子', document.getElementById('root')); -``` - -````md README.md active 12-13 - - -```js index.js -import { render } from '@mantou/gem'; - -render('这是一个 `` 例子', document.getElementById('root')); -``` - -```md README.md active 3-4 -# `` - -- `` 中的代码块代表一个文件 -- 默认第一个文件的状态为 `active`,如果手动指定状态,必须写文件名 +... ``` - - ```` - - ### 固定标题锚点 Hash {#fixed-hash} 默认会根据标题文本字段生成 hash,但有时你需要固定 hash,比如国际化时。 @@ -70,13 +51,13 @@ render('这是一个 `` 例子', document.getElementById('root')); > [!NOTE] -> 可以使用 `--template` 指定模板文件 +> 使用 `--template` 指定模板文件才能使用插槽 ## 插件 {#plugins} ### 使用插件 -`` 使用自定义元素作为插件系统,他们可以自定义渲染 Markdown 内容或者增强 `` 的能力。下面是内置插件 `` 的使用方式: +GemBook 使用自定义元素作为插件系统,他们可以自定义渲染 Markdown 内容或者增强 GemBook 的能力。下面是内置插件 `` 的使用方式: @@ -99,7 +80,7 @@ gem-book docs --plugin raw 在[这里](../003-plugins.md)查看所有内置插件。 > [!TIP] -> 在 MarkDown 中使用插件时 Attribute 不应该换行,否则会作为内联元素被 `

` 标签打断。 +> 在 Markdown 中使用插件时 Attribute 不应该换行,否则会作为内联元素被 `

` 标签打断。 > GemBook 内置插件支持自动导入,缺点是渲染文档后才会加载,有可能页面会闪烁。 ### 开发插件 diff --git a/packages/gem-book/docs/zh/002-guide/README.md b/packages/gem-book/docs/zh/002-guide/README.md index b237eff2..d552b2b3 100644 --- a/packages/gem-book/docs/zh/002-guide/README.md +++ b/packages/gem-book/docs/zh/002-guide/README.md @@ -5,11 +5,11 @@ navTitle: 指南 # 简介 -`gem-book` 将 [Markdown](https://zh.wikipedia.org/wiki/Markdown) 内容渲染成网站,根据目录结构生成页面。`gem-book` 是为 [Gem](https://github.com/mantou132/gem) 创建的文档生成工具,其本身也是使用 Gem 编写,和 Gem 是共生关系,它使用自定义元素 `` 渲染内容。 +GemBook 将 [Markdown](https://zh.wikipedia.org/wiki/Markdown) 内容渲染成网站,根据目录结构生成页面。GemBook 是为 [Gem](https://github.com/mantou132/gem) 创建的文档生成工具,其本身也是使用 Gem 编写,和 Gem 是共生关系,它使用自定义元素 `` 渲染内容。 ## 快速开始 -> [!NOTE] `gem-book` 依赖 [Node.js v14+](https://nodejs.org/),请确保 `node -v` 命令能够执行 +> [!WARNING] GemBook 依赖 [Node.js v18+](https://nodejs.org/),请确保 `node -v` 命令能够执行 ```bash # 创建文档 @@ -28,7 +28,7 @@ npx gem-book docs -t MyApp -i logo.png npx gem-book docs -t MyApp -i logo.png --home-mode # 构建前端资源 -npx gem-book docs -t MyApp -i logo.png --home-mode --build +npx gem-book docs -t MyApp -i logo.png --home-mode --build --output dist ``` diff --git a/packages/gem-book/docs/zh/003-plugins.md b/packages/gem-book/docs/zh/003-plugins.md index 6967cc5c..764d20de 100644 --- a/packages/gem-book/docs/zh/003-plugins.md +++ b/packages/gem-book/docs/zh/003-plugins.md @@ -6,7 +6,7 @@ isNav: true ## `` -用于显示几段相似的代码: +用于显示几段相似功能的代码: @@ -56,7 +56,7 @@ yarn add gem-book ## `` -动态导入模块,这可以用来按需加载插件,比如下面这个自定义元素是动态编译并加载的: +动态导入模块,这可以用来按需加载插件,比如下面这个自定义元素是动态(`.ts` 文件会使用 [esm.sh](https://esm.sh/) 编译 )加载的: @@ -83,7 +83,11 @@ yarn add gem-book ```html - + ``` @@ -98,7 +102,10 @@ import { render, html } from '@mantou/gem'; import 'duoyun-ui/elements/button'; -render(html`Time: ${new Date().toLocaleString()}`, document.getElementById('root')); +render( + html`Time: ${new Date().toLocaleString()}`, + document.getElementById('root'), +); ``` @@ -111,7 +118,10 @@ import { render, html } from '@mantou/gem'; import 'duoyun-ui/elements/button'; -render(html`Time: ${new Date().toLocaleString()}`, document.getElementById('root')); +render( + html`Time: ${new Date().toLocaleString()}`, + document.getElementById('root'), +); ``` diff --git a/packages/gem-book/docs/zh/README.md b/packages/gem-book/docs/zh/README.md index d1b866c3..2bd50caf 100644 --- a/packages/gem-book/docs/zh/README.md +++ b/packages/gem-book/docs/zh/README.md @@ -1,30 +1,30 @@ --- hero: - title: + title: GemBook desc: 简单、快速创建你的文档网站 actions: - text: 快速开始 link: ./002-guide/README.md features: - title: 开箱即用 - desc: 只需运行命令行就能打包所有前端资源,让所有注意力都能放在文档编写上 + desc: 只需运行一条命令就能构建所有前端资源,让所有注意力都能放在文档编写上。 - title: 高性能 - desc: 没有多余的依赖,整个应用将使用精简的代码流畅的运行 + desc: 没有多余的依赖,整个应用将使用精简的代码流畅的运行。 - title: 可插拔可扩展 - desc: 能将自定义元素插入已有的网站中;使用自定义元素也能非常方便的自定义展示文档 + desc: 能将自定义元素插入已有的网站中;使用自定义元素也能非常方便的自定义展示文档。 --- ## 轻松上手 ```bash # 创建文档 -mkdir docs && echo '# Hello !' > docs/readme.md +mkdir docs && echo '# Hello GemBook!' > docs/readme.md -# 启动本地服务打开文档站,修改文档将自动刷新 +# 启动本地服务打开文档站,修改文档将自动更新 npx gem-book docs # 构建前端资源 -npx gem-book docs --build +npx gem-book docs --build --output dist ``` ## 反馈与共建 diff --git a/packages/gem-book/gem-book.cli.json b/packages/gem-book/gem-book.cli.json index 2ee5af30..39fe5632 100644 --- a/packages/gem-book/gem-book.cli.json +++ b/packages/gem-book/gem-book.cli.json @@ -1,5 +1,6 @@ { - "title": "Gem-book", + "$schema": "./schema.json", + "title": "GemBook", "icon": "../../logo.png", "i18n": true, "homeMode": true, diff --git a/packages/gem-book/package.json b/packages/gem-book/package.json index 751c7d0b..78571c7f 100644 --- a/packages/gem-book/package.json +++ b/packages/gem-book/package.json @@ -1,6 +1,6 @@ { "name": "gem-book", - "version": "1.5.26", + "version": "1.5.28", "description": "Create your document website easily and quickly", "keywords": [ "doc", @@ -16,6 +16,7 @@ }, "typings": "index.d.ts", "files": [ + "schema.json", "/bin/", "/common/", "/element/", @@ -26,10 +27,11 @@ "/index.*" ], "scripts": { - "build:cli": "esbuild ./src/bin/index.ts --outdir=./bin --bundle --platform=node --minify --sourcemap --external:ts-loader --external:jsdom --external:webpack --external:webpack-dev-server --external:html-webpack-plugin --external:workbox-webpack-plugin", + "schema": "npx ts-json-schema-generator -p src/common/config.ts -t CliConfig -o schema.json", + "build:cli": "esbuild ./src/bin/index.ts --outdir=./bin --platform=node --sourcemap --bundle --external:jsdom --external:lodash --external:yaml --external:front-matter --external:commander --external:webpack --external:ts-loader --external:typescript --external:webpack-dev-server --external:html-webpack-plugin --external:copy-webpack-plugin --external:workbox-webpack-plugin", "start:cli": "yarn build:cli --watch", "docs": "node ./bin docs", - "start:docs": "cross-env PORT=8090 GEM_BOOK_DEV=true nodemon --watch bin --watch gem-book.cli.json --exec \"yarn docs\"", + "start:docs": "cross-env PORT=8090 GEM_BOOK_DEV=true nodemon --watch bin --exec \"yarn docs\"", "start": "concurrently npm:start:cli npm:start:docs", "build:website": "yarn build:cli && yarn docs --build --ga G-7X2Z4B2KV0", "build": "yarn build:cli && tsc -p ./tsconfig.build.json", diff --git a/packages/gem-book/public/custom-ws-client.js b/packages/gem-book/public/custom-ws-client.js new file mode 100644 index 00000000..0bf17577 --- /dev/null +++ b/packages/gem-book/public/custom-ws-client.js @@ -0,0 +1,45 @@ +/** + * @class + * @implements {import('webpack-dev-server/client/clients/WebSocketClient')} + * */ +export default class WebSocketClient { + /** + * @param {string} url + */ + constructor(url) { + this.client = new WebSocket(url); + this.client.onerror = (error) => { + // eslint-disable-next-line no-console + console.error(error); + }; + } + + /** + * @param {(...args: any[]) => void} f + */ + onOpen(f) { + this.client.onopen = f; + } + + /** + * @param {(...args: any[]) => void} f + */ + onClose(f) { + this.client.onclose = f; + } + + // call f with the message string as the first argument + /** + * @param {(...args: any[]) => void} f + */ + onMessage(f) { + this.client.onmessage = (e) => { + f(e.data); + document.querySelector('gem-book')?.dispatchEvent( + new CustomEvent('message', { + detail: e.data, + }), + ); + }; + } +} diff --git a/packages/gem-book/schema.json b/packages/gem-book/schema.json new file mode 100644 index 00000000..16e34a15 --- /dev/null +++ b/packages/gem-book/schema.json @@ -0,0 +1,213 @@ +{ + "$ref": "#/definitions/CliConfig", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CliConfig": { + "additionalProperties": false, + "properties": { + "base": { + "type": "string" + }, + "build": { + "type": "boolean" + }, + "config": { + "type": "string" + }, + "debug": { + "type": "boolean" + }, + "displayRank": { + "type": "boolean" + }, + "footer": { + "type": "string" + }, + "ga": { + "type": "string" + }, + "github": { + "type": "string" + }, + "homeMode": { + "type": "boolean" + }, + "i18n": { + "type": "boolean" + }, + "icon": { + "type": "string" + }, + "json": { + "type": "boolean" + }, + "nav": { + "items": { + "$ref": "#/definitions/NavItem" + }, + "type": "array" + }, + "output": { + "type": "string" + }, + "plugin": { + "items": { + "type": "string" + }, + "type": "array" + }, + "sourceBranch": { + "type": "string" + }, + "sourceDir": { + "type": "string" + }, + "template": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + "Feature": { + "additionalProperties": false, + "properties": { + "desc": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "desc" + ], + "type": "object" + }, + "Hero": { + "additionalProperties": false, + "properties": { + "actions": { + "items": { + "additionalProperties": false, + "properties": { + "link": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "text", + "link" + ], + "type": "object" + }, + "type": "array" + }, + "desc": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + "NavItem": { + "additionalProperties": false, + "properties": { + "children": { + "items": { + "$ref": "#/definitions/NavItem" + }, + "type": "array" + }, + "features": { + "items": { + "$ref": "#/definitions/Feature" + }, + "type": "array" + }, + "groups": { + "items": { + "additionalProperties": false, + "properties": { + "members": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "members" + ], + "type": "object" + }, + "type": "array" + }, + "hash": { + "type": "string" + }, + "hero": { + "$ref": "#/definitions/Hero", + "description": "below only homepage" + }, + "isNav": { + "type": "boolean" + }, + "link": { + "type": "string" + }, + "navOrder": { + "type": "number" + }, + "navTitle": { + "type": "string" + }, + "redirect": { + "description": "only file", + "type": "string" + }, + "reverse": { + "description": "only dir", + "type": "boolean" + }, + "sidebarIgnore": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "dir", + "file", + "heading" + ], + "type": "string" + } + }, + "required": [ + "link", + "title" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/packages/gem-book/src/bin/builder.ts b/packages/gem-book/src/bin/builder.ts index 5fe4f8dc..7f07c3b1 100644 --- a/packages/gem-book/src/bin/builder.ts +++ b/packages/gem-book/src/bin/builder.ts @@ -9,9 +9,9 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; import { GenerateSW } from 'workbox-webpack-plugin'; import { BookConfig, CliUniqueConfig } from '../common/config'; -import { DEV_THEME_FILE, STATS_FILE } from '../common/constant'; +import { STATS_FILE } from '../common/constant'; -import { resolveLocalPlugin, resolveTheme, isURL } from './utils'; +import { resolveLocalPlugin, resolveTheme, isURL, requireObject } from './utils'; const publicDir = path.resolve(__dirname, '../public'); const entryDir = path.resolve(__dirname, process.env.GEM_BOOK_DEV ? '../src/website' : '../website'); @@ -21,10 +21,6 @@ const pluginDir = path.resolve(__dirname, process.env.GEM_BOOK_DEV ? '../src/plu export function startBuilder(dir: string, options: Required, bookConfig: Partial) { const { debug, build, theme, template, output, icon, plugin, ga } = options; - if (path.extname(output) === '.json') { - return; - } - const plugins = [...plugin]; plugins.forEach((plugin, index) => { @@ -42,7 +38,8 @@ export function startBuilder(dir: string, options: Required, bo const isRemoteIcon = isURL(icon); const docsDir = path.resolve(dir); const outputDir = output ? path.resolve(output) : docsDir; - const { themeObject, resolveThemePath } = resolveTheme(theme); + const themePath = resolveTheme(theme); + const compiler = webpack({ mode: build ? 'production' : 'development', entry: [entryDir], @@ -90,12 +87,10 @@ export function startBuilder(dir: string, options: Required, bo }, }), new webpack.DefinePlugin({ - // dev mode 'process.env.DEV_MODE': !build, - // build mode - 'process.env.BOOK_CONFIG': JSON.stringify(JSON.stringify(bookConfig)), - 'process.env.THEME': JSON.stringify(JSON.stringify(themeObject)), - 'process.env.PLUGINS': JSON.stringify(JSON.stringify(plugins)), + 'process.env.BOOK_CONFIG': JSON.stringify(bookConfig), + 'process.env.THEME': JSON.stringify(requireObject(themePath)), + 'process.env.PLUGINS': JSON.stringify(plugins), 'process.env.GA_ID': JSON.stringify(ga), }), new CopyWebpackPlugin({ @@ -121,19 +116,6 @@ export function startBuilder(dir: string, options: Required, bo patterns: [{ from: docsDir, to: outputDir }], }) : ([] as any), - ) - .concat( - !build && resolveThemePath - ? new CopyWebpackPlugin({ - patterns: [ - { - from: resolveThemePath, - to: path.resolve(outputDir, DEV_THEME_FILE), - transform: () => JSON.stringify(themeObject), - }, - ], - }) - : ([] as any), ), devtool: debug && 'source-map', }); @@ -165,8 +147,11 @@ export function startBuilder(dir: string, options: Required, bo // https://github.com/webpack/webpack-dev-server/blob/master/examples/api/simple/server.js const server = new WebpackDevServer( { + hot: true, + liveReload: false, client: { overlay: false, + webSocketTransport: require.resolve('../public/custom-ws-client'), }, static: { directory: outputDir, @@ -186,8 +171,11 @@ export function startBuilder(dir: string, options: Required, bo ); server.startCallback((err) => { if (err) { + // eslint-disable-next-line no-console console.error(err); } }); + + return server; } } diff --git a/packages/gem-book/src/bin/index.ts b/packages/gem-book/src/bin/index.ts index 2711e63f..52943e93 100644 --- a/packages/gem-book/src/bin/index.ts +++ b/packages/gem-book/src/bin/index.ts @@ -1,8 +1,6 @@ #!/usr/bin/env node /** - * Automatically generate configuration from directory - * * @example * gem-book -c gem-book.cli.json docs * gem-book -t documentTitle docs @@ -14,11 +12,11 @@ import fs from 'fs'; import program from 'commander'; import mkdirp from 'mkdirp'; import getRepoInfo from 'git-repo-info'; -import { debounce } from 'lodash'; +import { throttle } from 'lodash'; import { version } from '../../package.json'; import { BookConfig, CliConfig, CliUniqueConfig, NavItem, SidebarConfig } from '../common/config'; -import { DEFAULT_FILE, DEFAULT_CLI_FILE, DEFAULT_SOURCE_BRANCH } from '../common/constant'; +import { DEFAULT_FILE, DEFAULT_CLI_FILE, DEFAULT_SOURCE_BRANCH, UPDATE_EVENT } from '../common/constant'; import { isIndexFile, parseFilename } from '../common/utils'; import { FrontMatter } from '../common/frontmatter'; @@ -35,16 +33,20 @@ import { readDirConfig, getIconDataUrl, getHash, + getFile, + resolveTheme, + requireObject, } from './utils'; import { startBuilder } from './builder'; import lang from './lang.json'; // https://developers.google.com/search/docs/advanced/crawling/localized-versions#language-codes +export const devServerEventTarget = new EventTarget(); + program.version(version, '-v, --version'); let docsRootDir = ''; -let useConfig = false; -const bookConfig: Partial = {}; -const cliConfig: Required = { +let bookConfig: BookConfig = {}; +let cliConfig: Required = { icon: '', output: '', i18n: false, @@ -55,11 +57,11 @@ const cliConfig: Required = { build: false, json: false, debug: false, + config: '', }; -function readConfig(configPath: string) { - const obj = require(path.resolve(process.cwd(), configPath)) as Partial; - useConfig = true; +function readConfig(fullPath: string) { + const obj = requireObject(fullPath) || {}; Object.keys(cliConfig).forEach((key: keyof CliUniqueConfig) => { if (key in obj) { const value = obj[key]; @@ -69,7 +71,7 @@ function readConfig(configPath: string) { // Overriding command line options is not allowed if (Array.isArray(cliConfigValue)) { - cliConfigValue.splice(cliConfigValue.length, 0, ...(value as any[])); + Object.assign(cliConfig, { [key]: [...new Set([...cliConfigValue, ...(value as any[])])] }); } else if (!cliConfigValue) { Object.assign(cliConfig, { [key]: value }); } @@ -219,36 +221,23 @@ async function generateBookConfig(dir: string) { bookConfig.sidebar = readDir(docsRootDir); } - // create file - const configPath = path.resolve(cliConfig.output || dir, cliConfig.output.endsWith('.json') ? '' : DEFAULT_FILE); - const configStr = JSON.stringify(bookConfig, null, 2) + '\n'; - // buildMode: embeds the configuration into front-end resources - if (!(!cliConfig.json && cliConfig.build)) { + if (cliConfig.json) { + const configPath = path.resolve(cliConfig.output || dir, cliConfig.output.endsWith('.json') ? '' : DEFAULT_FILE); + const configStr = JSON.stringify(bookConfig, null, 2) + '\n'; if (!isSomeContent(configPath, configStr)) { mkdirp.sync(path.dirname(configPath)); // Trigger rename event fs.writeFileSync(configPath, configStr); } } + // eslint-disable-next-line no-console console.log(`${new Date().toISOString()} config file updated! ${Date.now() - t}ms`); } -const debounceCommand = debounce(generateBookConfig, 300); - program .option('-t, --title ', 'document title', (title: string) => { bookConfig.title = title; }) - .option('-i, --icon <path>', 'project icon path or url', (path: string) => { - cliConfig.icon = path; - }) - .option( - '-o, --output <path>', - `output file or directory, default use docs dir, generate an \`${DEFAULT_FILE}\` file if only JSON is generated`, - (dir: string) => { - cliConfig.output = dir; - }, - ) .option('-d, --source-dir <dir>', 'github source dir, default use docs dir', (sourceDir: string) => { bookConfig.sourceDir = sourceDir; }) @@ -268,15 +257,15 @@ program .option('--footer <string>', 'footer content, support markdown format', (footer: string) => { bookConfig.footer = footer; }) - .option('--i18n', 'enabled i18n', () => { - cliConfig.i18n = true; - }) .option('--display-rank', 'sorting number is not displayed in the link', () => { bookConfig.displayRank = true; }) .option('--home-mode', 'use homepage mode', () => { bookConfig.homeMode = true; }) + .option('--only-file', 'not include heading navigation', () => { + bookConfig.onlyFile = true; + }) .option('--nav <title,link>', 'attach a nav item', (item: string) => { bookConfig.nav ||= []; const [title, link] = item.split(','); @@ -287,6 +276,19 @@ program bookConfig.nav.push({ title, link }); } }) + .option('-i, --icon <path>', 'project icon path or url', (path: string) => { + cliConfig.icon = path; + }) + .option( + '-o, --output <path>', + `output file or directory, default use docs dir, generate an \`${DEFAULT_FILE}\` file if only JSON is generated`, + (dir: string) => { + cliConfig.output = dir; + if (path.extname(dir) === '.json') { + cliConfig.json = true; + } + }, + ) .option('--plugin <name or path>', 'load plugin', (name: string) => { cliConfig.plugin.push(name); }) @@ -299,44 +301,129 @@ program .option('--theme <name or path>', 'theme path', (path) => { cliConfig.theme = path; }) - .option('--build', `output all front-end assets or \`${DEFAULT_FILE}\``, () => { + .option('--build', `output all front-end assets`, () => { cliConfig.build = true; }) + .option('--i18n', 'enabled i18n', () => { + cliConfig.i18n = true; + }) .option('--json', `only output \`${DEFAULT_FILE}\``, () => { cliConfig.json = true; }) - .option('--only-file', 'not include heading navigation', () => { - bookConfig.onlyFile = true; - }) .option('--debug', 'enabled debug mode', () => { cliConfig.debug = true; }) .option('--config <path>', `specify config file, default use \`${DEFAULT_CLI_FILE}\``, (configPath: string) => { - readConfig(configPath); + cliConfig.config = configPath; }) .arguments('<dir>') .action(async (dir: string) => { - if (!useConfig) { - try { - readConfig(DEFAULT_CLI_FILE); - } catch { - // - } - } + const initCliOptions = structuredClone(cliConfig); + const initBookConfig = structuredClone(bookConfig); docsRootDir = path.resolve(process.cwd(), dir); + + const configPath = path.resolve(process.cwd(), cliConfig.config || DEFAULT_CLI_FILE); + readConfig(configPath); + + const updateBookConfig = throttle( + async () => { + await generateBookConfig(dir); + devServerEventTarget.dispatchEvent( + Object.assign(new Event(UPDATE_EVENT), { + detail: { config: bookConfig }, + }), + ); + }, + 100, + { trailing: true }, + ); + + const watchTheme = () => { + const themePath = resolveTheme(cliConfig.theme); + if (themePath) { + return fs.watch( + themePath, + throttle( + () => { + devServerEventTarget.dispatchEvent( + Object.assign(new Event(UPDATE_EVENT), { + detail: { theme: requireObject(themePath) }, + }), + ); + }, + 100, + { trailing: true }, + ), + ); + } + }; + await generateBookConfig(dir); - if (!cliConfig.build) { - fs.watch(dir, { recursive: true }, (type, filePath) => { - if (type === 'rename' || (filePath && (isDirConfigFile(filePath) || isMdFile(filePath)))) { - debounceCommand(dir); + + if (cliConfig.debug) inspectObject(cliConfig); + + let server = cliConfig.json ? undefined : startBuilder(dir, cliConfig, bookConfig); + let themeWatcher = watchTheme(); + + if (server) { + devServerEventTarget.addEventListener(UPDATE_EVENT, ({ detail }: CustomEvent<string>) => { + server?.sendMessage(server.webSocketServer?.clients || [], UPDATE_EVENT, detail); + }); + + if (configPath) { + fs.watch( + configPath, + throttle( + async () => { + cliConfig = structuredClone(initCliOptions); + bookConfig = structuredClone(initBookConfig); + readConfig(configPath); + await generateBookConfig(dir); + server!.stopCallback(() => { + server = startBuilder(dir, cliConfig, bookConfig); + devServerEventTarget.dispatchEvent( + Object.assign(new Event(UPDATE_EVENT), { + detail: { config: bookConfig }, + }), + ); + themeWatcher?.close(); + themeWatcher = watchTheme(); + }); + }, + 100, + { trailing: true }, + ), + ); + } + + fs.watch(dir, { recursive: true }, async (type, filePath) => { + if (filePath && !isDirConfigFile(filePath) && !isMdFile(filePath)) { + devServerEventTarget.dispatchEvent( + Object.assign(new Event(UPDATE_EVENT), { + detail: { reload: true }, + }), + ); + } + + if (type === 'rename' || !filePath || isDirConfigFile(filePath)) { + return updateBookConfig(); + } + + const { content, metadataChanged } = getFile(path.resolve(dir, filePath), bookConfig.displayRank); + // hot reload + // https://nodejs.org/api/events.html#class-customevent + devServerEventTarget.dispatchEvent( + Object.assign(new Event(UPDATE_EVENT), { + detail: { filePath, content }, + }), + ); + + if (metadataChanged) { + updateBookConfig(); } }); } - if (!cliConfig.json) { - if (cliConfig.debug) inspectObject(cliConfig); - startBuilder(dir, cliConfig, bookConfig); - } }); program.parse(process.argv); diff --git a/packages/gem-book/src/bin/utils.ts b/packages/gem-book/src/bin/utils.ts index 4b554b15..005a1588 100644 --- a/packages/gem-book/src/bin/utils.ts +++ b/packages/gem-book/src/bin/utils.ts @@ -13,9 +13,8 @@ import YAML from 'yaml'; import { startCase } from 'lodash'; import Jimp from 'jimp'; -import { NavItem } from '../common/config'; import { FrontMatter } from '../common/frontmatter'; -import { isIndexFile, parseFilename, CUSTOM_HEADING_REG, normalizeId } from '../common/utils'; +import { isIndexFile, parseFilename, CUSTOM_HEADING_REG } from '../common/utils'; export async function getGithubUrl() { const repoDir = process.cwd(); @@ -58,28 +57,36 @@ export function resolveLocalPlugin(p: string) { if (inTheDir(pluginDir, plugin) && !lstatSync(plugin).isSymbolicLink()) { return; } - } catch { - // - } + } catch {} for (const ext of ['', '.js', '.ts']) { try { return require.resolve(path.resolve(process.cwd(), `${p}${ext}`)); - } catch { - // - } + } catch {} } } // Prefer built-in -export function resolveTheme(p?: string) { - if (!p) return { resolveThemePath: p, themeObject: null }; - let resolveThemePath = ''; +export function resolveTheme(p: string) { + if (!p) return; + try { - resolveThemePath = require.resolve(path.resolve(__dirname, `../themes/${p}`)); + return require.resolve(path.resolve(__dirname, `../themes/${p}`)); } catch { - resolveThemePath = require.resolve(path.resolve(process.cwd(), p)); + try { + return require.resolve(path.resolve(process.cwd(), p)); + } catch {} + } +} + +export function requireObject<T>(fullPath?: string) { + if (!fullPath) return; + delete require.cache[fullPath]; + try { + return require(fullPath) as T; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); } - return { resolveThemePath, themeObject: require(resolveThemePath) }; } export function checkRelativeLink(fullPath: string, docsRootDir: string) { @@ -109,6 +116,7 @@ export function checkRelativeLink(fullPath: string, docsRootDir: string) { currentNum += line.length + 1; } const position = `(${lineNum},${colNum})`; + // eslint-disable-next-line no-console console.warn(`\x1b[33m[${new Date().toISOString()}]: ${fullPath}${position} link warn: ${link}'\x1b[0m`); } }); @@ -129,6 +137,18 @@ export function getHash(fullPath: string) { return hash.digest('hex').substring(0, 8); } +export function getFile(fullPath: string, displayRank: boolean | undefined) { + const content = fullPath ? readFileSync(fullPath, 'utf-8') : ''; + return { + content, + metadataChanged: isMdFile(fullPath) + ? JSON.stringify(metadataRecord[fullPath]) !== JSON.stringify(getMetadata(fullPath, displayRank)) + : false, + }; +} + +const metadataRecord: Record<string, FrontMatter> = {}; + export function getMetadata(fullPath: string, displayRank: boolean | undefined) { const getTitle = () => { const basename = path.basename(fullPath); @@ -148,15 +168,20 @@ export function getMetadata(fullPath: string, displayRank: boolean | undefined) }; }; + let metadata: (FrontMatter & { title: string }) | undefined; if (statSync(fullPath).isDirectory()) { - return { + metadata = { title: getTitle(), ...readDirConfig(fullPath), }; } else if (isMdFile(fullPath)) { - return parseMd(fullPath); + metadata = parseMd(fullPath); + } else { + metadata = { title: '' }; } - return { title: '' }; + + metadataRecord[fullPath] = metadata; + return metadata; } export function isMdFile(filename: string) { @@ -185,6 +210,7 @@ export function isSomeContent(filePath: string, content: string) { } export function inspectObject(obj: any) { + // eslint-disable-next-line no-console console.log(util.inspect(obj, { colors: true, depth: null })); } diff --git a/packages/gem-book/src/common/config.ts b/packages/gem-book/src/common/config.ts index 709c299a..0d561e5f 100644 --- a/packages/gem-book/src/common/config.ts +++ b/packages/gem-book/src/common/config.ts @@ -27,15 +27,14 @@ interface CommonConfig { } export type BookConfig = { - redirects: Record<string, string>; - sidebar: SidebarConfig; + redirects?: Record<string, string>; + sidebar?: SidebarConfig; // navbar icon absolute path icon?: string; onlyFile?: boolean; } & CommonConfig; export interface CliUniqueConfig { - // relative path icon?: string; output?: string; i18n?: boolean; @@ -46,6 +45,7 @@ export interface CliUniqueConfig { build?: boolean; json?: boolean; debug?: boolean; + config?: string; } export type CliConfig = CliUniqueConfig & CommonConfig; diff --git a/packages/gem-book/src/common/constant.ts b/packages/gem-book/src/common/constant.ts index 47bd032f..10bfc10d 100644 --- a/packages/gem-book/src/common/constant.ts +++ b/packages/gem-book/src/common/constant.ts @@ -1,5 +1,6 @@ +export const UPDATE_EVENT = 'gen-book-update'; + export const DEFAULT_FILE = 'gem-book.json'; export const DEFAULT_CLI_FILE = 'gem-book.cli.json'; export const STATS_FILE = 'stats.json'; -export const DEV_THEME_FILE = '_dev-theme.json'; -export const DEFAULT_SOURCE_BRANCH = 'master'; +export const DEFAULT_SOURCE_BRANCH = 'main'; diff --git a/packages/gem-book/src/element/elements/footer.ts b/packages/gem-book/src/element/elements/footer.ts index a7da1685..c66cb039 100644 --- a/packages/gem-book/src/element/elements/footer.ts +++ b/packages/gem-book/src/element/elements/footer.ts @@ -46,7 +46,7 @@ export class Footer extends GemElement { } `, ) - : selfI18n.get('footer', (t) => html`<gem-link href="https://book.gemjs.org"><${t}></gem-link>`)} + : selfI18n.get('footer', (t) => html`<gem-link href="https://book.gemjs.org">${t}</gem-link>`)} `; } } diff --git a/packages/gem-book/src/element/elements/main.ts b/packages/gem-book/src/element/elements/main.ts index 6efdd463..350369a4 100644 --- a/packages/gem-book/src/element/elements/main.ts +++ b/packages/gem-book/src/element/elements/main.ts @@ -75,8 +75,15 @@ export class Main extends GemElement { constructor() { super(); Main.instance = this; + this.memo(() => { + const [, , _sToken, _frontmatter, _eToken, mdBody] = + this.content.match(/^(([\r\n\s]*---\s*(?:\r\n|\n))(.*?)((?:\r\n|\n)---\s*(?:\r\n|\n)?))?(.*)$/s) || []; + this.#content = mdBody; + }); } + #content = ''; + #hashChangeHandle = () => { const { hash, path } = history.getParams(); // 确保是页内跳转或者新页(mounted)跳转 @@ -309,7 +316,7 @@ export class Main extends GemElement { } } </style> - ${Main.parseMarkdown(this.content)} + ${Main.parseMarkdown(this.#content)} <style> ${linkStyle} </style> diff --git a/packages/gem-book/src/element/elements/sidebar.ts b/packages/gem-book/src/element/elements/sidebar.ts index 1fb1773a..71353296 100644 --- a/packages/gem-book/src/element/elements/sidebar.ts +++ b/packages/gem-book/src/element/elements/sidebar.ts @@ -132,8 +132,6 @@ export class SideBar extends GemElement { top: 0; } gem-book-nav-logo { - /**挡住橡皮条效果下面的线 */ - background: ${theme.backgroundColor}; border-block-end: 1px solid ${theme.borderColor}; } gem-book-nav-logo, diff --git a/packages/gem-book/src/element/helper/i18n.ts b/packages/gem-book/src/element/helper/i18n.ts index c3cee734..2d7f8921 100644 --- a/packages/gem-book/src/element/helper/i18n.ts +++ b/packages/gem-book/src/element/helper/i18n.ts @@ -4,13 +4,13 @@ export const Resources = { en: { editOnGithub: 'Edit this page on GitHub', createOnGithub: 'Create on GitHub', - footer: 'Generated by $1<gem-book>', + footer: 'Generated by $1<GemBook>', lastUpdated: 'Last Updated', }, zh: { editOnGithub: '在 Github 编辑此页', createOnGithub: '在 Github 上创建此页面', - footer: '通过 $1<gem-book> 生成', + footer: '通过 $1<GemBook> 生成', lastUpdated: '上次更新', }, }; diff --git a/packages/gem-book/src/element/index.ts b/packages/gem-book/src/element/index.ts index e8a6a1d9..4e361d1a 100644 --- a/packages/gem-book/src/element/index.ts +++ b/packages/gem-book/src/element/index.ts @@ -19,13 +19,15 @@ import { GemLightRouteElement, matchPath } from '@mantou/gem/elements/route'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; import { BookConfig } from '../common/config'; +import { UPDATE_EVENT } from '../common/constant'; import { theme, changeTheme, Theme, themeProps } from './helper/theme'; import { bookStore, updateBookConfig, locationStore } from './store'; -import { checkBuiltInPlugin } from './lib/utils'; +import { checkBuiltInPlugin, getRemotePath } from './lib/utils'; import { GemBookPluginElement } from './elements/plugin'; import { Loadbar } from './elements/loadbar'; import { Homepage } from './elements/homepage'; +import type { Main } from './elements/main'; import '@mantou/gem/elements/title'; import '@mantou/gem/elements/reflect'; @@ -82,6 +84,27 @@ export class GemBookElement extends GemElement { this.theme = theme; new MutationObserver(() => checkBuiltInPlugin(this)).observe(this, { childList: true }); document.currentScript?.addEventListener('load', () => checkBuiltInPlugin(this)); + this.addEventListener('message', ({ detail }: CustomEvent) => { + const event = JSON.parse(detail); + if (typeof event.data !== 'object') return; + const { filePath, content, config, theme, reload } = event.data; + if (event.type !== UPDATE_EVENT) return; + const routeELement = this.routeRef.element!; + if (reload) { + location.reload(); + } else if (theme) { + this.theme = theme; + } else if (config) { + this.config = config; + // 等待路由更新 + queueMicrotask(() => routeELement.update()); + } else if (routeELement.currentRoute?.pattern === '*') { + routeELement.update(); + } else if (getRemotePath(bookStore.getCurrentLink?.().originLink || '', bookStore.lang) === '/' + filePath) { + const firstElementChild = routeELement.firstElementChild! as Main; + firstElementChild.content = content; + } + }); } changeTheme(newTheme?: Partial<Theme>) { diff --git a/packages/gem-book/src/element/lib/fetch.ts b/packages/gem-book/src/element/lib/fetch.ts deleted file mode 100644 index d4376403..00000000 --- a/packages/gem-book/src/element/lib/fetch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getURL } from './utils'; - -const cache = new Map<string, string>(); - -export async function fetchDocument(link: string, lang: string, hash?: string) { - const mdPath = getURL(link, lang, hash); - let md = cache.get(mdPath); - if (!md) { - md = await (await fetch(mdPath)).text(); - cache.set(mdPath, md); - } - const [, , _sToken, _frontmatter, _eToken, mdBody] = - md.match(/^(([\r\n\s]*---\s*(?:\r\n|\n))(.*?)((?:\r\n|\n)---\s*(?:\r\n|\n)?))?(.*)$/s) || []; - - return mdBody; -} diff --git a/packages/gem-book/src/element/store.ts b/packages/gem-book/src/element/store.ts index cb139a4c..c3d58ed8 100644 --- a/packages/gem-book/src/element/store.ts +++ b/packages/gem-book/src/element/store.ts @@ -5,9 +5,8 @@ import { I18n } from '@mantou/gem/helper/i18n'; import { BookConfig, NavItem } from '../common/config'; import { selfI18n } from './helper/i18n'; -import { getLinkPath, getUserLink, NavItemWithLink, flatNav, capitalize } from './lib/utils'; +import { getLinkPath, getUserLink, NavItemWithLink, flatNav, capitalize, getURL } from './lib/utils'; import { getRenderer } from './lib/renderer'; -import { fetchDocument } from './lib/fetch'; import { GemBookElement } from '.'; @@ -34,35 +33,34 @@ interface CurrentBookConfig { export const [bookStore, updateBookStore] = useStore<Partial<CurrentBookConfig>>({}); -function getI18nSidebar(config: BookConfig | undefined) { +function getI18nSidebar(config: BookConfig = {}) { let sidebar: NavItem[] = []; let lang = ''; let langList: { code: string; name: string }[] = []; let languagechangeHandle = (_lang: string) => { // }; - if (config) { - if (config.sidebar instanceof Array) { - sidebar = config.sidebar; - } else { - const sidebarConfig = config.sidebar; - langList = Object.keys(config.sidebar).map((code) => ({ code, name: sidebarConfig[code].name })); - const fallbackLanguage = langList[0].code; - // detect language - const i18n = new I18n<any>({ fallbackLanguage, resources: sidebarConfig, cache: true, urlParamsType: 'path' }); - lang = i18n.currentLanguage; + + const sidebarConfig = config.sidebar || []; + if (sidebarConfig instanceof Array) { + sidebar = sidebarConfig; + } else { + langList = Object.keys(sidebarConfig).map((code) => ({ code, name: sidebarConfig[code].name })); + const fallbackLanguage = langList[0].code; + // detect language + const i18n = new I18n<any>({ fallbackLanguage, resources: sidebarConfig, cache: true, urlParamsType: 'path' }); + lang = i18n.currentLanguage; + history.basePath = `/${lang}`; + sidebar = sidebarConfig[lang].data; + languagechangeHandle = async (lang: string) => { + const { path, query, hash } = history.getParams(); + // will modify `history.getParams()` history.basePath = `/${lang}`; - sidebar = sidebarConfig[lang].data; - languagechangeHandle = async (lang: string) => { - const { path, query, hash } = history.getParams(); - // will modify `history.getParams()` - history.basePath = `/${lang}`; - await i18n.setLanguage(lang); - // Use custom anchors id to ensure that the hash is correct after i18n switching - history.replace({ path, query, hash }); - updateBookConfig(bookStore.config); - }; - } + await i18n.setLanguage(lang); + // Use custom anchors id to ensure that the hash is correct after i18n switching + history.replace({ path, query, hash }); + updateBookConfig(bookStore.config); + }; } if (lang) { @@ -154,9 +152,9 @@ function getLinkRouters(links: NavItemWithLink[], title: string, lang: string, d pattern: link, async getContent() { const renderer = getRenderer({ lang, link: originLink, displayRank }); - const content = await fetchDocument(originLink, lang, hash); + const content = await (await fetch(getURL(originLink, lang, hash))).text(); if (bookStore.isDevMode?.()) await new Promise((res) => setTimeout(res, 500)); - return html`<gem-book-main role="article" .renderer=${renderer} .content=${content}></gem-book-main>`; + return html`<gem-book-main role="article" .renderer=${renderer as any} .content=${content}></gem-book-main>`; }, data: item, }); diff --git a/packages/gem-book/src/plugins/import.ts b/packages/gem-book/src/plugins/import.ts index edf79bd0..0b5bb276 100644 --- a/packages/gem-book/src/plugins/import.ts +++ b/packages/gem-book/src/plugins/import.ts @@ -3,7 +3,7 @@ import type { GemBookElement } from '../element'; const esmBuilder = 'https://esm.sh/build'; customElements.whenDefined('gem-book').then(async () => { - const { build } = await import(/* webpackIgnore: true */ esmBuilder); + const esmBuilderPromise = import(/* webpackIgnore: true */ esmBuilder); const { GemBookPluginElement } = customElements.get('gem-book') as typeof GemBookElement; const { Gem } = GemBookPluginElement; @@ -35,7 +35,12 @@ customElements.whenDefined('gem-book').then(async () => { const resp = await fetch(url); if (resp.status === 404) throw new Error(resp.statusText || 'Not Found'); const content = await resp.text(); - const ret = await build({ + if (new URL(url, location.origin).pathname.endsWith('.js')) { + return await import(/* webpackIgnore: true */ url); + } + const ret = await ( + await esmBuilderPromise + ).build({ dependencies: this.#dependencies, code: ` const GemBookPluginElement = customElements.get('gem-book').GemBookPluginElement; diff --git a/packages/gem-book/src/plugins/raw.ts b/packages/gem-book/src/plugins/raw.ts index 7919f261..786c3332 100644 --- a/packages/gem-book/src/plugins/raw.ts +++ b/packages/gem-book/src/plugins/raw.ts @@ -55,7 +55,7 @@ customElements.whenDefined('gem-book').then(() => { content: '', }; - get #codelang() { + get #codeLang() { return this.codelang || this.src.split('.').pop() || ''; } @@ -83,7 +83,7 @@ customElements.whenDefined('gem-book').then(() => { if (!content) return html`<div class="loading">Loading...</div>`; return html` - <gem-book-pre codelang=${this.#codelang} highlight=${this.highlight} range=${this.range} + <gem-book-pre codelang=${this.#codeLang} highlight=${this.highlight} range=${this.range} >${content}</gem-book-pre > `; diff --git a/packages/gem-book/src/website/index.ts b/packages/gem-book/src/website/index.ts index 211c332f..45f2c6d0 100644 --- a/packages/gem-book/src/website/index.ts +++ b/packages/gem-book/src/website/index.ts @@ -1,12 +1,18 @@ import { css, history } from '@mantou/gem'; -import { DEFAULT_FILE, DEV_THEME_FILE } from '../common/constant'; import { GemBookElement } from '../element'; +import { BookConfig } from '../common/config'; import { theme as defaultTheme } from '../element/helper/theme'; -if (process.env.GA_ID) { +const gaId = process.env.GA_ID; +const dev = process.env.DEV_MODE as unknown as boolean; +const config = process.env.BOOK_CONFIG as unknown as BookConfig; +const theme = process.env.THEME as unknown as Record<string, string>; +const plugins = process.env.PLUGINS as unknown as string[]; + +if (gaId) { const script = document.createElement('script'); - script.src = `https://www.googletagmanager.com/gtag/js?id=${process.env.GA_ID}`; + script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`; script.onload = () => { function gtag(..._rest: any): any { // eslint-disable-next-line prefer-rest-params @@ -22,7 +28,7 @@ if (process.env.GA_ID) { }); } gtag('js', new Date()); - gtag('config', process.env.GA_ID, { send_page_view: false }); + gtag('config', gaId, { send_page_view: false }); send(); // https://book.gemjs.org/en/api/event window.addEventListener('routechange', send); @@ -42,42 +48,19 @@ function loadPlugin(plugin: string) { } } -const pluginSet = new Set(JSON.parse(String(process.env.PLUGINS)) as string[]); - -pluginSet.forEach(loadPlugin); - +plugins.forEach(loadPlugin); addEventListener('plugin', ({ detail }: CustomEvent) => { - if (pluginSet.has(detail)) return; + if (plugins.includes(detail)) return; // eslint-disable-next-line no-console console.info('GemBook auto load plugin:', detail); loadPlugin(detail); }); -const config = JSON.parse(String(process.env.BOOK_CONFIG)); -const theme = JSON.parse(String(process.env.THEME)); - -const devRender = () => { - const book = document.querySelector<GemBookElement>('gem-book') || new GemBookElement(); - book.src = `/${DEFAULT_FILE}`; - book.dev = true; - if (theme) { - fetch(`/${DEV_THEME_FILE}`) - .then((res) => res.json()) - .then((e) => { - book.theme = e; - }); - } - document.body.append(book); -}; - -const buildRender = () => { - const book = document.querySelector<GemBookElement>('gem-book') || new GemBookElement(); - book.config = config; - book.theme = theme; - document.body.append(book); -}; - -process.env.DEV_MODE ? devRender() : buildRender(); +const book = document.querySelector<GemBookElement>('gem-book') || new GemBookElement(); +book.config = config; +book.theme = theme; +book.dev = dev; +document.body.append(book); const style = document.createElement('style'); style.textContent = css` @@ -92,7 +75,7 @@ style.textContent = css` margin: 0; height: 100%; overflow: auto; - -webkit-overflow-scrolling: touch; + overscroll-behavior: none; } @media print { body { @@ -102,7 +85,7 @@ style.textContent = css` `; document.head.append(style); -if (!process.env.DEV_MODE) { +if (!dev) { window.addEventListener('load', () => { navigator.serviceWorker?.register('/service-worker.js'); }); diff --git a/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md b/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md index 49b4383e..786e690f 100644 --- a/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md +++ b/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md @@ -65,7 +65,16 @@ class MyElement extends GemElement { <gbp-sandpack dependencies="@mantou/gem"> ```js index.js -import { useStore, GemElement, render, html, attribute, property, connectStore, customElement } from '@mantou/gem'; +import { + useStore, + GemElement, + render, + html, + attribute, + property, + connectStore, + customElement, +} from '@mantou/gem'; const [store, update] = useStore({ count: 0, @@ -90,7 +99,10 @@ class MyElement extends GemElement { } } -render(html`<my-element name="world" .data=${{ a: 1 }}></my-element>`, document.getElementById('root')); +render( + html`<my-element name="world" .data=${{ a: 1 }}></my-element>`, + document.getElementById('root'), +); ``` </gbp-sandpack> diff --git a/packages/gem/docs/en/README.md b/packages/gem/docs/en/README.md index 055fc8e7..e8125b63 100644 --- a/packages/gem/docs/en/README.md +++ b/packages/gem/docs/en/README.md @@ -41,7 +41,13 @@ features: <gbp-sandpack dependencies="@mantou/gem, duoyun-ui"> ```ts -import { customElement, GemElement, html, render, connectStore } from '@mantou/gem'; +import { + customElement, + GemElement, + html, + render, + connectStore, +} from '@mantou/gem'; import { todoData, addItem } from './store'; @@ -72,8 +78,14 @@ export class AppRootElement extends GemElement { <todo-list></todo-list> <dy-heading lv="3">What needs to be done?</dy-heading> <dy-input-group> - <dy-input id="new-todo" @change=${this.#onChange} .value=${this.state.input}></dy-input> - <dy-button @click=${this.#onSubmit}>Add #${todoData.items.length + 1}</dy-button> + <dy-input + id="new-todo" + @change=${this.#onChange} + .value=${this.state.input} + ></dy-input> + <dy-button @click=${this.#onSubmit} + >Add #${todoData.items.length + 1}</dy-button + > </dy-input-group> `; }; @@ -81,7 +93,16 @@ export class AppRootElement extends GemElement { ``` ```ts todo-list.ts -import { customElement, GemElement, html, render, connectStore, css, createCSSSheet, adoptedStyle } from '@mantou/gem'; +import { + customElement, + GemElement, + html, + render, + connectStore, + css, + createCSSSheet, + adoptedStyle, +} from '@mantou/gem'; import { icons } from 'duoyun-ui/lib/icons'; import { todoData, deleteItem } from './store'; @@ -124,7 +145,10 @@ export class TodoListElement extends GemElement { (item) => html` <li> <span>${item}</span> - <dy-use .element=${icons.close} @click=${() => deleteItem(item)}></dy-use> + <dy-use + .element=${icons.close} + @click=${() => deleteItem(item)} + ></dy-use> </li> `, )} diff --git a/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md b/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md index c9c970ba..485a59c7 100644 --- a/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md +++ b/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md @@ -67,7 +67,16 @@ class MyElement extends GemElement { <gbp-sandpack dependencies="@mantou/gem"> ```js index.js -import { useStore, GemElement, render, html, attribute, property, connectStore, customElement } from '@mantou/gem'; +import { + useStore, + GemElement, + render, + html, + attribute, + property, + connectStore, + customElement, +} from '@mantou/gem'; const [store, update] = useStore({ count: 0, @@ -92,7 +101,10 @@ class MyElement extends GemElement { } } -render(html`<my-element name="world" .data=${{ a: 1 }}></my-element>`, document.getElementById('root')); +render( + html`<my-element name="world" .data=${{ a: 1 }}></my-element>`, + document.getElementById('root'), +); ``` </gbp-sandpack> diff --git a/packages/gem/docs/zh/README.md b/packages/gem/docs/zh/README.md index b43f22cf..60e5c8aa 100644 --- a/packages/gem/docs/zh/README.md +++ b/packages/gem/docs/zh/README.md @@ -41,7 +41,13 @@ features: <gbp-sandpack dependencies="@mantou/gem, duoyun-ui"> ```ts -import { customElement, GemElement, html, render, connectStore } from '@mantou/gem'; +import { + customElement, + GemElement, + html, + render, + connectStore, +} from '@mantou/gem'; import { todoData, addItem } from './store'; @@ -72,8 +78,14 @@ export class AppRootElement extends GemElement { <todo-list></todo-list> <dy-heading lv="3">需要做什么?</dy-heading> <dy-input-group> - <dy-input id="new-todo" @change=${this.#onChange} .value=${this.state.input}></dy-input> - <dy-button @click=${this.#onSubmit}>添加 #${todoData.items.length + 1}</dy-button> + <dy-input + id="new-todo" + @change=${this.#onChange} + .value=${this.state.input} + ></dy-input> + <dy-button @click=${this.#onSubmit} + >添加 #${todoData.items.length + 1}</dy-button + > </dy-input-group> `; }; @@ -81,7 +93,16 @@ export class AppRootElement extends GemElement { ``` ```ts todo-list.ts -import { customElement, GemElement, html, render, connectStore, css, createCSSSheet, adoptedStyle } from '@mantou/gem'; +import { + customElement, + GemElement, + html, + render, + connectStore, + css, + createCSSSheet, + adoptedStyle, +} from '@mantou/gem'; import { icons } from 'duoyun-ui/lib/icons'; import { todoData, deleteItem } from './store'; @@ -124,7 +145,10 @@ export class TodoListElement extends GemElement { (item) => html` <li> <span>${item}</span> - <dy-use .element=${icons.close} @click=${() => deleteItem(item)}></dy-use> + <dy-use + .element=${icons.close} + @click=${() => deleteItem(item)} + ></dy-use> </li> `, )} diff --git a/packages/gem/src/elements/base/route.ts b/packages/gem/src/elements/base/route.ts index f9e39e2f..e6b2b6fe 100644 --- a/packages/gem/src/elements/base/route.ts +++ b/packages/gem/src/elements/base/route.ts @@ -216,34 +216,9 @@ export class GemRouteElement extends GemElement<State> { this.#updateLocationStore(); return; } - const { route, params = {} } = GemRouteElement.findRoute(this.routes, path); - const { redirect, content, getContent } = route || {}; - if (redirect) { - history.replace({ path: redirect }); - return; - } - const title = route?.title; - if (title) updateStore(titleStore, { title }); - const contentOrLoader = content || getContent?.(params); - if (contentOrLoader instanceof Promise) { - this.loading(route!); - this.#lastLoader = contentOrLoader; - const isSomeLoader = () => this.#lastLoader === contentOrLoader; - contentOrLoader - .then((content) => { - if (isSomeLoader()) this.#setContent(route, params, content); - }) - .catch((err) => { - if (isSomeLoader()) this.error(err); - }); - return; - } - this.#setContent(route, params, contentOrLoader); - }, - () => { - const { path } = history.getParams(); - return [this.key, path, location.search]; + this.update(); }, + () => [this.key, history.getParams().path, location.search], ); } @@ -266,6 +241,31 @@ export class GemRouteElement extends GemElement<State> { ${content ?? html`<slot></slot>`} `; } + + update = () => { + const { route, params = {} } = GemRouteElement.findRoute(this.routes, history.getParams().path); + const { redirect, content, getContent } = route || {}; + if (redirect) { + history.replace({ path: redirect }); + return; + } + updateStore(titleStore, { title: route?.title }); + const contentOrLoader = content || getContent?.(params); + if (contentOrLoader instanceof Promise) { + this.loading(route!); + this.#lastLoader = contentOrLoader; + const isSomeLoader = () => this.#lastLoader === contentOrLoader; + contentOrLoader + .then((content) => { + if (isSomeLoader()) this.#setContent(route, params, content); + }) + .catch((err) => { + if (isSomeLoader()) this.error(err); + }); + return; + } + this.#setContent(route, params, contentOrLoader); + }; } export class GemLightRouteElement extends GemRouteElement {