Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/catalog.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_comment": "modules/catalog.json 是本地 mirror;source of truth 为 github:Tanya7z/sfmc-modules/main/index.json。业务模块已全部提取至 Tanya7z/sfmc-modules(最新 modules-v0.4.0)。本仓库 modules/packages/ 不再内置业务包;请用 tools/fetch-module.mjs / sfmc 从 registry 安装到 modules/packages/<id>/。",
"_comment": "modules/catalog.json 是本地 mirror;source of truth 为 github:Tanya7z/sfmc-modules/main/index.json。业务模块默认外置;本仓可保留开箱 fixture(如 afk)。安装/卸载后请用 tools/catalog-sync.mjs 或 fetch-module 投影。",
"version": 1,
"modules": [
{
Expand Down
48 changes: 15 additions & 33 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"ts-node": "^10.9.2",
"tsx": "^4.19.0",
"typescript": "^6.0.3",
"lodash": "^4.18.1"
"lodash": "^4.18.1",
"jszip": "^3.10.1"
},
"scripts": {
"start": "node sfmc/dist/main.js",
Expand Down
2 changes: 1 addition & 1 deletion sfmc/src/module-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async function dirSize(dir: string): Promise<{ totalBytes: number; fileCount: nu
}
}
}
walk(dir);
await walk(dir);
return { totalBytes, fileCount };
}

Expand Down
31 changes: 26 additions & 5 deletions sfmc/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,35 @@ function writeCache(cache: RegistryCache): void {
}
}

/**
* 解析 first-party index.json。
* 契约与 tools/lib/registry-index.mjs#parseRegistryIndex 保持一致:
* `{ modules: { <folder>: { repo, tag } } }`。忽略 `_` 前缀元数据键。
*/
export function parseRegistryIndex(json: unknown): RegistryIndex {
if (typeof json !== "object" || json === null || Array.isArray(json)) {
throw new Error("registry index must be a JSON object with a 'modules' field");
}
const root = json as Record<string, unknown>;
const modulesRaw = root.modules;
if (typeof modulesRaw !== "object" || modulesRaw === null || Array.isArray(modulesRaw)) {
throw new Error("registry index must have a 'modules' object mapping id → { repo, tag }");
}
const filtered: RegistryIndex = {};
for (const [k, v] of Object.entries(modulesRaw as Record<string, unknown>)) {
if (k.startsWith("_")) continue;
if (typeof v !== "object" || v === null || Array.isArray(v)) continue;
const entry = v as Record<string, unknown>;
if (typeof entry.repo !== "string" || typeof entry.tag !== "string") continue;
filtered[k] = { repo: entry.repo, tag: entry.tag };
}
return filtered;
}

async function fetchFresh(): Promise<RegistryIndex> {
const res = await fetch(INDEX_URL, { headers: { "User-Agent": "sfmc-cli" } });
if (!res.ok) throw new Error(`HTTP ${res.status} for ${INDEX_URL}`);
const json = (await res.json()) as unknown;
if (typeof json !== "object" || json === null || Array.isArray(json)) {
throw new Error("registry index must be a JSON object mapping id → { repo, tag }");
}
return json as RegistryIndex;
return parseRegistryIndex(await res.json());
}

export interface RegistryResult {
Expand Down
5 changes: 3 additions & 2 deletions tools/check-modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import fs from "node:fs";
import path from "node:path";
import { readCatalog, syncCatalogFromPackages } from "./lib/catalog.mjs";
import { scanInstalledPackages } from "./lib/packages.mjs";
import { folderFromEntryPath, scanInstalledPackages } from "./lib/packages.mjs";
import { ROOT } from "./lib/paths.mjs";
import { exists } from "./lib/io.mjs";

Expand Down Expand Up @@ -62,7 +62,8 @@ function main() {
const abs = path.join(ROOT, m.entry.path);
if (!exists(abs)) fail(`${m.id}: entry 不存在: ${m.entry.path}`);

const folder = String(m.entry.path).replace(/\\/g, "/").split("/")[2];
const folder = folderFromEntryPath(m.entry.path);
if (!folder) fail(`${m.id}: 无法从 entry.path 解析 packages 目录: ${m.entry.path}`);
const manifestPath = path.join(ROOT, "modules", "packages", folder, "sapi", "manifest.json");
if (!exists(manifestPath)) fail(`${m.id}: 缺少 packages/${folder}/sapi/manifest.json`);
let manifest;
Expand Down
26 changes: 9 additions & 17 deletions tools/fetch-module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { upsertCatalogEntry, removeCatalogEntry } from "./lib/catalog.mjs";
import { setModuleLockEnabled, removeModuleLock } from "./lib/lock.mjs";
import { PACKAGES_DIR, ROOT } from "./lib/paths.mjs";
import { exists } from "./lib/io.mjs";
import { parseRegistryIndex } from "./lib/registry-index.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TARGET = PACKAGES_DIR;
Expand Down Expand Up @@ -61,21 +62,7 @@ function writeCache(cache) {
async function fetchRegistryIndexFresh() {
const res = await fetch(DEFAULT_REGISTRY_INDEX_URL, { headers: { "User-Agent": "sfmc-fetch-module" } });
if (!res.ok) throw new Error(`HTTP ${res.status} for ${DEFAULT_REGISTRY_INDEX_URL}`);
const json = await res.json();
if (typeof json !== "object" || json === null || Array.isArray(json)) {
throw new Error("registry index must be a JSON object with a 'modules' field");
}
const modules = json.modules;
if (typeof modules !== "object" || modules === null || Array.isArray(modules)) {
throw new Error("registry index must have a 'modules' object mapping id → { repo, tag }");
}
/** @type {RegistryIndex} */
const filtered = {};
for (const [k, v] of Object.entries(modules)) {
if (k.startsWith("_")) continue;
filtered[k] = v;
}
return filtered;
return parseRegistryIndex(await res.json());
}

async function resolveRegistryIndex() {
Expand Down Expand Up @@ -294,8 +281,13 @@ async function unzip(zipPath, dstDir) {
const data = await fsp.readFile(zipPath);
const zip = await JSZip.loadAsync(data);
for (const e of Object.values(zip.files)) {
const out = path.join(dstDir, e.name);
if (e.dir) {
// Windows zip 常带 `\`;必须归一成 `/`,否则 Linux 会写出字面量 `sapi\manifest.json`
const rel = String(e.name).replace(/\\/g, "/").replace(/^\/+/, "");
if (!rel || rel.includes("..")) continue;
const parts = rel.replace(/\/$/, "").split("/").filter(Boolean);
if (parts.length === 0) continue;
const out = path.join(dstDir, ...parts);
if (e.dir || rel.endsWith("/")) {
await fsp.mkdir(out, { recursive: true });
continue;
}
Expand Down
19 changes: 8 additions & 11 deletions tools/lib/catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
*/
import { CATALOG_PATH } from "./paths.mjs";
import { readJson, writeJson } from "./io.mjs";
import { loadPackageCatalogEntry, projectCatalogEntry, scanInstalledPackages } from "./packages.mjs";
import {
folderFromEntryPath,
loadPackageCatalogEntry,
projectCatalogEntry,
scanInstalledPackages,
} from "./packages.mjs";

const DEFAULT_COMMENT =
"modules/catalog.json 是本地 mirror;source of truth 为 github:Tanya7z/sfmc-modules/main/index.json。条目由已安装的 modules/packages/<id>/sapi/manifest.json 投影生成(fetch-module install / catalog-sync)。空数组表示纯 SDK 仓,合法。";
Expand Down Expand Up @@ -65,7 +70,7 @@ export function upsertCatalogEntry(folder) {
if (!entry) throw new Error(`packages/${folder}: 无有效 sapi/manifest.json`);
const catalog = readCatalog();
const idx = catalog.modules.findIndex(
(m) => m.id === entry.id || (m.entry && folderFromEntry(m.entry.path) === folder)
(m) => m.id === entry.id || (m.entry && folderFromEntryPath(m.entry.path) === folder)
);
if (idx >= 0) catalog.modules[idx] = entry;
else catalog.modules.push(entry);
Expand All @@ -83,18 +88,10 @@ export function removeCatalogEntry(folderOrId) {
const catalog = readCatalog();
const idx = catalog.modules.findIndex((m) => {
if (m.id === folderOrId) return true;
return folderFromEntry(m.entry?.path) === folderOrId;
return folderFromEntryPath(m.entry?.path) === folderOrId;
});
if (idx < 0) return null;
const [removed] = catalog.modules.splice(idx, 1);
writeCatalog(catalog);
return removed;
}

/** @param {string | undefined} entryPath */
function folderFromEntry(entryPath) {
const m = String(entryPath || "")
.replace(/\\/g, "/")
.match(/modules\/packages\/([^/]+)\//);
return m ? m[1] : null;
}
3 changes: 0 additions & 3 deletions tools/lib/packages.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* tools/lib/packages.mjs — 扫描 packages/<id>/sapi/manifest.json 并投影为 catalog 条目
*/
/**
* tools/lib/packages.mjs — 扫描 packages/<id>/sapi/manifest.json 并投影为 catalog 条目
*/
Expand Down
36 changes: 36 additions & 0 deletions tools/lib/registry-index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* tools/lib/registry-index.mjs — 解析 Tanya7z/sfmc-modules index.json
*
* 契约(与 sfmc/src/registry.ts#parseRegistryIndex 保持一致):
* { modules: { <packageFolder>: { repo, tag } } }
* `_` 前缀键视为元数据并忽略。
*/

/**
* @typedef {{ repo: string, tag: string }} RegistryEntry
* @typedef {Record<string, RegistryEntry>} RegistryIndex
*/

/**
* @param {unknown} json
* @returns {RegistryIndex}
*/
export function parseRegistryIndex(json) {
if (typeof json !== "object" || json === null || Array.isArray(json)) {
throw new Error("registry index must be a JSON object with a 'modules' field");
}
const modules = /** @type {Record<string, unknown>} */ (json).modules;
if (typeof modules !== "object" || modules === null || Array.isArray(modules)) {
throw new Error("registry index must have a 'modules' object mapping id → { repo, tag }");
}
/** @type {RegistryIndex} */
const filtered = {};
for (const [k, v] of Object.entries(modules)) {
if (k.startsWith("_")) continue;
if (typeof v !== "object" || v === null || Array.isArray(v)) continue;
const entry = /** @type {Record<string, unknown>} */ (v);
if (typeof entry.repo !== "string" || typeof entry.tag !== "string") continue;
filtered[k] = { repo: entry.repo, tag: entry.tag };
}
return filtered;
}
Loading