@sfmc-bds/devkit 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @sfmc-bds/devkit
2
+
3
+ 模块作者工具核心:脚手架、监听重建、部署。供 VS Code/Cursor 扩展与脚本直接 import,**不**经 `sfmc` CLI。
4
+
5
+ ```ts
6
+ import { startModuleWatch, rebuildAndDeploy, scaffoldModule } from "@sfmc-bds/devkit";
7
+ ```
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @sfmc-bds/devkit — 模块作者工具核心(扩展 / CI 共用)
3
+ * Watch / scaffold 自洽;rebuild / 启停会解析并 spawn 已安装的 sfmc CLI(与工作目录无关)。
4
+ */
5
+ export { resolveLocalModuleRoot } from "./paths.js";
6
+ export { isValidModuleRoot, readModuleRootInfo, findModuleRootFromFile, type ModuleRootInfo, } from "./module-root.js";
7
+ export { isValidSfmcRoot } from "./sfmc-root.js";
8
+ export { resolveSfmcCli, type ResolveSfmcCliOptions } from "./sfmc-cli.js";
9
+ export { startModuleWatch, type ModuleWatchOptions } from "./watch.js";
10
+ export { scaffoldModule, type ScaffoldOptions } from "./scaffold.js";
11
+ export { rebuildAndDeploy, type RebuildOptions, type RebuildResult } from "./rebuild.js";
12
+ export { setModuleEnabled, type SetModuleEnabledOptions, type SetModuleEnabledResult, } from "./module-toggle.js";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @sfmc-bds/devkit — 模块作者工具核心(扩展 / CI 共用)
3
+ * Watch / scaffold 自洽;rebuild / 启停会解析并 spawn 已安装的 sfmc CLI(与工作目录无关)。
4
+ */
5
+ export { resolveLocalModuleRoot } from "./paths.js";
6
+ export { isValidModuleRoot, readModuleRootInfo, findModuleRootFromFile, } from "./module-root.js";
7
+ export { isValidSfmcRoot } from "./sfmc-root.js";
8
+ export { resolveSfmcCli } from "./sfmc-cli.js";
9
+ export { startModuleWatch } from "./watch.js";
10
+ export { scaffoldModule } from "./scaffold.js";
11
+ export { rebuildAndDeploy } from "./rebuild.js";
12
+ export { setModuleEnabled, } from "./module-toggle.js";
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/paths.ts", "../src/module-root.ts", "../src/sfmc-root.ts", "../src/sfmc-cli.ts", "../src/watch.ts", "../src/scaffold.ts", "../src/rebuild.ts", "../src/module-toggle.ts"],
4
+ "sourcesContent": ["import path from \"node:path\";\n\n/** \u89E3\u6790 `--from local[:path]` / `dir:` \u6216 cwd \u4E3A\u6A21\u5757\u6839\u3002 */\nexport function resolveLocalModuleRoot(args: { from?: string | null; cwd: string }): string {\n const raw = (args.from ?? \"\").trim();\n if (!raw) return path.resolve(args.cwd);\n if (raw === \"local\") return path.resolve(args.cwd);\n if (raw.startsWith(\"local:\")) {\n const p = raw.slice(\"local:\".length).trim();\n if (!p) return path.resolve(args.cwd);\n return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);\n }\n if (raw.startsWith(\"dir:\")) {\n const p = raw.slice(\"dir:\".length).trim();\n return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);\n }\n return path.resolve(args.cwd);\n}\n", "/**\n * \u6709\u6548\u6A21\u5757\u6839\u5224\u5B9A\uFF08\u6269\u5C55\u4E0E CLI \u5171\u7528\u6743\u5A01\uFF09\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport type ModuleRootInfo = {\n root: string;\n id: string;\n name?: string;\n schemaVersion: number;\n};\n\n/** \u76EE\u5F55\u662F\u5426\u4E3A\u6709\u6548 SFMC \u6A21\u5757\u6839\uFF1Apackage.json + sapi/manifest.json schemaVersion===2 + \u975E\u7A7A id */\nexport function isValidModuleRoot(dir: string): boolean {\n return readModuleRootInfo(dir) !== null;\n}\n\nexport function readModuleRootInfo(dir: string): ModuleRootInfo | null {\n const pkg = path.join(dir, \"package.json\");\n const manifestPath = path.join(dir, \"sapi\", \"manifest.json\");\n if (!fs.existsSync(pkg) || !fs.existsSync(manifestPath)) return null;\n try {\n const m = JSON.parse(fs.readFileSync(manifestPath, \"utf8\")) as {\n schemaVersion?: number;\n id?: string;\n name?: string;\n };\n if (m.schemaVersion !== 2) return null;\n if (typeof m.id !== \"string\" || !m.id.trim()) return null;\n return {\n root: path.resolve(dir),\n id: m.id.trim(),\n name: typeof m.name === \"string\" ? m.name : undefined,\n schemaVersion: 2,\n };\n } catch {\n return null;\n }\n}\n\n/** \u81EA filePath \u5411\u4E0A\u627E\u6700\u8FD1\u6709\u6548\u6A21\u5757\u6839\uFF1B\u627E\u4E0D\u5230\u8FD4\u56DE null */\nexport function findModuleRootFromFile(filePath: string): string | null {\n let cur = path.dirname(path.resolve(filePath));\n const root = path.parse(cur).root;\n while (true) {\n if (isValidModuleRoot(cur)) return cur;\n if (cur === root) return null;\n const parent = path.dirname(cur);\n if (parent === cur) return null;\n cur = parent;\n }\n}\n", "/**\n * SFMC \u5DE5\u4F5C\u76EE\u5F55\u5224\u5B9A\uFF08\u8FD0\u884C\u65F6\u6839\uFF1Aconfigs/\u3001modules/\uFF1B\u4E0D\u662F\u6E90\u7801\u4ED3\u5E93\u6839\uFF09\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** \u76EE\u5F55\u662F\u5426\u4E3A\u6709\u6548 SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B configs/ \u4E0E modules/\uFF09\u3002 */\nexport function isValidSfmcRoot(dir: string): boolean {\n if (!dir || !dir.trim()) return false;\n const root = path.resolve(dir);\n try {\n return (\n fs.statSync(path.join(root, \"configs\")).isDirectory() &&\n fs.statSync(path.join(root, \"modules\")).isDirectory()\n );\n } catch {\n return false;\n }\n}\n", "/**\n * \u89E3\u6790\u5E76 spawn sfmc CLI\u3002\n * CLI \u4E0E SFMC \u5DE5\u4F5C\u76EE\u5F55\u65E0\u5173\uFF1A\u4ECE\u672C\u5305/\u6269\u5C55\u5B89\u88C5\u6811\u3001PATH \u6216\u663E\u5F0F\u914D\u7F6E\u89E3\u6790\uFF1B\n * \u5DE5\u4F5C\u76EE\u5F55\u53EA\u4F5C\u4E3A spawn \u7684 cwd \u4E0E SFMC_ROOT\u3002\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isValidSfmcRoot } from \"./sfmc-root.js\";\n\n/** chalk.hex(\"#e06c75\") \u7684\u771F\u5F69\u5E8F\u5217\uFF1BCLI \u5931\u8D25\u6587\u6848\u7ECF c.red() \u5305\u88C5\u3002 */\nconst CLI_ANSI_RED = \"\\u001b[38;2;224;108;117m\";\n\nexport interface ResolveSfmcCliOptions {\n /** \u663E\u5F0F CLI \u5165\u53E3\uFF08.js/.mjs\uFF09\uFF1B\u4E5F\u53EF\u7528\u73AF\u5883\u53D8\u91CF SFMC_CLI\u3002 */\n cliPath?: string;\n}\n\nfunction tryRequireCli(from: string): string | null {\n try {\n const resolved = createRequire(from).resolve(\"@sfmc-bds/cli/cli\");\n return fs.existsSync(resolved) ? resolved : null;\n } catch {\n return null;\n }\n}\n\n/** \u81EA PATH \u4E0A\u7684 sfmc \u53EF\u6267\u884C\u6587\u4EF6\u65C1\u89E3\u6790 @sfmc-bds/cli\uFF08npm global / \u672C\u5730 .bin\uFF09\u3002 */\nfunction resolveCliFromPathEnv(): string | null {\n const pathEnv = process.env.PATH ?? \"\";\n const names =\n process.platform === \"win32\" ? [\"sfmc.cmd\", \"sfmc.exe\", \"sfmc.ps1\", \"sfmc\"] : [\"sfmc\"];\n for (const dir of pathEnv.split(path.delimiter)) {\n if (!dir) continue;\n let hit = false;\n for (const name of names) {\n if (fs.existsSync(path.join(dir, name))) {\n hit = true;\n break;\n }\n }\n if (!hit) continue;\n const nearCli = path.join(dir, \"..\", \"@sfmc-bds\", \"cli\", \"dist\", \"main.js\");\n if (fs.existsSync(nearCli)) return nearCli;\n const nearMeta = path.join(dir, \"..\", \"@sfmc-bds\", \"sfmc\", \"bin\", \"sfmc.mjs\");\n if (fs.existsSync(nearMeta)) return nearMeta;\n const fromBin = tryRequireCli(path.join(dir, \"..\", \"package.json\"));\n if (fromBin) return fromBin;\n }\n return null;\n}\n\n/**\n * \u89E3\u6790 sfmc CLI \u811A\u672C\u8DEF\u5F84\u3002\n * \u987A\u5E8F\uFF1A\u663E\u5F0F cliPath / SFMC_CLI \u2192 \u672C\u5305\u5B89\u88C5\u6811\uFF08@sfmc-bds/cli\uFF09\u2192 cwd \u7684 node_modules \u2192 PATH\u3002\n */\nexport function resolveSfmcCli(opts: ResolveSfmcCliOptions = {}): string {\n const explicit = (opts.cliPath || process.env.SFMC_CLI || \"\").trim();\n if (explicit) {\n const p = path.resolve(explicit);\n if (!fs.existsSync(p)) {\n throw new Error(`\u6307\u5B9A\u7684 sfmc CLI \u4E0D\u5B58\u5728\uFF1A${p}\uFF08\u68C0\u67E5 SFMC_CLI / sfmc.cliPath\uFF09`);\n }\n return p;\n }\n\n const here = fileURLToPath(import.meta.url);\n const fromInstall = tryRequireCli(here);\n if (fromInstall) return fromInstall;\n\n const fromCwd = tryRequireCli(path.join(process.cwd(), \"package.json\"));\n if (fromCwd) return fromCwd;\n\n const fromPath = resolveCliFromPathEnv();\n if (fromPath) return fromPath;\n\n throw new Error(\n \"\u65E0\u6CD5\u89E3\u6790 sfmc CLI\u3002\u8BF7\u5B89\u88C5 @sfmc-bds/cli\uFF08\u6216\u786E\u4FDD PATH \u4E0A\u6709 sfmc\uFF09\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF SFMC_CLI / \u6269\u5C55\u914D\u7F6E sfmc.cliPath\u3002\"\n );\n}\n\nfunction looksLikeCliFailure(output: string): boolean {\n if (output.includes(CLI_ANSI_RED)) return true;\n if (/\\[pack-manager\\].*required/.test(output)) return true;\n if (/^Error:/m.test(output)) return true;\n return false;\n}\n\nexport interface RunSfmcCliResult {\n ok: boolean;\n output: string;\n}\n\nexport interface RunSfmcCliOptions {\n cliPath?: string;\n}\n\n/** \u5728 SFMC \u5DE5\u4F5C\u76EE\u5F55\u4E0B\u6267\u884C `node <sfmc-cli> ...args`\uFF08env.SFMC_ROOT = \u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002 */\nexport async function runSfmcCli(\n sfmcRoot: string,\n args: string[],\n opts: RunSfmcCliOptions = {}\n): Promise<RunSfmcCliResult> {\n const root = path.resolve(sfmcRoot);\n if (!isValidSfmcRoot(root)) {\n return {\n ok: false,\n output: `\u65E0\u6548\u7684 SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF1A${root}\uFF08\u9700\u8981\u542B configs/ \u4E0E modules/\uFF0C\u4E0D\u5FC5\u542B\u6E90\u7801\u4ED3\u5E93\uFF09`,\n };\n }\n\n let script: string;\n try {\n script = resolveSfmcCli({ cliPath: opts.cliPath });\n } catch (e) {\n return { ok: false, output: e instanceof Error ? e.message : String(e) };\n }\n\n const r = await new Promise<{ code: number | null; output: string }>((resolve) => {\n const proc = spawn(process.execPath, [script, ...args], {\n cwd: root,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: { ...process.env, SFMC_ROOT: root },\n });\n let out = \"\";\n proc.stdout?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.stderr?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.on(\"exit\", (code) => resolve({ code, output: out }));\n proc.on(\"error\", (e) => resolve({ code: 1, output: e.message }));\n });\n\n const ok = r.code === 0 && !looksLikeCliFailure(r.output);\n return { ok, output: r.output };\n}\n", "/**\n * \u76D1\u542C\u6A21\u5757 sapi/src\uFF0C\u9632\u6296\u540E\u56DE\u8C03 onRebuild\uFF08\u7531\u8C03\u7528\u65B9\u6CE8\u5165 rebuildAndDeploy / \u5176\u5B83\uFF09\u3002\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface ModuleWatchOptions {\n moduleRoot: string;\n debounceMs?: number;\n log?: (line: string) => void;\n onRebuild: () => Promise<{ ok: boolean; message?: string }>;\n}\n\nexport function startModuleWatch(opts: ModuleWatchOptions): { stop: () => void } {\n const log = opts.log ?? ((line: string) => console.log(line));\n const debounceMs = opts.debounceMs ?? 200;\n const sapiDir = path.join(opts.moduleRoot, \"sapi\");\n const srcDir = path.join(sapiDir, \"src\");\n\n if (!fs.existsSync(sapiDir)) {\n log(`[devkit:watch] \u65E0 sapi \u76EE\u5F55: ${sapiDir}`);\n return { stop: () => {} };\n }\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let pending = new Set<string>();\n let running = false;\n let stopped = false;\n\n log(`[devkit:watch] \u76D1\u542C ${opts.moduleRoot}`);\n\n const runOnce = async () => {\n if (running || stopped) return;\n running = true;\n const changed = [...pending];\n pending = new Set();\n const t0 = Date.now();\n log(`[devkit:watch] rebuild (${changed.length})\u2026`);\n try {\n const r = await opts.onRebuild();\n const ms = Date.now() - t0;\n if (r.ok) log(`[devkit:watch] ok ${ms}ms`);\n else log(`[devkit:watch] fail ${ms}ms ${r.message ?? \"\"}`);\n } catch (e) {\n log(`[devkit:watch] error ${(e as Error).message}`);\n } finally {\n running = false;\n }\n };\n\n const onSrc = (rel: string) => {\n if (stopped) return;\n if (rel.includes(\"node_modules\") || rel.includes(\"dist\") || rel.endsWith(\".tgz\")) return;\n pending.add(rel);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => void runOnce(), debounceMs);\n };\n\n const watchers: fs.FSWatcher[] = [];\n if (fs.existsSync(srcDir)) {\n try {\n watchers.push(\n fs.watch(srcDir, { recursive: true, persistent: true }, (_e, filename) => {\n if (filename) onSrc(filename.toString());\n })\n );\n } catch {\n watchers.push(\n fs.watch(srcDir, { persistent: true }, (_e, filename) => {\n if (filename) onSrc(filename.toString());\n })\n );\n }\n }\n\n const watchMeta = (file: string, kind: string) => {\n if (!fs.existsSync(file)) return;\n watchers.push(\n fs.watch(file, { persistent: true }, (event) => {\n if (event === \"change\") log(`[devkit:watch] ${kind} \u5DF2\u6539\uFF0C\u9700\u91CD\u542F BDS\uFF08SAPI \u542F\u52A8\u671F\u7F13\u5B58\uFF09`);\n })\n );\n };\n watchMeta(path.join(sapiDir, \"manifest.json\"), \"manifest\");\n watchMeta(path.join(sapiDir, \"tsconfig.json\"), \"tsconfig\");\n\n return {\n stop() {\n stopped = true;\n if (debounceTimer) clearTimeout(debounceTimer);\n for (const w of watchers) {\n try {\n w.close();\n } catch {\n /* ignore */\n }\n }\n log(\"[devkit:watch] stopped\");\n },\n };\n}\n", "/**\n * \u811A\u624B\u67B6\uFF1A\u59D4\u6258 @sfmc-bds/tools/new-module\uFF08\u6216 monorepo packages/tools/new-module.mjs\uFF09\u3002\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { findMonorepoRoot } from \"@sfmc-bds/sdk/node/config\";\n\nexport interface ScaffoldOptions {\n /** \u76EE\u6807\u76EE\u5F55\uFF08\u987B\u4E3A\u7A7A\u6216\u53EF\u521B\u5EFA\uFF09\u3002 */\n targetDir: string;\n moduleId: string;\n displayName?: string;\n official?: boolean;\n}\n\nfunction resolveNewModuleScript(): string {\n const require = createRequire(import.meta.url);\n try {\n return require.resolve(\"@sfmc-bds/tools/new-module.mjs\");\n } catch {\n /* monorepo \u56DE\u9000 */\n }\n const monoRoot = findMonorepoRoot(process.cwd());\n const mono = monoRoot\n ? path.join(monoRoot, \"packages\", \"tools\", \"new-module.mjs\")\n : path.resolve(process.cwd(), \"packages/tools/new-module.mjs\");\n if (fs.existsSync(mono)) return mono;\n throw new Error(\"\u65E0\u6CD5\u89E3\u6790 new-module.mjs\uFF08\u5B89\u88C5 @sfmc-bds/tools \u6216\u5728 monorepo \u6839\u8FD0\u884C\uFF09\");\n}\n\nexport async function scaffoldModule(opts: ScaffoldOptions): Promise<{ ok: boolean; message: string }> {\n const dir = path.resolve(opts.targetDir);\n fs.mkdirSync(dir, { recursive: true });\n const script = resolveNewModuleScript();\n const args = [script, opts.moduleId];\n if (opts.displayName) args.push(\"--name\", opts.displayName);\n if (opts.official) args.push(\"--official\");\n\n return new Promise((resolve) => {\n const proc = spawn(process.execPath, args, {\n cwd: dir,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n env: process.env,\n });\n let out = \"\";\n proc.stdout?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.stderr?.on(\"data\", (d) => {\n out += d.toString();\n });\n proc.on(\"exit\", (code) => {\n resolve({\n ok: code === 0,\n message: out.trim() || (code === 0 ? `scaffolded ${opts.moduleId}` : `exit ${code}`),\n });\n });\n proc.on(\"error\", (e) => resolve({ ok: false, message: e.message }));\n });\n}\n", "/**\n * \u7EC4\u88C5\u5E76\u90E8\u7F72\u884C\u4E3A\u5305\uFF1A\u5728 SFMC_ROOT \u4E0B spawn `sfmc mod reload`\n * \uFF08\u4E0E CLI \u540C\u4E00\u5165\u53E3\uFF0C\u5B8C\u6574\u4F20\u53C2\uFF1B\u52FF\u76F4\u63A5\u88F8\u8C03 pack-manager verb\uFF09\u3002\n */\n\nimport { runSfmcCli } from \"./sfmc-cli.js\";\n\nexport interface RebuildOptions {\n /** SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B configs/\u3001modules/\uFF09\u3002 */\n sfmcRoot: string;\n /** \u4EC5\u6784\u5EFA\u90E8\u7F72\uFF0C\u4E0D\u5411 BDS \u53D1 reload\uFF08\u5BF9\u5E94 `mod reload --build-only`\uFF09\u3002 */\n buildOnly?: boolean;\n /** \u53EF\u9009\uFF1A\u663E\u5F0F sfmc CLI \u5165\u53E3\uFF1B\u9ED8\u8BA4\u7531\u5B89\u88C5\u6811 / PATH / SFMC_CLI \u89E3\u6790\u3002 */\n cliPath?: string;\n}\n\nexport interface RebuildResult {\n ok: boolean;\n output: string;\n}\n\nexport async function rebuildAndDeploy(opts: RebuildOptions): Promise<RebuildResult> {\n const args = [\"mod\", \"reload\"];\n if (opts.buildOnly) args.push(\"--build-only\");\n return runSfmcCli(opts.sfmcRoot, args, { cliPath: opts.cliPath });\n}\n", "/**\n * \u6A21\u5757\u542F\u505C\uFF1Aspawn `sfmc mod enable|disable`\uFF08\u5199 module-lock + best-effort \u70ED\u540C\u6B65\uFF09\u3002\n */\n\nimport { runSfmcCli, type RunSfmcCliResult } from \"./sfmc-cli.js\";\n\nexport interface SetModuleEnabledOptions {\n /** SFMC \u5DE5\u4F5C\u76EE\u5F55\uFF08\u542B modules/module-lock.json\uFF09\u3002 */\n sfmcRoot: string;\n /** \u6A21\u5757 logicalId / folderId / configKey\uFF08\u4E0E CLI `mod enable` \u4E00\u81F4\uFF09\u3002 */\n moduleId: string;\n enabled: boolean;\n /** \u53EF\u9009\uFF1A\u663E\u5F0F sfmc CLI \u5165\u53E3\uFF1B\u9ED8\u8BA4\u7531\u5B89\u88C5\u6811 / PATH / SFMC_CLI \u89E3\u6790\u3002 */\n cliPath?: string;\n}\n\nexport type SetModuleEnabledResult = RunSfmcCliResult;\n\n/** \u542F\u7528\u6216\u5173\u95ED\u6A21\u5757\uFF08\u4E0E CLI `mod enable|disable` \u540C\u6E90\uFF09\u3002 */\nexport async function setModuleEnabled(opts: SetModuleEnabledOptions): Promise<SetModuleEnabledResult> {\n const id = String(opts.moduleId || \"\").trim();\n if (!id) {\n return { ok: false, output: \"moduleId \u4E3A\u7A7A\" };\n }\n const action = opts.enabled ? \"enable\" : \"disable\";\n return runSfmcCli(opts.sfmcRoot, [\"mod\", action, id], { cliPath: opts.cliPath });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["path", "fs", "path", "fs", "path", "fs", "path", "fs", "path", "spawn", "createRequire", "fs", "path", "require"]
7
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 有效模块根判定(扩展与 CLI 共用权威)
3
+ */
4
+ export type ModuleRootInfo = {
5
+ root: string;
6
+ id: string;
7
+ name?: string;
8
+ schemaVersion: number;
9
+ };
10
+ /** 目录是否为有效 SFMC 模块根:package.json + sapi/manifest.json schemaVersion===2 + 非空 id */
11
+ export declare function isValidModuleRoot(dir: string): boolean;
12
+ export declare function readModuleRootInfo(dir: string): ModuleRootInfo | null;
13
+ /** 自 filePath 向上找最近有效模块根;找不到返回 null */
14
+ export declare function findModuleRootFromFile(filePath: string): string | null;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 有效模块根判定(扩展与 CLI 共用权威)
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ /** 目录是否为有效 SFMC 模块根:package.json + sapi/manifest.json schemaVersion===2 + 非空 id */
7
+ export function isValidModuleRoot(dir) {
8
+ return readModuleRootInfo(dir) !== null;
9
+ }
10
+ export function readModuleRootInfo(dir) {
11
+ const pkg = path.join(dir, "package.json");
12
+ const manifestPath = path.join(dir, "sapi", "manifest.json");
13
+ if (!fs.existsSync(pkg) || !fs.existsSync(manifestPath))
14
+ return null;
15
+ try {
16
+ const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
17
+ if (m.schemaVersion !== 2)
18
+ return null;
19
+ if (typeof m.id !== "string" || !m.id.trim())
20
+ return null;
21
+ return {
22
+ root: path.resolve(dir),
23
+ id: m.id.trim(),
24
+ name: typeof m.name === "string" ? m.name : undefined,
25
+ schemaVersion: 2,
26
+ };
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ /** 自 filePath 向上找最近有效模块根;找不到返回 null */
33
+ export function findModuleRootFromFile(filePath) {
34
+ let cur = path.dirname(path.resolve(filePath));
35
+ const root = path.parse(cur).root;
36
+ while (true) {
37
+ if (isValidModuleRoot(cur))
38
+ return cur;
39
+ if (cur === root)
40
+ return null;
41
+ const parent = path.dirname(cur);
42
+ if (parent === cur)
43
+ return null;
44
+ cur = parent;
45
+ }
46
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 模块启停:spawn `sfmc mod enable|disable`(写 module-lock + best-effort 热同步)。
3
+ */
4
+ import { type RunSfmcCliResult } from "./sfmc-cli.js";
5
+ export interface SetModuleEnabledOptions {
6
+ /** SFMC 工作目录(含 modules/module-lock.json)。 */
7
+ sfmcRoot: string;
8
+ /** 模块 logicalId / folderId / configKey(与 CLI `mod enable` 一致)。 */
9
+ moduleId: string;
10
+ enabled: boolean;
11
+ /** 可选:显式 sfmc CLI 入口;默认由安装树 / PATH / SFMC_CLI 解析。 */
12
+ cliPath?: string;
13
+ }
14
+ export type SetModuleEnabledResult = RunSfmcCliResult;
15
+ /** 启用或关闭模块(与 CLI `mod enable|disable` 同源)。 */
16
+ export declare function setModuleEnabled(opts: SetModuleEnabledOptions): Promise<SetModuleEnabledResult>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 模块启停:spawn `sfmc mod enable|disable`(写 module-lock + best-effort 热同步)。
3
+ */
4
+ import { runSfmcCli } from "./sfmc-cli.js";
5
+ /** 启用或关闭模块(与 CLI `mod enable|disable` 同源)。 */
6
+ export async function setModuleEnabled(opts) {
7
+ const id = String(opts.moduleId || "").trim();
8
+ if (!id) {
9
+ return { ok: false, output: "moduleId 为空" };
10
+ }
11
+ const action = opts.enabled ? "enable" : "disable";
12
+ return runSfmcCli(opts.sfmcRoot, ["mod", action, id], { cliPath: opts.cliPath });
13
+ }
@@ -0,0 +1,5 @@
1
+ /** 解析 `--from local[:path]` / `dir:` 或 cwd 为模块根。 */
2
+ export declare function resolveLocalModuleRoot(args: {
3
+ from?: string | null;
4
+ cwd: string;
5
+ }): string;
package/dist/paths.js ADDED
@@ -0,0 +1,20 @@
1
+ import path from "node:path";
2
+ /** 解析 `--from local[:path]` / `dir:` 或 cwd 为模块根。 */
3
+ export function resolveLocalModuleRoot(args) {
4
+ const raw = (args.from ?? "").trim();
5
+ if (!raw)
6
+ return path.resolve(args.cwd);
7
+ if (raw === "local")
8
+ return path.resolve(args.cwd);
9
+ if (raw.startsWith("local:")) {
10
+ const p = raw.slice("local:".length).trim();
11
+ if (!p)
12
+ return path.resolve(args.cwd);
13
+ return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);
14
+ }
15
+ if (raw.startsWith("dir:")) {
16
+ const p = raw.slice("dir:".length).trim();
17
+ return path.isAbsolute(p) ? path.resolve(p) : path.resolve(args.cwd, p);
18
+ }
19
+ return path.resolve(args.cwd);
20
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 组装并部署行为包:在 SFMC_ROOT 下 spawn `sfmc mod reload`
3
+ * (与 CLI 同一入口,完整传参;勿直接裸调 pack-manager verb)。
4
+ */
5
+ export interface RebuildOptions {
6
+ /** SFMC 工作目录(含 configs/、modules/)。 */
7
+ sfmcRoot: string;
8
+ /** 仅构建部署,不向 BDS 发 reload(对应 `mod reload --build-only`)。 */
9
+ buildOnly?: boolean;
10
+ /** 可选:显式 sfmc CLI 入口;默认由安装树 / PATH / SFMC_CLI 解析。 */
11
+ cliPath?: string;
12
+ }
13
+ export interface RebuildResult {
14
+ ok: boolean;
15
+ output: string;
16
+ }
17
+ export declare function rebuildAndDeploy(opts: RebuildOptions): Promise<RebuildResult>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 组装并部署行为包:在 SFMC_ROOT 下 spawn `sfmc mod reload`
3
+ * (与 CLI 同一入口,完整传参;勿直接裸调 pack-manager verb)。
4
+ */
5
+ import { runSfmcCli } from "./sfmc-cli.js";
6
+ export async function rebuildAndDeploy(opts) {
7
+ const args = ["mod", "reload"];
8
+ if (opts.buildOnly)
9
+ args.push("--build-only");
10
+ return runSfmcCli(opts.sfmcRoot, args, { cliPath: opts.cliPath });
11
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 脚手架:委托 @sfmc-bds/tools/new-module(或 monorepo packages/tools/new-module.mjs)。
3
+ */
4
+ export interface ScaffoldOptions {
5
+ /** 目标目录(须为空或可创建)。 */
6
+ targetDir: string;
7
+ moduleId: string;
8
+ displayName?: string;
9
+ official?: boolean;
10
+ }
11
+ export declare function scaffoldModule(opts: ScaffoldOptions): Promise<{
12
+ ok: boolean;
13
+ message: string;
14
+ }>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 脚手架:委托 @sfmc-bds/tools/new-module(或 monorepo packages/tools/new-module.mjs)。
3
+ */
4
+ import { spawn } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { findMonorepoRoot } from "@sfmc-bds/sdk/node/config";
9
+ function resolveNewModuleScript() {
10
+ const require = createRequire(import.meta.url);
11
+ try {
12
+ return require.resolve("@sfmc-bds/tools/new-module.mjs");
13
+ }
14
+ catch {
15
+ /* monorepo 回退 */
16
+ }
17
+ const monoRoot = findMonorepoRoot(process.cwd());
18
+ const mono = monoRoot
19
+ ? path.join(monoRoot, "packages", "tools", "new-module.mjs")
20
+ : path.resolve(process.cwd(), "packages/tools/new-module.mjs");
21
+ if (fs.existsSync(mono))
22
+ return mono;
23
+ throw new Error("无法解析 new-module.mjs(安装 @sfmc-bds/tools 或在 monorepo 根运行)");
24
+ }
25
+ export async function scaffoldModule(opts) {
26
+ const dir = path.resolve(opts.targetDir);
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ const script = resolveNewModuleScript();
29
+ const args = [script, opts.moduleId];
30
+ if (opts.displayName)
31
+ args.push("--name", opts.displayName);
32
+ if (opts.official)
33
+ args.push("--official");
34
+ return new Promise((resolve) => {
35
+ const proc = spawn(process.execPath, args, {
36
+ cwd: dir,
37
+ stdio: ["ignore", "pipe", "pipe"],
38
+ env: process.env,
39
+ });
40
+ let out = "";
41
+ proc.stdout?.on("data", (d) => {
42
+ out += d.toString();
43
+ });
44
+ proc.stderr?.on("data", (d) => {
45
+ out += d.toString();
46
+ });
47
+ proc.on("exit", (code) => {
48
+ resolve({
49
+ ok: code === 0,
50
+ message: out.trim() || (code === 0 ? `scaffolded ${opts.moduleId}` : `exit ${code}`),
51
+ });
52
+ });
53
+ proc.on("error", (e) => resolve({ ok: false, message: e.message }));
54
+ });
55
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 解析并 spawn sfmc CLI。
3
+ * CLI 与 SFMC 工作目录无关:从本包/扩展安装树、PATH 或显式配置解析;
4
+ * 工作目录只作为 spawn 的 cwd 与 SFMC_ROOT。
5
+ */
6
+ export interface ResolveSfmcCliOptions {
7
+ /** 显式 CLI 入口(.js/.mjs);也可用环境变量 SFMC_CLI。 */
8
+ cliPath?: string;
9
+ }
10
+ /**
11
+ * 解析 sfmc CLI 脚本路径。
12
+ * 顺序:显式 cliPath / SFMC_CLI → 本包安装树(@sfmc-bds/cli)→ cwd 的 node_modules → PATH。
13
+ */
14
+ export declare function resolveSfmcCli(opts?: ResolveSfmcCliOptions): string;
15
+ export interface RunSfmcCliResult {
16
+ ok: boolean;
17
+ output: string;
18
+ }
19
+ export interface RunSfmcCliOptions {
20
+ cliPath?: string;
21
+ }
22
+ /** 在 SFMC 工作目录下执行 `node <sfmc-cli> ...args`(env.SFMC_ROOT = 工作目录)。 */
23
+ export declare function runSfmcCli(sfmcRoot: string, args: string[], opts?: RunSfmcCliOptions): Promise<RunSfmcCliResult>;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * 解析并 spawn sfmc CLI。
3
+ * CLI 与 SFMC 工作目录无关:从本包/扩展安装树、PATH 或显式配置解析;
4
+ * 工作目录只作为 spawn 的 cwd 与 SFMC_ROOT。
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { createRequire } from "node:module";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { isValidSfmcRoot } from "./sfmc-root.js";
12
+ /** chalk.hex("#e06c75") 的真彩序列;CLI 失败文案经 c.red() 包装。 */
13
+ const CLI_ANSI_RED = "\u001b[38;2;224;108;117m";
14
+ function tryRequireCli(from) {
15
+ try {
16
+ const resolved = createRequire(from).resolve("@sfmc-bds/cli/cli");
17
+ return fs.existsSync(resolved) ? resolved : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** 自 PATH 上的 sfmc 可执行文件旁解析 @sfmc-bds/cli(npm global / 本地 .bin)。 */
24
+ function resolveCliFromPathEnv() {
25
+ const pathEnv = process.env.PATH ?? "";
26
+ const names = process.platform === "win32" ? ["sfmc.cmd", "sfmc.exe", "sfmc.ps1", "sfmc"] : ["sfmc"];
27
+ for (const dir of pathEnv.split(path.delimiter)) {
28
+ if (!dir)
29
+ continue;
30
+ let hit = false;
31
+ for (const name of names) {
32
+ if (fs.existsSync(path.join(dir, name))) {
33
+ hit = true;
34
+ break;
35
+ }
36
+ }
37
+ if (!hit)
38
+ continue;
39
+ const nearCli = path.join(dir, "..", "@sfmc-bds", "cli", "dist", "main.js");
40
+ if (fs.existsSync(nearCli))
41
+ return nearCli;
42
+ const nearMeta = path.join(dir, "..", "@sfmc-bds", "sfmc", "bin", "sfmc.mjs");
43
+ if (fs.existsSync(nearMeta))
44
+ return nearMeta;
45
+ const fromBin = tryRequireCli(path.join(dir, "..", "package.json"));
46
+ if (fromBin)
47
+ return fromBin;
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * 解析 sfmc CLI 脚本路径。
53
+ * 顺序:显式 cliPath / SFMC_CLI → 本包安装树(@sfmc-bds/cli)→ cwd 的 node_modules → PATH。
54
+ */
55
+ export function resolveSfmcCli(opts = {}) {
56
+ const explicit = (opts.cliPath || process.env.SFMC_CLI || "").trim();
57
+ if (explicit) {
58
+ const p = path.resolve(explicit);
59
+ if (!fs.existsSync(p)) {
60
+ throw new Error(`指定的 sfmc CLI 不存在:${p}(检查 SFMC_CLI / sfmc.cliPath)`);
61
+ }
62
+ return p;
63
+ }
64
+ const here = fileURLToPath(import.meta.url);
65
+ const fromInstall = tryRequireCli(here);
66
+ if (fromInstall)
67
+ return fromInstall;
68
+ const fromCwd = tryRequireCli(path.join(process.cwd(), "package.json"));
69
+ if (fromCwd)
70
+ return fromCwd;
71
+ const fromPath = resolveCliFromPathEnv();
72
+ if (fromPath)
73
+ return fromPath;
74
+ throw new Error("无法解析 sfmc CLI。请安装 @sfmc-bds/cli(或确保 PATH 上有 sfmc),或设置环境变量 SFMC_CLI / 扩展配置 sfmc.cliPath。");
75
+ }
76
+ function looksLikeCliFailure(output) {
77
+ if (output.includes(CLI_ANSI_RED))
78
+ return true;
79
+ if (/\[pack-manager\].*required/.test(output))
80
+ return true;
81
+ if (/^Error:/m.test(output))
82
+ return true;
83
+ return false;
84
+ }
85
+ /** 在 SFMC 工作目录下执行 `node <sfmc-cli> ...args`(env.SFMC_ROOT = 工作目录)。 */
86
+ export async function runSfmcCli(sfmcRoot, args, opts = {}) {
87
+ const root = path.resolve(sfmcRoot);
88
+ if (!isValidSfmcRoot(root)) {
89
+ return {
90
+ ok: false,
91
+ output: `无效的 SFMC 工作目录:${root}(需要含 configs/ 与 modules/,不必含源码仓库)`,
92
+ };
93
+ }
94
+ let script;
95
+ try {
96
+ script = resolveSfmcCli({ cliPath: opts.cliPath });
97
+ }
98
+ catch (e) {
99
+ return { ok: false, output: e instanceof Error ? e.message : String(e) };
100
+ }
101
+ const r = await new Promise((resolve) => {
102
+ const proc = spawn(process.execPath, [script, ...args], {
103
+ cwd: root,
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ env: { ...process.env, SFMC_ROOT: root },
106
+ });
107
+ let out = "";
108
+ proc.stdout?.on("data", (d) => {
109
+ out += d.toString();
110
+ });
111
+ proc.stderr?.on("data", (d) => {
112
+ out += d.toString();
113
+ });
114
+ proc.on("exit", (code) => resolve({ code, output: out }));
115
+ proc.on("error", (e) => resolve({ code: 1, output: e.message }));
116
+ });
117
+ const ok = r.code === 0 && !looksLikeCliFailure(r.output);
118
+ return { ok, output: r.output };
119
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SFMC 工作目录判定(运行时根:configs/、modules/;不是源码仓库根)
3
+ */
4
+ /** 目录是否为有效 SFMC 工作目录(含 configs/ 与 modules/)。 */
5
+ export declare function isValidSfmcRoot(dir: string): boolean;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SFMC 工作目录判定(运行时根:configs/、modules/;不是源码仓库根)
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ /** 目录是否为有效 SFMC 工作目录(含 configs/ 与 modules/)。 */
7
+ export function isValidSfmcRoot(dir) {
8
+ if (!dir || !dir.trim())
9
+ return false;
10
+ const root = path.resolve(dir);
11
+ try {
12
+ return (fs.statSync(path.join(root, "configs")).isDirectory() &&
13
+ fs.statSync(path.join(root, "modules")).isDirectory());
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 监听模块 sapi/src,防抖后回调 onRebuild(由调用方注入 rebuildAndDeploy / 其它)。
3
+ */
4
+ export interface ModuleWatchOptions {
5
+ moduleRoot: string;
6
+ debounceMs?: number;
7
+ log?: (line: string) => void;
8
+ onRebuild: () => Promise<{
9
+ ok: boolean;
10
+ message?: string;
11
+ }>;
12
+ }
13
+ export declare function startModuleWatch(opts: ModuleWatchOptions): {
14
+ stop: () => void;
15
+ };
package/dist/watch.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 监听模块 sapi/src,防抖后回调 onRebuild(由调用方注入 rebuildAndDeploy / 其它)。
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ export function startModuleWatch(opts) {
7
+ const log = opts.log ?? ((line) => console.log(line));
8
+ const debounceMs = opts.debounceMs ?? 200;
9
+ const sapiDir = path.join(opts.moduleRoot, "sapi");
10
+ const srcDir = path.join(sapiDir, "src");
11
+ if (!fs.existsSync(sapiDir)) {
12
+ log(`[devkit:watch] 无 sapi 目录: ${sapiDir}`);
13
+ return { stop: () => { } };
14
+ }
15
+ let debounceTimer = null;
16
+ let pending = new Set();
17
+ let running = false;
18
+ let stopped = false;
19
+ log(`[devkit:watch] 监听 ${opts.moduleRoot}`);
20
+ const runOnce = async () => {
21
+ if (running || stopped)
22
+ return;
23
+ running = true;
24
+ const changed = [...pending];
25
+ pending = new Set();
26
+ const t0 = Date.now();
27
+ log(`[devkit:watch] rebuild (${changed.length})…`);
28
+ try {
29
+ const r = await opts.onRebuild();
30
+ const ms = Date.now() - t0;
31
+ if (r.ok)
32
+ log(`[devkit:watch] ok ${ms}ms`);
33
+ else
34
+ log(`[devkit:watch] fail ${ms}ms ${r.message ?? ""}`);
35
+ }
36
+ catch (e) {
37
+ log(`[devkit:watch] error ${e.message}`);
38
+ }
39
+ finally {
40
+ running = false;
41
+ }
42
+ };
43
+ const onSrc = (rel) => {
44
+ if (stopped)
45
+ return;
46
+ if (rel.includes("node_modules") || rel.includes("dist") || rel.endsWith(".tgz"))
47
+ return;
48
+ pending.add(rel);
49
+ if (debounceTimer)
50
+ clearTimeout(debounceTimer);
51
+ debounceTimer = setTimeout(() => void runOnce(), debounceMs);
52
+ };
53
+ const watchers = [];
54
+ if (fs.existsSync(srcDir)) {
55
+ try {
56
+ watchers.push(fs.watch(srcDir, { recursive: true, persistent: true }, (_e, filename) => {
57
+ if (filename)
58
+ onSrc(filename.toString());
59
+ }));
60
+ }
61
+ catch {
62
+ watchers.push(fs.watch(srcDir, { persistent: true }, (_e, filename) => {
63
+ if (filename)
64
+ onSrc(filename.toString());
65
+ }));
66
+ }
67
+ }
68
+ const watchMeta = (file, kind) => {
69
+ if (!fs.existsSync(file))
70
+ return;
71
+ watchers.push(fs.watch(file, { persistent: true }, (event) => {
72
+ if (event === "change")
73
+ log(`[devkit:watch] ${kind} 已改,需重启 BDS(SAPI 启动期缓存)`);
74
+ }));
75
+ };
76
+ watchMeta(path.join(sapiDir, "manifest.json"), "manifest");
77
+ watchMeta(path.join(sapiDir, "tsconfig.json"), "tsconfig");
78
+ return {
79
+ stop() {
80
+ stopped = true;
81
+ if (debounceTimer)
82
+ clearTimeout(debounceTimer);
83
+ for (const w of watchers) {
84
+ try {
85
+ w.close();
86
+ }
87
+ catch {
88
+ /* ignore */
89
+ }
90
+ }
91
+ log("[devkit:watch] stopped");
92
+ },
93
+ };
94
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@sfmc-bds/devkit",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "SFMC module authoring toolkit — watch, scaffold, rebuild (for VS Code extension & tooling)",
5
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "engines": {
8
+ "node": ">=22.13.0"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "node build.mjs",
25
+ "typecheck": "tsc7 --noEmit -p tsconfig.json"
26
+ },
27
+ "dependencies": {
28
+ "@sfmc-bds/bds-tools": "^0.2.0-beta.8",
29
+ "@sfmc-bds/cli": "^0.2.0-beta.8",
30
+ "@sfmc-bds/tools": "^0.2.0-beta.9"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ }
36
+ }