From 0e8367a34a127fb99ae6d884052bd3da32503f49 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Jul 2026 12:08:56 +0000 Subject: [PATCH] =?UTF-8?q?fix(solid):=20sql-helpers=20=E6=9D=83=E5=A8=81?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=20+=20tx=20requires=20=E5=8D=95=E7=82=B9=20+?= =?UTF-8?q?=20npm-publish=20DRY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查 main@2dbb079(@sfmc-bds 发包)后的跟进补丁,覆盖并取代 #20: - DRY: economy 本地 sql() → sql-helpers;HttpDB.withModuleId 统一三抽屉 - DRY/LSP: tx.doService 去掉重复 requires 预检,错误码走 DispatchError - DRY: npm-publish 包清单抽到 tools/lib/npm-publish-packages.mjs - CI: npm-publish Node 对齐 ootb 的 22.22;SDK engines ≥22.13 Co-authored-by: Shiroha --- .github/workflows/npm-publish.yml | 59 ++--------------- db-server/package.json | 2 +- db-server/src/domain/economy.ts | 9 +-- db-server/src/lib/sql-helpers.ts | 32 ++++++--- db-server/src/runtime.test.ts | 66 ++++++++++++++++++- db-server/src/service-registry.ts | 4 -- db-server/src/tx-runner.ts | 39 ++++++----- docs/dev/npm-publish.md | 9 ++- modules/sdk/@sfmc-sdk/package.json | 2 +- .../sdk/@sfmc-sdk/src/sapi/config/client.ts | 4 +- modules/sdk/@sfmc-sdk/src/sapi/db/client.ts | 4 +- .../sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts | 10 +++ .../sdk/@sfmc-sdk/src/sapi/service/client.ts | 4 +- package-lock.json | 2 +- tools/lib/npm-publish-packages.mjs | 21 ++++++ tools/resolve-npm-publish-tag.mjs | 55 ++++++++++++++++ 16 files changed, 216 insertions(+), 106 deletions(-) create mode 100644 tools/lib/npm-publish-packages.mjs create mode 100644 tools/resolve-npm-publish-tag.mjs diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e487148..e98bf63 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -2,6 +2,7 @@ name: npm-publish # 按包 tag 触发 npm 发布,与 release.yml(SEA / v*)互不干扰。 # 示例: git tag @sfmc-bds/sdk@v0.1.0 && git push origin @sfmc-bds/sdk@v0.1.0 +# 可发包清单唯一来源: tools/lib/npm-publish-packages.mjs on: push: @@ -19,64 +20,14 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: "22.13" + # 与 ootb.yml 对齐: npm-run-all2 等要求 ≥22.22; db-server 需 ≥22.13 + node-version: "22.22" cache: npm registry-url: https://registry.npmjs.org - - name: parse tag + - name: parse tag + verify package version id: meta - shell: bash - run: | - TAG="${GITHUB_REF_NAME}" - PKG="${TAG%@v*}" - VER="${TAG##*@v}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "pkg=$PKG" >> "$GITHUB_OUTPUT" - echo "ver=$VER" >> "$GITHUB_OUTPUT" - - case "$PKG" in - @sfmc-bds/sdk) echo "workspace=@sfmc-bds/sdk" >> "$GITHUB_OUTPUT" ;; - @sfmc-bds/cli) echo "workspace=@sfmc-bds/cli" >> "$GITHUB_OUTPUT" ;; - @sfmc-bds/db-server) echo "workspace=@sfmc-bds/db-server" >> "$GITHUB_OUTPUT" ;; - @sfmc-bds/qq-bridge) echo "workspace=@sfmc-bds/qq-bridge" >> "$GITHUB_OUTPUT" ;; - @sfmc-bds/bds-tools) echo "workspace=@sfmc-bds/bds-tools" >> "$GITHUB_OUTPUT" ;; - *) - echo "::error::Unknown npm package tag: $PKG" - exit 1 - ;; - esac - - - name: verify package version - shell: bash - env: - WORKSPACE: ${{ steps.meta.outputs.workspace }} - VER: ${{ steps.meta.outputs.ver }} - run: | - node <<'EOF' - import fs from "node:fs"; - - const map = { - "@sfmc-bds/sdk": "modules/sdk/@sfmc-sdk/package.json", - "@sfmc-bds/cli": "sfmc/package.json", - "@sfmc-bds/db-server": "db-server/package.json", - "@sfmc-bds/qq-bridge": "qq-bridge/package.json", - "@sfmc-bds/bds-tools": "bds-tools/package.json", - }; - - const ws = process.env.WORKSPACE; - const expected = process.env.VER; - const pkgPath = map[ws]; - if (!pkgPath) { - console.error("No package.json mapping for", ws); - process.exit(1); - } - const ver = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version; - if (ver !== expected) { - console.error(`Tag version ${expected} != package.json version ${ver}`); - process.exit(1); - } - console.log(`Version OK: ${ws}@${ver}`); - EOF + run: node tools/resolve-npm-publish-tag.mjs - run: npm ci --no-audit --no-fund diff --git a/db-server/package.json b/db-server/package.json index cdfd712..e0a23ff 100644 --- a/db-server/package.json +++ b/db-server/package.json @@ -33,7 +33,7 @@ "build": "tsc -p tsconfig.json", "typecheck": "tsc --noEmit", "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", - "test": "node --test src/*.test.js", + "test": "npm run build && node --test dist/*.test.js", "format": "prettier --write \"src/**/*.{ts,tsx,js,json}\"", "prepublishOnly": "npm run build" }, diff --git a/db-server/src/domain/economy.ts b/db-server/src/domain/economy.ts index 06b39c0..bcea780 100644 --- a/db-server/src/domain/economy.ts +++ b/db-server/src/domain/economy.ts @@ -14,17 +14,10 @@ import type { EconomyAccountRow, EconomyTransactionRow } from "@sfmc-bds/sdk/con import type { DatabaseSync } from "node:sqlite"; import type { SQLStatement } from "sql-template-strings"; import { isValidIdempotencyKey } from "../lib/idempotency.js"; +import { sql } from "../lib/sql-helpers.js"; import type { TxResult } from "./transaction.js"; export type { TxResult }; -/** - * 表名是信任常量,必须嵌入 SQL 文本;值走 ? 绑定。 - * 不可对 sql-template-strings 插值表名(会变成 FROM ? → near "?": syntax error)。 - */ -function sql(text: string, values: unknown[] = []): { sql: string; values: unknown[] } { - return { sql: text, values }; -} - const TABLE_ACCOUNTS = "sfmc_economy_accounts"; const TABLE_TRANSACTIONS = "sfmc_economy_transactions"; const TABLE_IDEMPOTENCY = "sfmc_economy_idempotency"; diff --git a/db-server/src/lib/sql-helpers.ts b/db-server/src/lib/sql-helpers.ts index 2223b64..658eab4 100644 --- a/db-server/src/lib/sql-helpers.ts +++ b/db-server/src/lib/sql-helpers.ts @@ -1,21 +1,35 @@ /** - * lib/sql-helpers.ts — sql-template-strings 缺失的工具函数 + * lib/sql-helpers.ts — SQL 构造辅助 * - * 该库没有暴露 raw(...) / join(...) 这类便捷函数。抽出一个轻量封装。 + * sql-template-strings 会把每一次 ${} 插值都变成 `?` 绑定值。 + * 表名/列名等信任标识符必须嵌入 SQL 文本,不能走绑定 —— 否则会出现 + * `FROM ?` → `near "?": syntax error`(见 economy 域修复)。 */ import { SQL, type SQLStatement } from "sql-template-strings"; +/** 参数化查询信封,与 createQuery / AnyQuery 的格式 2 对齐 */ +export type BoundSql = { sql: string; values: unknown[] }; + /** - * 把一段静态 SQL 文本包装成 SQLStatement,使其可以嵌入其它 SQL 模板。 - * - 不携带任何占位符 / values; - * - 与 SQL\`...\` 一起使用时行为一致:`append()` 调用会把字符串合并进 strings 数组。 + * 构造「标识符已嵌入、值走 ?」的查询。 + * 表名等信任常量用模板字符串嵌入;用户输入只放进 values。 * - * 实现注意:sql-template-strings 的 SQL 函数对 strings array 长度 = 1 + values 长度 - * 的情况友好处理 —— 我们传入 `["text"]` 单元素数组,得到一个无占位符、无参数的 - * SQLStatement,正好等价于把 `text` 整段直接插入。 + * @example + * sql(`SELECT * FROM ${TABLE} WHERE id = ?`, [id]) + */ +export function sql(text: string, values: unknown[] = []): BoundSql { + return { sql: text, values }; +} + +/** + * 把一段静态 SQL 文本包装成 SQLStatement,仅可与 `.append(...)` 合用。 + * + * 警告(LSP):切勿写成 `SQL\`... FROM ${raw(table)} ...\`` —— 模板插值仍会变成 `?`, + * 与直接插值表名同病。正确用法: + * SQL`SELECT * FROM `.append(raw(TABLE)).append(SQL` WHERE id = ${id}`) + * 纯字符串 `.append(TABLE)` 效果相同;优先用 {@link sql}。 */ export function raw(text: string): SQLStatement { return SQL([text]) as SQLStatement; } - diff --git a/db-server/src/runtime.test.ts b/db-server/src/runtime.test.ts index 1809337..41000df 100644 --- a/db-server/src/runtime.test.ts +++ b/db-server/src/runtime.test.ts @@ -1,7 +1,10 @@ -import { deepEqual, equal, throws } from "node:assert/strict"; +import { deepEqual, equal, rejects, throws } from "node:assert/strict"; import test from "node:test"; +import { SQL } from "sql-template-strings"; import { assertIdentifier, quoteIdentifier } from "./lib/identifiers.js"; import { parseNodeVersion } from "./lib/runtime.js"; +import { raw, sql } from "./lib/sql-helpers.js"; +import { DispatchError, ServiceRegistry } from "./service-registry.js"; test("parses Node versions", () => { deepEqual(parseNodeVersion("22.5.1"), { major: 22, minor: 5, patch: 1 }); @@ -12,3 +15,64 @@ test("accepts only safe SQL identifiers", () => { equal(quoteIdentifier("sfmc_players", "table"), '"sfmc_players"'); throws(() => assertIdentifier("users; DROP TABLE x"), /Invalid SQL/); }); + +test("sql() embeds trusted identifiers and binds values", () => { + const TABLE = "sfmc_economy_accounts"; + const q = sql(`SELECT * FROM ${TABLE} WHERE player_id = ?`, ["p1"]); + equal(q.sql, "SELECT * FROM sfmc_economy_accounts WHERE player_id = ?"); + deepEqual(q.values, ["p1"]); +}); + +test("raw() only works via append — template interpolation still becomes ?", () => { + const TABLE = "sfmc_economy_accounts"; + const broken = SQL`SELECT * FROM ${raw(TABLE)} WHERE id = ${"p1"}`; + equal(broken.sql, "SELECT * FROM ? WHERE id = ?"); + + const ok = SQL`SELECT * FROM `.append(raw(TABLE)).append(SQL` WHERE id = ${"p1"}`); + equal(ok.sql, "SELECT * FROM sfmc_economy_accounts WHERE id = ?"); + deepEqual(ok.values, ["p1"]); +}); + +test("ServiceRegistry: provider self-call skips requires (LSP with tx.call path)", async () => { + const reg = new ServiceRegistry(); + reg.registerHandler("feature-economy", "economy.stats.monthly", async () => ({ ok: true })); + + const enabled = new Map([ + [ + "feature-economy", + { + id: "feature-economy", + version: "1.0.0", + permissions: [] as string[], + services: { provides: [{ name: "economy.stats.monthly" }], requires: [] as Array<{ name: string }> }, + db: { tables: [] as unknown[] }, + config: { key: "economy" }, + } as unknown as import("./manifest-loader.js").ModuleManifestV2, + ], + ]); + + const out = await reg.dispatch(enabled, "feature-economy", "economy.stats.monthly", {}); + deepEqual(out, { ok: true, result: { ok: true } }); +}); + +test("ServiceRegistry: missing service → no_such_service (not not_in_requires)", async () => { + const reg = new ServiceRegistry(); + const enabled = new Map([ + [ + "feature-chat", + { + id: "feature-chat", + version: "1.0.0", + permissions: [] as string[], + services: { provides: [], requires: [] }, + db: { tables: [] }, + config: { key: "chat" }, + } as unknown as import("./manifest-loader.js").ModuleManifestV2, + ], + ]); + + await rejects( + () => reg.dispatch(enabled, "feature-chat", "economy.account.get", {}), + (e: unknown) => e instanceof DispatchError && e.code === "no_such_service" + ); +}); diff --git a/db-server/src/service-registry.ts b/db-server/src/service-registry.ts index 8ef79af..cccd18d 100644 --- a/db-server/src/service-registry.ts +++ b/db-server/src/service-registry.ts @@ -102,10 +102,6 @@ export class ServiceRegistry { } } - /** 查询 service 提供方 moduleId(供 tx.call 与 HTTP 共用鉴权策略) */ - getProvider(name: string): string | undefined { - return this.handlers.get(name)?.moduleId; - } } export class DispatchError extends Error { diff --git a/db-server/src/tx-runner.ts b/db-server/src/tx-runner.ts index 1497ca2..1227836 100644 --- a/db-server/src/tx-runner.ts +++ b/db-server/src/tx-runner.ts @@ -32,7 +32,7 @@ import type { SchemaRegistry } from "./schema-registry.js"; import type { WhereExpr } from "./where.js"; import { compile } from "./where.js"; import { log } from "./lib/log.js"; -import type { ServiceRegistry } from "./service-registry.js"; +import { DispatchError, type ServiceRegistry } from "./service-registry.js"; import type { ModuleManifestV2 } from "./manifest-loader.js"; import { PermissionDeniedError, @@ -124,7 +124,14 @@ export type TxError = { ok: false; step: number; error: string; - code: "permission_denied" | "forbidden" | "no_such_service" | "not_in_requires" | "internal" | "no_such_table"; + code: + | "permission_denied" + | "forbidden" + | "no_such_service" + | "not_in_requires" + | "domain_error" + | "internal" + | "no_such_table"; }; export interface TxRequest { @@ -171,11 +178,19 @@ export class TxRunner { const code: TxError["code"] = err instanceof PermissionDeniedError ? "permission_denied" - : (err as { code?: string }).code === "no_such_service" - ? "no_such_service" - : (err as { code?: string }).code === "not_in_requires" - ? "not_in_requires" - : "internal"; + : err instanceof DispatchError && + (err.code === "no_such_service" || + err.code === "not_in_requires" || + err.code === "forbidden" || + err.code === "domain_error") + ? err.code + : (err as { code?: string }).code === "no_such_service" + ? "no_such_service" + : (err as { code?: string }).code === "not_in_requires" + ? "not_in_requires" + : (err as { code?: string }).code === "no_such_table" + ? "no_such_table" + : "internal"; log.warn(`[tx ${traceId}] step=${i} failed: ${(err as Error).message}`); return { ok: false, step: i, error: (err as Error).message, code }; } @@ -382,14 +397,8 @@ export class TxRunner { mod: ModuleManifestV2, step: TxStepService ): Promise { - // 与 ServiceRegistry.dispatch 对齐:提供方自调用跳过 requires(DRY/LSP) - const providerId = this.deps.serviceRegistry.getProvider(step.name); - const declared = mod.services.requires.find((r) => r.name === step.name); - if (!declared && providerId !== mod.id) { - const err = new Error(`模块 ${mod.id} 未声明 service.requires ${step.name}`); - (err as { code?: string }).code = "not_in_requires"; - throw err; - } + // requires / 自调用豁免的唯一权威在 ServiceRegistry.dispatch(DRY/LSP); + // 此处只补 dispatch 不做的 service: 权限门(与 HTTP service-routes 对齐)。 assertModulePermission(mod.id, mod.permissions, Perm.service(step.name)); const result = await this.deps.serviceRegistry.dispatch( this.deps.enabled, diff --git a/docs/dev/npm-publish.md b/docs/dev/npm-publish.md index a47d564..29b1f18 100644 --- a/docs/dev/npm-publish.md +++ b/docs/dev/npm-publish.md @@ -49,7 +49,8 @@ npm publish --workspace @sfmc-bds/db-server --access public ## CI 自动发布 -推送符合格式的 git tag 即可触发 [`.github/workflows/npm-publish.yml`](../../.github/workflows/npm-publish.yml): +推送符合格式的 git tag 即可触发 [`.github/workflows/npm-publish.yml`](../../.github/workflows/npm-publish.yml)。 +可发包清单的唯一权威来源是 [`tools/lib/npm-publish-packages.mjs`](../../tools/lib/npm-publish-packages.mjs)(workflow 解析/校验都读它,勿在 yaml 再抄一份)。 ```bash # 1. 先在对应 package.json 里 bump version @@ -62,10 +63,12 @@ Tag 中的版本号必须与 `package.json` 的 `version` 字段一致(不含 ## 模块包(可选双通道) -业务模块在 `Tanya7z/sfmc-modules` 以 `@sfmc-bds/module-` 命名。可选同样用 tag 发布: +业务模块在 `Tanya7z/sfmc-modules` 以 `@sfmc-bds/module-` 命名,可本地 `npm publish`。 +当前 `npm-publish.yml` **只覆盖** `tools/lib/npm-publish-packages.mjs` 中的平台包;模块 tag 不会走该 CI(避免与平台包清单漂移)。 ```bash -git tag @sfmc-bds/module-land@v0.1.0 +# 模块包请在 sfmc-modules 仓或本地发布,勿假设 @sfmc-bds/module-*@v* 会触发本仓 workflow +npm publish --workspace @sfmc-bds/module-land --access public ``` 模块 `package.json` 应声明 `"@sfmc-bds/sdk": "^0.1.0"`。 diff --git a/modules/sdk/@sfmc-sdk/package.json b/modules/sdk/@sfmc-sdk/package.json index 1de34a9..6364c86 100644 --- a/modules/sdk/@sfmc-sdk/package.json +++ b/modules/sdk/@sfmc-sdk/package.json @@ -119,7 +119,7 @@ } }, "engines": { - "node": ">=22.5.0" + "node": ">=22.13.0" }, "publishConfig": { "access": "public", diff --git a/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts index aba0986..3abcf84 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/config/client.ts @@ -41,9 +41,7 @@ export function clearConfigModuleContext(): void { * 之前 config 漏带 query → 即便注入了 token 也会 401(LSP/DRY 违规)。 */ function withModuleId(path: string): string { - if (!_moduleId) return path; - const sep = path.includes("?") ? "&" : "?"; - return `${path}${sep}moduleId=${encodeURIComponent(_moduleId)}`; + return HttpDB.withModuleId(path, _moduleId); } async function ensureLoaded(): Promise { diff --git a/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts index b377e3b..1a335ed 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/db/client.ts @@ -52,9 +52,7 @@ export class DbError extends Error { } function withModuleId(path: string): string { - if (!_moduleId) return path; - const sep = path.includes("?") ? "&" : "?"; - return `${path}${sep}moduleId=${encodeURIComponent(_moduleId)}`; + return HttpDB.withModuleId(path, _moduleId); } async function post(path: string, body: unknown): Promise { diff --git a/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts b/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts index 11969cc..6a3930d 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts @@ -24,6 +24,16 @@ export class HttpDB { this.authToken = token.trim(); } + /** + * 给路径附上 ?moduleId= / &moduleId=(db/config/service 客户端共用,DRY)。 + * verifyModuleAuth 只认 query 上的 moduleId。 + */ + static withModuleId(path: string, moduleId: string): string { + if (!moduleId) return path; + const sep = path.includes("?") ? "&" : "?"; + return `${path}${sep}moduleId=${encodeURIComponent(moduleId)}`; + } + static isAvailable(): boolean { return this.available; } diff --git a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts index 9ceefa8..ee89468 100644 --- a/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts +++ b/modules/sdk/@sfmc-sdk/src/sapi/service/client.ts @@ -44,9 +44,7 @@ export class ServiceError extends Error { } function withModuleId(path: string): string { - if (!_moduleId) return path; - const sep = path.includes("?") ? "&" : "?"; - return `${path}${sep}moduleId=${encodeURIComponent(_moduleId)}`; + return HttpDB.withModuleId(path, _moduleId); } export const service = { diff --git a/package-lock.json b/package-lock.json index 0b65bf0..5710a43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,7 +89,7 @@ "version": "0.1.0", "license": "ISC", "engines": { - "node": ">=22.5.0" + "node": ">=22.13.0" }, "peerDependencies": { "@minecraft/server": ">=2.10.0-beta", diff --git a/tools/lib/npm-publish-packages.mjs b/tools/lib/npm-publish-packages.mjs new file mode 100644 index 0000000..028682c --- /dev/null +++ b/tools/lib/npm-publish-packages.mjs @@ -0,0 +1,21 @@ +/** + * npm-publish 可发包清单 — 唯一权威来源(DRY)。 + * workflow / docs / pack:verify 应对齐本表,勿在 yaml 里再抄一份 case/map。 + */ + +/** npm 包名 → 相对仓库根的 package.json 路径 */ +export const NPM_PUBLISH_PACKAGES = { + "@sfmc-bds/sdk": "modules/sdk/@sfmc-sdk/package.json", + "@sfmc-bds/cli": "sfmc/package.json", + "@sfmc-bds/db-server": "db-server/package.json", + "@sfmc-bds/qq-bridge": "qq-bridge/package.json", + "@sfmc-bds/bds-tools": "bds-tools/package.json", +}; + +/** @returns {keyof typeof NPM_PUBLISH_PACKAGES | null} */ +export function resolvePublishPackage(pkg) { + if (Object.prototype.hasOwnProperty.call(NPM_PUBLISH_PACKAGES, pkg)) { + return pkg; + } + return null; +} diff --git a/tools/resolve-npm-publish-tag.mjs b/tools/resolve-npm-publish-tag.mjs new file mode 100644 index 0000000..da5b73c --- /dev/null +++ b/tools/resolve-npm-publish-tag.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * 解析 npm-publish tag,写出 GitHub Actions outputs。 + * Tag 形如: @sfmc-bds/sdk@v0.1.0 + * + * 用法(workflow): + * node tools/resolve-npm-publish-tag.mjs + * 依赖 env: GITHUB_REF_NAME, GITHUB_OUTPUT + */ +import fs from "node:fs"; +import { NPM_PUBLISH_PACKAGES, resolvePublishPackage } from "./lib/npm-publish-packages.mjs"; + +const tag = process.env.GITHUB_REF_NAME || ""; +const outFile = process.env.GITHUB_OUTPUT; + +if (!tag) { + console.error("GITHUB_REF_NAME is empty"); + process.exit(1); +} + +const atV = tag.lastIndexOf("@v"); +if (atV <= 0) { + console.error(`Invalid npm publish tag (expect @scope/name@vX.Y.Z): ${tag}`); + process.exit(1); +} + +const pkg = tag.slice(0, atV); +const ver = tag.slice(atV + 2); // after "@v" +const resolved = resolvePublishPackage(pkg); +if (!resolved) { + console.error(`Unknown npm package tag: ${pkg}`); + console.error(`Known: ${Object.keys(NPM_PUBLISH_PACKAGES).join(", ")}`); + process.exit(1); +} + +const pkgPath = NPM_PUBLISH_PACKAGES[resolved]; +const actual = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version; +if (actual !== ver) { + console.error(`Tag version ${ver} != package.json version ${actual} (${pkgPath})`); + process.exit(1); +} + +const lines = [ + `tag=${tag}`, + `pkg=${resolved}`, + `workspace=${resolved}`, + `ver=${ver}`, + `pkg_path=${pkgPath}`, +]; + +if (outFile) { + fs.appendFileSync(outFile, lines.join("\n") + "\n"); +} + +console.log(`Version OK: ${resolved}@${ver} (${pkgPath})`);