From 6c87e1a1b2ebc693d841462a57a6f9fa9a044dfc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 18:12:05 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(eslint-plugin):=20=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E5=8F=AF=E8=BF=90=E8=A1=8C=E6=B5=8B=E8=AF=95=E4=B8=8E=E5=B9=B2?= =?UTF-8?q?=E5=87=80=E5=8F=91=E5=B8=83=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exclude *.test.ts 后 npm test 不再 emit dist/**/*.test.js; 新增 tsconfig.test.json,并从发布 build 排除仅测试用的 rule-tester。 RuleTester 改用 node:test 的 describe/it/after,避免嵌套用例被取消。 Co-authored-by: Shiroha --- modules/sdk/@sfmc-eslint-plugin/README.md | 1 + modules/sdk/@sfmc-eslint-plugin/package.json | 3 ++- .../@sfmc-eslint-plugin/src/utils/rule-tester.ts | 15 +++++++++------ modules/sdk/@sfmc-eslint-plugin/tsconfig.json | 8 +++++++- .../sdk/@sfmc-eslint-plugin/tsconfig.test.json | 4 ++++ 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 modules/sdk/@sfmc-eslint-plugin/tsconfig.test.json diff --git a/modules/sdk/@sfmc-eslint-plugin/README.md b/modules/sdk/@sfmc-eslint-plugin/README.md index af7e7c77..09359190 100644 --- a/modules/sdk/@sfmc-eslint-plugin/README.md +++ b/modules/sdk/@sfmc-eslint-plugin/README.md @@ -63,5 +63,6 @@ export default [ ```bash npm run build -w @sfmc-bds/eslint-plugin +# 发布 build 不含 *.test.ts / rule-tester;测试走独立 tsconfig npm run test -w @sfmc-bds/eslint-plugin ``` diff --git a/modules/sdk/@sfmc-eslint-plugin/package.json b/modules/sdk/@sfmc-eslint-plugin/package.json index abd5d9b9..f687b030 100644 --- a/modules/sdk/@sfmc-eslint-plugin/package.json +++ b/modules/sdk/@sfmc-eslint-plugin/package.json @@ -37,7 +37,8 @@ "scripts": { "build": "tsc7 -p tsconfig.json", "typecheck": "tsc7 --noEmit -p tsconfig.json", - "test": "npm run build && node --test dist/**/*.test.js", + "build:test": "tsc7 -p tsconfig.test.json", + "test": "npm run build:test && node --test \"dist/**/*.test.js\"", "prepublishOnly": "npm run build" }, "peerDependencies": { diff --git a/modules/sdk/@sfmc-eslint-plugin/src/utils/rule-tester.ts b/modules/sdk/@sfmc-eslint-plugin/src/utils/rule-tester.ts index 65006ca9..79376058 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/utils/rule-tester.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/utils/rule-tester.ts @@ -1,11 +1,14 @@ import { RuleTester } from "@typescript-eslint/rule-tester"; -import test from "node:test"; +import { after, describe, it } from "node:test"; -/** 统一 RuleTester 与 node:test 桥接,避免各规则测试重复样板(DRY) */ -RuleTester.afterAll = () => {}; -RuleTester.describe = test; -RuleTester.it = test; -RuleTester.itOnly = test.only; +/** + * 统一 RuleTester 与 node:test 桥接(DRY)。 + * 须使用 describe/it(而非顶层 test),否则嵌套用例会被 parent 提前取消。 + */ +RuleTester.afterAll = after; +RuleTester.describe = describe; +RuleTester.it = it; +RuleTester.itOnly = it.only; export function createRuleTester(): RuleTester { return new RuleTester({ diff --git a/modules/sdk/@sfmc-eslint-plugin/tsconfig.json b/modules/sdk/@sfmc-eslint-plugin/tsconfig.json index e0245408..4a3368c9 100644 --- a/modules/sdk/@sfmc-eslint-plugin/tsconfig.json +++ b/modules/sdk/@sfmc-eslint-plugin/tsconfig.json @@ -14,5 +14,11 @@ "noEmitOnError": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/**/*.test.ts"] + "exclude": [ + "node_modules", + "dist", + "src/**/*.test.ts", + "src/utils/rule-tester.ts" + ] } + diff --git a/modules/sdk/@sfmc-eslint-plugin/tsconfig.test.json b/modules/sdk/@sfmc-eslint-plugin/tsconfig.test.json new file mode 100644 index 00000000..a8a05124 --- /dev/null +++ b/modules/sdk/@sfmc-eslint-plugin/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist"] +} From 940c11a56d8cb7cb2e71731167e4399329388b6c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 18:12:05 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix(npm):=20pack:verify=20=E4=BB=8E=20NPM?= =?UTF-8?q?=5FPUBLISH=5FPACKAGES=20=E6=B4=BE=E7=94=9F=EF=BC=88DRY=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 注册 eslint-plugin 后根脚本仍手抄 npm pack 列表会漏包; 改为 tools/pack-verify.mjs 遍历权威清单,并补齐发布文档表格。 Co-authored-by: Shiroha --- docs/dev/npm-publish.md | 1 + package.json | 2 +- tools/pack-verify.mjs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tools/pack-verify.mjs diff --git a/docs/dev/npm-publish.md b/docs/dev/npm-publish.md index 4a38db35..5000877d 100644 --- a/docs/dev/npm-publish.md +++ b/docs/dev/npm-publish.md @@ -7,6 +7,7 @@ | npm 包 | 目录 | 说明 | |--------|------|------| | `@sfmc-bds/sdk` | `modules/sdk/@sfmc-sdk/` | 模块作者 SDK(SAPI + Node) | +| `@sfmc-bds/eslint-plugin` | `modules/sdk/@sfmc-eslint-plugin/` | SFMC 约定 ESLint 规则 | | `@sfmc-bds/cli` | `sfmc/` | 管理 CLI(`sfmc` 命令) | | `@sfmc-bds/db-server` | `db-server/` | SQLite HTTP REST 后端 | | `@sfmc-bds/qq-bridge` | `qq-bridge/` | QQ ↔ MC 桥接 | diff --git a/package.json b/package.json index cbfcf570..50b6e486 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "module-core:typecheck": "npm run sdk:typecheck", "module-core:test": "echo \"no sdk tests yet\"", "module-core:verify": "npm run sdk:verify", - "pack:verify": "npm run build --workspaces --if-present && npm pack -w @sfmc-bds/sdk && npm pack -w @sfmc-bds/cli && npm pack -w @sfmc-bds/db-server && npm pack -w @sfmc-bds/qq-bridge && npm pack -w @sfmc-bds/bds-tools && npm pack -w @sfmc-bds/tools && npm pack -w @sfmc-bds/sfmc", + "pack:verify": "node tools/pack-verify.mjs", "check-modules": "node tools/check-modules.mjs", "catalog-sync": "node tools/catalog-sync.mjs", "check-ootb": "node tools/check-ootb.mjs", diff --git a/tools/pack-verify.mjs b/tools/pack-verify.mjs new file mode 100644 index 00000000..f9eff1b8 --- /dev/null +++ b/tools/pack-verify.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/** + * 本地发布前 pack 冒烟 — 包清单唯一来源 NPM_PUBLISH_PACKAGES(DRY)。 + * 用法: node tools/pack-verify.mjs + */ +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NPM_PUBLISH_PACKAGES, + assertPublishPackageInWorkspaces, +} from "./lib/npm-publish-packages.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +function run(cmd, args) { + const r = spawnSync(cmd, args, { cwd: ROOT, stdio: "inherit", shell: false }); + if (r.error) throw r.error; + if (r.status !== 0) process.exit(r.status ?? 1); +} + +run("npm", ["run", "build", "--workspaces", "--if-present"]); + +for (const name of Object.keys(NPM_PUBLISH_PACKAGES)) { + assertPublishPackageInWorkspaces(name, ROOT); + console.log(`\n[pack:verify] npm pack -w ${name}`); + run("npm", ["pack", "-w", name]); +} + +console.log(`\n[pack:verify] ok — ${Object.keys(NPM_PUBLISH_PACKAGES).length} packages`); From c40f392f14d23766f9151db4c1f14a9a20d81ae7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 18:12:05 +0000 Subject: [PATCH 3/3] =?UTF-8?q?refactor(eslint-plugin):=20=E6=94=B6?= =?UTF-8?q?=E6=95=9B=20import=20=E8=AE=BF=E9=97=AE=E5=99=A8=E4=B8=8E=20SDK?= =?UTF-8?q?=20exports=EF=BC=88DRY/DIP=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽取 visitModuleSourceLiterals;SDK 公开子路径优先读 package.json#exports; plugin.meta.version 从本包 package.json 读取,避免双份版本。 Co-authored-by: Shiroha --- modules/sdk/@sfmc-eslint-plugin/src/index.ts | 8 ++- .../rules/no-cross-module-source-import.ts | 25 ++----- .../src/rules/no-platform-internal-import.ts | 27 ++++--- .../src/rules/no-sdk-deep-import.ts | 19 ++--- .../src/rules/no-sdk-private-export.ts | 21 ++---- .../src/rules/no-sfmc-sdk-alias.ts | 19 ++--- .../src/utils/module-source-visitor.ts | 30 ++++++++ .../src/utils/sdk-public-exports.ts | 72 +++++++++++++++++-- 8 files changed, 139 insertions(+), 82 deletions(-) create mode 100644 modules/sdk/@sfmc-eslint-plugin/src/utils/module-source-visitor.ts diff --git a/modules/sdk/@sfmc-eslint-plugin/src/index.ts b/modules/sdk/@sfmc-eslint-plugin/src/index.ts index 1a658539..adcd9155 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/index.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/index.ts @@ -1,4 +1,5 @@ import type { ESLint } from "eslint"; +import { createRequire } from "node:module"; import { allRules, createFlatConfig, @@ -6,14 +7,17 @@ import { rules, } from "./configs/recommended.js"; +const require = createRequire(import.meta.url); +const pkg = require("../package.json") as { name: string; version: string }; + /** * ESLint.Plugin.rules 期望核心 RuleDefinition,而 @typescript-eslint 的 RuleModule * 在 create/context 形状上更窄;运行时兼容,此处用断言对齐发布面契约(LSP 边界)。 */ const plugin = { meta: { - name: "@sfmc-bds/eslint-plugin", - version: "0.1.0", + name: pkg.name, + version: pkg.version, }, rules, } as unknown as ESLint.Plugin; diff --git a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-cross-module-source-import.ts b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-cross-module-source-import.ts index 90667adc..deb1b060 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-cross-module-source-import.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-cross-module-source-import.ts @@ -1,6 +1,7 @@ import path from "node:path"; import type { TSESTree } from "@typescript-eslint/utils"; import { createRule } from "../utils/create-rule.js"; +import { visitModuleSourceLiterals } from "../utils/module-source-visitor.js"; function toPosix(p: string): string { return p.replace(/\\/g, "/"); @@ -46,12 +47,12 @@ export const noCrossModuleSourceImport = createRule({ const filename = toPosix(context.filename); const own = packageRootOf(filename); - function check(source: string, node: TSESTree.Node): void { + return visitModuleSourceLiterals((source, sourceNode) => { const norm = toPosix(source); if (/^@sfmc-bds\/module-[a-z0-9-]+(?:\/|$)/.test(norm)) { if (/\/sapi\/src(?:\/|$)/.test(norm)) { - context.report({ node, messageId: "crossSource", data: { source } }); + context.report({ node: sourceNode, messageId: "crossSource", data: { source } }); } return; } @@ -65,7 +66,7 @@ export const noCrossModuleSourceImport = createRule({ } const other = packageRootOf(resolved); if (other && other !== own) { - context.report({ node, messageId: "crossSource", data: { source } }); + context.report({ node: sourceNode, messageId: "crossSource", data: { source } }); } return; } @@ -73,23 +74,9 @@ export const noCrossModuleSourceImport = createRule({ if (/\/(?:packages|modules\/packages)\/[^/]+\/sapi\/src(?:\/|$)/.test(norm)) { const hit = packageRootOf(norm); if (!own || (hit && hit !== own)) { - context.report({ node, messageId: "crossSource", data: { source } }); + context.report({ node: sourceNode, messageId: "crossSource", data: { source } }); } } - } - - function onSource( - node: TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration - ) { - const src = node.source; - if (!src || src.type !== "Literal" || typeof src.value !== "string") return; - check(src.value, src); - } - - return { - ImportDeclaration: onSource, - ExportNamedDeclaration: onSource, - ExportAllDeclaration: onSource, - }; + }); }, }); diff --git a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-platform-internal-import.ts b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-platform-internal-import.ts index e5b3e28e..cf82231b 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-platform-internal-import.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-platform-internal-import.ts @@ -1,7 +1,16 @@ import type { TSESTree } from "@typescript-eslint/utils"; import { createRule } from "../utils/create-rule.js"; +import { visitModuleSourceLiterals } from "../utils/module-source-visitor.js"; -const DEFAULT_PACKAGES = ["db-server", "qq-bridge", "bds-tools", "@sfmc-bds/db-server", "@sfmc-bds/qq-bridge", "@sfmc-bds/bds-tools", "@sfmc-bds/cli"]; +const DEFAULT_PACKAGES = [ + "db-server", + "qq-bridge", + "bds-tools", + "@sfmc-bds/db-server", + "@sfmc-bds/qq-bridge", + "@sfmc-bds/bds-tools", + "@sfmc-bds/cli", +]; const DEFAULT_FRAGMENTS = [ "/db-server/", "/qq-bridge/", @@ -73,18 +82,8 @@ export const noPlatformInternalImport = createRule({ } } - function onSource( - node: TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration - ) { - const src = node.source; - if (!src || src.type !== "Literal" || typeof src.value !== "string") return; - check(src.value, src); - } - - return { - ImportDeclaration: onSource, - ExportNamedDeclaration: onSource, - ExportAllDeclaration: onSource, - }; + return visitModuleSourceLiterals((source, sourceNode) => { + check(source, sourceNode); + }); }, }); diff --git a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-deep-import.ts b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-deep-import.ts index 7d09855f..5e86673d 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-deep-import.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-deep-import.ts @@ -1,5 +1,5 @@ -import type { TSESTree } from "@typescript-eslint/utils"; import { createRule } from "../utils/create-rule.js"; +import { visitModuleSourceLiterals } from "../utils/module-source-visitor.js"; /** * 禁止相对路径深挖 SDK 源码 @@ -19,25 +19,16 @@ export const noSdkDeepImport = createRule({ }, defaultOptions: [], create(context) { - function checkSource( - node: TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration - ) { - const src = node.source; - if (!src || src.type !== "Literal" || typeof src.value !== "string") return; - const v = src.value.replace(/\\/g, "/"); + return visitModuleSourceLiterals((source, sourceNode) => { + const v = source.replace(/\\/g, "/"); if ( v.includes("@sfmc-sdk/src") || v.includes("modules/sdk/") || /(?:^|\/)@sfmc-sdk(?:\/|$)/.test(v) ) { if (v.startsWith("@sfmc-bds/sdk")) return; - context.report({ node: src, messageId: "usePublic" }); + context.report({ node: sourceNode, messageId: "usePublic" }); } - } - return { - ImportDeclaration: checkSource, - ExportNamedDeclaration: checkSource, - ExportAllDeclaration: checkSource, - }; + }); }, }); diff --git a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-private-export.ts b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-private-export.ts index 2be3d616..63a6747f 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-private-export.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sdk-private-export.ts @@ -1,6 +1,6 @@ -import type { TSESTree } from "@typescript-eslint/utils"; import { createRule } from "../utils/create-rule.js"; import { checkSdkImportPath } from "../utils/sdk-public-exports.js"; +import { visitModuleSourceLiterals } from "../utils/module-source-visitor.js"; type Options = [{ extraAllowed?: string[] }]; @@ -32,21 +32,12 @@ export const noSdkPrivateExport = createRule({ }, defaultOptions: [{}], create(context, [options]) { - function check( - node: TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration - ) { - const src = node.source; - if (!src || src.type !== "Literal" || typeof src.value !== "string") return; - const kind = checkSdkImportPath(src.value, options.extraAllowed ?? []); - if (kind === "bare") context.report({ node: src, messageId: "bare" }); + return visitModuleSourceLiterals((source, sourceNode) => { + const kind = checkSdkImportPath(source, options.extraAllowed ?? []); + if (kind === "bare") context.report({ node: sourceNode, messageId: "bare" }); else if (kind === "private") { - context.report({ node: src, messageId: "private", data: { source: src.value } }); + context.report({ node: sourceNode, messageId: "private", data: { source } }); } - } - return { - ImportDeclaration: check, - ExportNamedDeclaration: check, - ExportAllDeclaration: check, - }; + }); }, }); diff --git a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sfmc-sdk-alias.ts b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sfmc-sdk-alias.ts index 30a6c62f..72ec1fad 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sfmc-sdk-alias.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/rules/no-sfmc-sdk-alias.ts @@ -1,5 +1,5 @@ -import type { TSESTree } from "@typescript-eslint/utils"; import { createRule } from "../utils/create-rule.js"; +import { visitModuleSourceLiterals } from "../utils/module-source-visitor.js"; /** * 禁止 @sfmc/sdk 别名,统一 @sfmc-bds/sdk @@ -19,19 +19,10 @@ export const noSfmcSdkAlias = createRule({ }, defaultOptions: [], create(context) { - function checkSource( - node: TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration - ) { - const src = node.source; - if (!src || src.type !== "Literal" || typeof src.value !== "string") return; - if (src.value === "@sfmc/sdk" || src.value.startsWith("@sfmc/sdk/")) { - context.report({ node: src, messageId: "useOfficial" }); + return visitModuleSourceLiterals((source, sourceNode) => { + if (source === "@sfmc/sdk" || source.startsWith("@sfmc/sdk/")) { + context.report({ node: sourceNode, messageId: "useOfficial" }); } - } - return { - ImportDeclaration: checkSource, - ExportNamedDeclaration: checkSource, - ExportAllDeclaration: checkSource, - }; + }); }, }); diff --git a/modules/sdk/@sfmc-eslint-plugin/src/utils/module-source-visitor.ts b/modules/sdk/@sfmc-eslint-plugin/src/utils/module-source-visitor.ts new file mode 100644 index 00000000..0965e3a2 --- /dev/null +++ b/modules/sdk/@sfmc-eslint-plugin/src/utils/module-source-visitor.ts @@ -0,0 +1,30 @@ +/** + * import/export 源字面量访问器 — 规则侧统一入口,避免重复样板(DRY)。 + */ +import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; + +export type ModuleSourceDeclaration = + | TSESTree.ImportDeclaration + | TSESTree.ExportNamedDeclaration + | TSESTree.ExportAllDeclaration; + +export type ModuleSourceLiteral = TSESTree.StringLiteral; + +/** + * 对 Import/Export* 的字符串 source 逐条回调。 + * @param check (source, sourceNode, declaration) + */ +export function visitModuleSourceLiterals( + check: (source: string, sourceNode: ModuleSourceLiteral, node: ModuleSourceDeclaration) => void +): TSESLint.RuleListener { + function onSource(node: ModuleSourceDeclaration): void { + const src = node.source; + if (!src || src.type !== "Literal" || typeof src.value !== "string") return; + check(src.value, src as ModuleSourceLiteral, node); + } + return { + ImportDeclaration: onSource, + ExportNamedDeclaration: onSource, + ExportAllDeclaration: onSource, + }; +} diff --git a/modules/sdk/@sfmc-eslint-plugin/src/utils/sdk-public-exports.ts b/modules/sdk/@sfmc-eslint-plugin/src/utils/sdk-public-exports.ts index 5c491583..69951c0e 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/utils/sdk-public-exports.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/utils/sdk-public-exports.ts @@ -1,8 +1,15 @@ /** - * 与 @sfmc-bds/sdk package.json#exports 同步的公开子路径白名单。 - * 更新 SDK exports 时请同步本列表。 + * @sfmc-bds/sdk 公开 exports 白名单。 + * 权威来源:sibling / 已安装 `@sfmc-bds/sdk` 的 package.json#exports(DIP/DRY); + * 读不到时回落到与当前 SDK 对齐的静态表。 */ -export const SDK_PUBLIC_EXPORTS = [ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** 与当前 SDK exports 对齐的回落表(无 SDK 包时使用) */ +export const SDK_PUBLIC_EXPORTS_FALLBACK = [ "contracts", "logs", "sapi/sdk", @@ -20,6 +27,63 @@ export const SDK_PUBLIC_EXPORTS = [ ] as const; const SDK_PREFIX = "@sfmc-bds/sdk"; +const require = createRequire(import.meta.url); + +let cachedExports: string[] | null = null; + +function parseExportsKeys(pkgPath: string): string[] | null { + try { + if (!fs.existsSync(pkgPath)) return null; + const raw = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { + exports?: Record; + }; + const keys = Object.keys(raw.exports ?? {}) + .map((k) => (k === "." ? null : k.replace(/^\.\//, ""))) + .filter((k): k is string => !!k && !k.includes("*")); + return keys.length > 0 ? keys : null; + } catch { + return null; + } +} + +/** + * 从 monorepo sibling 或已安装的 @sfmc-bds/sdk 解析 exports 子路径。 + * `schemas/*` 由 checkSdkImportPath 前缀放行,不进本列表。 + */ +export function loadSdkPublicExports(): readonly string[] { + if (cachedExports) return cachedExports; + + const here = path.dirname(fileURLToPath(import.meta.url)); + const candidates: string[] = [ + // dist/utils 或 src/utils → modules/sdk/@sfmc-sdk/package.json + path.resolve(here, "..", "..", "..", "@sfmc-sdk", "package.json"), + ]; + + try { + candidates.unshift(require.resolve("@sfmc-bds/sdk/package.json")); + } catch { + // 未安装 SDK 时忽略 + } + + for (const pkgPath of candidates) { + const keys = parseExportsKeys(pkgPath); + if (keys) { + cachedExports = keys; + return cachedExports; + } + } + + cachedExports = [...SDK_PUBLIC_EXPORTS_FALLBACK]; + return cachedExports; +} + +/** @deprecated 兼容旧名;请优先 loadSdkPublicExports() */ +export const SDK_PUBLIC_EXPORTS = SDK_PUBLIC_EXPORTS_FALLBACK; + +/** 测试用:清空 exports 缓存 */ +export function clearSdkPublicExportsCache(): void { + cachedExports = null; +} /** 检查 import 源是否为合法公开入口;非法返回错误原因,合法返回 null */ export function checkSdkImportPath( @@ -32,7 +96,7 @@ export function checkSdkImportPath( const sub = source.slice(SDK_PREFIX.length + 1); if (sub.startsWith("schemas/")) return null; // schemas/* 公开 - const allowed = new Set([...SDK_PUBLIC_EXPORTS, ...extraAllowed]); + const allowed = new Set([...loadSdkPublicExports(), ...extraAllowed]); if (allowed.has(sub)) return null; return "private"; }