diff --git a/.changeset/README.md b/.changeset/README.md index 2570d76..20f193d 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -16,25 +16,20 @@ npm run changeset # 选受影响的包 + patch|minor|major,写中文摘要 ``` -## 本地发版(维护者) +然后 **push `main`** 即可:CI 会开/更新 Version Packages PR;合并该 PR 后自动 `ci-release-packages`(publish → tag → push → GitHub Release)。 -当前在 pre mode,请用 **prerelease**: +本地一般 **不需要** 手动跑发版命令。 -```bash -npm run prerelease-packages -``` - -入口为 `node tools/run-release.mjs --pre`,内部依次:assert pre → ensure changeset → version → commit → tag → push → publish → GitHub Pre-release。 +## CI 发版入口 -退出 pre 且达标后,正式发版: - -```bash -npx changeset pre exit -npm run release-packages # run-release.mjs --stable → npm latest + GitHub Release -``` +根 `package.json`: -## CI +| 脚本 | 谁调 | +|------|------| +| `version-packages` | changesets/action(Version PR) | +| `ci-release-packages` | 合并 Version PR 后的 publish 步骤 | +| `build:publishable` | CI build 可发包拓扑 | -`changeset-release.yml` 在 `main` 上开 Version PR;合并后跑 `ci-release-packages`(`run-release.mjs --ci`:publish → tag → push tags → gh release,无交互)。 +应急单包:`.github/workflows/npm-publish.yml`(`workflow_dispatch`)。 -详情见 [docs/dev/publish.md](../docs/dev/publish.md)(平台包附录)。 +详情见 [docs/zh/dev/publish.md](../docs/zh/dev/publish.md)。 diff --git a/AGENTS.md b/AGENTS.md index 6804726..d25d4a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,7 +169,6 @@ npm run start # sfmc CLI REPL npm run lint # 先 build eslint-plugin,再 eslint . npm run typecheck npm run verify # 平台集成自检(CI 默认) -npm run catalog-sync # 装模块后投影 catalog(维护命令) npm run syncpack:lint ``` @@ -215,9 +214,7 @@ npm start -- update ```bash node packages/tools/verify.mjs -node packages/tools/catalog-sync.mjs -node packages/tools/check-modules.mjs -sfmc mod install +sfmc mod install # 在独立 SFMC 根,非主仓 ``` ## SAPI debug 与 Sentry diff --git a/docs/en/guide/modules.mdx b/docs/en/guide/modules.mdx index 90997b3..bda704f 100644 --- a/docs/en/guide/modules.mdx +++ b/docs/en/guide/modules.mdx @@ -73,11 +73,8 @@ The list below is generated at build time from the `sfmc-modules` registry (loca -## Related checks (monorepo) +## Related checks (user SFMC root) -```bash -npm run catalog-sync -npm run check-modules -``` +Module install and catalog projection are done by `sfmc mod install` (the monorepo does not host business modules). Official module service index: [Module services](../api/modules/index.md). diff --git a/docs/en/guide/troubleshooting.mdx b/docs/en/guide/troubleshooting.mdx index 69cca14..fef05e3 100644 --- a/docs/en/guide/troubleshooting.mdx +++ b/docs/en/guide/troubleshooting.mdx @@ -10,8 +10,6 @@ Pick a symptom and work through the steps; the full checklist below remains for ```bash npm run verify -npm run check-modules -npm run catalog-sync ``` ### db-server won't start @@ -31,7 +29,7 @@ curl http://127.0.0.1:3001/api/health 1. Read the load summary in the console and `/.sfmc/logs/` 2. Run `mod build` and check esbuild errors -3. `npm run check-modules` (monorepo) +3. In your SFMC root, confirm the module is `mod install`ed and enabled in lock 4. Inbox conflicts blocking pre-start scan (see [Add-ons](./addons.md)) ### Module enabled but no effect in-game diff --git a/docs/zh/dev/contributing.md b/docs/zh/dev/contributing.md index 8a53246..564d63c 100644 --- a/docs/zh/dev/contributing.md +++ b/docs/zh/dev/contributing.md @@ -34,8 +34,6 @@ npm run verify | 命令 | 说明 | | ------ | ------ | | `npm run verify` | 平台集成自检 | -| `npm run catalog-sync` | packages → catalog | -| `npm run check-modules` | catalog + manifest | ### 文档 @@ -57,11 +55,10 @@ npx syncpack format --check | 命令 | 说明 | | ------ | ------ | -| `npm run changeset` | 添加 changeset | -| `npm run prerelease-packages` | 本地 beta 一键 | -| `npm run pack:verify` | `npm pack` 冒烟 | +| `npm run changeset` | 添加 changeset;push `main` 后 CI 开 Version PR | +| `npm run pack:verify` | 可选:`npm pack` 冒烟 | -当前 **beta-only**。模块作者发包见 [发布你的模块](./publish.md) 附录。 +日常发版:**合并 Version PR 即可**,无需本地 `prerelease` / `release`。当前 **beta-only**。模块作者发包见 [发布你的模块](./publish.md)。 ## 仓库规则 diff --git a/docs/zh/dev/module-author.mdx b/docs/zh/dev/module-author.mdx index 0dfcb4d..b3298ca 100644 --- a/docs/zh/dev/module-author.mdx +++ b/docs/zh/dev/module-author.mdx @@ -185,7 +185,7 @@ v3 是**完全可选**的能力扩展:不写 v3 字段也能 boot;旧 v2 模 ### 兼容性与迁移 -- **v2 → v3 自动迁移**:沙箱与 `check-modules` 都内置自动 migrate(`migrateV2toV3`)。v2 manifest 加载时直接补 `schemaVersion: 3`、保留所有原有字段,`semantic` 视为空。 +- **v2 → v3 自动迁移**:沙箱与 CLI `check-modules`(`mod install` 后)都内置自动 migrate(`migrateV2toV3`)。v2 manifest 加载时直接补 `schemaVersion: 3`、保留所有原有字段,`semantic` 视为空。 - **缺省 semantic 合法**:v3 manifest 可以不写 `semantic`,等价于「暂时没声明语义」——后续可逐字段补全。 - **运行时校验**:保留 v2 的硬校验(id/name/type/configKey/requires/permissions/services 必填);`semantic` 子字段类型错时给 `errors: string[]`,但不拒绝启动(沙箱会剥掉坏字段、用 v3 顶层继续)。 @@ -193,8 +193,8 @@ v3 是**完全可选**的能力扩展:不写 v3 字段也能 boot;旧 v2 模 | 工具 | 行为 | | --- | --- | -| `tools/check-modules.mjs` | v2 校验路径不变;v3 manifest 走新校验(`semantic` 字段类型错会报错)。 | -| `catalog-sync` / CLI `fetch-module` | 投影时把 `semantic` 块原样写入 catalog(仅读取,不主动注入)。 | +| CLI `scripts/module-install/check-modules.mjs` | `mod install` 后校验;v3 `semantic` 类型错会报错 | +| CLI `fetch-module` | 投影时把 `semantic` 块原样写入 catalog(仅读取,不主动注入) | ### 与缺口 1 / 3 的衔接 @@ -205,4 +205,4 @@ v3 `semantic` 不是终点,是**数据源**: `publicApi` 当前只支持单签名;多载/重载留 TODO,等下一轮按真实模块诉求再加。 -> v2 manifest 作者请保持现状不动;只有新增模块或主动升级时再写 v3。`fetch-module` 与 `catalog-sync` 不会因为写错 v3 而拒绝模块——错就报在 `check-modules`,不必动 v2 字段。 +> v2 manifest 作者请保持现状不动;只有新增模块或主动升级时再写 v3。`fetch-module` 不会因为写错 v3 而拒绝模块——错就报在 install 后的 check-modules,不必动 v2 字段。 diff --git a/docs/zh/dev/tools.md b/docs/zh/dev/tools.md index 4d37681..837f3dc 100644 --- a/docs/zh/dev/tools.md +++ b/docs/zh/dev/tools.md @@ -1,57 +1,22 @@ # 工具脚本 -`packages/tools/` 为 **monorepo 私有**脚本(不发 npm)。模块安装在 `@sfmc-bds/cli`,脚手架在 `@sfmc-bds/devkit`;作者日常用扩展「SFMC Module」。Watch / 重建见 `packages/devkit/`。 - -**目录约定:** `packages/*` = 平台包;`modules/packages/*` = 业务模块。`catalog-sync` 扫描的是后者。 +`packages/tools/` 为 **monorepo 私有**脚本(不发 npm)。模块安装在 `@sfmc-bds/cli`,脚手架在 `@sfmc-bds/devkit`。 ## 常用 | 命令 | 作用 | |------|------| | `npm run verify` | 平台集成自检(CI 默认) | -| `npm run catalog-sync` | 扫 `modules/packages` → `catalog.json` | -| `npm run check-modules` | 离线校验 catalog + manifest | -| `sfmc mod search` / `sfmc mod install ` | 查注册表 / 安装(`@sfmc-bds/cli`) | -| `npx sfmc-new-module ` | 在空目录生成单包根(`@sfmc-bds/devkit`) | - -## 平台自检 - -主仓默认无业务模块。`npm run verify` 一次完成离线检查 + db-server 集成 + 隔离 `SFMC_ROOT` 模拟。 - -| 脚本 | 何时用 | -|------|--------| -| `packages/tools/verify.mjs` | CI / 本地「平台能不能跑」 | -| `packages/tools/catalog-sync.mjs` | 装包后投影 catalog | -| `packages/tools/check-modules.mjs` | 离线校验(薄封装 → CLI 权威实现) | - -约定检查见 [ESLint 约定](./eslint.md)。 +| `sfmc mod install ` | 安装模块(独立 SFMC 根,非主仓) | +| `npx sfmc-new-module ` | 脚手架(`@sfmc-bds/devkit`) | -## fetch-module(CLI) +主仓默认 **不装业务模块**;catalog / lock 由用户环境里的 `mod install` 维护,主仓不再提供 `catalog-sync` / `check-modules` 入口。 -```bash -sfmc mod install afk -sfmc mod install foo --from dir:/path --link -sfmc mod uninstall afk -# 等价:node packages/cli/scripts/module-install/fetch-module.mjs … -``` +## 发版 -默认注册表:`Tanya7z/sfmc-modules` 的 `index.json`(`npm` 字段优先)。 - -## new-module(devkit) - -```bash -mkdir my-mod && cd my-mod -npx sfmc-new-module my-mod --name "我的模块" -# monorepo:node packages/devkit/scripts/new-module.mjs … -``` - -`--official` → `@sfmc-bds/module-*`。脚手架会写入 `eslint.config.js` 与 `.vscode/`。也可用 [sfmc-module-template](https://github.com/Tanya7z/sfmc-module-template) 或扩展 `SFMC: New Module`。 - -(作者测试)见 [测试沙箱](./testing.md)(`npm test`)。 +日常:`npx changeset` → push `main` → Version Packages PR → 合并后 CI `ci-release-packages`。 ## 共享库 -- 模块 catalog / lock / install:`packages/cli/scripts/module-install/lib/` -- 仓内自检 / 发版:`packages/tools/lib/`(`paths`、`proc`、`npm-publish-packages` 等) - -新脚本优先复用,勿复制 lock/catalog 读写。 +- 模块 catalog / lock / install:`packages/cli/scripts/module-install/` +- 仓内自检 / 发版:`packages/tools/lib/` diff --git a/docs/zh/guide/modules.mdx b/docs/zh/guide/modules.mdx index ef91aa6..e80d481 100644 --- a/docs/zh/guide/modules.mdx +++ b/docs/zh/guide/modules.mdx @@ -73,12 +73,9 @@ sfmc mod reload -## 相关校验(monorepo) +## 相关校验(用户 SFMC 根) -```bash -npm run catalog-sync -npm run check-modules -``` +模块安装与 catalog 投影由 `sfmc mod install` 完成(主仓不装业务模块)。 官方模块服务清单见 [模块服务目录](../api/modules/index.md)。 diff --git a/docs/zh/guide/troubleshooting.mdx b/docs/zh/guide/troubleshooting.mdx index f4a6e6f..1da5174 100644 --- a/docs/zh/guide/troubleshooting.mdx +++ b/docs/zh/guide/troubleshooting.mdx @@ -10,8 +10,6 @@ ```bash npm run verify -npm run check-modules -npm run catalog-sync ``` ### db-server 起不来 @@ -31,7 +29,7 @@ curl http://127.0.0.1:3001/api/health 1. 看控制台装载摘要与 `/.sfmc/logs/` 2. `mod build` 看 esbuild 是否报错 -3. `npm run check-modules`(monorepo) +3. 独立 SFMC 根下确认模块已 `mod install` 且 lock 为 enabled 4. 收件箱冲突是否挡住启动前扫描(见 [附加包](./addons.md)) ### 模块已 enable 但游戏里没效果 diff --git a/modules/sdk/@sfmc-sdk/schemas/module_catalog.schema.json b/modules/sdk/@sfmc-sdk/schemas/module_catalog.schema.json index 946fa9f..7dd0aef 100644 --- a/modules/sdk/@sfmc-sdk/schemas/module_catalog.schema.json +++ b/modules/sdk/@sfmc-sdk/schemas/module_catalog.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/schemas/module_catalog.schema.json", "title": "SFMC module catalog", - "description": "modules/catalog.json 本地 mirror;权威源为 github:Tanya7z/sfmc-modules。安装/卸载后请用 catalog-sync 或 fetch-module 投影。", + "description": "modules/catalog.json 本地 mirror;权威源为 github:Tanya7z/sfmc-modules。安装/卸载后由 fetch-module(sfmc mod install)投影。", "type": "object", "additionalProperties": false, "properties": { diff --git a/package-lock.json b/package-lock.json index 6223256..15cfd91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "modules/sdk/@sfmc-sdk": { "name": "@sfmc-bds/sdk", - "version": "0.2.0-beta.8", + "version": "0.2.0-beta.9", "license": "ISC", "engines": { "node": ">=22.13.0" @@ -4625,10 +4625,10 @@ }, "packages/bds-tools": { "name": "@sfmc-bds/bds-tools", - "version": "0.2.0-beta.8", + "version": "0.2.0-beta.9", "license": "AGPL-3.0-only", "dependencies": { - "@sfmc-bds/sdk": "^0.2.0-beta.8", + "@sfmc-bds/sdk": "^0.2.0-beta.9", "jszip": "^3.10.1", "node-html-parser": "^9.0.0" }, @@ -4638,12 +4638,12 @@ }, "packages/cli": { "name": "@sfmc-bds/cli", - "version": "0.2.0-beta.8", + "version": "0.2.0-beta.9", "license": "AGPL-3.0-only", "dependencies": { "@clack/prompts": "^1.7.0", - "@sfmc-bds/bds-tools": "^0.2.0-beta.8", - "@sfmc-bds/sdk": "^0.2.0-beta.8", + "@sfmc-bds/bds-tools": "^0.2.0-beta.9", + "@sfmc-bds/sdk": "^0.2.0-beta.9", "chalk": "^6.0.0" }, "bin": { @@ -4656,10 +4656,10 @@ }, "packages/db-server": { "name": "@sfmc-bds/db-server", - "version": "0.2.0-beta.4", + "version": "0.2.0-beta.5", "license": "AGPL-3.0-only", "dependencies": { - "@sfmc-bds/sdk": "^0.2.0-beta.8", + "@sfmc-bds/sdk": "^0.2.0-beta.9", "sql-template-strings": "^2.2.2" }, "engines": { @@ -4684,14 +4684,14 @@ }, "packages/meta": { "name": "@sfmc-bds/sfmc", - "version": "0.2.0-beta.4", + "version": "0.2.0-beta.5", "license": "AGPL-3.0-only", "dependencies": { - "@sfmc-bds/bds-tools": "^0.2.0-beta.8", - "@sfmc-bds/cli": "^0.2.0-beta.8", - "@sfmc-bds/db-server": "^0.2.0-beta.4", - "@sfmc-bds/qq-bridge": "^0.1.1-beta.3", - "@sfmc-bds/sdk": "^0.2.0-beta.8" + "@sfmc-bds/bds-tools": "^0.2.0-beta.9", + "@sfmc-bds/cli": "^0.2.0-beta.9", + "@sfmc-bds/db-server": "^0.2.0-beta.5", + "@sfmc-bds/qq-bridge": "^0.2.0-beta.4", + "@sfmc-bds/sdk": "^0.2.0-beta.9" }, "bin": { "sfmc": "bin/sfmc.mjs" @@ -4702,10 +4702,10 @@ }, "packages/qq-bridge": { "name": "@sfmc-bds/qq-bridge", - "version": "0.1.1-beta.3", + "version": "0.2.0-beta.4", "license": "AGPL-3.0-only", "dependencies": { - "@sfmc-bds/sdk": "^0.2.0-beta.8", + "@sfmc-bds/sdk": "^0.2.0-beta.9", "ws": "^8.21.1" }, "engines": { @@ -4717,7 +4717,7 @@ "license": "AGPL-3.0-only", "dependencies": { "@sfmc-bds/devkit": "^0.1.0-beta.1", - "@sfmc-bds/sdk": "^0.2.0-beta.8" + "@sfmc-bds/sdk": "^0.2.0-beta.9" }, "devDependencies": { "@types/vscode": "^1.85.0", @@ -4729,15 +4729,13 @@ }, "packages/tools": { "name": "@sfmc-bds/tools", - "version": "0.2.0-beta.9", + "version": "0.2.0-beta.10", "license": "AGPL-3.0-only", "dependencies": { - "@sfmc-bds/bds-tools": "^0.2.0-beta.8", - "@sfmc-bds/sdk": "^0.2.0-beta.8" + "@sfmc-bds/bds-tools": "^0.2.0-beta.9", + "@sfmc-bds/sdk": "^0.2.0-beta.9" }, "bin": { - "sfmc-catalog-sync": "catalog-sync.mjs", - "sfmc-check-modules": "check-modules.mjs", "sfmc-esbuild-transpile": "esbuild-transpile.mjs", "sfmc-verify": "verify.mjs", "tsc7": "tsc7.mjs" diff --git a/package.json b/package.json index 0c3d5dd..e5bde79 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,11 @@ "scripts": { "build": "npm run build --workspaces --if-present", "build:publishable": "node packages/tools/build-publishable.mjs", - "catalog-sync": "node packages/tools/catalog-sync.mjs", "changeset": "changeset", - "check-modules": "node packages/tools/check-modules.mjs", "ci-release-packages": "node packages/tools/run-release.mjs --ci", "docs": "node packages/tools/docs.mjs", "lint": "npm run build --workspace @sfmc-bds/eslint-plugin && eslint .", "pack:verify": "node packages/tools/pack-verify.mjs", - "prerelease-packages": "node packages/tools/run-release.mjs --pre", - "release-packages": "node packages/tools/run-release.mjs --stable", "start": "node packages/cli/dist/main.js", "syncpack:fix": "node packages/tools/syncpack-fix.mjs", "typecheck": "npm run typecheck --workspaces --if-present", diff --git a/packages/tools/README.md b/packages/tools/README.md index 6967bb1..dd9cd8c 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -11,16 +11,11 @@ ```bash npm run verify # 平台集成自检(CI) -npm run catalog-sync -npm run check-modules npx sfmc-esbuild-transpile # 各包 build npx tsc7 --noEmit # typecheck npm run docs -- serve|build ``` -构建 bin 经 workspace 链接到根 `node_modules/.bin/`;包内脚本请用 bin 名,勿写 `../../../tools/...`。 +发版走 Changesets:**push `main` → Version Packages PR → 合并后 CI 自动 `ci-release-packages`**。本地一般不手动发。 -## 约定 - -- 脚本一律 `.mjs` + `// @ts-check` -- changeset / docs / pack-verify 等发版基建也在此目录,仅 CI / 维护者使用 +构建 bin 经 workspace 链接;包内脚本请用 bin 名,勿写 `../../../tools/...`。 diff --git a/packages/tools/catalog-sync.mjs b/packages/tools/catalog-sync.mjs deleted file mode 100755 index 6d3b223..0000000 --- a/packages/tools/catalog-sync.mjs +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env node -// @ts-check -/** - * 薄封装 → catalog sync(权威实现在 @sfmc-bds/cli) - * 用法: node packages/tools/catalog-sync.mjs - */ -import { syncCatalogFromPackages } from "../cli/scripts/module-install/lib/catalog.mjs"; -import { pruneOrphanModuleLocks, readLock, setModuleLockEnabled } from "../cli/scripts/module-install/lib/lock.mjs"; -import { die } from "./lib/io.mjs"; - -try { - const { catalog, count } = syncCatalogFromPackages(); - const ids = new Set(catalog.modules.map((m) => m.id)); - pruneOrphanModuleLocks(ids); - - const lock = readLock(); - for (const m of catalog.modules) { - if (!lock.modules[m.id]) { - setModuleLockEnabled(m.id, m.enabledByDefault !== false); - } - } - - console.log(`[catalog-sync] OK — ${count} module(s) → modules/catalog.json`); - for (const m of catalog.modules) { - console.log(` ${m.id.padEnd(28)} folder=${m.entry.path.split("/")[2]}`); - } -} catch (e) { - const message = e instanceof Error ? e.message : String(e); - die("catalog-sync", message); -} diff --git a/packages/tools/changeset-assert-mode.mjs b/packages/tools/changeset-assert-mode.mjs index bd9bab3..05dc8df 100644 --- a/packages/tools/changeset-assert-mode.mjs +++ b/packages/tools/changeset-assert-mode.mjs @@ -23,8 +23,8 @@ const state = readPreState(); if (mode === "pre" && !pre) { console.error( - "[changeset] 当前不在 pre mode。正式发版请用 npm run release-packages;" + - "若要重新进入 beta: npx changeset pre enter beta" + "[changeset] 当前不在 pre mode。正式通道请先 `npx changeset pre exit`;" + + "日常发版走 CI(push main → Version PR)。若要重新进入 beta: npx changeset pre enter beta" ); process.exit(1); } @@ -32,7 +32,7 @@ if (mode === "pre" && !pre) { if (mode === "stable" && pre) { console.error( `[changeset] 仓库仍处于 pre mode (tag=${state?.tag ?? "beta"})。` + - `请用 npm run prerelease-packages,或达标后执行 npx changeset pre exit 再 release-packages。` + `达标后执行 npx changeset pre exit;日常发版走 CI Version PR。` ); process.exit(1); } diff --git a/packages/tools/check-minecraft-versions.mjs b/packages/tools/check-minecraft-versions.mjs deleted file mode 100644 index 427f015..0000000 --- a/packages/tools/check-minecraft-versions.mjs +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env node -// @ts-check -/** - * tools/check-minecraft-versions.mjs — 校验 @minecraft/* 版本与主仓权威 pin 一致 - * - * 权威来源:根 package.json devDependencies + overrides(见 tools/lib/minecraft-versions.mjs) - * - * 用法: - * node tools/check-minecraft-versions.mjs - * node tools/check-minecraft-versions.mjs --modules-root ../sfmc-modules - * SFMC_MODULES_ROOT=/path/to/sfmc-modules node tools/check-minecraft-versions.mjs - */ -import fs from "node:fs"; -import path from "node:path"; -import { ROOT } from "./lib/paths.mjs"; -import { - findPackageJsonFiles, - readCanonicalFromRootPkg, - readMinecraftDeps, - validateCanonicalPins, - validateOverrides, - validatePackageMinecraftDeps, -} from "./lib/minecraft-versions.mjs"; - -/** @param {string} msg */ -function fail(msg) { - console.error(`[check-minecraft] FAIL: ${msg}`); - process.exit(1); -} - -function resolveModulesRoot() { - const argIdx = process.argv.indexOf("--modules-root"); - if (argIdx !== -1 && process.argv[argIdx + 1]) { - return path.resolve(process.argv[argIdx + 1]); - } - if (process.env.SFMC_MODULES_ROOT) { - return path.resolve(process.env.SFMC_MODULES_ROOT); - } - const sibling = path.resolve(ROOT, "..", "sfmc-modules"); - if (fs.existsSync(path.join(sibling, "package.json"))) return sibling; - return null; -} - -function main() { - const rootPkgPath = path.join(ROOT, "package.json"); - const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf8")); - const canonical = readCanonicalFromRootPkg(ROOT); - - if (Object.keys(canonical).length === 0) { - fail("根 package.json 未声明 @minecraft/* devDependencies"); - } - - /** @type {string[]} */ - const errors = []; - errors.push(...validateCanonicalPins(rootPkg.devDependencies)); - errors.push(...validateOverrides(rootPkg.overrides ?? {}, canonical)); - - /** @type {string[]} */ - const scanRoots = [ROOT]; - const modulesRoot = resolveModulesRoot(); - if (modulesRoot) scanRoots.push(modulesRoot); - - /** @type {string[]} */ - const pkgFiles = []; - for (const scanRoot of scanRoots) { - pkgFiles.push(...findPackageJsonFiles(scanRoot)); - } - - for (const pkgPath of pkgFiles.sort()) { - const { name, deps } = readMinecraftDeps(pkgPath); - if (Object.keys(deps).length === 0) continue; - errors.push( - ...validatePackageMinecraftDeps({ - canonical, - pkgPath: path.relative(ROOT, pkgPath) || pkgPath, - pkgName: name, - deps, - }) - ); - } - - console.log("[check-minecraft] 权威版本:"); - for (const [name, ver] of Object.entries(canonical).sort()) { - console.log(` ${name}: ${ver}`); - } - if (modulesRoot) { - console.log(`[check-minecraft] 已扫描 sfmc-modules: ${modulesRoot}`); - } - - if (errors.length > 0) { - console.error(`[check-minecraft] ${errors.length} 个问题:`); - for (const e of errors) console.error(` - ${e}`); - process.exit(1); - } - - console.log(`[check-minecraft] OK (${pkgFiles.length} package.json scanned)`); -} - -main(); diff --git a/packages/tools/check-modules.mjs b/packages/tools/check-modules.mjs deleted file mode 100755 index fe35336..0000000 --- a/packages/tools/check-modules.mjs +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node -// @ts-check -/** - * 薄封装 → @sfmc-bds/cli module-install/check-modules(权威实现) - */ -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; -import { runCheckModules } from "../cli/scripts/module-install/check-modules.mjs"; - -export { runCheckModules }; - -const __main = fileURLToPath(import.meta.url); -if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__main)) { - const doSync = process.argv.includes("--sync"); - const result = runCheckModules({ sync: doSync }); - if (!result.ok) { - console.error(`[check-modules] FAIL: ${result.error}`); - process.exit(1); - } - console.log(`[check-modules] ${result.summary}`); -} diff --git a/packages/tools/lib/minecraft-versions.mjs b/packages/tools/lib/minecraft-versions.mjs deleted file mode 100644 index d2dcbc4..0000000 --- a/packages/tools/lib/minecraft-versions.mjs +++ /dev/null @@ -1,163 +0,0 @@ -// @ts-check -/** - * tools/lib/minecraft-versions.mjs — @minecraft/* 版本权威来源与扫描 - * - * 权威版本:主仓根 package.json 的 devDependencies(@minecraft/*), - * 安装时以 overrides 为准;校验时 devDependencies 须为精确版本(无 ^/~/>)。 - */ -import fs from "node:fs"; -import path from "node:path"; - -/** SDK 允许 peer 使用最低兼容范围;业务模块不得自行声明 @minecraft/* */ -export const SDK_PEER_RANGE_ALLOWED = new Set(["@sfmc-bds/sdk", "@sfmc/sdk"]); - -const MIN_RANGE_RE = /^>=/; - -/** 业务模块包名:@sfmc-bds/module-* */ -export function isBusinessModulePkg(pkgName) { - return typeof pkgName === "string" && pkgName.startsWith("@sfmc-bds/module-"); -} - -/** - * @param {Record} devDeps - * @param {Record} [overrides] - * @returns {Record} - */ -export function readCanonicalVersions(devDeps, overrides = {}) { - /** @type {Record} */ - const canonical = {}; - for (const [name, ver] of Object.entries(devDeps ?? {})) { - if (name.startsWith("@minecraft/")) canonical[name] = ver; - } - for (const [name, ver] of Object.entries(overrides ?? {})) { - if (name.startsWith("@minecraft/")) canonical[name] = ver; - } - return canonical; -} - -/** - * @param {string} root 仓库根 - * @returns {Record} - */ -export function readCanonicalFromRootPkg(root) { - const pkgPath = path.join(root, "package.json"); - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); - return readCanonicalVersions(pkg.devDependencies, pkg.overrides); -} - -/** - * @param {string} dir - * @param {string[]} [acc] - * @returns {string[]} - */ -export function findPackageJsonFiles(dir, acc = []) { - let entries; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { - return acc; - } - for (const ent of entries) { - if (ent.name === "node_modules" || ent.name === "dist" || ent.name === ".git") continue; - const full = path.join(dir, ent.name); - if (ent.isDirectory()) { - findPackageJsonFiles(full, acc); - } else if (ent.name === "package.json") { - acc.push(full); - } - } - return acc; -} - -/** - * @param {string} pkgPath - * @returns {{ rel: string, name: string, deps: Record }} - */ -export function readMinecraftDeps(pkgPath) { - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); - /** @type {Record} */ - const deps = {}; - for (const section of ["dependencies", "devDependencies", "peerDependencies"]) { - for (const [name, ver] of Object.entries(pkg[section] ?? {})) { - if (name.startsWith("@minecraft/")) deps[name] = ver; - } - } - return { rel: pkgPath, name: pkg.name ?? pkgPath, deps }; -} - -/** - * devDependencies 中的 @minecraft/* 必须为精确版本(约束写入点) - * @param {Record} devDeps - * @returns {string[]} - */ -export function validateCanonicalPins(devDeps) { - /** @type {string[]} */ - const errors = []; - for (const [name, ver] of Object.entries(devDeps ?? {})) { - if (!name.startsWith("@minecraft/")) continue; - if (/^[\^~>]/.test(ver) || ver.includes("||") || ver.includes(" - ")) { - errors.push(`根 devDependencies ${name}="${ver}" 须改为精确版本`); - } - } - return errors; -} - -/** - * @param {Record} overrides - * @param {Record} canonical - * @returns {string[]} - */ -export function validateOverrides(overrides, canonical) { - /** @type {string[]} */ - const errors = []; - for (const name of Object.keys(canonical)) { - if (!overrides[name]) { - errors.push(`根 overrides 缺少 ${name}(应与 devDependencies 一致)`); - } else if (overrides[name] !== canonical[name]) { - errors.push( - `根 overrides ${name}="${overrides[name]}" 与 devDependencies="${canonical[name]}" 不一致` - ); - } - } - return errors; -} - -/** - * @param {object} opts - * @param {Record} opts.canonical - * @param {string} opts.pkgPath - * @param {string} opts.pkgName - * @param {Record} opts.deps - * @returns {string[]} - */ -export function validatePackageMinecraftDeps({ canonical, pkgPath, pkgName, deps }) { - /** @type {string[]} */ - const errors = []; - const allowRange = SDK_PEER_RANGE_ALLOWED.has(pkgName); - - // 业务模块:类型来自仓库根 hoisted devDependencies,不得再声明 @minecraft/* - if (isBusinessModulePkg(pkgName) && Object.keys(deps).length > 0) { - for (const [name, ver] of Object.entries(deps)) { - errors.push( - `${pkgPath}: 业务模块勿声明 ${name}="${ver}"(请只用仓库根 devDependencies pin)` - ); - } - return errors; - } - - for (const [name, ver] of Object.entries(deps)) { - const expected = canonical[name]; - if (!expected) { - errors.push(`${pkgPath}: 未在权威清单中声明的 ${name}="${ver}"`); - continue; - } - if (allowRange && MIN_RANGE_RE.test(ver)) continue; - if (ver !== expected) { - errors.push(`${pkgPath}: ${name}="${ver}" 应为 "${expected}"`); - } - if (!allowRange && /^[\^~>]/.test(ver)) { - errors.push(`${pkgPath}: ${name}="${ver}" 须精确 pin,勿用范围`); - } - } - return errors; -} diff --git a/packages/tools/lib/verify/offline.mjs b/packages/tools/lib/verify/offline.mjs index dc20dcb..b657738 100644 --- a/packages/tools/lib/verify/offline.mjs +++ b/packages/tools/lib/verify/offline.mjs @@ -1,22 +1,19 @@ // @ts-check /** - * 离线阶段:Node、仓库文件、schema、configs 播种、已装模块 catalog 一致性 + * 离线阶段:Node、仓库文件、schema、configs 播种、CLI / fetch-module 冒烟 * - * 主仓默认无业务模块 — 不自动 catalog-sync;仅当 packages/ 有目录时校验 catalog。 + * 主仓默认无业务模块 — 不做 catalog-sync / check-modules。 */ import path from "node:path"; import process from "node:process"; import { NPM_PUBLISH_PACKAGES, assertPublishPackageInWorkspaces } from "../npm-publish-packages.mjs"; import { exists } from "../io.mjs"; import { - CONFIGS_DIR, FETCH_MODULE, ROOT, SFMC_DIST, - TOOLS_PKG_DIR, } from "../paths.mjs"; import { runSync } from "../proc.mjs"; -import { runCheckModules } from "../../check-modules.mjs"; import { coreConfigsReady, seedCoreConfigs } from "./seed-configs.mjs"; const SCHEMA_FILES = [ @@ -50,8 +47,6 @@ export function runOfflinePhase(reporter) { "modules/catalog.json", "packages/tools/verify.mjs", "packages/cli/scripts/module-install/fetch-module.mjs", - "packages/tools/catalog-sync.mjs", - "packages/tools/check-modules.mjs", ...Object.values(NPM_PUBLISH_PACKAGES), ]; const missing = required.filter((f) => !exists(path.join(ROOT, f))); @@ -88,12 +83,6 @@ export function runOfflinePhase(reporter) { else reporter.fail("configs/ 核心文件齐全", ready.error); } - { - const check = runCheckModules(); - if (check.ok) reporter.pass(check.summary ?? "check-modules 通过"); - else reporter.fail("check-modules", check.error ?? "未知错误"); - } - { const sdkDist = path.join(ROOT, "modules", "sdk", "@sfmc-sdk", "dist"); if (exists(sdkDist)) reporter.pass("@sfmc-bds/sdk dist 存在"); diff --git a/packages/tools/package.json b/packages/tools/package.json index 18dc5e8..d634198 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,39 +1,37 @@ -{ - "name": "@sfmc-bds/tools", - "version": "0.2.0-beta.10", - "private": true, - "description": "SFMC monorepo-only scripts — verify, catalog-sync, release, build bins (not published to npm)", - "author": "Shiroha7z", - "license": "AGPL-3.0-only", - "type": "module", - "homepage": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/packages/tools#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/DogeLakeDev/ScriptsForMinecraftServer.git", - "directory": "packages/tools" - }, - "bugs": { - "url": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/issues" - }, - "engines": { - "node": ">=22.13.0" - }, - "exports": { - "./package.json": "./package.json", - "./tsc7": "./tsc7.mjs" - }, - "bin": { - "sfmc-catalog-sync": "./catalog-sync.mjs", - "sfmc-check-modules": "./check-modules.mjs", - "sfmc-esbuild-transpile": "./esbuild-transpile.mjs", - "sfmc-verify": "./verify.mjs", - "tsc7": "./tsc7.mjs" - }, - "scripts": { - "build": "node -e \"process.exit(0)\"" - }, - "dependencies": { - "@sfmc-bds/bds-tools": "^0.2.0-beta.9", - "@sfmc-bds/sdk": "^0.2.0-beta.9" - } -} +{ + "name": "@sfmc-bds/tools", + "version": "0.2.0-beta.10", + "private": true, + "description": "SFMC monorepo-only scripts — verify, docs, release, build bins (not published to npm)", + "author": "Shiroha7z", + "license": "AGPL-3.0-only", + "type": "module", + "homepage": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/packages/tools#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/DogeLakeDev/ScriptsForMinecraftServer.git", + "directory": "packages/tools" + }, + "bugs": { + "url": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/issues" + }, + "engines": { + "node": ">=22.13.0" + }, + "exports": { + "./package.json": "./package.json", + "./tsc7": "./tsc7.mjs" + }, + "bin": { + "sfmc-esbuild-transpile": "./esbuild-transpile.mjs", + "sfmc-verify": "./verify.mjs", + "tsc7": "./tsc7.mjs" + }, + "scripts": { + "build": "node -e \"process.exit(0)\"" + }, + "dependencies": { + "@sfmc-bds/bds-tools": "^0.2.0-beta.9", + "@sfmc-bds/sdk": "^0.2.0-beta.9" + } +} diff --git a/packages/tools/run-release.mjs b/packages/tools/run-release.mjs index 430d356..7ce44ff 100644 --- a/packages/tools/run-release.mjs +++ b/packages/tools/run-release.mjs @@ -3,9 +3,9 @@ /** * tools/run-release.mjs — 一键发版编排(替代根 package.json 里一长串 run-s) * - * node tools/run-release.mjs --pre # 本地 beta:assert → … → publish → gh prerelease - * node tools/run-release.mjs --stable # 本地正式:assert → … → publish → gh latest - * node tools/run-release.mjs --ci # CI:仅 publish → tag → push → gh(版本已 bump) + * node packages/tools/run-release.mjs --ci # CI:publish → tag → push → gh(版本已 bump) + * node packages/tools/run-release.mjs --pre # 应急本地 beta(日常请走 CI Version PR) + * node packages/tools/run-release.mjs --stable # 应急本地正式 */ import { spawnSync } from "node:child_process"; import path from "node:path"; @@ -24,9 +24,9 @@ const mode = process.argv.includes("--ci") if (!mode) { console.error(`用法: - node packages/tools/run-release.mjs --pre # beta 本地一键发版 - node packages/tools/run-release.mjs --stable # 正式本地一键发版 - node packages/tools/run-release.mjs --ci # CI:publish + tag + push + gh release`); + node packages/tools/run-release.mjs --ci # CI / npm run ci-release-packages + node packages/tools/run-release.mjs --pre # 应急本地 beta(日常请走 CI) + node packages/tools/run-release.mjs --stable # 应急本地正式`); process.exit(2); } diff --git a/packages/tools/verify-module-publish.mjs b/packages/tools/verify-module-publish.mjs deleted file mode 100644 index 395e35f..0000000 --- a/packages/tools/verify-module-publish.mjs +++ /dev/null @@ -1,462 +0,0 @@ -#!/usr/bin/env node -// @ts-check -/** - * tools/verify-module-publish.mjs — 模块包发布前的全方位预检 - * - * 在 `npm publish` 之前跑一遍;模板 CI / 本地均可触发: - * - * 1) manifest v2 字段完整性(id / configKey / permissions / services) - * 2) package.json#name 与 manifest.id 一致(折叠规则:feature-* → folder) - * 3) package.json#files 含 sapi/(避免发空 tarball) - * 4) npm pack --dry-run 解析 tarball 清单(确认 sapi/ 实际入包) - * 5) sapi/src/** 至少 1 个 .ts 文件,且含 ModuleRegistry.register 调用 - * 6) sdk 依赖 pin 在 @sfmc-bds/sdk(避免拼错 @sfmc/sdk 旧名) - * 7) 跨模块源码 import 检查(避免深挖其它模块业务代码) - * - * 退出码:0 全部通过;1 任一项失败。失败时打印每条 cause + 修复建议。 - */ - -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import path from "node:path"; -import process from "node:process"; -import { spawn } from "node:child_process"; - -const CWD = process.cwd(); - -/** - * @param {any} msg - */ -function die(msg, code = 1) { - console.error(`[verify-module-publish] ${msg}`); - process.exit(code); -} - -const KEYS = { - schemaVersion: 2, - required: ["id", "name", "type", "configKey", "permissions", "services", "requires"], -}; - -/** - * @type {{ kind: any; name: any; msg: any; }[]} - */ -const checks = []; -let pass = 0; -let warn = 0; -let fail = 0; - -/** - * @param {string} kind - * @param {string} name - * @param {string} msg - */ -function record(kind, name, msg) { - checks.push({ kind, name, msg }); - if (kind === "pass") pass++; - else if (kind === "warn") warn++; - else fail++; -} - -/* ────────────────────────────────────────────────────────────────── - * 1) manifest.json v2 字段完整性 - * ──────────────────────────────────────────────────────────────── */ -async function checkManifest() { - const manifestPath = path.join(CWD, "sapi", "manifest.json"); - if (!fs.existsSync(manifestPath)) { - record("fail", "manifest 存在", `未找到 sapi/manifest.json(在 ${CWD})`); - return null; - } - let manifest; - try { - manifest = JSON.parse(await fsp.readFile(manifestPath, "utf8")); - } catch (e) { - // @ts-ignore - record("fail", "manifest 可解析", `sapi/manifest.json 解析失败: ${e.message}`); - return null; - } - if (manifest.schemaVersion !== KEYS.schemaVersion) { - record("fail", "schemaVersion=2", `当前 ${manifest.schemaVersion ?? "未设置"};固定为 2`); - } else { - record("pass", "schemaVersion=2", "OK"); - } - for (const k of KEYS.required) { - if (manifest[k] === undefined) { - record("fail", `manifest.${k}`, `缺失`); - } else { - record("pass", `manifest.${k}`, "存在"); - } - } - if (manifest.services && typeof manifest.services === "object") { - if (!Array.isArray(manifest.services.provides)) { - record("fail", "manifest.services.provides 数组", `当前类型 ${typeof manifest.services.provides}`); - } - if (!Array.isArray(manifest.services.requires)) { - record("fail", "manifest.services.requires 数组", `当前类型 ${typeof manifest.services.requires}`); - } else { - const refs = manifest.services.requires.map((/** @type {{ name: any; }} */ r) => (r && typeof r === "object" ? r.name : r)); - const dup = refs.filter((/** @type {any} */ n, /** @type {any} */ i) => refs.indexOf(n) !== i); - if (dup.length) record("fail", "manifest.services.requires 无重复", `重复: ${[...new Set(dup)].join(", ")}`); - } - } - return manifest; -} - -/* ────────────────────────────────────────────────────────────────── - * 2) package.json#name 与 manifest.id 折叠一致 - * folder → @scope/module- - * feature- → 同上 - * ──────────────────────────────────────────────────────────────── */ -/** - * @param {{ id: any; }} manifest - */ -function folderFromManifest(manifest) { - const id = manifest?.id; - if (!id) return null; - if (id.startsWith("feature-") || id.startsWith("core-")) return id.slice(id.indexOf("-") + 1); - return id; -} - -/** - * @param {any} manifest - */ -async function checkPackageNameAlignment(manifest) { - const pkgPath = path.join(CWD, "package.json"); - if (!fs.existsSync(pkgPath)) { - record("fail", "package.json 存在", "未找到 package.json"); - return; - } - let pkg; - try { - pkg = JSON.parse(await fsp.readFile(pkgPath, "utf8")); - } catch (e) { - // @ts-ignore - record("fail", "package.json 可解析", e.message); - return; - } - const expectedFolder = folderFromManifest(manifest); - if (!expectedFolder) return; /* checkManifest 已报缺 id */ - /* 接受 @/module- 或 @/sfmc-module- */ - const re = new RegExp(`^@[^/]+/(?:sfmc-)?module-${expectedFolder}$`); - if (!re.test(pkg.name ?? "")) { - record( - "fail", - "package.name 与 manifest.id 一致", - `name=${pkg.name},期望匹配 ${re}(folder=${expectedFolder})` - ); - } else { - record("pass", "package.name 与 manifest.id 一致", pkg.name); - } - /* version */ - if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(pkg.version ?? "")) { - record("fail", "package.version semver", `当前 ${pkg.version}`); - } else { - record("pass", "package.version semver", pkg.version); - } -} - -/* ────────────────────────────────────────────────────────────────── - * 3) package.json#files 含 sapi/ - * ──────────────────────────────────────────────────────────────── */ -async function checkFilesField() { - const pkgPath = path.join(CWD, "package.json"); - if (!fs.existsSync(pkgPath)) { - record("fail", "package.files 数组", "未找到 package.json"); - return; - } - let pkg; - try { - pkg = JSON.parse(await fsp.readFile(pkgPath, "utf8")); - } catch { - return; /* 已在 checkPackageNameAlignment 报 */ - } - const files = pkg.files; - if (!Array.isArray(files)) { - record("fail", "package.files 数组", `未声明 files;npm pack 会按 .gitignore 默认 → tarball 可能不含 sapi/`); - return; - } - if (!files.includes("sapi")) { - record("fail", "package.files 含 sapi/", `当前 ${JSON.stringify(files)}`); - } else { - record("pass", "package.files 含 sapi/", "OK"); - } -} - -/* ────────────────────────────────────────────────────────────────── - * 4) npm pack --dry-run 解析 tarball 内容 - * ──────────────────────────────────────────────────────────────── */ -/* 极简 tar 头解析:512-byte 块,name 在 offset 0(100 字节 null-padded), - * size 在 offset 124(12 字节八进制 null-padded);typeflag 在 offset 156('0' = 普通文件,'5' = 目录)。 - * 末尾两个全零 512 块终止。返回 entries 名字数组(相对 tar 根,含目录与文件)。 */ -/** - * @param {NonSharedBuffer} buf - */ -function listTarEntries(buf) { - const files = []; - const BLOCK = 512; - for (let off = 0; off + BLOCK <= buf.length; off += BLOCK) { - const header = buf.subarray(off, off + BLOCK); - /* name 是 null 结尾 */ - const nameEnd = header.indexOf(0); - if (nameEnd < 0) break; /* 全零块,终止 */ - const name = header.subarray(0, nameEnd).toString("utf8"); - /* size 是 octal 字符串 */ - const sizeStr = header.subarray(124, 136).toString("utf8").replace(/\0/g, "").trim(); - const size = parseInt(sizeStr, 8) || 0; - const typeflag = String.fromCharCode(header[156]); - if (name === "") break; - if (typeflag !== "5") files.push(name); /* 跳过纯目录 */ - /* size blocks + 1 header */ - off += Math.ceil(size / BLOCK) * BLOCK; - } - return files; -} - -/** - * @param {readonly string[]} args - */ -function runNpm(args) { - const isWin = process.platform === "win32"; - const npmCli = path.join(process.execPath.replace(/[^\\/]+$/, ""), "node_modules", "npm", "bin", "npm-cli.js"); - if (isWin && fs.existsSync(npmCli)) { - return new Promise((resolve) => { - const proc = spawn(process.execPath, [npmCli, ...args], { - cwd: CWD, - stdio: ["ignore", "pipe", "pipe"], - env: { ...process.env, npm_config_loglevel: "notice" }, - }); - let stdout = ""; - let stderr = ""; - proc.stdout?.on("data", (d) => (stdout += d.toString())); - proc.stderr?.on("data", (d) => (stderr += d.toString())); - proc.on("exit", (code) => { - /* npm 11+ notice 输出到 stderr、tgz 文件名输出到 stdout; - * 合并时 stderr 放前,让 stdout 的 tgz 文件名落在最后一行(pop 可得)。 */ - const merged = stderr + stdout; - resolve({ code, stdout: merged, stderr }); - }); - proc.on("error", (e) => resolve({ code: -1, stdout: "", stderr: e.message })); - }); - } - return new Promise((resolve) => { - const proc = spawn("npm", args, { cwd: CWD, stdio: ["ignore", "pipe", "pipe"] }); - let stdout = ""; - let stderr = ""; - proc.stdout?.on("data", (d) => (stdout += d.toString())); - proc.stderr?.on("data", (d) => (stderr += d.toString())); - proc.on("exit", (code) => resolve({ code, stdout: stdout + stderr, stderr })); - proc.on("error", (e) => resolve({ code: -1, stdout: "", stderr: e.message })); - }); -} - -async function checkTarballContents() { - const os = await import("node:os"); - const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "sfmc-vm-tgz-")); - try { - const r = await runNpm(["pack"]); - if (process.env.VERIFY_DEBUG) console.error("[debug] npm pack code=", r.code, "stdout=", JSON.stringify(r.stdout)); - if (r.code !== 0) { - record("fail", "npm pack 成功", r.stderr.trim() || `exit ${r.code}`); - return; - } - const tgzName = r.stdout.trim().split(/\r?\n/).pop() ?? ""; - const tgzPath = path.join(CWD, tgzName); - if (!tgzName || !fs.existsSync(tgzPath)) { - record("fail", "tarball 生成", `未找到产物: cwd=${CWD}, stdout=${r.stdout.trim().split("\n").slice(-3).join(" | ")}`); - return; - } - const buffer = await fsp.readFile(tgzPath); - /* npm pack 产物是 .tar.gz(不是 zip)。用 zlib 解 gzip + 极简 tar 头解析。 */ - const zlib = await import("node:zlib"); - const decompressed = zlib.gunzipSync(buffer); - const files = listTarEntries(decompressed); - if (files.length === 0) { - record("fail", "tarball 含文件", `tgz 大小 ${buffer.length}B 但无 entries`); - return; - } - record("pass", "tarball 含文件", `${files.length} 文件入包`); - const must = ["package.json", "sapi/manifest.json"]; - for (const f of must) { - if (!files.some((t) => t === `package/${f}` || t === f)) { - record("fail", `tarball 含 ${f}`, `缺失;当前前 5 个: ${files.slice(0, 5).join(", ")}`); - } else { - record("pass", `tarball 含 ${f}`, "OK"); - } - } - const srcFiles = files.filter((f) => f.includes("sapi/src/")); - if (srcFiles.length === 0) { - record("warn", "tarball 含 sapi/src/**", "无源码文件;模块可能空跑"); - } else { - record("pass", "tarball 含 sapi/src/**", `${srcFiles.length} 文件`); - } - } finally { - /* 清理 CWD 下的临时 tgz(npm pack 写到这里) */ - try { - for (const e of fs.readdirSync(CWD)) { - if (e.endsWith(".tgz") && e.includes("module")) { - try { fs.unlinkSync(path.join(CWD, e)); } catch {} - } - } - } catch {} - await fsp.rm(tmp, { recursive: true, force: true }).catch(() => {}); - } -} - -/* ────────────────────────────────────────────────────────────────── - * 5) sapi/src/** 至少 1 个 .ts,且含 ModuleRegistry.register 调用 - * ──────────────────────────────────────────────────────────────── */ -async function checkEntryRegistration() { - const srcDir = path.join(CWD, "sapi", "src"); - if (!fs.existsSync(srcDir)) { - record("fail", "sapi/src/ 存在", `未找到目录 ${srcDir}`); - return; - } - /** - * @type {string[]} - */ - const tsFiles = []; - const walk = (/** @type {fs.PathLike} */ dir) => { - for (const e of fs.readdirSync(dir, { withFileTypes: true })) { - // @ts-ignore - const p = path.join(dir, e.name); - if (e.isDirectory()) walk(p); - else if (e.isFile() && /\.tsx?$/.test(e.name)) tsFiles.push(p); - } - }; - walk(srcDir); - if (tsFiles.length === 0) { - record("fail", "sapi/src 含 .ts", "无"); - return; - } - let hasRegister = false; - for (const f of tsFiles) { - const text = await fsp.readFile(f, "utf8"); - if (/ModuleRegistry\.register\s*\(/.test(text)) { - hasRegister = true; - break; - } - } - if (!hasRegister) { - record("fail", "至少一个 .ts 含 ModuleRegistry.register", "全部命中不到;模块不会被 BP 装载"); - } else { - record("pass", "sapi/src 含 ModuleRegistry.register", "OK"); - } -} - -/* ────────────────────────────────────────────────────────────────── - * 6) sdk 依赖 pin 正确 - * ──────────────────────────────────────────────────────────────── */ -async function checkSdkPin() { - const pkgPath = path.join(CWD, "package.json"); - if (!fs.existsSync(pkgPath)) { - record("fail", "无 @sfmc/sdk 旧别名", "未找到 package.json"); - record("fail", "声明 @sfmc-bds/sdk 依赖", "未找到 package.json"); - return; - } - let pkg; - try { - pkg = JSON.parse(await fsp.readFile(pkgPath, "utf8")); - } catch { - return; /* 已在前面报 */ - } - const sections = ["dependencies", "devDependencies", "peerDependencies"]; - const issues = []; - for (const s of sections) { - const deps = pkg[s] ?? {}; - if (deps["@sfmc/sdk"]) issues.push(`${s}.@sfmc/sdk(旧别名)`); - } - if (issues.length) { - record("fail", "无 @sfmc/sdk 旧别名", `发现 ${issues.join(", ")}`); - } else { - record("pass", "无 @sfmc/sdk 旧别名", "OK"); - } - const sdkVer = pkg.dependencies?.["@sfmc-bds/sdk"] ?? pkg.devDependencies?.["@sfmc-bds/sdk"] ?? pkg.peerDependencies?.["@sfmc-bds/sdk"]; - if (!sdkVer) { - record("fail", "声明 @sfmc-bds/sdk 依赖", `未在 dependencies/devDependencies/peerDependencies 声明`); - } else { - record("pass", "声明 @sfmc-bds/sdk 依赖", `version=${sdkVer}`); - } -} - -/* ────────────────────────────────────────────────────────────────── - * 7) 跨模块源码 import 检查(不要 import 其它模块私有代码) - * 启发式:@sfmc-bds/module-* 以外的相对 ../ 深挖一律警告 - * ──────────────────────────────────────────────────────────────── */ -async function checkCrossModuleImports() { - const srcDir = path.join(CWD, "sapi", "src"); - if (!fs.existsSync(srcDir)) return; - /** - * @type {string[]} - */ - const tsFiles = []; - const walk = (/** @type {fs.PathLike} */ dir) => { - for (const e of fs.readdirSync(dir, { withFileTypes: true })) { - // @ts-ignore - const p = path.join(dir, e.name); - if (e.isDirectory()) walk(p); - else if (e.isFile() && /\.(ts|tsx)$/.test(e.name)) tsFiles.push(p); - } - }; - walk(srcDir); - /* 启发式规则: - * - 允许:import "@sfmc-bds/sdk/..." 与 "node:*" - * - 警告:相对路径超过 2 级 (../../..) - * - 警告:import 含 "module-" 但非 "@sfmc-bds/module-" 自身 - */ - for (const f of tsFiles) { - const text = await fsp.readFile(f, "utf8"); - const matches = text.matchAll(/from\s+["']([^"']+)["']/g); - for (const m of matches) { - const spec = m[1]; - if (spec.startsWith("@sfmc-bds/sdk")) continue; - if (spec.startsWith("node:")) continue; - if (spec.startsWith("@minecraft/")) continue; - if (spec.startsWith(".")) { - const upLevels = (spec.match(/\.\.\//g) || []).length; - if (upLevels >= 2) { - record("warn", `${path.relative(CWD, f)} 相对深挖`, `${spec}(${upLevels} 级;建议改用 typed client)`); - } - } - if (spec.includes("module-") && !spec.startsWith(".")) { - record("warn", `${path.relative(CWD, f)} 引用未授权模块`, `${spec}(应通过 service.get 或 typed client)`); - } - } - } -} - -/* ────────────────────────────────────────────────────────────────── - * 入口 - * ──────────────────────────────────────────────────────────────── */ -async function main() { - console.log(`[verify-module-publish] cwd: ${CWD}`); - /* 包一层:单个 check 抛错不影响其它(之前是直接 await 串行,整体崩) */ - const wrap = (/** @type {string} */ name, /** @type {{ (): Promise; (): Promise; (): Promise; (): Promise; (): Promise; (): Promise; (): Promise; (): Promise; }} */ fn) => fn().catch((/** @type {{ message: any; }} */ e) => record("fail", name, `抛错: ${e?.message ?? String(e)}`)); - const manifest = await wrap("manifest 读取", () => checkManifest()); - if (manifest) { - await wrap("packageNameAlignment", () => checkPackageNameAlignment(manifest)); - await wrap("filesField", checkFilesField); - await wrap("entryRegistration", checkEntryRegistration); - await wrap("sdkPin", checkSdkPin); - } - await wrap("tarballContents", checkTarballContents); - await wrap("crossModuleImports", checkCrossModuleImports); - - /* 输出 */ - for (const c of checks) { - const icon = c.kind === "pass" ? "✓" : c.kind === "warn" ? "!" : "✗"; - const color = c.kind === "pass" ? "\x1b[32m" : c.kind === "warn" ? "\x1b[33m" : "\x1b[31m"; - const reset = "\x1b[0m"; - console.log(` ${color}${icon}${reset} ${c.name} ${c.kind === "pass" ? "" : c.msg}`); - } - - console.log(""); - console.log(`[verify-module-publish] ${pass} pass · ${warn} warn · ${fail} fail`); - if (fail > 0) { - console.error(`[verify-module-publish] FAILED(${fail} 项硬错)。修完再跑 npm publish。`); - process.exit(1); - } else { - console.log(`[verify-module-publish] OK(${warn} 项警告;不阻塞 publish)。`); - process.exit(0); - } -} - -main().catch((e) => die(e?.message ?? String(e))); \ No newline at end of file diff --git a/packages/tools/verify-module-publish.test.mjs b/packages/tools/verify-module-publish.test.mjs deleted file mode 100644 index edd7527..0000000 --- a/packages/tools/verify-module-publish.test.mjs +++ /dev/null @@ -1,168 +0,0 @@ -// @ts-check -/** - * verify-module-publish.test.mjs — pre-publish 守门脚本测试 - * - * 用真实模板仓路径(已 commit 的 sfmc-module-template)作为 good fixture; - * 用临时目录构造 bad fixture(缺 manifest、缺 files、错误 sdk 等)。 - */ -import assert from "node:assert/strict"; -import test from "node:test"; -import fs from "node:fs/promises"; -import path from "node:path"; -import os from "node:os"; -import { spawn } from "node:child_process"; - -const TEMPLATE = path.resolve("D:/", "#WorkPlace", "#MCBEProjects", "sfmc-module-template"); -const TOOLS = path.resolve("D:/", "#WorkPlace", "#MCBEProjects", "ScriptsForMinecraftServer", "tools"); -const VERIFY_SCRIPT = path.join(TOOLS, "verify-module-publish.mjs"); - -/** - * @param {string} cmd - * @param {readonly string[]} args - */ -function run(cmd, args, opts = {}) { - return new Promise((resolve) => { - // @ts-ignore - const proc = spawn(cmd, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] }); - let stdout = ""; - let stderr = ""; - proc.stdout?.on("data", (d) => (stdout += d.toString())); - proc.stderr?.on("data", (d) => (stderr += d.toString())); - proc.on("exit", (code) => resolve({ code, stdout, stderr })); - proc.on("error", (e) => resolve({ code: -1, stdout, stderr: `${stderr}${e.message}` })); - }); -} - -/** - * @param {string} cwd - */ -async function runVerify(cwd) { - return run(process.execPath, [VERIFY_SCRIPT], { cwd }); -} - -test("模板仓:所有硬检查通过(warn 可允许)", async () => { - const r = await runVerify(TEMPLATE); - assert.equal(r.code, 0, `unexpected exit; stdout=${r.stdout}\nstderr=${r.stderr}`); - assert.match(r.stdout, /OK(/); -}); - -test("坏 fixture:缺 manifest.json → exit 1", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "sfmc-vm-bad-")); - try { - const r = await runVerify(tmp); - assert.notEqual(r.code, 0); - /* manifest 缺失会先报;script 还在 manifest 检查阶段,未必进入后续检查 */ - assert.ok(/未找到 sapi\/manifest\.json|npm pack 未报告/.test(r.stdout), `got stdout: ${r.stdout}`); - } finally { - await fs.rm(tmp, { recursive: true, force: true }); - } -}); - -test("坏 fixture:manifest schemaVersion=1 → fail", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "sfmc-vm-bad-")); - try { - await fs.mkdir(path.join(tmp, "sapi", "src"), { recursive: true }); - await fs.writeFile( - path.join(tmp, "sapi", "manifest.json"), - JSON.stringify({ - schemaVersion: 1, - id: "feature-x", - name: "x", - type: "feature", - configKey: "x", - permissions: [], - services: { provides: [], requires: [] }, - requires: [], - }) - ); - await fs.writeFile(path.join(tmp, "sapi", "src", "index.ts"), "export const x = 1;\n"); - await fs.writeFile( - path.join(tmp, "package.json"), - JSON.stringify({ - name: "@scope/module-x", - version: "1.0.0", - type: "module", - files: ["sapi"], - dependencies: { "@sfmc-bds/sdk": "^0.2.0-beta.5" }, - }) - ); - const r = await runVerify(tmp); - assert.notEqual(r.code, 0); - assert.match(r.stdout, /schemaVersion=2/); - } finally { - await fs.rm(tmp, { recursive: true, force: true }); - } -}); - -test("坏 fixture:package.name 不匹配 manifest.id → fail", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "sfmc-vm-bad-")); - try { - await fs.mkdir(path.join(tmp, "sapi", "src"), { recursive: true }); - await fs.writeFile( - path.join(tmp, "sapi", "manifest.json"), - JSON.stringify({ - schemaVersion: 2, - id: "feature-foo", - name: "foo", - type: "feature", - configKey: "foo", - permissions: ["config:read:foo"], - services: { provides: [], requires: [] }, - requires: [], - }) - ); - await fs.writeFile(path.join(tmp, "sapi", "src", "index.ts"), "export const x = 1;\n"); - await fs.writeFile( - path.join(tmp, "package.json"), - JSON.stringify({ - name: "@alice/sfmc-module-other", - version: "1.0.0", - type: "module", - files: ["sapi"], - dependencies: { "@sfmc-bds/sdk": "^0.2.0-beta.5" }, - }) - ); - const r = await runVerify(tmp); - assert.notEqual(r.code, 0); - assert.match(r.stdout, /package\.name 与 manifest\.id 一致/); - } finally { - await fs.rm(tmp, { recursive: true, force: true }); - } -}); - -test("坏 fixture:使用旧别名 @sfmc/sdk → fail", async () => { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "sfmc-vm-bad-")); - try { - await fs.mkdir(path.join(tmp, "sapi", "src"), { recursive: true }); - await fs.writeFile( - path.join(tmp, "sapi", "manifest.json"), - JSON.stringify({ - schemaVersion: 2, - id: "feature-foo", - name: "foo", - type: "feature", - configKey: "foo", - permissions: ["config:read:foo"], - services: { provides: [], requires: [] }, - requires: [], - }) - ); - await fs.writeFile(path.join(tmp, "sapi", "src", "index.ts"), "export const x = 1;\n"); - await fs.writeFile( - path.join(tmp, "package.json"), - JSON.stringify({ - name: "@alice/sfmc-module-foo", - version: "1.0.0", - type: "module", - files: ["sapi"], - dependencies: { "@sfmc/sdk": "^0.1.0" }, - }) - ); - const r = await runVerify(tmp); - assert.notEqual(r.code, 0); - /* old alias 检查可能在 npm pack 报错前/后执行;任一提示出现即 OK */ - assert.ok(/@sfmc\/sdk 旧别名|package\.name 与 manifest\.id|schemaVersion|npm pack/.test(r.stdout), `got: ${r.stdout}`); - } finally { - await fs.rm(tmp, { recursive: true, force: true }); - } -}); \ No newline at end of file diff --git a/packages/tools/verify.mjs b/packages/tools/verify.mjs index 1bbf537..3f9ab9c 100644 --- a/packages/tools/verify.mjs +++ b/packages/tools/verify.mjs @@ -3,8 +3,7 @@ /** * tools/verify.mjs — 平台集成自检(主仓唯一 CI 门禁) * - * 主仓默认无业务模块:不测 catalog-sync;已装包时 check-modules 校验 catalog 一致性。 - * 一次 db 生命周期覆盖平台 API + 模块 REST(有模块时才 enable/disable)。 + * 主仓默认无业务模块(业务装在独立环境)。 * * 用法: * node packages/tools/verify.mjs # 全量(离线 + db + 隔离 SFMC_ROOT) @@ -34,7 +33,7 @@ function printHelp() { console.log(`用法: node packages/tools/verify.mjs [选项] 选项: - --offline 仅离线检查(Node、文件、schema、configs、check-modules、CLI) + --offline 仅离线检查(Node、文件、schema、configs、CLI) --skip-isolated 跳过隔离 SFMC_ROOT 模拟 -h, --help 显示帮助