mirror of
https://github.com/hansjone/mind-map.git
synced 2026-10-09 03:20:49 +08:00
feat: ship prebuilt plugin bundle for remote dsh install
Remote github installs only get packages/dsh-plugin; without bundle the host tried pnpm build and hung. Pack web+server into bundle/ and start from it so other machines can run after dsh plugin add. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d95f033d1f
commit
b23959400d
13 changed files with 28325 additions and 41 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -11,3 +11,5 @@ uploads
|
||||||
apps/web/dist
|
apps/web/dist
|
||||||
apps/server/dist
|
apps/server/dist
|
||||||
packages/*/dist
|
packages/*/dist
|
||||||
|
# keep packages/dsh-plugin/bundle (prebuilt for remote install)
|
||||||
|
packages/dsh-plugin/bundle/_testdata
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,19 @@ async function callDshBridge(path: string, init?: RequestInit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
export const mindMapRoot = path.resolve(__dirname, "../../..");
|
/** Monorepo root, or plugin package root when launched from packaged bundle. */
|
||||||
|
export const mindMapRoot = process.env.MINDMAP_ROOT
|
||||||
|
? path.resolve(process.env.MINDMAP_ROOT)
|
||||||
|
: path.resolve(__dirname, "../../..");
|
||||||
|
|
||||||
|
export function resolveWebDist(root = mindMapRoot) {
|
||||||
|
if (process.env.MINDMAP_WEB_DIST) {
|
||||||
|
return path.resolve(process.env.MINDMAP_WEB_DIST);
|
||||||
|
}
|
||||||
|
const bundled = path.join(root, "bundle", "web");
|
||||||
|
if (fs.existsSync(path.join(bundled, "index.html"))) return bundled;
|
||||||
|
return path.join(root, "apps", "web", "dist");
|
||||||
|
}
|
||||||
|
|
||||||
export function loadDotEnv(root = mindMapRoot) {
|
export function loadDotEnv(root = mindMapRoot) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -642,14 +654,13 @@ export function startMindMapServer(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Static UI: cwd is mind-map root when launched by the plugin.
|
// Static UI: monorepo apps/web/dist, or packaged bundle/web via MINDMAP_WEB_DIST.
|
||||||
const webDistRel = "apps/web/dist";
|
const webDistAbs = resolveWebDist(root);
|
||||||
const webDistAbs = path.join(root, "apps/web/dist");
|
if (fs.existsSync(path.join(webDistAbs, "index.html"))) {
|
||||||
if (fs.existsSync(webDistAbs) || fs.existsSync(path.join(process.cwd(), webDistRel))) {
|
|
||||||
app.use(
|
app.use(
|
||||||
"/*",
|
"/*",
|
||||||
serveStatic({
|
serveStatic({
|
||||||
root: fs.existsSync(webDistAbs) ? webDistAbs : webDistRel,
|
root: webDistAbs,
|
||||||
index: "index.html",
|
index: "index.html",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -659,13 +670,10 @@ export function startMindMapServer(
|
||||||
if (p.startsWith("/api") || p === "/health" || p.startsWith("/ws") || p.startsWith("/uploads")) {
|
if (p.startsWith("/api") || p === "/health" || p.startsWith("/ws") || p.startsWith("/uploads")) {
|
||||||
return c.notFound()
|
return c.notFound()
|
||||||
}
|
}
|
||||||
const indexPath = path.join(
|
const indexPath = path.join(webDistAbs, "index.html");
|
||||||
fs.existsSync(webDistAbs) ? webDistAbs : path.join(process.cwd(), webDistRel),
|
|
||||||
"index.html",
|
|
||||||
);
|
|
||||||
if (!fs.existsSync(indexPath)) {
|
if (!fs.existsSync(indexPath)) {
|
||||||
return c.text(
|
return c.text(
|
||||||
"Mind Map UI not built. From mind-map repo run: pnpm --filter @mind-map/web build",
|
"Mind Map UI not built. From mind-map repo run: pnpm --filter @mind-map/web build && pnpm pack:plugin",
|
||||||
503,
|
503,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -674,7 +682,7 @@ export function startMindMapServer(
|
||||||
} else {
|
} else {
|
||||||
app.get("/", (c) =>
|
app.get("/", (c) =>
|
||||||
c.text(
|
c.text(
|
||||||
"Mind Map UI missing (apps/web/dist). Run: pnpm --filter @mind-map/web build",
|
`Mind Map UI missing (${webDistAbs}). Run: pnpm pack:plugin`,
|
||||||
503,
|
503,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@
|
||||||
"dev:server": "pnpm --filter @mind-map/server dev",
|
"dev:server": "pnpm --filter @mind-map/server dev",
|
||||||
"dev:web": "pnpm --filter @mind-map/web dev",
|
"dev:web": "pnpm --filter @mind-map/web dev",
|
||||||
"build": "pnpm -r run build",
|
"build": "pnpm -r run build",
|
||||||
|
"pack:plugin": "node packages/dsh-plugin/scripts/pack-bundle.mjs",
|
||||||
"test": "pnpm -r run test",
|
"test": "pnpm -r run test",
|
||||||
"start": "pnpm --filter @mind-map/server start",
|
"start": "pnpm --filter @mind-map/server start",
|
||||||
"typecheck": "pnpm -r run typecheck",
|
"typecheck": "pnpm -r run typecheck",
|
||||||
"stress": "tsx scripts/stress-10k.mts"
|
"stress": "tsx scripts/stress-10k.mts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.12",
|
||||||
"tsx": "^4.19.3"
|
"tsx": "^4.19.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,30 @@
|
||||||
# dsh-mind-map 本地安装到 DSH web profile(开发用)
|
# dsh-mind-map 安装
|
||||||
|
|
||||||
```powershell
|
## 远程安装(推荐)
|
||||||
$plugin = "d:\project\chatgpt\mind-map\packages\dsh-plugin"
|
|
||||||
# 用 dsh plugin add 链到本地包,或在 profile 里 link
|
仓库需包含预构建 `packages/dsh-plugin/bundle`(维护者执行 `pnpm pack:plugin` 后推送)。
|
||||||
dsh plugin --profile web add -w $plugin
|
|
||||||
|
```bash
|
||||||
|
dsh plugin --profile web add -w "github:hansjone/mind-map#path:packages/dsh-plugin"
|
||||||
```
|
```
|
||||||
|
|
||||||
装好后:
|
装好后重启 `dsh web`,硬刷新浏览器。依赖:`dsh-better-sidebar`。
|
||||||
|
|
||||||
1. 重启 `dsh web`,浏览器硬刷新(Ctrl+Shift+R)
|
启动日志应出现:
|
||||||
2. 右侧栏「+」选 **思维导图**,或 设置 → Mind Map → **打开思维导图 Tab**
|
|
||||||
3. 拓扑占满右侧内容区;对话继续用中间 DSH 输入框
|
|
||||||
|
|
||||||
依赖:已安装 `dsh-better-sidebar`(提供 `ctx.betterSidebar.registerTab`)。
|
```text
|
||||||
|
[dsh-mind-map] starting packaged bundle/server.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
若仍出现 `building web UI…`,说明装到的版本没有 `bundle/`,请更新插件或改用本地完整仓库。
|
||||||
|
|
||||||
|
## 本地开发(link 整个 monorepo)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd d:\project\chatgpt\mind-map
|
||||||
|
pnpm install
|
||||||
|
pnpm pack:plugin
|
||||||
|
dsh plugin --profile web add -w "d:\project\chatgpt\mind-map\packages\dsh-plugin"
|
||||||
|
```
|
||||||
|
|
||||||
|
开发改 UI 时可继续用 monorepo 源码启动(无 bundle 时会自动 `pnpm --filter @mind-map/web build`)。
|
||||||
|
|
|
||||||
1
packages/dsh-plugin/bundle/README.txt
Normal file
1
packages/dsh-plugin/bundle/README.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Prebuilt assets for remote dsh plugin install. Regenerated by: node packages/dsh-plugin/scripts/pack-bundle.mjs
|
||||||
28067
packages/dsh-plugin/bundle/server.mjs
Normal file
28067
packages/dsh-plugin/bundle/server.mjs
Normal file
File diff suppressed because it is too large
Load diff
1
packages/dsh-plugin/bundle/web/assets/index-BK6FV7kG.css
Normal file
1
packages/dsh-plugin/bundle/web/assets/index-BK6FV7kG.css
Normal file
File diff suppressed because one or more lines are too long
51
packages/dsh-plugin/bundle/web/assets/index-iTIYRavW.js
Normal file
51
packages/dsh-plugin/bundle/web/assets/index-iTIYRavW.js
Normal file
File diff suppressed because one or more lines are too long
13
packages/dsh-plugin/bundle/web/index.html
Normal file
13
packages/dsh-plugin/bundle/web/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Mind Map</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-iTIYRavW.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-BK6FV7kG.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
* Start / stop the mind-map Hono server as a child process.
|
* Start / stop the mind-map Hono server as a child process.
|
||||||
* Lifetime is tied to the DSH host: when the plugin disposes / process exits,
|
* Lifetime is tied to the DSH host: when the plugin disposes / process exits,
|
||||||
* the canvas server is killed (including process tree on Windows).
|
* the canvas server is killed (including process tree on Windows).
|
||||||
|
*
|
||||||
|
* Two layouts:
|
||||||
|
* - Packaged (remote `dsh plugin add`): packages/dsh-plugin/bundle/{web,server.mjs}
|
||||||
|
* - Monorepo (local link): ../../.. mind-map root with apps/web + apps/server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, execSync } from 'node:child_process'
|
import { spawn, execSync } from 'node:child_process'
|
||||||
|
|
@ -11,7 +15,26 @@ import { fileURLToPath } from 'node:url'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
export const mindMapRoot = join(__dirname, '../../..')
|
/** dsh-mind-map package root (…/dsh-mind-map or …/packages/dsh-plugin) */
|
||||||
|
export const pluginRoot = join(__dirname, '..')
|
||||||
|
/** Monorepo root when linked from source; otherwise same as pluginRoot when packaged. */
|
||||||
|
export const mindMapRoot = (() => {
|
||||||
|
const mono = join(__dirname, '../../..')
|
||||||
|
if (existsSync(join(mono, 'apps', 'web', 'package.json'))) return mono
|
||||||
|
return pluginRoot
|
||||||
|
})()
|
||||||
|
|
||||||
|
function bundledServer() {
|
||||||
|
return join(pluginRoot, 'bundle', 'server.mjs')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundledWebIndex() {
|
||||||
|
return join(pluginRoot, 'bundle', 'web', 'index.html')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPackagedInstall() {
|
||||||
|
return existsSync(bundledServer()) && existsSync(bundledWebIndex())
|
||||||
|
}
|
||||||
|
|
||||||
function resolveTsxCliRel() {
|
function resolveTsxCliRel() {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
|
|
@ -33,6 +56,9 @@ function resolveTsxCliRel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureWebDist() {
|
function ensureWebDist() {
|
||||||
|
// Remote/plugin install ships prebuilt assets — never run pnpm here.
|
||||||
|
if (isPackagedInstall()) return
|
||||||
|
|
||||||
const indexHtml = join(mindMapRoot, 'apps', 'web', 'dist', 'index.html')
|
const indexHtml = join(mindMapRoot, 'apps', 'web', 'dist', 'index.html')
|
||||||
const watchSrc = [
|
const watchSrc = [
|
||||||
join(mindMapRoot, 'apps', 'web', 'src', 'TopologyCanvas.tsx'),
|
join(mindMapRoot, 'apps', 'web', 'src', 'TopologyCanvas.tsx'),
|
||||||
|
|
@ -55,6 +81,16 @@ function ensureWebDist() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!needsBuild) return
|
if (!needsBuild) return
|
||||||
|
|
||||||
|
if (!existsSync(join(mindMapRoot, 'pnpm-workspace.yaml'))) {
|
||||||
|
throw new Error(
|
||||||
|
'mind-map web UI is missing and this is not a monorepo checkout. ' +
|
||||||
|
'Reinstall a release that includes packages/dsh-plugin/bundle ' +
|
||||||
|
'(github:hansjone/mind-map#path:packages/dsh-plugin after maintainer runs pnpm pack:plugin), ' +
|
||||||
|
'or clone the full repo and link locally.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[dsh-mind-map] building web UI (apps/web/dist missing or stale)…')
|
console.log('[dsh-mind-map] building web UI (apps/web/dist missing or stale)…')
|
||||||
try {
|
try {
|
||||||
execSync('pnpm --filter @mind-map/web build', {
|
execSync('pnpm --filter @mind-map/web build', {
|
||||||
|
|
@ -187,14 +223,7 @@ export function createRuntime() {
|
||||||
await new Promise((r) => setTimeout(r, 400))
|
await new Promise((r) => setTimeout(r, 400))
|
||||||
}
|
}
|
||||||
|
|
||||||
const entryAbs = join(mindMapRoot, 'apps/server/src/index.ts')
|
const packaged = isPackagedInstall()
|
||||||
const distAbs = join(mindMapRoot, 'apps/server/dist/index.js')
|
|
||||||
const useTs = existsSync(entryAbs)
|
|
||||||
const scriptAbs = useTs ? entryAbs : distAbs
|
|
||||||
if (!existsSync(scriptAbs)) {
|
|
||||||
throw new Error(`mind-map server entry not found under ${mindMapRoot}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
PORT: String(port),
|
PORT: String(port),
|
||||||
|
|
@ -205,8 +234,24 @@ export function createRuntime() {
|
||||||
}
|
}
|
||||||
if (dataDir) env.DATA_DIR = dataDir
|
if (dataDir) env.DATA_DIR = dataDir
|
||||||
|
|
||||||
const scriptRel = useTs ? 'apps/server/src/index.ts' : 'apps/server/dist/index.js'
|
let cwd
|
||||||
let args
|
let args
|
||||||
|
if (packaged) {
|
||||||
|
cwd = pluginRoot
|
||||||
|
env.MINDMAP_ROOT = pluginRoot
|
||||||
|
env.MINDMAP_WEB_DIST = join(pluginRoot, 'bundle', 'web')
|
||||||
|
args = [bundledServer()]
|
||||||
|
console.log('[dsh-mind-map] starting packaged bundle/server.mjs')
|
||||||
|
} else {
|
||||||
|
cwd = mindMapRoot
|
||||||
|
const entryAbs = join(mindMapRoot, 'apps/server/src/index.ts')
|
||||||
|
const distAbs = join(mindMapRoot, 'apps/server/dist/index.js')
|
||||||
|
const useTs = existsSync(entryAbs)
|
||||||
|
const scriptAbs = useTs ? entryAbs : distAbs
|
||||||
|
if (!existsSync(scriptAbs)) {
|
||||||
|
throw new Error(`mind-map server entry not found under ${mindMapRoot}`)
|
||||||
|
}
|
||||||
|
const scriptRel = useTs ? 'apps/server/src/index.ts' : 'apps/server/dist/index.js'
|
||||||
if (useTs) {
|
if (useTs) {
|
||||||
const tsxCli = resolveTsxCliRel()
|
const tsxCli = resolveTsxCliRel()
|
||||||
if (!tsxCli) throw new Error('tsx not found; run pnpm install in mind-map')
|
if (!tsxCli) throw new Error('tsx not found; run pnpm install in mind-map')
|
||||||
|
|
@ -214,9 +259,10 @@ export function createRuntime() {
|
||||||
} else {
|
} else {
|
||||||
args = [scriptRel]
|
args = [scriptRel]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
child = spawn(process.execPath, args, {
|
child = spawn(process.execPath, args, {
|
||||||
cwd: mindMapRoot,
|
cwd,
|
||||||
env,
|
env,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
|
@ -277,8 +323,9 @@ export function createRuntime() {
|
||||||
...last,
|
...last,
|
||||||
running: Boolean(child && !child.killed),
|
running: Boolean(child && !child.killed),
|
||||||
pid: child?.pid || null,
|
pid: child?.pid || null,
|
||||||
|
packaged: isPackagedInstall(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { start, stop, status, mindMapRoot }
|
return { start, stop, status, mindMapRoot, pluginRoot }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,14 @@
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib",
|
"lib",
|
||||||
|
"bundle",
|
||||||
"cordis.patch.yml",
|
"cordis.patch.yml",
|
||||||
"README.md"
|
"README.md",
|
||||||
|
"INSTALL.md"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test test/*.test.js"
|
"test": "node --test test/*.test.js",
|
||||||
|
"pack": "node scripts/pack-bundle.mjs"
|
||||||
},
|
},
|
||||||
"dsh": {
|
"dsh": {
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|
|
||||||
71
packages/dsh-plugin/scripts/pack-bundle.mjs
Normal file
71
packages/dsh-plugin/scripts/pack-bundle.mjs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* Pack prebuilt web UI + server into packages/dsh-plugin/bundle
|
||||||
|
* so `dsh plugin add github:…#path:packages/dsh-plugin` works without monorepo.
|
||||||
|
*/
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
cpSync,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
|
const repoRoot = join(pluginRoot, "../..");
|
||||||
|
const bundleRoot = join(pluginRoot, "bundle");
|
||||||
|
const webOut = join(bundleRoot, "web");
|
||||||
|
const serverOut = join(bundleRoot, "server.mjs");
|
||||||
|
|
||||||
|
function run(cmd, cwd = repoRoot) {
|
||||||
|
console.log(`[pack] ${cmd}`);
|
||||||
|
execSync(cmd, { cwd, stdio: "inherit", env: process.env, windowsHide: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Build workspace libs + web
|
||||||
|
run("pnpm --filter @mind-map/shared build");
|
||||||
|
run("pnpm --filter @mind-map/graph-core build");
|
||||||
|
run("pnpm --filter @mind-map/graph-store build");
|
||||||
|
run("pnpm --filter @mind-map/layout-engine build");
|
||||||
|
// dream-skin ships TS+JSON; esbuild resolves it without a separate tsc emit
|
||||||
|
run("pnpm --filter @mind-map/web build");
|
||||||
|
|
||||||
|
const webDist = join(repoRoot, "apps/web/dist");
|
||||||
|
if (!existsSync(join(webDist, "index.html"))) {
|
||||||
|
throw new Error("apps/web/dist/index.html missing after build");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fresh bundle dir
|
||||||
|
rmSync(bundleRoot, { recursive: true, force: true });
|
||||||
|
mkdirSync(webOut, { recursive: true });
|
||||||
|
cpSync(webDist, webOut, { recursive: true });
|
||||||
|
|
||||||
|
// 3) Bundle server (workspace + npm deps inlined; node builtins stay external)
|
||||||
|
console.log("[pack] esbuild apps/server → bundle/server.mjs");
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [join(repoRoot, "apps/server/src/index.ts")],
|
||||||
|
bundle: true,
|
||||||
|
platform: "node",
|
||||||
|
format: "esm",
|
||||||
|
outfile: serverOut,
|
||||||
|
packages: "bundle",
|
||||||
|
banner: {
|
||||||
|
js: "import { createRequire as __mmCr } from 'node:module'; const require = __mmCr(import.meta.url);",
|
||||||
|
},
|
||||||
|
logLevel: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsSync(serverOut)) {
|
||||||
|
throw new Error("bundle/server.mjs was not produced");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(bundleRoot, "README.txt"),
|
||||||
|
"Prebuilt assets for remote dsh plugin install. Regenerated by: node packages/dsh-plugin/scripts/pack-bundle.mjs\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[pack] OK →", bundleRoot);
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -8,6 +8,9 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
esbuild:
|
||||||
|
specifier: ^0.25.12
|
||||||
|
version: 0.25.12
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.19.3
|
specifier: ^4.19.3
|
||||||
version: 4.23.13
|
version: 4.23.13
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue