@tbox.cn/app-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3154 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
7
|
+
|
|
8
|
+
// src/commands/create.ts
|
|
9
|
+
import { existsSync as existsSync3, readdirSync, realpathSync } from "fs";
|
|
10
|
+
import { appendFile, cp, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import path4 from "path";
|
|
12
|
+
|
|
13
|
+
// src/manifest.ts
|
|
14
|
+
import { existsSync, readFileSync } from "fs";
|
|
15
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
16
|
+
import path from "path";
|
|
17
|
+
function manifestPath(appDir) {
|
|
18
|
+
return path.join(appDir, ".tbox", "app.json");
|
|
19
|
+
}
|
|
20
|
+
function readAppManifest(appDir) {
|
|
21
|
+
const file = manifestPath(appDir);
|
|
22
|
+
if (!existsSync(file)) return null;
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
25
|
+
const templateVersion = raw?.templateVersion;
|
|
26
|
+
if (typeof templateVersion !== "string" || templateVersion.trim().length === 0) return null;
|
|
27
|
+
return {
|
|
28
|
+
templateVersion: templateVersion.trim(),
|
|
29
|
+
npmModules: Array.isArray(raw?.npmModules) ? raw.npmModules : []
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function writeAppManifest(appDir, manifest) {
|
|
36
|
+
const file = manifestPath(appDir);
|
|
37
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
38
|
+
await writeFile(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
39
|
+
}
|
|
40
|
+
function upsertNpmModule(manifest, entry) {
|
|
41
|
+
const rest = manifest.npmModules.filter((m) => m.id !== entry.id);
|
|
42
|
+
return { ...manifest, npmModules: [...rest, entry] };
|
|
43
|
+
}
|
|
44
|
+
function removeNpmModule(manifest, id) {
|
|
45
|
+
return { ...manifest, npmModules: manifest.npmModules.filter((m) => m.id !== id) };
|
|
46
|
+
}
|
|
47
|
+
function updateNpmModule(manifest, id, patch) {
|
|
48
|
+
return {
|
|
49
|
+
...manifest,
|
|
50
|
+
npmModules: manifest.npmModules.map((m) => m.id === id ? { ...m, ...patch } : m)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function isValidModuleId(id) {
|
|
54
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(id) && !id.includes("..");
|
|
55
|
+
}
|
|
56
|
+
function assertValidModuleId(id, source) {
|
|
57
|
+
if (!isValidModuleId(id)) {
|
|
58
|
+
throw new Error(`\u975E\u6CD5\u6A21\u5757 id "${id}"\uFF08\u6765\u81EA ${source}\uFF09\uFF1A\u4EC5\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/\u4E2D\u5212\u7EBF`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/source.ts
|
|
63
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, mkdtempSync, rmSync } from "fs";
|
|
64
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
65
|
+
import { tmpdir } from "os";
|
|
66
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
67
|
+
import path3 from "path";
|
|
68
|
+
|
|
69
|
+
// src/registry.ts
|
|
70
|
+
import { execFileSync } from "child_process";
|
|
71
|
+
import path2 from "path";
|
|
72
|
+
var cachedRegistry = null;
|
|
73
|
+
function resolveRegistry(pkgName) {
|
|
74
|
+
if (pkgName?.startsWith("@tbox.cn/")) {
|
|
75
|
+
try {
|
|
76
|
+
const scopeOut = execFileSync("pnpm", ["config", "get", "@tbox.cn:registry"], { encoding: "utf8" });
|
|
77
|
+
const scopeReg = scopeOut.trim().split(/\r?\n/).pop() ?? "";
|
|
78
|
+
if (scopeReg) return scopeReg;
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (cachedRegistry) return cachedRegistry;
|
|
83
|
+
try {
|
|
84
|
+
const out = execFileSync("pnpm", ["config", "get", "registry"], { encoding: "utf8" });
|
|
85
|
+
cachedRegistry = out.trim().split(/\r?\n/).pop() ?? "https://registry.npmjs.org/";
|
|
86
|
+
} catch {
|
|
87
|
+
cachedRegistry = "https://registry.npmjs.org/";
|
|
88
|
+
}
|
|
89
|
+
return cachedRegistry;
|
|
90
|
+
}
|
|
91
|
+
function isRegistrySpec(spec) {
|
|
92
|
+
return /^@[^/]+\/[^@/]+(@.+)?$/.test(spec);
|
|
93
|
+
}
|
|
94
|
+
function specName(spec) {
|
|
95
|
+
return spec.replace(/@[^/]+$/, "");
|
|
96
|
+
}
|
|
97
|
+
function packFromRegistry(spec, outDir, registry) {
|
|
98
|
+
const reg = registry ?? resolveRegistry(specName(spec));
|
|
99
|
+
const viewArgs = ["view", spec, "dist.tarball"];
|
|
100
|
+
if (registry) viewArgs.push("--registry", registry);
|
|
101
|
+
const out = execFileSync("pnpm", viewArgs, { encoding: "utf8" });
|
|
102
|
+
const url = out.trim().split(/\r?\n/).pop() ?? "";
|
|
103
|
+
if (!url) throw new Error(`registry \u62C9\u53D6\u5931\u8D25\uFF1A${spec} \u65E0 dist.tarball\uFF08registry=${reg}\uFF09`);
|
|
104
|
+
const file = url.split("/").pop() ?? `${spec.replace(/^@/, "").replace(/\//g, "-").split("@")[0]}.tgz`;
|
|
105
|
+
const dest = path2.join(path2.resolve(outDir), file);
|
|
106
|
+
execFileSync("curl", ["-fsSL", "-o", dest, url], { stdio: "ignore" });
|
|
107
|
+
return dest;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/source.ts
|
|
111
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "dist-ssr", ".git", ".tbox"]);
|
|
112
|
+
async function walkDir(dir, base, out) {
|
|
113
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const full = path3.join(dir, entry.name);
|
|
116
|
+
const rel = path3.relative(base, full).split(path3.sep).join("/");
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
119
|
+
await walkDir(full, base, out);
|
|
120
|
+
} else if (entry.isFile()) {
|
|
121
|
+
out.push(rel);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
var LocalDirSource = class {
|
|
126
|
+
constructor(dir) {
|
|
127
|
+
this.dir = dir;
|
|
128
|
+
this.name = path3.basename(path3.resolve(dir));
|
|
129
|
+
}
|
|
130
|
+
dir;
|
|
131
|
+
name;
|
|
132
|
+
getDir() {
|
|
133
|
+
return this.dir;
|
|
134
|
+
}
|
|
135
|
+
async listFiles() {
|
|
136
|
+
const files = [];
|
|
137
|
+
await walkDir(this.dir, this.dir, files);
|
|
138
|
+
return files.sort();
|
|
139
|
+
}
|
|
140
|
+
async readFile(rel) {
|
|
141
|
+
return readFile(path3.join(this.dir, rel), "utf8");
|
|
142
|
+
}
|
|
143
|
+
/** 目录是否可读(存在且含 package.json) */
|
|
144
|
+
static isUsable(dir) {
|
|
145
|
+
return existsSync2(path3.join(dir, "package.json"));
|
|
146
|
+
}
|
|
147
|
+
resolveVersion() {
|
|
148
|
+
return readPackageNameVersion(this.dir).version;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
function readPackageNameVersion(pkgDir) {
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
raw = readFileSync2(path3.join(pkgDir, "package.json"), "utf8");
|
|
155
|
+
} catch {
|
|
156
|
+
return { version: "0.0.0" };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const pkg = JSON.parse(raw);
|
|
160
|
+
return { name: pkg?.name, version: pkg?.version ?? "0.0.0" };
|
|
161
|
+
} catch {
|
|
162
|
+
return { version: "0.0.0" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
var RegistrySource = class {
|
|
166
|
+
constructor(spec, registry) {
|
|
167
|
+
this.spec = spec;
|
|
168
|
+
this.registry = registry;
|
|
169
|
+
this.name = spec;
|
|
170
|
+
this.packDir = mkdtempSync(path3.join(tmpdir(), "tbox-reg-"));
|
|
171
|
+
}
|
|
172
|
+
spec;
|
|
173
|
+
registry;
|
|
174
|
+
name;
|
|
175
|
+
inner = null;
|
|
176
|
+
packDir;
|
|
177
|
+
resolvedName = "";
|
|
178
|
+
resolvedVersion = "";
|
|
179
|
+
resolve() {
|
|
180
|
+
if (this.inner) return this.inner;
|
|
181
|
+
const pkgName = this.spec.replace(/@[^/]+$/, "");
|
|
182
|
+
const reg = this.registry ?? resolveRegistry(pkgName);
|
|
183
|
+
console.log(`\u{1F4E6} \u4ECE registry \u62C9\u53D6 ${this.spec}\uFF08${reg}\uFF09`);
|
|
184
|
+
const tgzPath = packFromRegistry(this.spec, this.packDir, this.registry);
|
|
185
|
+
this.inner = new TarballSource(tgzPath);
|
|
186
|
+
let name;
|
|
187
|
+
let version = "";
|
|
188
|
+
try {
|
|
189
|
+
const pkg = JSON.parse(readFileSync2(path3.join(this.inner.getDir(), "package.json"), "utf8"));
|
|
190
|
+
name = pkg.name;
|
|
191
|
+
version = pkg.version ?? "";
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
this.resolvedName = name ?? this.spec;
|
|
195
|
+
this.resolvedVersion = version;
|
|
196
|
+
console.log(` \u2192 ${this.resolvedName}@${this.resolvedVersion}`);
|
|
197
|
+
return this.inner;
|
|
198
|
+
}
|
|
199
|
+
listFiles() {
|
|
200
|
+
return Promise.resolve(this.resolve()).then((s) => s.listFiles());
|
|
201
|
+
}
|
|
202
|
+
readFile(rel) {
|
|
203
|
+
return Promise.resolve(this.resolve()).then((s) => s.readFile(rel));
|
|
204
|
+
}
|
|
205
|
+
resolveVersion() {
|
|
206
|
+
this.resolve();
|
|
207
|
+
return this.resolvedVersion;
|
|
208
|
+
}
|
|
209
|
+
getDir() {
|
|
210
|
+
return this.resolve().getDir();
|
|
211
|
+
}
|
|
212
|
+
get resolvedPackage() {
|
|
213
|
+
return { name: this.resolvedName, version: this.resolvedVersion };
|
|
214
|
+
}
|
|
215
|
+
dispose() {
|
|
216
|
+
if (this.inner) this.inner.dispose();
|
|
217
|
+
try {
|
|
218
|
+
if (this.packDir.startsWith(tmpdir())) {
|
|
219
|
+
rmSync(this.packDir, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
function openSource(sourcePath, registry) {
|
|
226
|
+
if (sourcePath.endsWith(".tgz") && existsSync2(sourcePath)) {
|
|
227
|
+
const tarball = new TarballSource(sourcePath);
|
|
228
|
+
return { sourceDir: tarball.getDir(), source: tarball };
|
|
229
|
+
}
|
|
230
|
+
if (!existsSync2(sourcePath) && isRegistrySpec(sourcePath)) {
|
|
231
|
+
const regSource = new RegistrySource(sourcePath, registry);
|
|
232
|
+
return { sourceDir: regSource.getDir(), source: regSource };
|
|
233
|
+
}
|
|
234
|
+
return { sourceDir: sourcePath, source: new LocalDirSource(sourcePath) };
|
|
235
|
+
}
|
|
236
|
+
var TarballSource = class {
|
|
237
|
+
name;
|
|
238
|
+
inner;
|
|
239
|
+
tmpRoot;
|
|
240
|
+
constructor(tgzPath) {
|
|
241
|
+
const abs = path3.resolve(tgzPath);
|
|
242
|
+
this.name = path3.basename(abs, ".tgz");
|
|
243
|
+
this.tmpRoot = mkdtempSync(path3.join(tmpdir(), "tbox-tgz-"));
|
|
244
|
+
execFileSync2("tar", ["-xzf", abs, "-C", this.tmpRoot], { stdio: "ignore" });
|
|
245
|
+
const pkgDir = path3.join(this.tmpRoot, "package");
|
|
246
|
+
this.inner = new LocalDirSource(existsSync2(pkgDir) ? pkgDir : this.tmpRoot);
|
|
247
|
+
}
|
|
248
|
+
getDir() {
|
|
249
|
+
return this.inner.getDir();
|
|
250
|
+
}
|
|
251
|
+
listFiles() {
|
|
252
|
+
return this.inner.listFiles();
|
|
253
|
+
}
|
|
254
|
+
readFile(rel) {
|
|
255
|
+
return this.inner.readFile(rel);
|
|
256
|
+
}
|
|
257
|
+
resolveVersion() {
|
|
258
|
+
return this.inner.resolveVersion();
|
|
259
|
+
}
|
|
260
|
+
/** 释放临时目录(仅删本实例创建的 tmpRoot,防误删系统 tmp 父目录) */
|
|
261
|
+
dispose() {
|
|
262
|
+
try {
|
|
263
|
+
if (this.tmpRoot.startsWith(tmpdir())) {
|
|
264
|
+
rmSync(this.tmpRoot, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/commands/create.ts
|
|
272
|
+
var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "dist-ssr", ".git", ".DS_Store"]);
|
|
273
|
+
var EXCLUDE_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml"]);
|
|
274
|
+
var TEMPLATE_WORKSPACE_FILE = "pnpm-workspace.template.yaml";
|
|
275
|
+
var APP_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
276
|
+
function buildPlatformOverrides(tgzDir, appDir) {
|
|
277
|
+
const files = readdirSync(tgzDir).filter((f) => /^tbox\.cn-app-(sdk|contracts)-.+\.tgz$/.test(f)).sort();
|
|
278
|
+
if (files.length === 0) return null;
|
|
279
|
+
const appReal = realpathSync(appDir);
|
|
280
|
+
const seen = /* @__PURE__ */ new Set();
|
|
281
|
+
const lines = [];
|
|
282
|
+
for (const f of files) {
|
|
283
|
+
const rest = f.replace(/^tbox\.cn-app-/, "").replace(/\.tgz$/, "");
|
|
284
|
+
const i = rest.lastIndexOf("-");
|
|
285
|
+
const name = "@tbox.cn/app-" + rest.slice(0, i);
|
|
286
|
+
if (seen.has(name)) {
|
|
287
|
+
throw new Error(`--local-deps \u76EE\u5F55\u542B\u540C\u4E00\u5E73\u53F0\u5305\u591A\u7248\u672C\uFF08overrides \u91CD\u590D key \u6B67\u4E49\uFF0C\u987B\u5355\u7248\u672C\u76EE\u5F55\uFF09: ${name}`);
|
|
288
|
+
}
|
|
289
|
+
seen.add(name);
|
|
290
|
+
const rel = path4.relative(appReal, realpathSync(path4.join(tgzDir, f)));
|
|
291
|
+
lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(`file:${rel}`)}`);
|
|
292
|
+
}
|
|
293
|
+
return "\n# create --local-deps \u6CE8\u5165\uFF1A\u5E73\u53F0\u5305\u7ECF file: \u672C\u5730\u53D1\u5E03\u6001 tgz \u6D88\u8D39\uFF08005 v0.4.0 \u9A8C\u8BC1\u6001\uFF1B\u751F\u4EA7 B \u6863\u79FB\u9664\u672C\u5757\u8D70 registry\uFF09\noverrides:\n" + lines.join("\n") + "\n";
|
|
294
|
+
}
|
|
295
|
+
function validateLocalDeps(tgzDir) {
|
|
296
|
+
const files = readdirSync(tgzDir).filter((f) => /^tbox\.cn-app-(sdk|contracts)-.+\.tgz$/.test(f));
|
|
297
|
+
if (files.length === 0) {
|
|
298
|
+
throw new Error(`--local-deps \u76EE\u5F55\u65E0\u5E73\u53F0\u5305\u4EA7\u7269\uFF08\u7F3A tbox.cn-app-(sdk|contracts)-*.tgz\uFF09: ${tgzDir}`);
|
|
299
|
+
}
|
|
300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
301
|
+
for (const f of files) {
|
|
302
|
+
const rest = f.replace(/^tbox\.cn-app-/, "").replace(/\.tgz$/, "");
|
|
303
|
+
const i = rest.lastIndexOf("-");
|
|
304
|
+
const name = "@tbox.cn/app-" + rest.slice(0, i);
|
|
305
|
+
if (seen.has(name)) {
|
|
306
|
+
throw new Error(`--local-deps \u76EE\u5F55\u542B\u540C\u4E00\u5E73\u53F0\u5305\u591A\u7248\u672C\uFF08overrides \u91CD\u590D key \u6B67\u4E49\uFF0C\u987B\u5355\u7248\u672C\u76EE\u5F55\uFF09: ${name}`);
|
|
307
|
+
}
|
|
308
|
+
seen.add(name);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function createApp(opts) {
|
|
312
|
+
const target = path4.resolve(process.cwd(), opts.name);
|
|
313
|
+
if (existsSync3(target) && !opts.force) {
|
|
314
|
+
throw new Error(`\u76EE\u5F55\u5DF2\u5B58\u5728: ${opts.name}\uFF08\u52A0 --force \u8986\u76D6\uFF09`);
|
|
315
|
+
}
|
|
316
|
+
const rawSource = opts.source;
|
|
317
|
+
const source = path4.resolve(rawSource);
|
|
318
|
+
const isReg = !existsSync3(source) && isRegistrySpec(rawSource);
|
|
319
|
+
const fromTgz = source.endsWith(".tgz");
|
|
320
|
+
if (!isReg && !fromTgz && !LocalDirSource.isUsable(source)) {
|
|
321
|
+
throw new Error(`\u6A21\u677F\u6E90\u65E0\u6548\uFF08\u7F3A package.json\uFF09: ${source}`);
|
|
322
|
+
}
|
|
323
|
+
if (!isReg && fromTgz && !existsSync3(source)) {
|
|
324
|
+
throw new Error(`\u6A21\u677F\u6E90\u65E0\u6548\uFF08tgz \u4E0D\u5B58\u5728\uFF09: ${source}`);
|
|
325
|
+
}
|
|
326
|
+
if (isReg && opts.localDeps) {
|
|
327
|
+
throw new Error("registry \u6E90\uFF08--source @scope/pkg\uFF09\u4E0E --local-deps \u4E92\u65A5\uFF1Aregistry \u5B89\u88C5\u65E0\u9700/\u4E0D\u5141\u8BB8\u672C\u5730\u4F9D\u8D56\u6CE8\u5165");
|
|
328
|
+
}
|
|
329
|
+
if (opts.localDeps) {
|
|
330
|
+
validateLocalDeps(path4.resolve(opts.localDeps));
|
|
331
|
+
}
|
|
332
|
+
const opened = openSource(isReg ? rawSource : source, opts.registry);
|
|
333
|
+
const sourceDir = opened.sourceDir;
|
|
334
|
+
try {
|
|
335
|
+
const srcManifest = readAppManifest(sourceDir);
|
|
336
|
+
if (!srcManifest) {
|
|
337
|
+
throw new Error(`\u6A21\u677F\u7ED3\u6784\u5F02\u5E38: ${source} \u7684 .tbox/app.json \u7F3A\u5931\u3001\u635F\u574F\u6216\u7F3A\u5C11 templateVersion\uFF08\u51FA\u751F\u7248\u672C\uFF09\u5B57\u6BB5`);
|
|
338
|
+
}
|
|
339
|
+
await cp(sourceDir, target, {
|
|
340
|
+
recursive: true,
|
|
341
|
+
force: true,
|
|
342
|
+
filter: (src) => {
|
|
343
|
+
const name = path4.basename(src);
|
|
344
|
+
if (EXCLUDE_FILES.has(name)) return false;
|
|
345
|
+
if (EXCLUDE_DIRS.has(name)) return false;
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const tplWs = path4.join(target, TEMPLATE_WORKSPACE_FILE);
|
|
350
|
+
if (existsSync3(tplWs)) {
|
|
351
|
+
await rename(tplWs, path4.join(target, APP_WORKSPACE_FILE));
|
|
352
|
+
}
|
|
353
|
+
if (opts.localDeps) {
|
|
354
|
+
const block = buildPlatformOverrides(path4.resolve(opts.localDeps), target);
|
|
355
|
+
await appendFile(path4.join(target, APP_WORKSPACE_FILE), block, "utf8");
|
|
356
|
+
}
|
|
357
|
+
const pkgFile = path4.join(target, "package.json");
|
|
358
|
+
if (existsSync3(pkgFile)) {
|
|
359
|
+
const pkg = JSON.parse(await readFile2(pkgFile, "utf8"));
|
|
360
|
+
pkg.name = opts.name;
|
|
361
|
+
await writeFile2(pkgFile, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
362
|
+
}
|
|
363
|
+
const manifest = { templateVersion: srcManifest.templateVersion, npmModules: [] };
|
|
364
|
+
await writeAppManifest(target, manifest);
|
|
365
|
+
console.log(`\u2705 \u5DF2\u521B\u5EFA\u5E94\u7528 ${opts.name}`);
|
|
366
|
+
if (isReg) {
|
|
367
|
+
console.log(` \u26A1 \u5DF2\u4ECE registry \u6A21\u677F\u5305 ${opened.source.name} \u5C55\u5F00\uFF08\u53D1\u5E03\u6001\uFF09`);
|
|
368
|
+
} else if (fromTgz) {
|
|
369
|
+
console.log(` \u26A1 \u5DF2\u4ECE\u6A21\u677F\u5305 ${path4.basename(source)} \u5C55\u5F00\uFF08\u53D1\u5E03\u6001\uFF09`);
|
|
370
|
+
} else {
|
|
371
|
+
console.log(` \u26A1 \u5DF2\u4ECE\u6A21\u677F\u76EE\u5F55\u5C55\u5F00\uFF08\u672C\u5730\u5F00\u53D1\u6E90\uFF09`);
|
|
372
|
+
}
|
|
373
|
+
if (opts.localDeps) {
|
|
374
|
+
console.log(` \u26A1 \u5DF2\u6CE8\u5165\u5E73\u53F0 overrides\uFF08--local-deps: ${opts.localDeps}\uFF0Cinstall \u79BB\u7EBF\u6D88\u8D39\u672C\u5730\u53D1\u5E03\u6001\u4EA7\u7269\uFF09`);
|
|
375
|
+
}
|
|
376
|
+
console.log(` \u26A1 \u5DF2\u751F\u6210 pnpm-workspace.yaml\uFF08\u5E94\u7528 workspace\uFF09`);
|
|
377
|
+
console.log(` \u4E0B\u4E00\u6B65: cd ${opts.name} && pnpm install`);
|
|
378
|
+
console.log(` \u{1F510} \u9274\u6743\uFF08006\uFF09: \u751F\u4EA7\u8BF7\u7531\u5E73\u53F0\u6CE8\u5165 AUTH_TOKEN_SECRET \u4E14\u786E\u4FDD NODE_ENV=production\uFF08dev-login \u5F3A\u5236\u5173\u95ED\uFF09\uFF1B`);
|
|
379
|
+
console.log(` \u672C\u5730\u5F00\u53D1\u9ED8\u8BA4\u5F00\u542F dev-login\uFF08\u975E production\uFF09\uFF1B\u5982\u9700\u5173\u95ED\u8BBE AUTH_DEV_LOGIN_DISABLED=true\u3002`);
|
|
380
|
+
} finally {
|
|
381
|
+
if (opened.source instanceof TarballSource) opened.source.dispose();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/commands/add.ts
|
|
386
|
+
import { existsSync as existsSync19, readFileSync as readFileSync8, readdirSync as readdirSync2 } from "fs";
|
|
387
|
+
import path23 from "path";
|
|
388
|
+
|
|
389
|
+
// src/expand.ts
|
|
390
|
+
import { mkdir as mkdir2, rm, writeFile as writeFile3 } from "fs/promises";
|
|
391
|
+
import path5 from "path";
|
|
392
|
+
|
|
393
|
+
// src/pkg-family.ts
|
|
394
|
+
var PLATFORM_RE = /^@tbox\.cn\/app-(sdk|contracts)/;
|
|
395
|
+
function isPlatform(name) {
|
|
396
|
+
return PLATFORM_RE.test(name);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/expand.ts
|
|
400
|
+
function isWorkspacePkg(name) {
|
|
401
|
+
return isPlatform(name);
|
|
402
|
+
}
|
|
403
|
+
function rewritePlatformRefs(deps) {
|
|
404
|
+
for (const name of Object.keys(deps)) {
|
|
405
|
+
if (isWorkspacePkg(name)) deps[name] = "catalog:";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function rewriteExportsToSrc(pkg) {
|
|
409
|
+
const exports = pkg.exports;
|
|
410
|
+
if (!exports || typeof exports !== "object") return;
|
|
411
|
+
const next = {};
|
|
412
|
+
for (const [sub, val] of Object.entries(exports)) {
|
|
413
|
+
if (typeof val !== "object" || val === null) continue;
|
|
414
|
+
const base = sub === "." ? "./src/index.ts" : `./src/${sub.replace(/^\.\//, "")}/index.ts`;
|
|
415
|
+
next[sub] = { types: base, import: base };
|
|
416
|
+
}
|
|
417
|
+
pkg.exports = next;
|
|
418
|
+
}
|
|
419
|
+
async function expandCodegen(appDir, sourceDir, entry) {
|
|
420
|
+
const src = new LocalDirSource(sourceDir);
|
|
421
|
+
const targetDir = path5.join(appDir, "packages", entry.id);
|
|
422
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
423
|
+
await mkdir2(targetDir, { recursive: true });
|
|
424
|
+
for (const rel of await src.listFiles()) {
|
|
425
|
+
if (rel === "package.json") continue;
|
|
426
|
+
const content = await src.readFile(rel);
|
|
427
|
+
const out = path5.join(targetDir, rel);
|
|
428
|
+
await mkdir2(path5.dirname(out), { recursive: true });
|
|
429
|
+
await writeFile3(out, content);
|
|
430
|
+
}
|
|
431
|
+
const rawPkg = await src.readFile("package.json");
|
|
432
|
+
const pkg = JSON.parse(rawPkg);
|
|
433
|
+
pkg.name = `@app/${entry.id}`;
|
|
434
|
+
pkg.private = true;
|
|
435
|
+
if (pkg.dependencies && typeof pkg.dependencies === "object") {
|
|
436
|
+
rewritePlatformRefs(pkg.dependencies);
|
|
437
|
+
}
|
|
438
|
+
if (pkg.peerDependencies && typeof pkg.peerDependencies === "object") {
|
|
439
|
+
rewritePlatformRefs(pkg.peerDependencies);
|
|
440
|
+
}
|
|
441
|
+
rewriteExportsToSrc(pkg);
|
|
442
|
+
await writeFile3(path5.join(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
443
|
+
}
|
|
444
|
+
var LOCAL_SERVER_ENTRY = (id) => `import type { ServerContext, ServerModule } from '@tbox.cn/app-sdk/server';
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* ${id} \u672C\u5730\u6A21\u5757\uFF08local \u6A21\u5F0F\uFF0Capp \u81EA\u6709\u5305\uFF09\u6CE8\u518C\u5165\u53E3\u3002
|
|
448
|
+
* \u5728\u6B64\u6CE8\u518C\u5DE5\u5177 / \u5361\u7247 meta / handler / \u8DEF\u7531 / Service\uFF08\u53EA\u6CE8\u518C\u4E0D\u89E3\u6790\uFF0CresolveService \u4EC5 handle/\u4E8B\u4EF6\u56DE\u8C03\u5185\uFF09\u3002
|
|
449
|
+
* cards map \u7684 key \u7EA6\u5B9A = meta.cardType\uFF1B\u65E0\u5361\u7247\u6A21\u5757\u5199\u7A7A map\u3002
|
|
450
|
+
*/
|
|
451
|
+
const cards = {} as const;
|
|
452
|
+
|
|
453
|
+
export const serverModule = {
|
|
454
|
+
cards,
|
|
455
|
+
// \u53C2\u6570\u547D\u540D _ctx\uFF1A\u9AA8\u67B6\u65B9\u6CD5\u4F53\u4E3A\u7A7A\uFF0C\u89C4\u907F tsconfig.base noUnusedParameters\uFF08TS6133\uFF09
|
|
456
|
+
register(_ctx: ServerContext): void {
|
|
457
|
+
// _ctx.tools.register(...);
|
|
458
|
+
// _ctx.cards.registerMap(cards);
|
|
459
|
+
// _ctx.handlers.register({ id: '...', handle: async () => ({ cards: [], text: '' }) });
|
|
460
|
+
// _ctx.services.register('member', member);
|
|
461
|
+
},
|
|
462
|
+
} satisfies ServerModule;
|
|
463
|
+
`;
|
|
464
|
+
var LOCAL_SERVICE = `/**
|
|
465
|
+
* Service \u5C42\uFF08\u4E1A\u52A1\u4E13\u5C5E\u5B9E\u73B0\uFF0C\u76F4\u63A5 import \u6216\u7ECF ctx.services \u6CE8\u518C\u4F9B\u8DE8\u6A21\u5757\u6D88\u8D39\uFF09\u3002
|
|
466
|
+
*/
|
|
467
|
+
export {};
|
|
468
|
+
`;
|
|
469
|
+
var LOCAL_HANDLER = `/**
|
|
470
|
+
* Handler \u5B9A\u4E49\uFF08001 \xA73.2\uFF09\u3002
|
|
471
|
+
*/
|
|
472
|
+
import type { HandlerDefinition } from '@tbox.cn/app-sdk/server';
|
|
473
|
+
|
|
474
|
+
export const exampleHandler: HandlerDefinition = {
|
|
475
|
+
id: 'example',
|
|
476
|
+
handle: async () => ({ cards: [], text: '\u672C\u5730\u6A21\u5757\u793A\u4F8B' }),
|
|
477
|
+
};
|
|
478
|
+
`;
|
|
479
|
+
var LOCAL_CLIENT_INDEX = `/**
|
|
480
|
+
* \u5BA2\u6237\u7AEF\u5165\u53E3\uFF1A\u5BFC\u51FA clientModule\uFF08CLI sync \u88C5\u914D\u8BFB\u53D6\uFF1B\u4E0E serverModule \u9010\u884C\u5BF9\u79F0\uFF09\u3002
|
|
481
|
+
* cards map \u7684 key \u7EA6\u5B9A = cardType\uFF1B\u65E0\u5361\u7247\u6A21\u5757\u5199\u7A7A map\u3002
|
|
482
|
+
*/
|
|
483
|
+
import type { ClientContext, ClientModule } from '@tbox.cn/app-sdk/client';
|
|
484
|
+
|
|
485
|
+
const cards = {} as const;
|
|
486
|
+
|
|
487
|
+
export const clientModule = {
|
|
488
|
+
cards,
|
|
489
|
+
// \u53C2\u6570\u547D\u540D _ctx\uFF1A\u9AA8\u67B6\u65B9\u6CD5\u4F53\u4E3A\u7A7A\uFF0C\u89C4\u907F tsconfig.base noUnusedParameters\uFF08TS6133\uFF09
|
|
490
|
+
register(_ctx: ClientContext): void {
|
|
491
|
+
// _ctx.cards.registerMap(cards);
|
|
492
|
+
},
|
|
493
|
+
} satisfies ClientModule;
|
|
494
|
+
`;
|
|
495
|
+
var LOCAL_ROOT_INDEX = `/**
|
|
496
|
+
* \u672C\u5730\u6A21\u5757\u4E3B\u5165\u53E3\uFF08package.json exports "." \u6307\u5411\uFF09\u3002
|
|
497
|
+
* \u6D88\u8D39\u65B9\u901A\u5E38\u8D70 ./server / ./client \u5B50\u8DEF\u5F84\uFF1B\u4E3B\u5165\u53E3\u805A\u5408\u5BFC\u51FA\u4FBF\u4E8E\u76F4\u63A5\u5F15\u7528\u3002
|
|
498
|
+
*/
|
|
499
|
+
export * from './server';
|
|
500
|
+
export * from './client';
|
|
501
|
+
`;
|
|
502
|
+
var LOCAL_TSCONFIG = `{
|
|
503
|
+
"extends": "../../tsconfig.base.json",
|
|
504
|
+
"compilerOptions": {
|
|
505
|
+
"jsx": "react-jsx",
|
|
506
|
+
"types": ["node"]
|
|
507
|
+
},
|
|
508
|
+
"include": ["src"]
|
|
509
|
+
}
|
|
510
|
+
`;
|
|
511
|
+
async function createLocalSkeleton(appDir, shortName) {
|
|
512
|
+
const id = shortName.startsWith("module-") ? shortName : `module-${shortName}`;
|
|
513
|
+
const dir = path5.join(appDir, "packages", id);
|
|
514
|
+
await rm(dir, { recursive: true, force: true });
|
|
515
|
+
await mkdir2(path5.join(dir, "src/server"), { recursive: true });
|
|
516
|
+
await mkdir2(path5.join(dir, "src/client"), { recursive: true });
|
|
517
|
+
const pkg = {
|
|
518
|
+
name: `@app/${id}`,
|
|
519
|
+
version: "0.1.0",
|
|
520
|
+
private: true,
|
|
521
|
+
type: "module",
|
|
522
|
+
exports: {
|
|
523
|
+
".": { types: "./src/index.ts", import: "./src/index.ts" },
|
|
524
|
+
"./server": { types: "./src/server/index.ts", import: "./src/server/index.ts" },
|
|
525
|
+
"./client": { types: "./src/client/index.ts", import: "./src/client/index.ts" }
|
|
526
|
+
},
|
|
527
|
+
// 本地模块自带工具链:typecheck/test 可在生成应用内直接跑(pnpm verify 全 @app/* 范围)
|
|
528
|
+
scripts: { typecheck: "tsc --noEmit", test: "vitest run --passWithNoTests" },
|
|
529
|
+
dependencies: {
|
|
530
|
+
"@tbox.cn/app-sdk": "catalog:",
|
|
531
|
+
"@tbox.cn/app-contracts": "catalog:"
|
|
532
|
+
},
|
|
533
|
+
peerDependencies: {
|
|
534
|
+
zod: "catalog:",
|
|
535
|
+
react: "catalog:",
|
|
536
|
+
"@mastra/core": "catalog:"
|
|
537
|
+
},
|
|
538
|
+
devDependencies: {
|
|
539
|
+
typescript: "catalog:",
|
|
540
|
+
vitest: "catalog:",
|
|
541
|
+
"@types/react": "catalog:",
|
|
542
|
+
"@types/node": "catalog:"
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const component = {
|
|
546
|
+
schemaVersion: 1,
|
|
547
|
+
name: id,
|
|
548
|
+
version: "0.1.0",
|
|
549
|
+
kind: "business",
|
|
550
|
+
risk: { level: "low", writeBoundary: "source" },
|
|
551
|
+
distribution: { defaultMode: "local" },
|
|
552
|
+
contributes: {
|
|
553
|
+
handlers: [],
|
|
554
|
+
tools: [],
|
|
555
|
+
cards: [],
|
|
556
|
+
routes: [],
|
|
557
|
+
pages: [],
|
|
558
|
+
tabs: []
|
|
559
|
+
},
|
|
560
|
+
dependencies: { modules: [] },
|
|
561
|
+
env: []
|
|
562
|
+
};
|
|
563
|
+
await writeFile3(path5.join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
564
|
+
await writeFile3(
|
|
565
|
+
path5.join(dir, "tbox.component.json"),
|
|
566
|
+
JSON.stringify(component, null, 2) + "\n",
|
|
567
|
+
"utf8"
|
|
568
|
+
);
|
|
569
|
+
await writeFile3(path5.join(dir, "tsconfig.json"), LOCAL_TSCONFIG, "utf8");
|
|
570
|
+
await writeFile3(path5.join(dir, "src/server/index.ts"), LOCAL_SERVER_ENTRY(id), "utf8");
|
|
571
|
+
await writeFile3(path5.join(dir, "src/server/service.ts"), LOCAL_SERVICE, "utf8");
|
|
572
|
+
await writeFile3(path5.join(dir, "src/server/handler.ts"), LOCAL_HANDLER, "utf8");
|
|
573
|
+
await writeFile3(path5.join(dir, "src/client/index.ts"), LOCAL_CLIENT_INDEX, "utf8");
|
|
574
|
+
await writeFile3(path5.join(dir, "src/index.ts"), LOCAL_ROOT_INDEX, "utf8");
|
|
575
|
+
return id;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/apps-deps.ts
|
|
579
|
+
import { existsSync as existsSync4 } from "fs";
|
|
580
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
581
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
582
|
+
import path6 from "path";
|
|
583
|
+
var APP_TARGETS = ["apps/server/package.json", "apps/client/package.json"];
|
|
584
|
+
async function updateAppsDependency(appDir, pkgName, action) {
|
|
585
|
+
let changed = false;
|
|
586
|
+
for (const rel of APP_TARGETS) {
|
|
587
|
+
const file = path6.join(appDir, rel);
|
|
588
|
+
if (!existsSync4(file)) continue;
|
|
589
|
+
const pkg = JSON.parse(readFileSync3(file, "utf8"));
|
|
590
|
+
const deps = pkg.dependencies ??= {};
|
|
591
|
+
if (action === "add" && !deps[pkgName]) {
|
|
592
|
+
deps[pkgName] = "workspace:*";
|
|
593
|
+
changed = true;
|
|
594
|
+
} else if (action === "remove" && deps[pkgName]) {
|
|
595
|
+
delete deps[pkgName];
|
|
596
|
+
changed = true;
|
|
597
|
+
}
|
|
598
|
+
if (changed) await writeFile4(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
599
|
+
}
|
|
600
|
+
return changed;
|
|
601
|
+
}
|
|
602
|
+
async function removePackageDir(appDir, id) {
|
|
603
|
+
const dir = path6.join(appDir, "packages", id);
|
|
604
|
+
if (!existsSync4(dir)) return false;
|
|
605
|
+
const { rm: rm2 } = await import("fs/promises");
|
|
606
|
+
await rm2(dir, { recursive: true, force: true });
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/sync-all.ts
|
|
611
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
612
|
+
import { readFile as readFile3, writeFile as writeFile6 } from "fs/promises";
|
|
613
|
+
import path9 from "path";
|
|
614
|
+
|
|
615
|
+
// src/assembly.ts
|
|
616
|
+
var ASSEMBLY_FILES = [
|
|
617
|
+
{ path: "apps/server/src/modules.ts", kind: "server-modules" },
|
|
618
|
+
{ path: "apps/client/src/modules.ts", kind: "client-modules" }
|
|
619
|
+
];
|
|
620
|
+
function camelCaseId(id) {
|
|
621
|
+
return id.split(/[^A-Za-z0-9]+/).filter(Boolean).map(
|
|
622
|
+
(part, i) => i === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
623
|
+
).join("");
|
|
624
|
+
}
|
|
625
|
+
function importAlias(kind, id) {
|
|
626
|
+
return camelCaseId(id);
|
|
627
|
+
}
|
|
628
|
+
function contributesTo(kind, m) {
|
|
629
|
+
return KINDS[kind].contributes(m);
|
|
630
|
+
}
|
|
631
|
+
function idFromPkg(pkg) {
|
|
632
|
+
return pkg.replace(/^@[^/]+\//, "").replace(/\/(server|client)$/, "").replace(/\/$/, "");
|
|
633
|
+
}
|
|
634
|
+
var KINDS = {
|
|
635
|
+
"server-modules": {
|
|
636
|
+
mapMarker: "register",
|
|
637
|
+
importExport: "serverModule",
|
|
638
|
+
importSuffix: "server",
|
|
639
|
+
entryPattern: /^\s*([A-Za-z_$][\w$]*),\s*$/,
|
|
640
|
+
header: "const modules = [",
|
|
641
|
+
footer: "];",
|
|
642
|
+
contributes: (m) => m.hasServerEntry
|
|
643
|
+
},
|
|
644
|
+
"client-modules": {
|
|
645
|
+
mapMarker: "register",
|
|
646
|
+
importExport: "clientModule",
|
|
647
|
+
importSuffix: "client",
|
|
648
|
+
entryPattern: /^\s*([A-Za-z_$][\w$]*),\s*$/,
|
|
649
|
+
header: "const modules = [",
|
|
650
|
+
footer: "];",
|
|
651
|
+
// 客户端装配仅收录"有卡片 + 有 clientModule 入口"的模块;老形态(无 clientModule)跳过并由
|
|
652
|
+
// assembly-drift 告警提示升级(D8:新 app + 老模块 → 探测缺失跳过)
|
|
653
|
+
contributes: (m) => m.hasClientEntry && m.hasCards
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
var IMPORT_LINE_PATTERN = /^import\s*\{\s*([\w$]+)\s+as\s+([\w$]+)\s*\}\s*from\s*['"]([^'"]+)['"];\s*$/;
|
|
657
|
+
function extractBlock(content, marker) {
|
|
658
|
+
const begin = `// @tbox:${marker}-begin`;
|
|
659
|
+
const end = `// @tbox:${marker}-end`;
|
|
660
|
+
const bIdx = content.indexOf(begin);
|
|
661
|
+
if (bIdx === -1) return null;
|
|
662
|
+
const eIdx = content.indexOf(end, bIdx);
|
|
663
|
+
if (eIdx === -1) return null;
|
|
664
|
+
const start = bIdx + begin.length;
|
|
665
|
+
return { block: content.slice(start, eIdx), start, end: eIdx };
|
|
666
|
+
}
|
|
667
|
+
function replaceBlock(content, range, newBlock) {
|
|
668
|
+
return content.slice(0, range.start) + newBlock + content.slice(range.end);
|
|
669
|
+
}
|
|
670
|
+
function parseAssemblyFile(content, kind) {
|
|
671
|
+
const spec = KINDS[kind];
|
|
672
|
+
const moduleIds = [];
|
|
673
|
+
const nonStandardLines = [];
|
|
674
|
+
const broken = [];
|
|
675
|
+
const imports = extractBlock(content, "imports");
|
|
676
|
+
if (imports) {
|
|
677
|
+
for (const line of imports.block.split(/\r?\n/)) {
|
|
678
|
+
const m = line.match(IMPORT_LINE_PATTERN);
|
|
679
|
+
if (m) {
|
|
680
|
+
moduleIds.push(idFromPkg(m[3]));
|
|
681
|
+
} else if (line.trim()) {
|
|
682
|
+
nonStandardLines.push(`imports: ${line.trim()}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
broken.push("imports");
|
|
687
|
+
}
|
|
688
|
+
const map = extractBlock(content, spec.mapMarker);
|
|
689
|
+
if (map) {
|
|
690
|
+
const lines = map.block.split(/\r?\n/);
|
|
691
|
+
const headIdx = lines.findIndex((l) => l.trim().startsWith(spec.header));
|
|
692
|
+
const tailIdx = [...lines].reverse().findIndex((l) => l.trim() === spec.footer);
|
|
693
|
+
const lastIdx = tailIdx === -1 ? -1 : lines.length - 1 - tailIdx;
|
|
694
|
+
if (headIdx === -1 || lastIdx <= headIdx) {
|
|
695
|
+
broken.push(spec.mapMarker);
|
|
696
|
+
} else {
|
|
697
|
+
for (const line of lines.slice(headIdx + 1, lastIdx)) {
|
|
698
|
+
if (!line.match(spec.entryPattern) && line.trim()) {
|
|
699
|
+
nonStandardLines.push(`${spec.mapMarker}: ${line.trim()}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
broken.push(spec.mapMarker);
|
|
705
|
+
}
|
|
706
|
+
const aliasToId = /* @__PURE__ */ new Map();
|
|
707
|
+
if (imports) {
|
|
708
|
+
for (const line of imports.block.split(/\r?\n/)) {
|
|
709
|
+
const m = line.match(IMPORT_LINE_PATTERN);
|
|
710
|
+
if (m) aliasToId.set(m[2], idFromPkg(m[3]));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const mapIds = [];
|
|
714
|
+
if (map) {
|
|
715
|
+
const lines = map.block.split(/\r?\n/);
|
|
716
|
+
const headIdx = lines.findIndex((l) => l.trim().startsWith(spec.header));
|
|
717
|
+
const tailIdx = [...lines].reverse().findIndex((l) => l.trim() === spec.footer);
|
|
718
|
+
const lastIdx = tailIdx === -1 ? -1 : lines.length - 1 - tailIdx;
|
|
719
|
+
if (headIdx !== -1 && lastIdx > headIdx) {
|
|
720
|
+
for (const line of lines.slice(headIdx + 1, lastIdx)) {
|
|
721
|
+
const m = line.match(spec.entryPattern);
|
|
722
|
+
if (m) {
|
|
723
|
+
const id = aliasToId.get(m[1]);
|
|
724
|
+
if (id) mapIds.push(id);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
moduleIds: [.../* @__PURE__ */ new Set([...moduleIds, ...mapIds])],
|
|
731
|
+
nonStandardLines,
|
|
732
|
+
broken
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function syncAssemblyFile(content, kind, declared, force = false) {
|
|
736
|
+
const spec = KINDS[kind];
|
|
737
|
+
const warnings = [];
|
|
738
|
+
const desired = declared.filter(spec.contributes);
|
|
739
|
+
const desiredByAlias = new Map(desired.map((m) => [importAlias(kind, m.id), m]));
|
|
740
|
+
let next = content;
|
|
741
|
+
let changed = false;
|
|
742
|
+
const importsRange = extractBlock(next, "imports");
|
|
743
|
+
if (!importsRange) {
|
|
744
|
+
warnings.push(`[${kind}] imports \u951A\u533A\u7F3A\u5931/\u7834\u574F\uFF0C\u8DF3\u8FC7`);
|
|
745
|
+
return { content: next, changed, warnings, moduleIds: [] };
|
|
746
|
+
}
|
|
747
|
+
const importLines = importsRange.block.split(/\r?\n/);
|
|
748
|
+
const keptImports = [];
|
|
749
|
+
for (const line of importLines) {
|
|
750
|
+
const m = line.match(IMPORT_LINE_PATTERN);
|
|
751
|
+
if (!m) {
|
|
752
|
+
if (line.trim() === "") continue;
|
|
753
|
+
if (!force) warnings.push(`[${kind}] imports \u951A\u533A\u975E\u6807\u884C\u4FDD\u7559: ${line.trim()}`);
|
|
754
|
+
if (!force) keptImports.push(line);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const localName = m[2];
|
|
758
|
+
const pkg = m[3];
|
|
759
|
+
const target = desiredByAlias.get(localName);
|
|
760
|
+
if (target && idFromPkg(target.pkg) === idFromPkg(pkg)) {
|
|
761
|
+
keptImports.push(line);
|
|
762
|
+
} else if (target) {
|
|
763
|
+
keptImports.push(specImportLine(kind, localName, target.pkg));
|
|
764
|
+
changed = true;
|
|
765
|
+
} else {
|
|
766
|
+
changed = true;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const m of desired) {
|
|
770
|
+
const localName = importAlias(kind, m.id);
|
|
771
|
+
if (!keptImports.some((l) => l.includes(`as ${localName} } from`))) {
|
|
772
|
+
keptImports.push(specImportLine(kind, localName, m.pkg));
|
|
773
|
+
changed = true;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const newImportsBlock = normalizeBlock(keptImports.join("\n"));
|
|
777
|
+
if (newImportsBlock !== importsRange.block) {
|
|
778
|
+
next = replaceBlock(next, importsRange, newImportsBlock);
|
|
779
|
+
changed = true;
|
|
780
|
+
}
|
|
781
|
+
const mapRange = extractBlock(next, spec.mapMarker);
|
|
782
|
+
if (!mapRange) {
|
|
783
|
+
warnings.push(`[${kind}] ${spec.mapMarker} \u951A\u533A\u7F3A\u5931/\u7834\u574F\uFF0C\u8DF3\u8FC7`);
|
|
784
|
+
return { content: next, changed, warnings, moduleIds: [] };
|
|
785
|
+
}
|
|
786
|
+
const mapLines = mapRange.block.split(/\r?\n/);
|
|
787
|
+
const headIdx = mapLines.findIndex((l) => l.trim().startsWith(spec.header));
|
|
788
|
+
const reverseTailIdx = [...mapLines].reverse().findIndex((l) => l.trim() === spec.footer);
|
|
789
|
+
const tailIdx = reverseTailIdx === -1 ? -1 : mapLines.length - 1 - reverseTailIdx;
|
|
790
|
+
if (headIdx === -1 || tailIdx <= headIdx) {
|
|
791
|
+
if (force) {
|
|
792
|
+
const forcedBlock = normalizeBlock([spec.header, ...desired.map((m) => specEntryLine(kind, importAlias(kind, m.id))), spec.footer].join("\n"));
|
|
793
|
+
if (forcedBlock !== mapRange.block) {
|
|
794
|
+
next = replaceBlock(next, mapRange, forcedBlock);
|
|
795
|
+
changed = true;
|
|
796
|
+
}
|
|
797
|
+
return { content: next, changed, warnings, moduleIds: desired.map((m) => m.id) };
|
|
798
|
+
}
|
|
799
|
+
warnings.push(`[${kind}] ${spec.mapMarker} \u951A\u533A\u7ED3\u6784\u4E0D\u7B26\u5408\u9884\u671F\uFF0C\u964D\u7EA7\u4E3A\u544A\u8B66\u4E0D\u8986\u76D6`);
|
|
800
|
+
return { content: next, changed, warnings, moduleIds: [] };
|
|
801
|
+
}
|
|
802
|
+
const preservedHead = mapLines.slice(0, headIdx);
|
|
803
|
+
const preservedTail = mapLines.slice(tailIdx + 1);
|
|
804
|
+
const body = mapLines.slice(headIdx + 1, tailIdx);
|
|
805
|
+
const keptEntries = [];
|
|
806
|
+
for (const line of body) {
|
|
807
|
+
const m = line.match(spec.entryPattern);
|
|
808
|
+
if (!m) {
|
|
809
|
+
if (line.trim() === "") continue;
|
|
810
|
+
if (!force) warnings.push(`[${kind}] ${spec.mapMarker} \u951A\u533A\u975E\u6807\u884C\u4FDD\u7559: ${line.trim()}`);
|
|
811
|
+
if (!force) keptEntries.push(line);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const localName = m[1];
|
|
815
|
+
if (desiredByAlias.has(localName)) {
|
|
816
|
+
keptEntries.push(specEntryLine(kind, localName));
|
|
817
|
+
} else {
|
|
818
|
+
changed = true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const m of desired) {
|
|
822
|
+
const localName = importAlias(kind, m.id);
|
|
823
|
+
if (!keptEntries.some((l) => l.includes(localName))) {
|
|
824
|
+
keptEntries.push(specEntryLine(kind, localName));
|
|
825
|
+
changed = true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const newMapBlock = normalizeBlock(
|
|
829
|
+
[...preservedHead, spec.header, ...keptEntries, spec.footer, ...preservedTail].join("\n")
|
|
830
|
+
);
|
|
831
|
+
if (newMapBlock !== mapRange.block) {
|
|
832
|
+
next = replaceBlock(next, mapRange, newMapBlock);
|
|
833
|
+
changed = true;
|
|
834
|
+
}
|
|
835
|
+
return { content: next, changed, warnings, moduleIds: desired.map((m) => m.id) };
|
|
836
|
+
}
|
|
837
|
+
function normalizeBlock(block) {
|
|
838
|
+
const lines = block.split(/\r?\n/);
|
|
839
|
+
while (lines.length > 0 && lines[0].trim() === "") lines.shift();
|
|
840
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
841
|
+
return "\n" + lines.join("\n") + "\n";
|
|
842
|
+
}
|
|
843
|
+
function specImportLine(kind, localName, pkg) {
|
|
844
|
+
const spec = KINDS[kind];
|
|
845
|
+
return `import { ${spec.importExport} as ${localName} } from '${pkg}/${spec.importSuffix}';`;
|
|
846
|
+
}
|
|
847
|
+
function specEntryLine(kind, localName) {
|
|
848
|
+
return ` ${localName},`;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/declared.ts
|
|
852
|
+
import { existsSync as existsSync5 } from "fs";
|
|
853
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
854
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
855
|
+
import path7 from "path";
|
|
856
|
+
|
|
857
|
+
// src/component-schema.ts
|
|
858
|
+
import { z } from "zod";
|
|
859
|
+
var cardContribSchema = z.object({
|
|
860
|
+
cardType: z.string(),
|
|
861
|
+
schemaVersion: z.number().optional()
|
|
862
|
+
});
|
|
863
|
+
var pageContribSchema = z.object({
|
|
864
|
+
route: z.string(),
|
|
865
|
+
component: z.string()
|
|
866
|
+
});
|
|
867
|
+
var tabContribSchema = z.object({
|
|
868
|
+
route: z.string(),
|
|
869
|
+
label: z.string(),
|
|
870
|
+
icon: z.string().optional(),
|
|
871
|
+
order: z.number().optional()
|
|
872
|
+
});
|
|
873
|
+
var moduleDependencySchema = z.object({
|
|
874
|
+
id: z.string(),
|
|
875
|
+
range: z.string().optional(),
|
|
876
|
+
required: z.boolean().default(true)
|
|
877
|
+
});
|
|
878
|
+
var componentDescriptorSchema = z.object({
|
|
879
|
+
schemaVersion: z.number().default(1),
|
|
880
|
+
name: z.string(),
|
|
881
|
+
version: z.string().default("0.0.0"),
|
|
882
|
+
kind: z.enum(["platform", "business", "third-party"]).default("business"),
|
|
883
|
+
risk: z.object({
|
|
884
|
+
level: z.enum(["high", "medium", "low"]).default("low"),
|
|
885
|
+
writeBoundary: z.enum(["sdk-enforced", "source"]).default("source")
|
|
886
|
+
}).default({}),
|
|
887
|
+
distribution: z.object({
|
|
888
|
+
defaultMode: z.enum(["hybrid", "sdk", "codegen", "local"]).default("codegen")
|
|
889
|
+
}).default({}),
|
|
890
|
+
contributes: z.object({
|
|
891
|
+
handlers: z.array(z.string()).default([]),
|
|
892
|
+
tools: z.array(z.string()).default([]),
|
|
893
|
+
cards: z.array(cardContribSchema).default([]),
|
|
894
|
+
routes: z.array(z.string()).default([]),
|
|
895
|
+
pages: z.array(pageContribSchema).default([]),
|
|
896
|
+
tabs: z.array(tabContribSchema).default([])
|
|
897
|
+
}).default({}),
|
|
898
|
+
dependencies: z.object({
|
|
899
|
+
modules: z.array(moduleDependencySchema).default([])
|
|
900
|
+
}).default({}),
|
|
901
|
+
env: z.array(z.string()).default([])
|
|
902
|
+
}).strict();
|
|
903
|
+
function parseComponentDescriptor(raw) {
|
|
904
|
+
const result = componentDescriptorSchema.safeParse(raw);
|
|
905
|
+
if (result.success) return { ok: true, value: result.data };
|
|
906
|
+
return {
|
|
907
|
+
ok: false,
|
|
908
|
+
errors: result.error.issues.map(
|
|
909
|
+
(issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
910
|
+
)
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/declared.ts
|
|
915
|
+
function readComponentFile(dir) {
|
|
916
|
+
const file = path7.join(dir, "tbox.component.json");
|
|
917
|
+
if (!existsSync5(file)) return null;
|
|
918
|
+
try {
|
|
919
|
+
const parsed = parseComponentDescriptor(JSON.parse(readFileSync4(file, "utf8")));
|
|
920
|
+
return parsed.ok ? parsed.value : null;
|
|
921
|
+
} catch {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function probeModuleObjectExport(dir, sub, symbol) {
|
|
926
|
+
const file = path7.join(dir, "src", sub, "index.ts");
|
|
927
|
+
if (!existsSync5(file)) return false;
|
|
928
|
+
try {
|
|
929
|
+
const src = readFileSync4(file, "utf8");
|
|
930
|
+
return new RegExp(`export\\s+const\\s+${symbol}\\b`).test(src);
|
|
931
|
+
} catch {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function listLocalModules(appDir) {
|
|
936
|
+
const packagesDir = path7.join(appDir, "packages");
|
|
937
|
+
const result = [];
|
|
938
|
+
if (!existsSync5(packagesDir)) return result;
|
|
939
|
+
const entries = await readdir2(packagesDir, { withFileTypes: true });
|
|
940
|
+
for (const entry of entries) {
|
|
941
|
+
if (!entry.isDirectory()) continue;
|
|
942
|
+
const dir = path7.join(packagesDir, entry.name);
|
|
943
|
+
if (!existsSync5(path7.join(dir, "package.json"))) continue;
|
|
944
|
+
const descriptor = readComponentFile(dir);
|
|
945
|
+
if (descriptor) {
|
|
946
|
+
result.push({ id: descriptor.name ?? entry.name, dir, descriptor });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return result.sort((a, b) => a.id.localeCompare(b.id));
|
|
950
|
+
}
|
|
951
|
+
function readDescriptorFromNodeModules(appDir, pkg) {
|
|
952
|
+
try {
|
|
953
|
+
const p = path7.join(appDir, "node_modules", ...pkg.split("/"), "tbox.component.json");
|
|
954
|
+
if (!existsSync5(p)) return null;
|
|
955
|
+
const parsed = parseComponentDescriptor(JSON.parse(readFileSync4(p, "utf8")));
|
|
956
|
+
return parsed.ok ? parsed.value : null;
|
|
957
|
+
} catch {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function collectDeclared(appDir) {
|
|
962
|
+
const manifest = readAppManifest(appDir);
|
|
963
|
+
const declared = [];
|
|
964
|
+
const seen = /* @__PURE__ */ new Set();
|
|
965
|
+
for (const entry of manifest?.npmModules ?? []) {
|
|
966
|
+
seen.add(entry.id);
|
|
967
|
+
const local = path7.join(appDir, "packages", entry.id);
|
|
968
|
+
const descriptor = readComponentFile(local) ?? readDescriptorFromNodeModules(appDir, entry.package);
|
|
969
|
+
const probeDir = existsSync5(path7.join(local, "src")) ? local : path7.join(appDir, "node_modules", ...entry.package.split("/"));
|
|
970
|
+
const c = descriptor?.contributes;
|
|
971
|
+
declared.push({
|
|
972
|
+
id: entry.id,
|
|
973
|
+
pkg: entry.mode === "sdk" ? entry.package : `@app/${entry.id}`,
|
|
974
|
+
mode: entry.mode,
|
|
975
|
+
hasServerEntry: descriptor !== null && probeModuleObjectExport(probeDir, "server", "serverModule"),
|
|
976
|
+
hasClientEntry: descriptor !== null && probeModuleObjectExport(probeDir, "client", "clientModule"),
|
|
977
|
+
hasCards: (c?.cards?.length ?? 0) > 0
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
for (const mod of await listLocalModules(appDir)) {
|
|
981
|
+
if (seen.has(mod.id)) continue;
|
|
982
|
+
const c = mod.descriptor.contributes;
|
|
983
|
+
declared.push({
|
|
984
|
+
id: mod.id,
|
|
985
|
+
pkg: `@app/${mod.id}`,
|
|
986
|
+
mode: "local",
|
|
987
|
+
hasServerEntry: probeModuleObjectExport(mod.dir, "server", "serverModule"),
|
|
988
|
+
hasClientEntry: probeModuleObjectExport(mod.dir, "client", "clientModule"),
|
|
989
|
+
hasCards: (c.cards?.length ?? 0) > 0
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return declared;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/deps-merge.ts
|
|
996
|
+
import { writeFile as writeFile5 } from "fs/promises";
|
|
997
|
+
import path8 from "path";
|
|
998
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
999
|
+
function readJsonOrNull(p) {
|
|
1000
|
+
try {
|
|
1001
|
+
return JSON.parse(readFileSyncSafe(p) ?? "{}");
|
|
1002
|
+
} catch {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function mergeModuleDeps(appDir, targetPackageJson, moduleDeps) {
|
|
1007
|
+
const file = path8.join(appDir, targetPackageJson);
|
|
1008
|
+
const pkg = readJsonOrNull(file) ?? {};
|
|
1009
|
+
const deps = pkg.dependencies ?? {};
|
|
1010
|
+
const merged = {};
|
|
1011
|
+
const conflicts = [];
|
|
1012
|
+
for (const [name, spec] of Object.entries(moduleDeps)) {
|
|
1013
|
+
if (isPlatform(name)) continue;
|
|
1014
|
+
if (spec === "workspace:*" || spec.startsWith("catalog:")) continue;
|
|
1015
|
+
const existing = deps[name];
|
|
1016
|
+
if (!existing) {
|
|
1017
|
+
deps[name] = spec;
|
|
1018
|
+
merged[name] = spec;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (existing === spec) continue;
|
|
1022
|
+
const inter = intersectRanges(existing, spec);
|
|
1023
|
+
if (inter) {
|
|
1024
|
+
if (inter !== existing) {
|
|
1025
|
+
deps[name] = inter;
|
|
1026
|
+
merged[name] = inter;
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
conflicts.push(`${name}: \u5DF2\u6709 ${existing}\uFF0C\u6A21\u5757\u8981\u6C42 ${spec}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
pkg.dependencies = deps;
|
|
1033
|
+
await writeFile5(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
1034
|
+
return { merged, conflicts };
|
|
1035
|
+
}
|
|
1036
|
+
function intersectRanges(a, b) {
|
|
1037
|
+
const clean = (s) => s.replace(/^\^/, "");
|
|
1038
|
+
const pa = parseVer(clean(a));
|
|
1039
|
+
const pb = parseVer(clean(b));
|
|
1040
|
+
if (!pa || !pb) return null;
|
|
1041
|
+
if (pa.major !== pb.major) return null;
|
|
1042
|
+
if (pa.minor > pb.minor) return a;
|
|
1043
|
+
if (pb.minor > pa.minor) return b;
|
|
1044
|
+
return pa.patch > pb.patch ? a : b;
|
|
1045
|
+
}
|
|
1046
|
+
function parseVer(v) {
|
|
1047
|
+
const m = v.match(/^(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
1048
|
+
if (!m) return null;
|
|
1049
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
1050
|
+
}
|
|
1051
|
+
function readFileSyncSafe(p) {
|
|
1052
|
+
try {
|
|
1053
|
+
return readFileSync5(p, "utf8");
|
|
1054
|
+
} catch {
|
|
1055
|
+
return void 0;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/sync-all.ts
|
|
1060
|
+
async function syncAll(appDir, opts = {}) {
|
|
1061
|
+
const declared = await collectDeclared(appDir);
|
|
1062
|
+
const warnings = [];
|
|
1063
|
+
const changedFiles = [];
|
|
1064
|
+
const depConflicts = [];
|
|
1065
|
+
for (const file of ASSEMBLY_FILES) {
|
|
1066
|
+
const full = path9.join(appDir, file.path);
|
|
1067
|
+
if (!existsSync6(full)) {
|
|
1068
|
+
warnings.push(`\u7F3A\u5C11\u88C5\u914D\u6587\u4EF6 ${file.path}`);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const content = await readFile3(full, "utf8");
|
|
1072
|
+
const result = syncAssemblyFile(content, file.kind, declared, opts.force ?? false);
|
|
1073
|
+
warnings.push(...result.warnings.map((w) => `${file.path}: ${w}`));
|
|
1074
|
+
if (result.changed) {
|
|
1075
|
+
await writeFile6(full, result.content, "utf8");
|
|
1076
|
+
changedFiles.push(file.path);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const targets = ["apps/server/package.json", "apps/client/package.json"];
|
|
1080
|
+
for (const mod of declared) {
|
|
1081
|
+
const pkgFile = path9.join(appDir, "packages", mod.id, "package.json");
|
|
1082
|
+
if (!existsSync6(pkgFile)) continue;
|
|
1083
|
+
let deps = {};
|
|
1084
|
+
try {
|
|
1085
|
+
deps = JSON.parse(readFileSync6(pkgFile, "utf8")).dependencies ?? {};
|
|
1086
|
+
} catch {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
for (const target of targets) {
|
|
1090
|
+
if (!existsSync6(path9.join(appDir, target))) continue;
|
|
1091
|
+
const result = await mergeModuleDeps(appDir, target, deps);
|
|
1092
|
+
depConflicts.push(...result.conflicts.map((c) => `${mod.id} \u2192 ${target}: ${c}`));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { declared, changedFiles, warnings, depConflicts };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/diff.ts
|
|
1099
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1100
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1101
|
+
import path10 from "path";
|
|
1102
|
+
async function collectActual(appDir) {
|
|
1103
|
+
const result = {};
|
|
1104
|
+
for (const file of ASSEMBLY_FILES) {
|
|
1105
|
+
const full = path10.join(appDir, file.path);
|
|
1106
|
+
if (!existsSync7(full)) {
|
|
1107
|
+
result[file.kind] = { moduleIds: [], nonStandardLines: [], broken: ["missing-file"] };
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
const content = await readFile4(full, "utf8");
|
|
1111
|
+
const parsed = parseAssemblyFile(content, file.kind);
|
|
1112
|
+
result[file.kind] = {
|
|
1113
|
+
moduleIds: parsed.moduleIds,
|
|
1114
|
+
nonStandardLines: parsed.nonStandardLines,
|
|
1115
|
+
broken: parsed.broken
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/doctor/rules/workspace-dag.ts
|
|
1122
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1123
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1124
|
+
import path12 from "path";
|
|
1125
|
+
|
|
1126
|
+
// src/doctor/util.ts
|
|
1127
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1128
|
+
import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
|
|
1129
|
+
import path11 from "path";
|
|
1130
|
+
async function walkFiles(dir, base, out) {
|
|
1131
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1132
|
+
for (const entry of entries) {
|
|
1133
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
|
|
1134
|
+
const full = path11.join(dir, entry.name);
|
|
1135
|
+
if (entry.isDirectory()) await walkFiles(full, base, out);
|
|
1136
|
+
else if (entry.isFile() && /\.(ts|tsx|mjs|js)$/.test(entry.name)) {
|
|
1137
|
+
out.push(path11.relative(base, full).split(path11.sep).join("/"));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
async function readTextOrEmpty(p) {
|
|
1142
|
+
try {
|
|
1143
|
+
return await readFile5(p, "utf8");
|
|
1144
|
+
} catch {
|
|
1145
|
+
return "";
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function readJsonOrNull2(p) {
|
|
1149
|
+
try {
|
|
1150
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function readPkgJson(dir) {
|
|
1156
|
+
return readJsonOrNull2(path11.join(dir, "package.json"));
|
|
1157
|
+
}
|
|
1158
|
+
function intersectRanges2(a, b) {
|
|
1159
|
+
const clean = (s) => s.replace(/^\^/, "");
|
|
1160
|
+
const pa = parseVer2(clean(a));
|
|
1161
|
+
const pb = parseVer2(clean(b));
|
|
1162
|
+
if (!pa || !pb) return null;
|
|
1163
|
+
if (pa.major !== pb.major) return null;
|
|
1164
|
+
if (pa.minor > pb.minor) return a;
|
|
1165
|
+
if (pb.minor > pa.minor) return b;
|
|
1166
|
+
return pa.patch > pb.patch ? a : b;
|
|
1167
|
+
}
|
|
1168
|
+
function parseVer2(v) {
|
|
1169
|
+
const m = v.match(/^(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
1170
|
+
if (!m) return null;
|
|
1171
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/doctor/rules/workspace-dag.ts
|
|
1175
|
+
async function workspaceDagRule(ctx) {
|
|
1176
|
+
const issues = [];
|
|
1177
|
+
const packagesDir = path12.join(ctx.appDir, "packages");
|
|
1178
|
+
if (!existsSync8(packagesDir)) return issues;
|
|
1179
|
+
for (const entry of await readdir4(packagesDir, { withFileTypes: true })) {
|
|
1180
|
+
if (!entry.isDirectory()) continue;
|
|
1181
|
+
const dir = path12.join(packagesDir, entry.name);
|
|
1182
|
+
if (!existsSync8(path12.join(dir, "tbox.component.json"))) continue;
|
|
1183
|
+
const pkg = readPkgJson(dir);
|
|
1184
|
+
const allDeps = { ...pkg?.dependencies ?? {}, ...pkg?.peerDependencies ?? {} };
|
|
1185
|
+
for (const name of Object.keys(allDeps)) {
|
|
1186
|
+
if (/^@app\/(module|scenario)-/.test(name) || /^@tbox\.cn\/app-(module|scenario)-/.test(name)) {
|
|
1187
|
+
issues.push({
|
|
1188
|
+
rule: "workspace-dag",
|
|
1189
|
+
level: "error",
|
|
1190
|
+
message: `${entry.name} \u4F9D\u8D56\u4E1A\u52A1\u6A21\u5757 ${name}\uFF08module \u4E0D\u5F97\u4F9D\u8D56 module/scenario\uFF09`
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return issues;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/doctor/rules/register-pure.ts
|
|
1199
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1200
|
+
import { readdir as readdir5 } from "fs/promises";
|
|
1201
|
+
import path13 from "path";
|
|
1202
|
+
|
|
1203
|
+
// src/doctor/ast.ts
|
|
1204
|
+
import ts from "typescript";
|
|
1205
|
+
function parseSource(src) {
|
|
1206
|
+
return ts.createSourceFile("entry.ts", src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
1207
|
+
}
|
|
1208
|
+
function unwrapAs(expr) {
|
|
1209
|
+
if (!expr) return expr;
|
|
1210
|
+
if (ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) return expr.expression;
|
|
1211
|
+
return expr;
|
|
1212
|
+
}
|
|
1213
|
+
function findFunctionBody(src, name) {
|
|
1214
|
+
const sf = parseSource(src);
|
|
1215
|
+
let result = null;
|
|
1216
|
+
function visit(node) {
|
|
1217
|
+
if (result) return;
|
|
1218
|
+
if (ts.isFunctionDeclaration(node) && node.name?.text === name && node.body) {
|
|
1219
|
+
result = node.body;
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (ts.isVariableStatement(node) && node.declarationList.declarations.some(
|
|
1223
|
+
(d) => ts.isIdentifier(d.name) && d.name.text === name
|
|
1224
|
+
)) {
|
|
1225
|
+
const decl = node.declarationList.declarations.find(
|
|
1226
|
+
(d) => ts.isIdentifier(d.name) && d.name.text === name
|
|
1227
|
+
);
|
|
1228
|
+
const init = unwrapAs(decl.initializer);
|
|
1229
|
+
if (init && ts.isArrowFunction(init) && init.body && ts.isBlock(init.body)) {
|
|
1230
|
+
result = init.body;
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const methodBody = objectMethodBody(init, "register");
|
|
1234
|
+
if (methodBody) {
|
|
1235
|
+
result = methodBody;
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
ts.forEachChild(node, visit);
|
|
1240
|
+
}
|
|
1241
|
+
visit(sf);
|
|
1242
|
+
return result;
|
|
1243
|
+
}
|
|
1244
|
+
function objectMethodBody(init, methodName) {
|
|
1245
|
+
if (!init || !ts.isObjectLiteralExpression(init)) return null;
|
|
1246
|
+
for (const prop of init.properties) {
|
|
1247
|
+
if (ts.isMethodDeclaration(prop) && prop.name.getText() === methodName && prop.body) {
|
|
1248
|
+
return prop.body;
|
|
1249
|
+
}
|
|
1250
|
+
if (ts.isPropertyAssignment(prop) && prop.name.getText() === methodName && prop.initializer) {
|
|
1251
|
+
const fn = prop.initializer;
|
|
1252
|
+
if (ts.isArrowFunction(fn) && fn.body && ts.isBlock(fn.body)) return fn.body;
|
|
1253
|
+
if (ts.isFunctionExpression(fn) && fn.body) return fn.body;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
function findModuleRegisterBody(src, objectName) {
|
|
1259
|
+
const sf = parseSource(src);
|
|
1260
|
+
let result = null;
|
|
1261
|
+
function visit(node) {
|
|
1262
|
+
if (result) return;
|
|
1263
|
+
if (ts.isVariableStatement(node) && node.declarationList.declarations.some(
|
|
1264
|
+
(d) => ts.isIdentifier(d.name) && d.name.text === objectName
|
|
1265
|
+
)) {
|
|
1266
|
+
const decl = node.declarationList.declarations.find(
|
|
1267
|
+
(d) => ts.isIdentifier(d.name) && d.name.text === objectName
|
|
1268
|
+
);
|
|
1269
|
+
const methodBody = objectMethodBody(unwrapAs(decl.initializer), "register");
|
|
1270
|
+
if (methodBody) {
|
|
1271
|
+
result = methodBody;
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
ts.forEachChild(node, visit);
|
|
1276
|
+
}
|
|
1277
|
+
visit(sf);
|
|
1278
|
+
return result ?? findFunctionBody(src, "registerServer");
|
|
1279
|
+
}
|
|
1280
|
+
function hasTopLevelResolveService(body) {
|
|
1281
|
+
let found = false;
|
|
1282
|
+
function visit(node) {
|
|
1283
|
+
if (found) return;
|
|
1284
|
+
if (ts.isArrowFunction(node)) return;
|
|
1285
|
+
if (ts.isCallExpression(node)) {
|
|
1286
|
+
const expr = node.expression;
|
|
1287
|
+
if (ts.isIdentifier(expr) && expr.text === "resolveService") {
|
|
1288
|
+
found = true;
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const text = expr.getText();
|
|
1292
|
+
if (text === "ctx.resolveService" || text.endsWith(".services.resolve")) {
|
|
1293
|
+
found = true;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
ts.forEachChild(node, visit);
|
|
1298
|
+
}
|
|
1299
|
+
visit(body);
|
|
1300
|
+
return found;
|
|
1301
|
+
}
|
|
1302
|
+
function hasCardsRegisterCall(body) {
|
|
1303
|
+
let found = false;
|
|
1304
|
+
function visit(node) {
|
|
1305
|
+
if (found) return;
|
|
1306
|
+
if (ts.isCallExpression(node)) {
|
|
1307
|
+
const text = node.expression.getText();
|
|
1308
|
+
if (text.endsWith(".cards.registerMap") || text.endsWith(".cards.register")) {
|
|
1309
|
+
found = true;
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
ts.forEachChild(node, visit);
|
|
1314
|
+
}
|
|
1315
|
+
visit(body);
|
|
1316
|
+
return found;
|
|
1317
|
+
}
|
|
1318
|
+
function extractCardsRegisterArgs(src) {
|
|
1319
|
+
const sf = parseSource(src);
|
|
1320
|
+
const args = [];
|
|
1321
|
+
function visit(node) {
|
|
1322
|
+
if (ts.isCallExpression(node)) {
|
|
1323
|
+
const exprText = node.expression.getText();
|
|
1324
|
+
if (exprText.endsWith(".cards.register")) {
|
|
1325
|
+
const arg = node.arguments[0];
|
|
1326
|
+
if (arg) args.push(arg.getText().replace(/['"]/g, ""));
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
ts.forEachChild(node, visit);
|
|
1330
|
+
}
|
|
1331
|
+
visit(sf);
|
|
1332
|
+
return args;
|
|
1333
|
+
}
|
|
1334
|
+
function extractRelativeImports(src) {
|
|
1335
|
+
const sf = parseSource(src);
|
|
1336
|
+
const map = {};
|
|
1337
|
+
function visit(node) {
|
|
1338
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
1339
|
+
const spec = node.moduleSpecifier.text;
|
|
1340
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
1341
|
+
const clause = node.importClause;
|
|
1342
|
+
if (clause?.name) {
|
|
1343
|
+
map[clause.name.text] = spec;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
ts.forEachChild(node, visit);
|
|
1348
|
+
}
|
|
1349
|
+
visit(sf);
|
|
1350
|
+
return map;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/doctor/rules/register-pure.ts
|
|
1354
|
+
async function registerPureRule(ctx) {
|
|
1355
|
+
const issues = [];
|
|
1356
|
+
const packagesDir = path13.join(ctx.appDir, "packages");
|
|
1357
|
+
if (!existsSync9(packagesDir)) return issues;
|
|
1358
|
+
for (const entry of await readdir5(packagesDir, { withFileTypes: true })) {
|
|
1359
|
+
if (!entry.isDirectory()) continue;
|
|
1360
|
+
const entryFile = path13.join(packagesDir, entry.name, "src/server/index.ts");
|
|
1361
|
+
if (!existsSync9(entryFile)) continue;
|
|
1362
|
+
const src = await readTextOrEmpty(entryFile);
|
|
1363
|
+
const body = findModuleRegisterBody(src, "serverModule");
|
|
1364
|
+
if (body && hasTopLevelResolveService(body)) {
|
|
1365
|
+
issues.push({
|
|
1366
|
+
rule: "register-pure",
|
|
1367
|
+
level: "error",
|
|
1368
|
+
message: `${entry.name}: \u6CE8\u518C\u5165\u53E3\uFF08serverModule.register\uFF09\u5185\u51FA\u73B0 resolveService\uFF08\u53EA\u5141\u8BB8\u5728 handle/\u4E8B\u4EF6\u56DE\u8C03\u5185\uFF09`
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return issues;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/doctor/module-cards.ts
|
|
1376
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1377
|
+
import path14 from "path";
|
|
1378
|
+
import ts2 from "typescript";
|
|
1379
|
+
function unwrapAs2(expr) {
|
|
1380
|
+
if (ts2.isAsExpression(expr) || ts2.isSatisfiesExpression(expr)) return expr.expression;
|
|
1381
|
+
return expr;
|
|
1382
|
+
}
|
|
1383
|
+
function parseCardsObject(src, objectName) {
|
|
1384
|
+
const sf = parseSource(src);
|
|
1385
|
+
let result = null;
|
|
1386
|
+
function mapFromObject(obj) {
|
|
1387
|
+
const map = {};
|
|
1388
|
+
for (const prop of obj.properties) {
|
|
1389
|
+
if (!ts2.isPropertyAssignment(prop)) continue;
|
|
1390
|
+
const name = prop.name;
|
|
1391
|
+
const key = ts2.isIdentifier(name) ? name.text : ts2.isStringLiteral(name) ? name.text : "";
|
|
1392
|
+
if (key) map[key] = prop.initializer;
|
|
1393
|
+
}
|
|
1394
|
+
return map;
|
|
1395
|
+
}
|
|
1396
|
+
for (const stmt of sf.statements) {
|
|
1397
|
+
if (!ts2.isVariableStatement(stmt)) continue;
|
|
1398
|
+
for (const d of stmt.declarationList.declarations) {
|
|
1399
|
+
if (!ts2.isIdentifier(d.name) || !d.initializer) continue;
|
|
1400
|
+
if (d.name.text === "cards") {
|
|
1401
|
+
const init = unwrapAs2(d.initializer);
|
|
1402
|
+
if (ts2.isObjectLiteralExpression(init)) {
|
|
1403
|
+
result = mapFromObject(init);
|
|
1404
|
+
return result;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (objectName && d.name.text === objectName) {
|
|
1408
|
+
const init = unwrapAs2(d.initializer);
|
|
1409
|
+
if (ts2.isObjectLiteralExpression(init)) {
|
|
1410
|
+
for (const prop of init.properties) {
|
|
1411
|
+
if (ts2.isPropertyAssignment(prop) && ts2.isIdentifier(prop.name) && prop.name.text === "cards") {
|
|
1412
|
+
const cardsInit = unwrapAs2(prop.initializer);
|
|
1413
|
+
if (ts2.isObjectLiteralExpression(cardsInit)) {
|
|
1414
|
+
result = mapFromObject(cardsInit);
|
|
1415
|
+
return result;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return result;
|
|
1424
|
+
}
|
|
1425
|
+
function parseClientCardsMap(src) {
|
|
1426
|
+
const map = parseCardsObject(src, "clientModule");
|
|
1427
|
+
return map ? Object.keys(map) : [];
|
|
1428
|
+
}
|
|
1429
|
+
function extractMetaFields(src) {
|
|
1430
|
+
const ct = src.match(/cardType:\s*['"]([^'"]+)['"]/);
|
|
1431
|
+
const sv = src.match(/schemaVersion:\s*(\d+)/);
|
|
1432
|
+
return {
|
|
1433
|
+
cardType: ct?.[1],
|
|
1434
|
+
schemaVersion: sv ? Number(sv[1]) : void 0
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function resolveTsPath(base, rel) {
|
|
1438
|
+
const candidates = [rel, `${rel}.ts`, `${rel}.tsx`, `${rel}/index.ts`, `${rel}/index.tsx`];
|
|
1439
|
+
for (const c of candidates) {
|
|
1440
|
+
if (existsSync10(path14.join(base, c))) return path14.join(base, c);
|
|
1441
|
+
}
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
function resolveValueCardType(expr, imports, baseDir) {
|
|
1445
|
+
const e = unwrapAs2(expr);
|
|
1446
|
+
if (ts2.isObjectLiteralExpression(e)) {
|
|
1447
|
+
const fields = extractMetaFields(e.getText());
|
|
1448
|
+
return { cardType: fields.cardType, schemaVersion: fields.schemaVersion };
|
|
1449
|
+
}
|
|
1450
|
+
if (ts2.isStringLiteral(e)) {
|
|
1451
|
+
return { cardType: e.text };
|
|
1452
|
+
}
|
|
1453
|
+
if (ts2.isIdentifier(e)) {
|
|
1454
|
+
const rel = imports[e.text];
|
|
1455
|
+
if (!rel) return null;
|
|
1456
|
+
const resolved = resolveTsPath(baseDir, rel.replace(/^\.\//, ""));
|
|
1457
|
+
if (!resolved) return null;
|
|
1458
|
+
return { resolved };
|
|
1459
|
+
}
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
async function resolveServerRegisteredCards(modDir) {
|
|
1463
|
+
const entryFile = path14.join(modDir, "src/server/index.ts");
|
|
1464
|
+
if (!existsSync10(entryFile)) return { cardTypes: [], schemaVersions: {}, keyMismatches: [], registerMissing: false };
|
|
1465
|
+
const entrySrc = await readTextOrEmpty(entryFile);
|
|
1466
|
+
const imports = extractRelativeImports(entrySrc);
|
|
1467
|
+
const baseDir = path14.join(modDir, "src/server");
|
|
1468
|
+
const cardTypes = [];
|
|
1469
|
+
const schemaVersions = {};
|
|
1470
|
+
const keyMismatches = [];
|
|
1471
|
+
const cardsMap = parseCardsObject(entrySrc, "serverModule");
|
|
1472
|
+
if (cardsMap) {
|
|
1473
|
+
let registerMissing = false;
|
|
1474
|
+
if (Object.keys(cardsMap).length > 0) {
|
|
1475
|
+
const registerBody = findModuleRegisterBody(entrySrc, "serverModule");
|
|
1476
|
+
registerMissing = !registerBody || !hasCardsRegisterCall(registerBody);
|
|
1477
|
+
}
|
|
1478
|
+
for (const [key, expr] of Object.entries(cardsMap)) {
|
|
1479
|
+
const value = resolveValueCardType(expr, imports, baseDir);
|
|
1480
|
+
if (!value) continue;
|
|
1481
|
+
if (value.resolved) {
|
|
1482
|
+
const metaSrc = await readTextOrEmpty(value.resolved);
|
|
1483
|
+
const fields = extractMetaFields(metaSrc);
|
|
1484
|
+
if (fields.cardType) {
|
|
1485
|
+
cardTypes.push(fields.cardType);
|
|
1486
|
+
if (fields.schemaVersion) schemaVersions[fields.cardType] = fields.schemaVersion;
|
|
1487
|
+
if (key !== fields.cardType) keyMismatches.push({ key, cardType: fields.cardType });
|
|
1488
|
+
}
|
|
1489
|
+
} else if (value.cardType) {
|
|
1490
|
+
cardTypes.push(value.cardType);
|
|
1491
|
+
if (value.schemaVersion) schemaVersions[value.cardType] = value.schemaVersion;
|
|
1492
|
+
if (key !== value.cardType) keyMismatches.push({ key, cardType: value.cardType });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return { cardTypes: [...new Set(cardTypes)], schemaVersions, keyMismatches, registerMissing };
|
|
1496
|
+
}
|
|
1497
|
+
for (const arg of extractCardsRegisterArgs(entrySrc)) {
|
|
1498
|
+
if (arg.includes("cardType:")) {
|
|
1499
|
+
const fields2 = extractMetaFields(arg);
|
|
1500
|
+
if (fields2.cardType) {
|
|
1501
|
+
cardTypes.push(fields2.cardType);
|
|
1502
|
+
if (fields2.schemaVersion) schemaVersions[fields2.cardType] = fields2.schemaVersion;
|
|
1503
|
+
}
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (/^[a-zA-Z0-9][\w-]*$/.test(arg) && !imports[arg]) {
|
|
1507
|
+
cardTypes.push(arg);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
const rel = imports[arg];
|
|
1511
|
+
if (!rel) continue;
|
|
1512
|
+
const resolved = resolveTsPath(baseDir, rel.replace(/^\.\//, ""));
|
|
1513
|
+
if (!resolved) continue;
|
|
1514
|
+
const metaSrc = await readTextOrEmpty(resolved);
|
|
1515
|
+
const fields = extractMetaFields(metaSrc);
|
|
1516
|
+
if (fields.cardType) {
|
|
1517
|
+
cardTypes.push(fields.cardType);
|
|
1518
|
+
if (fields.schemaVersion) schemaVersions[fields.cardType] = fields.schemaVersion;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return { cardTypes: [...new Set(cardTypes)], schemaVersions, keyMismatches, registerMissing: false };
|
|
1522
|
+
}
|
|
1523
|
+
function hasModuleObject(src, objectName) {
|
|
1524
|
+
const sf = parseSource(src);
|
|
1525
|
+
return sf.statements.some(
|
|
1526
|
+
(stmt) => ts2.isVariableStatement(stmt) && stmt.declarationList.declarations.some(
|
|
1527
|
+
(d) => ts2.isIdentifier(d.name) && d.name.text === objectName
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
async function resolveClientRegisterMissing(modDir) {
|
|
1532
|
+
const entryFile = path14.join(modDir, "src/client/index.ts");
|
|
1533
|
+
if (!existsSync10(entryFile)) return false;
|
|
1534
|
+
const entrySrc = await readTextOrEmpty(entryFile);
|
|
1535
|
+
if (!hasModuleObject(entrySrc, "clientModule")) return false;
|
|
1536
|
+
const cardsMap = parseCardsObject(entrySrc, "clientModule");
|
|
1537
|
+
if (!cardsMap || Object.keys(cardsMap).length === 0) return false;
|
|
1538
|
+
const registerBody = findModuleRegisterBody(entrySrc, "clientModule");
|
|
1539
|
+
return !registerBody || !hasCardsRegisterCall(registerBody);
|
|
1540
|
+
}
|
|
1541
|
+
async function collectModuleCards(ctx) {
|
|
1542
|
+
const modules = await listLocalModules(ctx.appDir);
|
|
1543
|
+
const result = [];
|
|
1544
|
+
for (const mod of modules) {
|
|
1545
|
+
const declared = {};
|
|
1546
|
+
for (const c of mod.descriptor.contributes.cards ?? []) {
|
|
1547
|
+
declared[c.cardType] = c.schemaVersion ?? 1;
|
|
1548
|
+
}
|
|
1549
|
+
const clientFile = path14.join(mod.dir, "src/client/index.ts");
|
|
1550
|
+
const clientMap = existsSync10(clientFile) ? parseClientCardsMap(await readTextOrEmpty(clientFile)) : [];
|
|
1551
|
+
const server = await resolveServerRegisteredCards(mod.dir);
|
|
1552
|
+
result.push({
|
|
1553
|
+
id: mod.id,
|
|
1554
|
+
declared,
|
|
1555
|
+
clientMap,
|
|
1556
|
+
serverRegistered: server.cardTypes,
|
|
1557
|
+
serverSchemaVersions: server.schemaVersions,
|
|
1558
|
+
serverKeyMismatches: server.keyMismatches,
|
|
1559
|
+
serverRegisterMissing: server.registerMissing,
|
|
1560
|
+
clientRegisterMissing: await resolveClientRegisterMissing(mod.dir)
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
return result;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/doctor/rules/card-type-consistent.ts
|
|
1567
|
+
async function cardTypeConsistentRule(ctx) {
|
|
1568
|
+
const issues = [];
|
|
1569
|
+
for (const mod of await collectModuleCards(ctx)) {
|
|
1570
|
+
const declared = Object.keys(mod.declared);
|
|
1571
|
+
if (declared.length === 0 && mod.serverKeyMismatches.length === 0 && !mod.serverRegisterMissing && !mod.clientRegisterMissing) {
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
for (const mm of mod.serverKeyMismatches) {
|
|
1575
|
+
issues.push({
|
|
1576
|
+
rule: "card-type-consistent",
|
|
1577
|
+
level: "error",
|
|
1578
|
+
message: `${mod.id}: serverModule.cards key "${mm.key}" !== meta.cardType "${mm.cardType}"\uFF08key \u7EA6\u5B9A = cardType\uFF09`
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
if (mod.serverRegisterMissing) {
|
|
1582
|
+
issues.push({
|
|
1583
|
+
rule: "card-type-consistent",
|
|
1584
|
+
level: "error",
|
|
1585
|
+
message: `${mod.id}: serverModule \u58F0\u660E\u4E86 cards \u4F46 register \u5185\u672A\u8C03\u7528 ctx.cards.registerMap(cards)\uFF08\u6CE8\u518C\u52A8\u4F5C\u7F3A\u5931\uFF0C\u8FD0\u884C\u65F6\u7F3A\u5361\uFF09`
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
if (mod.clientRegisterMissing) {
|
|
1589
|
+
issues.push({
|
|
1590
|
+
rule: "card-type-consistent",
|
|
1591
|
+
level: "error",
|
|
1592
|
+
message: `${mod.id}: clientModule \u58F0\u660E\u4E86 cards \u4F46 register \u5185\u672A\u8C03\u7528 ctx.cards.registerMap(cards)\uFF08\u6CE8\u518C\u52A8\u4F5C\u7F3A\u5931\uFF0C\u8FD0\u884C\u65F6\u7F3A\u5361\uFF09`
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
const missingClient = declared.filter((t) => !mod.clientMap.includes(t));
|
|
1596
|
+
if (missingClient.length > 0) {
|
|
1597
|
+
issues.push({
|
|
1598
|
+
rule: "card-type-consistent",
|
|
1599
|
+
level: "error",
|
|
1600
|
+
message: `${mod.id}: \u58F0\u660E\u5361\u7247 ${missingClient.join(",")} \u672A\u5728\u5BA2\u6237\u7AEF cards map \u5BFC\u51FA`
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
const missingServer = declared.filter((t) => !mod.serverRegistered.includes(t));
|
|
1604
|
+
if (missingServer.length > 0) {
|
|
1605
|
+
issues.push({
|
|
1606
|
+
rule: "card-type-consistent",
|
|
1607
|
+
level: "error",
|
|
1608
|
+
message: `${mod.id}: \u58F0\u660E\u5361\u7247 ${missingServer.join(",")} \u672A\u5728\u670D\u52A1\u7AEF\u6CE8\u518C\uFF08serverModule.cards meta / cards.register\uFF09`
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
const extraClient = mod.clientMap.filter((t) => !declared.includes(t));
|
|
1612
|
+
if (extraClient.length > 0) {
|
|
1613
|
+
issues.push({
|
|
1614
|
+
rule: "card-type-consistent",
|
|
1615
|
+
level: "warning",
|
|
1616
|
+
message: `${mod.id}: \u5BA2\u6237\u7AEF\u5BFC\u51FA\u672A\u58F0\u660E\u7684\u5361\u7247 ${extraClient.join(",")}`
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
const extraServer = mod.serverRegistered.filter((t) => !declared.includes(t));
|
|
1620
|
+
if (extraServer.length > 0) {
|
|
1621
|
+
issues.push({
|
|
1622
|
+
rule: "card-type-consistent",
|
|
1623
|
+
level: "warning",
|
|
1624
|
+
message: `${mod.id}: \u670D\u52A1\u7AEF\u6CE8\u518C\u672A\u58F0\u660E\u7684\u5361\u7247 ${extraServer.join(",")}`
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return issues;
|
|
1629
|
+
}
|
|
1630
|
+
async function clientCardCoverRule(ctx) {
|
|
1631
|
+
const issues = [];
|
|
1632
|
+
const mods = await collectModuleCards(ctx);
|
|
1633
|
+
const registered = {};
|
|
1634
|
+
for (const mod of mods) {
|
|
1635
|
+
for (const t of mod.serverRegistered) {
|
|
1636
|
+
if (t in registered) registered[t] += `,${mod.id}`;
|
|
1637
|
+
else registered[t] = mod.id;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (Object.keys(registered).length === 0) return issues;
|
|
1641
|
+
const assembled = /* @__PURE__ */ new Set();
|
|
1642
|
+
for (const mod of mods) {
|
|
1643
|
+
for (const t of mod.clientMap) assembled.add(t);
|
|
1644
|
+
}
|
|
1645
|
+
for (const [cardType, moduleIds] of Object.entries(registered)) {
|
|
1646
|
+
if (!assembled.has(cardType)) {
|
|
1647
|
+
issues.push({
|
|
1648
|
+
rule: "client-card-cover",
|
|
1649
|
+
level: "error",
|
|
1650
|
+
message: `\u670D\u52A1\u7AEF\u6CE8\u518C\u5361\u7247 ${cardType}\uFF08\u6A21\u5757 ${moduleIds}\uFF09\u672A\u51FA\u73B0\u5728\u5E94\u7528\u5BA2\u6237\u7AEF\u88C5\u914D\uFF08\u6F0F\u88C5\u914D\uFF09`
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return issues;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/doctor/rules/contract-version.ts
|
|
1658
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1659
|
+
import { readdir as readdir6, stat as stat2 } from "fs/promises";
|
|
1660
|
+
import path15 from "path";
|
|
1661
|
+
async function contractVersionRule(ctx) {
|
|
1662
|
+
const issues = [];
|
|
1663
|
+
const packagesDir = path15.join(ctx.appDir, "packages");
|
|
1664
|
+
if (!existsSync11(packagesDir)) return issues;
|
|
1665
|
+
const installed = /* @__PURE__ */ new Map();
|
|
1666
|
+
for (const entry of await readdir6(packagesDir, { withFileTypes: true })) {
|
|
1667
|
+
if (!entry.isDirectory()) continue;
|
|
1668
|
+
const pkg = readPkgJson(path15.join(packagesDir, entry.name));
|
|
1669
|
+
if (pkg?.name && pkg.name.startsWith("@tbox.cn/app-contracts")) {
|
|
1670
|
+
installed.set(pkg.name, pkg.version ?? "0.0.0");
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const nmScope = path15.join(ctx.appDir, "node_modules", "@tbox.cn");
|
|
1674
|
+
if (existsSync11(nmScope)) {
|
|
1675
|
+
for (const entry of await readdir6(nmScope, { withFileTypes: true })) {
|
|
1676
|
+
const full = path15.join(nmScope, entry.name);
|
|
1677
|
+
let isDir = entry.isDirectory();
|
|
1678
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
1679
|
+
try {
|
|
1680
|
+
isDir = (await stat2(full)).isDirectory();
|
|
1681
|
+
} catch {
|
|
1682
|
+
isDir = false;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (!isDir) continue;
|
|
1686
|
+
const pkg = readPkgJson(full);
|
|
1687
|
+
if (pkg?.name && pkg.name.startsWith("@tbox.cn/app-contracts")) {
|
|
1688
|
+
installed.set(pkg.name, pkg.version ?? "0.0.0");
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
for (const entry of await readdir6(packagesDir, { withFileTypes: true })) {
|
|
1693
|
+
if (!entry.isDirectory()) continue;
|
|
1694
|
+
const dir = path15.join(packagesDir, entry.name);
|
|
1695
|
+
if (!existsSync11(path15.join(dir, "tbox.component.json"))) continue;
|
|
1696
|
+
const pkg = readPkgJson(dir);
|
|
1697
|
+
const allDeps = { ...pkg?.dependencies ?? {}, ...pkg?.peerDependencies ?? {} };
|
|
1698
|
+
for (const [name, range] of Object.entries(allDeps)) {
|
|
1699
|
+
if (!name.startsWith("@tbox.cn/app-contracts")) continue;
|
|
1700
|
+
const installedVersion = installed.get(name);
|
|
1701
|
+
if (!installedVersion) continue;
|
|
1702
|
+
if (range === "*" || range.startsWith("workspace:") || range.startsWith("catalog:")) continue;
|
|
1703
|
+
const cleanRange = range.replace(/^\^/, "");
|
|
1704
|
+
if (cleanRange === installedVersion) continue;
|
|
1705
|
+
const inter = intersectRanges2(range, installedVersion);
|
|
1706
|
+
if (!inter) {
|
|
1707
|
+
issues.push({
|
|
1708
|
+
rule: "contract-version",
|
|
1709
|
+
level: "error",
|
|
1710
|
+
message: `${entry.name}: \u5951\u7EA6 ${name} \u8303\u56F4 ${range} \u4E0E\u5B9E\u88C5 ${installedVersion} \u65E0\u4EA4\u96C6`
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return issues;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/doctor/rules/module-deps-assembled.ts
|
|
1719
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1720
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
1721
|
+
import path16 from "path";
|
|
1722
|
+
async function moduleDepsAssembledRule(ctx) {
|
|
1723
|
+
const issues = [];
|
|
1724
|
+
const installedIds = /* @__PURE__ */ new Set();
|
|
1725
|
+
const packagesDir = path16.join(ctx.appDir, "packages");
|
|
1726
|
+
if (existsSync12(packagesDir)) {
|
|
1727
|
+
for (const entry of await readdir7(packagesDir, { withFileTypes: true })) {
|
|
1728
|
+
if (entry.isDirectory()) installedIds.add(entry.name);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
for (const m of ctx.declared) installedIds.add(m.id);
|
|
1732
|
+
const assembledIds = /* @__PURE__ */ new Set();
|
|
1733
|
+
for (const file of ASSEMBLY_FILES) {
|
|
1734
|
+
for (const id of ctx.actual[file.kind]?.moduleIds ?? []) assembledIds.add(id);
|
|
1735
|
+
}
|
|
1736
|
+
const depGraph = /* @__PURE__ */ new Map();
|
|
1737
|
+
const localMods = await listLocalModules(ctx.appDir);
|
|
1738
|
+
for (const mod of localMods) {
|
|
1739
|
+
const deps = (mod.descriptor.dependencies?.modules ?? []).map((d) => d.id);
|
|
1740
|
+
depGraph.set(mod.id, deps);
|
|
1741
|
+
}
|
|
1742
|
+
const WHITE = 0;
|
|
1743
|
+
const GRAY = 1;
|
|
1744
|
+
const BLACK = 2;
|
|
1745
|
+
const color = /* @__PURE__ */ new Map();
|
|
1746
|
+
const cycleStacks = [];
|
|
1747
|
+
function dfs(id, stack) {
|
|
1748
|
+
color.set(id, GRAY);
|
|
1749
|
+
stack.push(id);
|
|
1750
|
+
for (const dep of depGraph.get(id) ?? []) {
|
|
1751
|
+
const c = color.get(dep) ?? WHITE;
|
|
1752
|
+
if (c === GRAY) {
|
|
1753
|
+
const idx = stack.indexOf(dep);
|
|
1754
|
+
if (idx >= 0) cycleStacks.push([...stack.slice(idx), dep]);
|
|
1755
|
+
} else if (c === WHITE) {
|
|
1756
|
+
dfs(dep, stack);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
stack.pop();
|
|
1760
|
+
color.set(id, BLACK);
|
|
1761
|
+
}
|
|
1762
|
+
for (const id of depGraph.keys()) {
|
|
1763
|
+
if ((color.get(id) ?? WHITE) === WHITE) dfs(id, []);
|
|
1764
|
+
}
|
|
1765
|
+
for (const cycle of cycleStacks) {
|
|
1766
|
+
issues.push({
|
|
1767
|
+
rule: "module-deps-assembled",
|
|
1768
|
+
level: "error",
|
|
1769
|
+
message: `dependencies.modules \u58F0\u660E\u5C42\u5B58\u5728\u5FAA\u73AF\u4F9D\u8D56\uFF1A${cycle.join(" \u2192 ")}`
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
for (const mod of localMods) {
|
|
1773
|
+
for (const dep of mod.descriptor.dependencies?.modules ?? []) {
|
|
1774
|
+
if (dep.required && !installedIds.has(dep.id)) {
|
|
1775
|
+
issues.push({
|
|
1776
|
+
rule: "module-deps-assembled",
|
|
1777
|
+
level: "error",
|
|
1778
|
+
message: `${mod.id}: \u4F9D\u8D56\u6A21\u5757 ${dep.id} \u672A\u5B89\u88C5\uFF08\u7F3A\u5931 packages/${dep.id} \u6216 npm \u6E05\u5355\uFF09`
|
|
1779
|
+
});
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const depIsBusiness = localMods.some((m) => m.id === dep.id);
|
|
1783
|
+
if (depIsBusiness && dep.required && !assembledIds.has(dep.id)) {
|
|
1784
|
+
issues.push({
|
|
1785
|
+
rule: "module-deps-assembled",
|
|
1786
|
+
level: "error",
|
|
1787
|
+
message: `${mod.id}: \u4F9D\u8D56\u4E1A\u52A1\u6A21\u5757 ${dep.id} \u5DF2\u5B89\u88C5\u4F46\u672A\u88C5\u914D\uFF08modules.ts \u7F3A\u884C\uFF0C\u8FD0\u884C sync\uFF09`
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return issues;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// src/doctor/rules/singleton.ts
|
|
1796
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1797
|
+
import { readdir as readdir8 } from "fs/promises";
|
|
1798
|
+
import { createRequire } from "module";
|
|
1799
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
1800
|
+
import path17 from "path";
|
|
1801
|
+
async function singletonRule(ctx) {
|
|
1802
|
+
const issues = [];
|
|
1803
|
+
const candidates = ["react", "zod", "@mastra/core"];
|
|
1804
|
+
const dirs = ["apps/client", "apps/server", "packages/app-sdk", "packages/contracts"];
|
|
1805
|
+
const packagesDir = path17.join(ctx.appDir, "packages");
|
|
1806
|
+
if (existsSync13(packagesDir)) {
|
|
1807
|
+
for (const entry of await readdir8(packagesDir, { withFileTypes: true })) {
|
|
1808
|
+
if (entry.isDirectory() && existsSync13(path17.join(packagesDir, entry.name, "package.json"))) {
|
|
1809
|
+
dirs.push(path17.join("packages", entry.name));
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
for (const name of candidates) {
|
|
1814
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1815
|
+
for (const rel of dirs) {
|
|
1816
|
+
const dir = path17.join(ctx.appDir, rel);
|
|
1817
|
+
if (!existsSync13(path17.join(dir, "package.json"))) continue;
|
|
1818
|
+
const resolved = resolveFrom(dir, name);
|
|
1819
|
+
if (resolved) {
|
|
1820
|
+
const list = seen.get(resolved) ?? [];
|
|
1821
|
+
list.push(rel);
|
|
1822
|
+
seen.set(resolved, list);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (seen.size > 1) {
|
|
1826
|
+
issues.push({
|
|
1827
|
+
rule: "singleton",
|
|
1828
|
+
level: "error",
|
|
1829
|
+
message: `${name} \u89E3\u6790\u7ED3\u679C\u4E0D\u552F\u4E00\uFF1A${[...seen.entries()].map(([p, dirs2]) => `${p}(${dirs2.join(",")})`).join(" vs ")}`
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return issues;
|
|
1834
|
+
}
|
|
1835
|
+
function resolveFrom(dir, name) {
|
|
1836
|
+
try {
|
|
1837
|
+
const req = createRequire(path17.join(dir, "__noop__.js"));
|
|
1838
|
+
return realpathSync2(req.resolve(name));
|
|
1839
|
+
} catch {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/doctor/rules/sdk-internal-import.ts
|
|
1845
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1846
|
+
import path18 from "path";
|
|
1847
|
+
var SDK_PUBLIC_ENTRIES = /* @__PURE__ */ new Set([
|
|
1848
|
+
"@tbox.cn/app-sdk",
|
|
1849
|
+
"@tbox.cn/app-sdk/server",
|
|
1850
|
+
"@tbox.cn/app-sdk/client",
|
|
1851
|
+
"@tbox.cn/app-sdk/platform"
|
|
1852
|
+
]);
|
|
1853
|
+
var FRAMEWORK_INTERNAL_PATTERNS = [
|
|
1854
|
+
{ re: /^@mastra\/core/, label: "@mastra/core*\uFF08\u6846\u67B6\u5185\u90E8\uFF0C\u6539\u7ECF @tbox.cn/app-sdk\uFF09" },
|
|
1855
|
+
{ re: /^@ag-ui\//, label: "@ag-ui/*\uFF08\u6846\u67B6\u5185\u90E8\uFF0C\u6539\u7ECF @tbox.cn/app-sdk\uFF09" },
|
|
1856
|
+
{ re: /^@ai-sdk\//, label: "@ai-sdk/*\uFF08\u6846\u67B6\u5185\u90E8\uFF09" },
|
|
1857
|
+
{ re: /^ai$/, label: "ai\uFF08\u6846\u67B6\u5185\u90E8\uFF09" },
|
|
1858
|
+
{ re: /^zustand$/, label: "zustand\uFF08\u6846\u67B6\u5185\u90E8\uFF09" }
|
|
1859
|
+
];
|
|
1860
|
+
async function sdkInternalImportRule(ctx) {
|
|
1861
|
+
const issues = [];
|
|
1862
|
+
const roots = ["apps", "packages"];
|
|
1863
|
+
for (const root of roots) {
|
|
1864
|
+
const rootDir = path18.join(ctx.appDir, root);
|
|
1865
|
+
if (!existsSync14(rootDir)) continue;
|
|
1866
|
+
const files = [];
|
|
1867
|
+
await walkFiles(rootDir, ctx.appDir, files);
|
|
1868
|
+
for (const rel of files) {
|
|
1869
|
+
if (rel.startsWith("packages/app-sdk/") || rel.startsWith("packages/contracts/")) continue;
|
|
1870
|
+
const src = await readTextOrEmpty(path18.join(ctx.appDir, rel));
|
|
1871
|
+
for (const spec of src.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
|
|
1872
|
+
const target = spec[1];
|
|
1873
|
+
if (target.startsWith("@tbox.cn/app-sdk/") && !SDK_PUBLIC_ENTRIES.has(target)) {
|
|
1874
|
+
issues.push({
|
|
1875
|
+
rule: "sdk-internal-import",
|
|
1876
|
+
level: "error",
|
|
1877
|
+
message: `${rel}: \u4E0D\u5141\u8BB8 import SDK \u5185\u90E8\u8DEF\u5F84 ${target}`
|
|
1878
|
+
});
|
|
1879
|
+
} else if (target.startsWith("@tbox.cn/app-contracts/")) {
|
|
1880
|
+
issues.push({
|
|
1881
|
+
rule: "sdk-internal-import",
|
|
1882
|
+
level: "error",
|
|
1883
|
+
message: `${rel}: \u4E0D\u5141\u8BB8 import \u5951\u7EA6\u5B50\u8DEF\u5F84 ${target}\uFF08\u4EC5 @tbox.cn/app-contracts\uFF09`
|
|
1884
|
+
});
|
|
1885
|
+
} else if (isModuleFile(ctx.appDir, rel)) {
|
|
1886
|
+
for (const pat of FRAMEWORK_INTERNAL_PATTERNS) {
|
|
1887
|
+
if (pat.re.test(target)) {
|
|
1888
|
+
issues.push({
|
|
1889
|
+
rule: "sdk-internal-import",
|
|
1890
|
+
level: "error",
|
|
1891
|
+
message: `${rel}: \u6A21\u5757\u4E0D\u5141\u8BB8\u76F4\u63A5 import ${pat.label}`
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return issues;
|
|
1900
|
+
}
|
|
1901
|
+
function isModuleFile(appDir, rel) {
|
|
1902
|
+
const m = rel.match(/^packages\/([^/]+)\//);
|
|
1903
|
+
if (!m) return false;
|
|
1904
|
+
return existsSync14(path18.join(appDir, "packages", m[1], "tbox.component.json"));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/doctor/rules/assembly-drift.ts
|
|
1908
|
+
function assemblyDriftRule(ctx) {
|
|
1909
|
+
const issues = [];
|
|
1910
|
+
for (const file of ASSEMBLY_FILES) {
|
|
1911
|
+
const declaredIds = new Set(
|
|
1912
|
+
ctx.declared.filter((m) => contributesTo(file.kind, m)).map((m) => m.id)
|
|
1913
|
+
);
|
|
1914
|
+
const actualIds = new Set(ctx.actual[file.kind]?.moduleIds ?? []);
|
|
1915
|
+
const missing = [...declaredIds].filter((id) => !actualIds.has(id));
|
|
1916
|
+
const extra = [...actualIds].filter((id) => !declaredIds.has(id));
|
|
1917
|
+
if (missing.length > 0) {
|
|
1918
|
+
issues.push({
|
|
1919
|
+
rule: "assembly-drift",
|
|
1920
|
+
level: "warning",
|
|
1921
|
+
message: `${file.path}: \u5DF2\u58F0\u660E\u672A\u88C5\u914D \u2192 ${missing.join(", ")}`
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
if (extra.length > 0) {
|
|
1925
|
+
issues.push({
|
|
1926
|
+
rule: "assembly-drift",
|
|
1927
|
+
level: "warning",
|
|
1928
|
+
message: `${file.path}: \u5DF2\u88C5\u914D\u672A\u58F0\u660E \u2192 ${extra.join(", ")}`
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
const broken = ctx.actual[file.kind]?.broken ?? [];
|
|
1932
|
+
if (broken.length > 0) {
|
|
1933
|
+
issues.push({
|
|
1934
|
+
rule: "assembly-drift",
|
|
1935
|
+
level: "warning",
|
|
1936
|
+
message: `${file.path}: \u951A\u533A\u7834\u574F \u2192 ${broken.join(", ")}`
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return issues;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// src/doctor/rules/schema-version.ts
|
|
1944
|
+
async function schemaVersionRule(ctx) {
|
|
1945
|
+
const issues = [];
|
|
1946
|
+
for (const mod of await collectModuleCards(ctx)) {
|
|
1947
|
+
for (const [cardType, declaredSv] of Object.entries(mod.declared)) {
|
|
1948
|
+
const serverSv = mod.serverSchemaVersions[cardType] ?? 1;
|
|
1949
|
+
if (serverSv !== declaredSv) {
|
|
1950
|
+
issues.push({
|
|
1951
|
+
rule: "schema-version",
|
|
1952
|
+
level: "error",
|
|
1953
|
+
message: `${mod.id}: \u5361\u7247 ${cardType} contributes.schemaVersion=${declaredSv} \u4E0E meta schemaVersion=${serverSv} \u4E0D\u4E00\u81F4`
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return issues;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// src/doctor/rules/naked-write-route.ts
|
|
1962
|
+
import { existsSync as existsSync15 } from "fs";
|
|
1963
|
+
import path19 from "path";
|
|
1964
|
+
var ALLOWED_POST_PREFIXES = ["/webhook", "/upload", "/demo", "/run"];
|
|
1965
|
+
async function nakedWriteRouteRule(ctx) {
|
|
1966
|
+
const issues = [];
|
|
1967
|
+
for (const mod of await listLocalModules(ctx.appDir)) {
|
|
1968
|
+
const serverDir = path19.join(mod.dir, "src/server");
|
|
1969
|
+
if (!existsSync15(serverDir)) continue;
|
|
1970
|
+
const files = [];
|
|
1971
|
+
await walkFiles(serverDir, mod.dir, files);
|
|
1972
|
+
for (const rel of files) {
|
|
1973
|
+
const src = await readTextOrEmpty(path19.join(mod.dir, rel));
|
|
1974
|
+
for (const m of src.matchAll(/\.(?:post|put|patch|delete)\(\s*['"]([^'"]+)['"]/g)) {
|
|
1975
|
+
const route = m[1];
|
|
1976
|
+
if (!route.startsWith("/")) continue;
|
|
1977
|
+
const allowed = ALLOWED_POST_PREFIXES.some((p) => route.startsWith(p));
|
|
1978
|
+
if (!allowed) {
|
|
1979
|
+
issues.push({
|
|
1980
|
+
rule: "naked-write-route",
|
|
1981
|
+
level: "warning",
|
|
1982
|
+
message: `${mod.id}/${rel}: \u88F8\u5199\u8DEF\u7531 ${m[0].replace("(", " (")} \u5EFA\u8BAE\u6539\u8D70 ctx.actions.registerAction\uFF08\u5168\u5C40\u52A8\u4F5C\u5206\u53D1\u5668\uFF09`
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return issues;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/doctor/rules/descriptor-schema.ts
|
|
1992
|
+
import { existsSync as existsSync16 } from "fs";
|
|
1993
|
+
import { readdir as readdir9 } from "fs/promises";
|
|
1994
|
+
import path20 from "path";
|
|
1995
|
+
async function descriptorSchemaRule(ctx) {
|
|
1996
|
+
const issues = [];
|
|
1997
|
+
const packagesDir = path20.join(ctx.appDir, "packages");
|
|
1998
|
+
if (!existsSync16(packagesDir)) return issues;
|
|
1999
|
+
for (const entry of await readdir9(packagesDir, { withFileTypes: true })) {
|
|
2000
|
+
if (!entry.isDirectory()) continue;
|
|
2001
|
+
const file = path20.join(packagesDir, entry.name, "tbox.component.json");
|
|
2002
|
+
if (!existsSync16(file)) continue;
|
|
2003
|
+
try {
|
|
2004
|
+
const parsed = parseComponentDescriptor(JSON.parse(await readTextOrEmpty(file)));
|
|
2005
|
+
if (!parsed.ok) {
|
|
2006
|
+
issues.push({
|
|
2007
|
+
rule: "descriptor-schema",
|
|
2008
|
+
level: "error",
|
|
2009
|
+
message: `${entry.name}/tbox.component.json: ${parsed.errors.join("; ")}`
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
} catch {
|
|
2013
|
+
issues.push({
|
|
2014
|
+
rule: "descriptor-schema",
|
|
2015
|
+
level: "error",
|
|
2016
|
+
message: `${entry.name}/tbox.component.json: JSON \u89E3\u6790\u5931\u8D25`
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return issues;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/doctor/rules/workspace-ref-resolved.ts
|
|
2024
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2025
|
+
import { readdir as readdir10 } from "fs/promises";
|
|
2026
|
+
import path21 from "path";
|
|
2027
|
+
async function workspaceRefResolvedRule(ctx) {
|
|
2028
|
+
const issues = [];
|
|
2029
|
+
if (existsSync17(path21.join(ctx.appDir, "pnpm-workspace.template.yaml"))) return issues;
|
|
2030
|
+
const memberIndex = /* @__PURE__ */ new Map();
|
|
2031
|
+
for (const root of ["apps", "packages"]) {
|
|
2032
|
+
const rootDir = path21.join(ctx.appDir, root);
|
|
2033
|
+
if (!existsSync17(rootDir)) continue;
|
|
2034
|
+
for (const entry of await readdir10(rootDir, { withFileTypes: true })) {
|
|
2035
|
+
if (!entry.isDirectory()) continue;
|
|
2036
|
+
const dir = path21.join(rootDir, entry.name);
|
|
2037
|
+
const pkg = readPkgJson(dir);
|
|
2038
|
+
if (pkg?.name) memberIndex.set(pkg.name, path21.join(root, entry.name));
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (memberIndex.size === 0) return issues;
|
|
2042
|
+
for (const [name, rel] of memberIndex) {
|
|
2043
|
+
const pkg = readPkgJson(path21.join(ctx.appDir, rel));
|
|
2044
|
+
if (!pkg) continue;
|
|
2045
|
+
for (const key of ["dependencies", "peerDependencies", "devDependencies"]) {
|
|
2046
|
+
const deps = pkg[key];
|
|
2047
|
+
if (!deps || typeof deps !== "object") continue;
|
|
2048
|
+
for (const [depName, spec] of Object.entries(deps)) {
|
|
2049
|
+
if (typeof spec !== "string" || !spec.startsWith("workspace:")) continue;
|
|
2050
|
+
if (!memberIndex.has(depName)) {
|
|
2051
|
+
issues.push({
|
|
2052
|
+
rule: "workspace-ref-resolved",
|
|
2053
|
+
level: "error",
|
|
2054
|
+
message: `${rel}: workspace:* \u5F15\u7528 ${depName} \u672A\u547D\u4E2D\u4EFB\u4F55\u6210\u5458\uFF08packages/ \u6216 apps/\uFF09`
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return issues;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/doctor/rules/legacy-platform-files.ts
|
|
2064
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2065
|
+
import path22 from "path";
|
|
2066
|
+
var LEGACY_PATHS = [
|
|
2067
|
+
"apps/server/src/auth",
|
|
2068
|
+
"apps/server/src/agent",
|
|
2069
|
+
"apps/server/src/gateway",
|
|
2070
|
+
"apps/server/src/http",
|
|
2071
|
+
"apps/server/src/plugins/integration.ts",
|
|
2072
|
+
"apps/server/src/config/tts-config.ts",
|
|
2073
|
+
"apps/server/src/config/model-providers.ts",
|
|
2074
|
+
"apps/client/src/env",
|
|
2075
|
+
"apps/client/src/bridge",
|
|
2076
|
+
"apps/client/src/adapters",
|
|
2077
|
+
"apps/client/src/services/http.ts",
|
|
2078
|
+
"apps/client/src/services/auth-client.ts",
|
|
2079
|
+
"apps/client/src/services/user-id.ts",
|
|
2080
|
+
"apps/client/src/services/tbox-session.ts"
|
|
2081
|
+
];
|
|
2082
|
+
function legacyPlatformFilesRule(ctx) {
|
|
2083
|
+
const issues = [];
|
|
2084
|
+
for (const rel of LEGACY_PATHS) {
|
|
2085
|
+
if (existsSync18(path22.join(ctx.appDir, rel))) {
|
|
2086
|
+
issues.push({
|
|
2087
|
+
rule: "legacy-platform-files",
|
|
2088
|
+
level: "error",
|
|
2089
|
+
message: `${rel} \u4E3A\u65E7\u5F62\u6001\u5E73\u53F0\u6587\u4EF6\uFF08007\uFF1A\u5E73\u53F0\u80FD\u529B\u5DF2\u4E0B\u6C89 @tbox.cn/app-sdk\uFF09\u3002\u8BF7\u5220\u9664\u5E76\u6539\u7528 SDK API\uFF08server: createAuthRuntime/createPlatformApiRouter/createTtsProxy/createModelProvider/createPluginIntegration\uFF1Bclient: createAuthClient/createPlatformAdapters/createPlatformApi/configureRuntime\uFF09\uFF0C\u8BE6\u89C1 docs/proposals/007\u3002`
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return issues;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/doctor/index.ts
|
|
2097
|
+
var DOCTOR_RULES = [
|
|
2098
|
+
{ name: "workspace-dag", run: workspaceDagRule },
|
|
2099
|
+
{ name: "register-pure", run: registerPureRule },
|
|
2100
|
+
{ name: "card-type-consistent", run: cardTypeConsistentRule },
|
|
2101
|
+
{ name: "client-card-cover", run: clientCardCoverRule },
|
|
2102
|
+
{ name: "contract-version", run: contractVersionRule },
|
|
2103
|
+
{ name: "module-deps-assembled", run: moduleDepsAssembledRule },
|
|
2104
|
+
{ name: "singleton", run: singletonRule },
|
|
2105
|
+
{ name: "sdk-internal-import", run: sdkInternalImportRule },
|
|
2106
|
+
{ name: "assembly-drift", run: assemblyDriftRule },
|
|
2107
|
+
{ name: "schema-version", run: schemaVersionRule },
|
|
2108
|
+
{ name: "naked-write-route", run: nakedWriteRouteRule },
|
|
2109
|
+
{ name: "descriptor-schema", run: descriptorSchemaRule },
|
|
2110
|
+
{ name: "workspace-ref-resolved", run: workspaceRefResolvedRule },
|
|
2111
|
+
{ name: "legacy-platform-files", run: legacyPlatformFilesRule }
|
|
2112
|
+
];
|
|
2113
|
+
async function runDoctor(ctx, opts) {
|
|
2114
|
+
const issues = [];
|
|
2115
|
+
const only = opts?.only?.length ? new Set(opts.only) : null;
|
|
2116
|
+
const skip = new Set(opts?.skip ?? []);
|
|
2117
|
+
for (const rule of DOCTOR_RULES) {
|
|
2118
|
+
if (only && !only.has(rule.name)) continue;
|
|
2119
|
+
if (skip.has(rule.name)) continue;
|
|
2120
|
+
try {
|
|
2121
|
+
issues.push(...await rule.run(ctx));
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
issues.push({
|
|
2124
|
+
rule: rule.name,
|
|
2125
|
+
level: "error",
|
|
2126
|
+
message: `\u89C4\u5219\u6267\u884C\u5F02\u5E38: ${err?.message ?? String(err)}`
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return issues;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// src/deps-resolve.ts
|
|
2134
|
+
function isContractId(id, localIds) {
|
|
2135
|
+
return !localIds.has(id) || id.startsWith("contracts-");
|
|
2136
|
+
}
|
|
2137
|
+
async function resolveDependencies(appDir, rootIds, declared, extraRoots = []) {
|
|
2138
|
+
const mods = await listLocalModules(appDir);
|
|
2139
|
+
const localIds = new Set(mods.map((m) => m.id));
|
|
2140
|
+
const installedIds = new Set(declared?.map((m) => m.id) ?? []);
|
|
2141
|
+
const pkgVersion = /* @__PURE__ */ new Map();
|
|
2142
|
+
for (const mod of mods) {
|
|
2143
|
+
pkgVersion.set(mod.id, mod.descriptor.version ?? "0.0.0");
|
|
2144
|
+
}
|
|
2145
|
+
const extraDeps = new Map(extraRoots.map((r) => [r.id, r.deps]));
|
|
2146
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2147
|
+
const edges = [];
|
|
2148
|
+
const queue = [...rootIds, ...extraRoots.map((r) => r.id)];
|
|
2149
|
+
while (queue.length > 0) {
|
|
2150
|
+
const id = queue.shift();
|
|
2151
|
+
if (visited.has(id)) continue;
|
|
2152
|
+
visited.add(id);
|
|
2153
|
+
const mod = mods.find((m) => m.id === id);
|
|
2154
|
+
const deps = extraDeps.get(id) ?? mod?.descriptor.dependencies?.modules ?? [];
|
|
2155
|
+
for (const dep of deps) {
|
|
2156
|
+
edges.push(dep);
|
|
2157
|
+
if (!visited.has(dep.id) && (localIds.has(dep.id) || extraDeps.has(dep.id))) queue.push(dep.id);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const WHITE = 0;
|
|
2161
|
+
const GRAY = 1;
|
|
2162
|
+
const BLACK = 2;
|
|
2163
|
+
const color = /* @__PURE__ */ new Map();
|
|
2164
|
+
const cycles = [];
|
|
2165
|
+
function dfs(id, stack) {
|
|
2166
|
+
color.set(id, GRAY);
|
|
2167
|
+
stack.push(id);
|
|
2168
|
+
for (const dep of adjacency.get(id) ?? []) {
|
|
2169
|
+
const c = color.get(dep) ?? WHITE;
|
|
2170
|
+
if (c === GRAY) {
|
|
2171
|
+
const idx = stack.indexOf(dep);
|
|
2172
|
+
if (idx >= 0) cycles.push([...stack.slice(idx), dep]);
|
|
2173
|
+
} else if (c === WHITE) {
|
|
2174
|
+
dfs(dep, stack);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
stack.pop();
|
|
2178
|
+
color.set(id, BLACK);
|
|
2179
|
+
}
|
|
2180
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2181
|
+
const modById = new Map(mods.map((m) => [m.id, m]));
|
|
2182
|
+
for (const id of visited) {
|
|
2183
|
+
const mod = modById.get(id);
|
|
2184
|
+
const deps = extraDeps.get(id) ?? mod?.descriptor.dependencies?.modules ?? [];
|
|
2185
|
+
adjacency.set(id, deps.filter((d) => visited.has(d.id)).map((d) => d.id));
|
|
2186
|
+
}
|
|
2187
|
+
for (const id of visited) {
|
|
2188
|
+
if ((color.get(id) ?? WHITE) === WHITE) dfs(id, []);
|
|
2189
|
+
}
|
|
2190
|
+
const toInstall = [...visited].filter((id) => !installedIds.has(id));
|
|
2191
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2192
|
+
for (const id of toInstall) inDegree.set(id, 0);
|
|
2193
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
2194
|
+
for (const id of toInstall) {
|
|
2195
|
+
for (const dep of adjacency.get(id) ?? []) {
|
|
2196
|
+
if (inDegree.has(dep)) {
|
|
2197
|
+
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
2198
|
+
const list = dependents.get(dep);
|
|
2199
|
+
if (list) list.push(id);
|
|
2200
|
+
else dependents.set(dep, [id]);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
const ready = toInstall.filter((id) => (inDegree.get(id) ?? 0) === 0);
|
|
2205
|
+
const order = [];
|
|
2206
|
+
while (ready.length > 0) {
|
|
2207
|
+
const id = ready.shift();
|
|
2208
|
+
order.push(id);
|
|
2209
|
+
for (const dependent of dependents.get(id) ?? []) {
|
|
2210
|
+
const nd = (inDegree.get(dependent) ?? 0) - 1;
|
|
2211
|
+
inDegree.set(dependent, nd);
|
|
2212
|
+
if (nd === 0) ready.push(dependent);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (order.length !== toInstall.length) {
|
|
2216
|
+
}
|
|
2217
|
+
const contracts = /* @__PURE__ */ new Set();
|
|
2218
|
+
const business = /* @__PURE__ */ new Set();
|
|
2219
|
+
const conflicts = [];
|
|
2220
|
+
for (const edge of edges) {
|
|
2221
|
+
const id = edge.id;
|
|
2222
|
+
if (!installedIds.has(id) || !pkgVersion.has(id)) {
|
|
2223
|
+
if (isContractId(id, localIds)) contracts.add(id);
|
|
2224
|
+
else if (!isContractId(id, localIds)) business.add(id);
|
|
2225
|
+
}
|
|
2226
|
+
if (edge.range && pkgVersion.has(id)) {
|
|
2227
|
+
const targetVersion = pkgVersion.get(id);
|
|
2228
|
+
if (edge.range !== "*" && !edge.range.startsWith("workspace:")) {
|
|
2229
|
+
const inter = intersectRanges2(edge.range, targetVersion);
|
|
2230
|
+
if (!inter) {
|
|
2231
|
+
conflicts.push(`${id}: \u4F9D\u8D56\u8303\u56F4 ${edge.range} \u4E0E\u76EE\u6807\u7248\u672C ${targetVersion} \u65E0\u4EA4\u96C6`);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
return {
|
|
2237
|
+
order,
|
|
2238
|
+
contracts: [...contracts],
|
|
2239
|
+
business: [...business],
|
|
2240
|
+
cycles,
|
|
2241
|
+
conflicts
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// src/commands/add.ts
|
|
2246
|
+
function readSourceDescriptor(sourceDir) {
|
|
2247
|
+
const file = path23.join(sourceDir, "tbox.component.json");
|
|
2248
|
+
if (!existsSync19(file)) return null;
|
|
2249
|
+
try {
|
|
2250
|
+
const parsed = parseComponentDescriptor(JSON.parse(readFileSync8(file, "utf8")));
|
|
2251
|
+
return parsed.ok ? parsed.value : null;
|
|
2252
|
+
} catch {
|
|
2253
|
+
return null;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function resolveDepSource(sourcePath, depId) {
|
|
2257
|
+
if (!sourcePath) return null;
|
|
2258
|
+
const dir = path23.dirname(sourcePath);
|
|
2259
|
+
const siblingDir = path23.join(dir, depId);
|
|
2260
|
+
if (existsSync19(path23.join(siblingDir, "package.json"))) return siblingDir;
|
|
2261
|
+
try {
|
|
2262
|
+
const tgz = readdirSync2(dir).find(
|
|
2263
|
+
(f) => f.endsWith(".tgz") && f.includes(`-${depId}-`)
|
|
2264
|
+
);
|
|
2265
|
+
if (tgz) return path23.join(dir, tgz);
|
|
2266
|
+
} catch {
|
|
2267
|
+
}
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
function isInstalled(appDir, id) {
|
|
2271
|
+
if (existsSync19(path23.join(appDir, "packages", id))) return true;
|
|
2272
|
+
const manifest = readAppManifest(appDir);
|
|
2273
|
+
return manifest?.npmModules.some((m) => m.id === id && m.mode === "sdk") ?? false;
|
|
2274
|
+
}
|
|
2275
|
+
async function addModule(opts) {
|
|
2276
|
+
await addModuleWithDeps(opts, /* @__PURE__ */ new Set(), false);
|
|
2277
|
+
}
|
|
2278
|
+
async function addModuleWithDeps(opts, visited, asDependency) {
|
|
2279
|
+
const tarballs = [];
|
|
2280
|
+
try {
|
|
2281
|
+
await addModuleWithDepsCore(opts, visited, asDependency, tarballs);
|
|
2282
|
+
} finally {
|
|
2283
|
+
for (const t of tarballs) t.dispose();
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
async function addModuleWithDepsCore(opts, visited, asDependency, tarballs) {
|
|
2287
|
+
const appDir = process.cwd();
|
|
2288
|
+
if (!readAppManifest(appDir)) throw new Error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F tbox-app \u5E94\u7528\uFF08\u7F3A .tbox/app.json\uFF09");
|
|
2289
|
+
let id;
|
|
2290
|
+
let sourceDescriptor = null;
|
|
2291
|
+
let opened = null;
|
|
2292
|
+
const isReg = !!opts.source && isRegistrySpec(opts.source);
|
|
2293
|
+
if (opts.mode === "local") {
|
|
2294
|
+
const short = opts.pkg;
|
|
2295
|
+
id = short.startsWith("module-") ? short : `module-${short}`;
|
|
2296
|
+
assertValidModuleId(id, `--mode local ${opts.pkg}`);
|
|
2297
|
+
} else {
|
|
2298
|
+
if (!opts.source) throw new Error(`--mode ${opts.mode} \u9700\u8981 --source <\u76EE\u5F55\u6216 tgz \u6216 registry \u5305>`);
|
|
2299
|
+
const isRegSource = isReg && !existsSync19(path23.resolve(opts.source));
|
|
2300
|
+
if (!isRegSource && !opts.source.endsWith(".tgz") && !LocalDirSource.isUsable(opts.source)) {
|
|
2301
|
+
throw new Error(`\u6A21\u5757\u6E90\u65E0\u6548\uFF08\u7F3A package.json\uFF09: ${opts.source}`);
|
|
2302
|
+
}
|
|
2303
|
+
opened = openSource(opts.source, opts.registry);
|
|
2304
|
+
if (opened.source instanceof TarballSource || opened.source instanceof RegistrySource) {
|
|
2305
|
+
tarballs.push(opened.source);
|
|
2306
|
+
}
|
|
2307
|
+
const pkgOnly = opts.pkg.replace(/@[^/]+$/, "");
|
|
2308
|
+
id = pkgOnly.replace(/^@tbox\.cn\/app-/, "").replace(/^@[^/]+\//, "");
|
|
2309
|
+
assertValidModuleId(id, `--mode ${opts.mode} ${opts.pkg}`);
|
|
2310
|
+
sourceDescriptor = readSourceDescriptor(opened.sourceDir);
|
|
2311
|
+
}
|
|
2312
|
+
if (asDependency && isInstalled(appDir, id)) {
|
|
2313
|
+
console.log(` \u26A0\uFE0F \u4F9D\u8D56 ${id} \u5DF2\u5B89\u88C5\uFF0C\u8DF3\u8FC7`);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (!asDependency && isInstalled(appDir, id)) {
|
|
2317
|
+
const where = existsSync19(path23.join(appDir, "packages", id)) ? `packages/${id}` : "manifest \u5DF2\u767B\u8BB0\uFF08sdk \u5305\uFF0C\u65E0\u672C\u5730\u76EE\u5F55\uFF09";
|
|
2318
|
+
throw new Error(`\u6A21\u5757 ${id} \u5DF2\u5B58\u5728\uFF08${where}\uFF09\u3002\u82E5\u8981\u91CD\u65B0\u5C55\u5F00\u8BF7\u5148 remove \u6216\u624B\u52A8\u5904\u7406\uFF1B\u88C5\u914D\u552F\u4E00\u8986\u76D6\u5165\u53E3\u4E3A sync --force`);
|
|
2319
|
+
}
|
|
2320
|
+
if (visited.has(id)) return;
|
|
2321
|
+
visited.add(id);
|
|
2322
|
+
if (!opts.noDeps) {
|
|
2323
|
+
const ownDeps = sourceDescriptor?.dependencies?.modules ?? [];
|
|
2324
|
+
const resolution = await resolveDependencies(appDir, [], void 0, [
|
|
2325
|
+
{ id, deps: ownDeps }
|
|
2326
|
+
]);
|
|
2327
|
+
if (resolution.cycles.length > 0) {
|
|
2328
|
+
throw new Error(
|
|
2329
|
+
`\u4F9D\u8D56\u73AF\uFF1A${resolution.cycles.map((c) => c.join(" \u2192 ")).join("\uFF1B")}`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
for (const c of resolution.conflicts) console.warn(` \u26A0\uFE0F \u4F9D\u8D56\u7248\u672C\u51B2\u7A81: ${c}`);
|
|
2333
|
+
const installSeq = [
|
|
2334
|
+
...resolution.contracts,
|
|
2335
|
+
...resolution.order
|
|
2336
|
+
].filter((depId) => depId !== id && !isInstalled(appDir, depId));
|
|
2337
|
+
for (const depId of [...new Set(installSeq)]) {
|
|
2338
|
+
const depPkg = /^(module|scenario|contracts)-/.test(depId) ? `@tbox.cn/app-${depId}` : `@tbox.cn/${depId}`;
|
|
2339
|
+
const depSource = isReg ? depPkg : resolveDepSource(opts.source, depId);
|
|
2340
|
+
if (!depSource) {
|
|
2341
|
+
console.warn(` \u26A0\uFE0F \u4F9D\u8D56 ${depId} \u672A\u5B89\u88C5\u4E14\u672A\u627E\u5230\u6E90\uFF08--source \u5144\u5F1F\u76EE\u5F55/tgz\uFF09\uFF0C\u53EF --no-deps \u8DF3\u8FC7`);
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
const depOpened = openSource(depSource, opts.registry);
|
|
2345
|
+
if (depOpened.source instanceof TarballSource || depOpened.source instanceof RegistrySource) {
|
|
2346
|
+
tarballs.push(depOpened.source);
|
|
2347
|
+
}
|
|
2348
|
+
const depIsBusiness = existsSync19(path23.join(depOpened.sourceDir, "tbox.component.json"));
|
|
2349
|
+
await addModuleWithDeps(
|
|
2350
|
+
{ pkg: depPkg, mode: depIsBusiness ? "codegen" : "sdk", source: depSource, registry: opts.registry },
|
|
2351
|
+
visited,
|
|
2352
|
+
true
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
let entry;
|
|
2357
|
+
if (opts.mode === "local") {
|
|
2358
|
+
const short = opts.pkg;
|
|
2359
|
+
const localId = short.startsWith("module-") ? short : `module-${short}`;
|
|
2360
|
+
await createLocalSkeleton(appDir, short);
|
|
2361
|
+
entry = { id: localId, package: `@app/${localId}`, version: "0.1.0", mode: "local" };
|
|
2362
|
+
await updateAppsDependency(appDir, `@app/${localId}`, "add");
|
|
2363
|
+
} else {
|
|
2364
|
+
const sourceDir = opened.sourceDir;
|
|
2365
|
+
const version = opened.source.resolveVersion();
|
|
2366
|
+
if (opts.mode === "codegen") {
|
|
2367
|
+
await expandCodegen(appDir, sourceDir, { id, package: `@app/${id}`, version, mode: "codegen", baseVersion: version });
|
|
2368
|
+
await updateAppsDependency(appDir, `@app/${id}`, "add");
|
|
2369
|
+
}
|
|
2370
|
+
entry = {
|
|
2371
|
+
id,
|
|
2372
|
+
package: opts.pkg.replace(/@[^/]+$/, ""),
|
|
2373
|
+
version,
|
|
2374
|
+
mode: opts.mode,
|
|
2375
|
+
...opts.mode === "codegen" ? { baseVersion: version } : {}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
const manifest = readAppManifest(appDir);
|
|
2379
|
+
await writeAppManifest(appDir, upsertNpmModule(manifest, entry));
|
|
2380
|
+
const sync = await syncAll(appDir);
|
|
2381
|
+
if (sync.changedFiles.length > 0) {
|
|
2382
|
+
console.log(` \u88C5\u914D\u66F4\u65B0: ${sync.changedFiles.join(", ")}`);
|
|
2383
|
+
}
|
|
2384
|
+
for (const w of sync.warnings) console.warn(` \u26A0\uFE0F ${w}`);
|
|
2385
|
+
for (const c of sync.depConflicts) console.warn(` \u26A0\uFE0F \u4F9D\u8D56\u51B2\u7A81: ${c}`);
|
|
2386
|
+
const actual = await collectActual(appDir);
|
|
2387
|
+
const issues = await runDoctor({ appDir, declared: sync.declared, actual });
|
|
2388
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
2389
|
+
if (errors.length > 0) {
|
|
2390
|
+
console.error("\u274C doctor \u9519\u8BEF\uFF1A");
|
|
2391
|
+
for (const e of errors) console.error(` - [${e.rule}] ${e.message}`);
|
|
2392
|
+
process.exitCode = 1;
|
|
2393
|
+
}
|
|
2394
|
+
console.log(`\u2705 \u5DF2\u5B89\u88C5 ${entry.package}\uFF08mode=${opts.mode}${asDependency ? ", \u4F9D\u8D56" : ""}\uFF09`);
|
|
2395
|
+
if (opts.mode !== "local") {
|
|
2396
|
+
console.log(` \u4E0B\u4E00\u6B65: pnpm install && tbox-app module doctor`);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// src/commands/remove.ts
|
|
2401
|
+
async function removeModule(id, opts = {}) {
|
|
2402
|
+
const appDir = process.cwd();
|
|
2403
|
+
assertValidModuleId(id, "remove \u53C2\u6570");
|
|
2404
|
+
const manifest = readAppManifest(appDir);
|
|
2405
|
+
if (!manifest) throw new Error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F tbox-app \u5E94\u7528\uFF08\u7F3A .tbox/app.json\uFF09");
|
|
2406
|
+
if (!opts.force) {
|
|
2407
|
+
const blockers = [];
|
|
2408
|
+
for (const mod of await listLocalModules(appDir)) {
|
|
2409
|
+
if (mod.id === id) continue;
|
|
2410
|
+
const dep = (mod.descriptor.dependencies?.modules ?? []).find((d) => d.id === id);
|
|
2411
|
+
if (dep && dep.required) blockers.push(mod.id);
|
|
2412
|
+
}
|
|
2413
|
+
if (blockers.length > 0) {
|
|
2414
|
+
throw new Error(`\u6A21\u5757 ${id} \u88AB\u4F9D\u8D56\uFF0C\u65E0\u6CD5\u79FB\u9664\uFF1A${blockers.join(", ")}\uFF08--force \u5FFD\u7565\uFF09`);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const entry = manifest.npmModules.find((m) => m.id === id);
|
|
2418
|
+
const isNpmModule = Boolean(entry);
|
|
2419
|
+
if (isNpmModule) {
|
|
2420
|
+
await writeAppManifest(appDir, removeNpmModule(manifest, id));
|
|
2421
|
+
}
|
|
2422
|
+
const removedDir = await removePackageDir(appDir, id);
|
|
2423
|
+
if (removedDir) console.log(` \u5DF2\u5220\u9664 packages/${id}`);
|
|
2424
|
+
else if (entry?.mode === "sdk") console.log(` \u5DF2\u79FB\u9664 manifest \u767B\u8BB0\uFF08sdk \u5305\uFF0C\u65E0\u672C\u5730\u76EE\u5F55\uFF09`);
|
|
2425
|
+
if (entry?.mode === "codegen" || entry?.mode === "local" || !isNpmModule) {
|
|
2426
|
+
await updateAppsDependency(appDir, `@app/${id}`, "remove");
|
|
2427
|
+
}
|
|
2428
|
+
const sync = await syncAll(appDir);
|
|
2429
|
+
if (sync.changedFiles.length > 0) {
|
|
2430
|
+
console.log(` \u88C5\u914D\u66F4\u65B0: ${sync.changedFiles.join(", ")}`);
|
|
2431
|
+
}
|
|
2432
|
+
for (const w of sync.warnings) console.warn(` \u26A0\uFE0F ${w}`);
|
|
2433
|
+
for (const c of sync.depConflicts) console.warn(` \u26A0\uFE0F \u4F9D\u8D56\u51B2\u7A81: ${c}`);
|
|
2434
|
+
const actual = await collectActual(appDir);
|
|
2435
|
+
const issues = await runDoctor({ appDir, declared: sync.declared, actual });
|
|
2436
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
2437
|
+
if (errors.length > 0) {
|
|
2438
|
+
console.error("\u274C doctor \u9519\u8BEF\uFF1A");
|
|
2439
|
+
for (const e of errors) console.error(` - [${e.rule}] ${e.message}`);
|
|
2440
|
+
process.exitCode = 1;
|
|
2441
|
+
}
|
|
2442
|
+
console.log(`\u2705 \u5DF2\u79FB\u9664 ${id}`);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/commands/sync.ts
|
|
2446
|
+
async function syncCommand(opts = {}) {
|
|
2447
|
+
const appDir = process.cwd();
|
|
2448
|
+
const sync = await syncAll(appDir, { force: opts.force ?? false });
|
|
2449
|
+
if (sync.changedFiles.length === 0) {
|
|
2450
|
+
console.log("\u88C5\u914D\u5DF2\u540C\u6B65\uFF0C\u65E0\u53D8\u66F4");
|
|
2451
|
+
} else {
|
|
2452
|
+
console.log(`\u5DF2\u66F4\u65B0\u88C5\u914D\u6587\u4EF6: ${sync.changedFiles.join(", ")}`);
|
|
2453
|
+
}
|
|
2454
|
+
for (const w of sync.warnings) console.warn(` \u26A0\uFE0F ${w}`);
|
|
2455
|
+
for (const c of sync.depConflicts) console.warn(` \u26A0\uFE0F \u4F9D\u8D56\u51B2\u7A81: ${c}`);
|
|
2456
|
+
const actual = await collectActual(appDir);
|
|
2457
|
+
const issues = await runDoctor({ appDir, declared: sync.declared, actual });
|
|
2458
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
2459
|
+
if (errors.length > 0) {
|
|
2460
|
+
console.error("\u274C doctor \u9519\u8BEF\uFF1A");
|
|
2461
|
+
for (const e of errors) console.error(` - [${e.rule}] ${e.message}`);
|
|
2462
|
+
process.exitCode = 1;
|
|
2463
|
+
} else {
|
|
2464
|
+
console.log("\u2705 doctor \u65E0\u9519\u8BEF");
|
|
2465
|
+
}
|
|
2466
|
+
for (const w of issues.filter((i) => i.level === "warning")) {
|
|
2467
|
+
console.warn(` \u26A0\uFE0F [${w.rule}] ${w.message}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/commands/list.ts
|
|
2472
|
+
async function listCommand() {
|
|
2473
|
+
const appDir = process.cwd();
|
|
2474
|
+
const manifest = readAppManifest(appDir);
|
|
2475
|
+
const declared = await collectDeclared(appDir);
|
|
2476
|
+
console.log(`\u6A21\u677F\u7248\u672C: ${manifest?.templateVersion ?? "\u672A\u77E5"}`);
|
|
2477
|
+
if (declared.length === 0) {
|
|
2478
|
+
console.log("\uFF08\u65E0\u5DF2\u88C5\u6A21\u5757\uFF09");
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
for (const m of declared) {
|
|
2482
|
+
console.log(` ${m.id.padEnd(28)} ${m.pkg} [${m.mode}]`);
|
|
2483
|
+
}
|
|
2484
|
+
const npmIds = new Set((manifest?.npmModules ?? []).map((m) => m.id));
|
|
2485
|
+
const localOnly = (await listLocalModules(appDir)).filter((m) => !npmIds.has(m.id));
|
|
2486
|
+
if (localOnly.length > 0) {
|
|
2487
|
+
console.log(" \u672C\u5730\u6A21\u5757\uFF08\u672A\u5728 app.json \u767B\u8BB0\uFF09:");
|
|
2488
|
+
for (const m of localOnly) console.log(` ${m.id}`);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/commands/diff.ts
|
|
2493
|
+
async function diffCommand(module) {
|
|
2494
|
+
const appDir = process.cwd();
|
|
2495
|
+
const declared = await collectDeclared(appDir);
|
|
2496
|
+
const actual = await collectActual(appDir);
|
|
2497
|
+
let dirty = false;
|
|
2498
|
+
for (const file of ASSEMBLY_FILES) {
|
|
2499
|
+
const kind = file.kind;
|
|
2500
|
+
const declaredIds = declared.filter((m) => contributesTo(kind, m)).map((m) => m.id);
|
|
2501
|
+
const actualIds = actual[kind]?.moduleIds ?? [];
|
|
2502
|
+
let missing = declaredIds.filter((id) => !actualIds.includes(id));
|
|
2503
|
+
let extra = actualIds.filter((id) => !declaredIds.includes(id));
|
|
2504
|
+
if (module) {
|
|
2505
|
+
missing = missing.filter((id) => id === module);
|
|
2506
|
+
extra = extra.filter((id) => id === module);
|
|
2507
|
+
}
|
|
2508
|
+
const broken = actual[kind]?.broken ?? [];
|
|
2509
|
+
const nonStandard = actual[kind]?.nonStandardLines ?? [];
|
|
2510
|
+
const lines = [];
|
|
2511
|
+
if (broken.length > 0) lines.push(`\u26A0\uFE0F \u951A\u533A\u7834\u574F: ${broken.join(", ")}`);
|
|
2512
|
+
if (missing.length > 0) lines.push(` - \u5DF2\u58F0\u660E\u672A\u88C5\u914D: ${missing.join(", ")}`);
|
|
2513
|
+
if (extra.length > 0) lines.push(` + \u5DF2\u88C5\u914D\u672A\u58F0\u660E: ${extra.join(", ")}`);
|
|
2514
|
+
for (const ns of nonStandard) lines.push(` ? \u975E\u6807\u884C: ${ns}`);
|
|
2515
|
+
if (lines.length > 0) {
|
|
2516
|
+
dirty = true;
|
|
2517
|
+
console.log(`${file.path}`);
|
|
2518
|
+
for (const l of lines) console.log(` ${l}`);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
if (!dirty) {
|
|
2522
|
+
console.log("\u65E0\u6F02\u79FB\uFF1A\u58F0\u660E\u96C6\u5408\u4E0E\u5B9E\u9645\u88C5\u914D\u96C6\u5408\u4E00\u81F4");
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// src/commands/doctor.ts
|
|
2527
|
+
async function doctorCommand(opts = {}) {
|
|
2528
|
+
const appDir = process.cwd();
|
|
2529
|
+
const declared = await collectDeclared(appDir);
|
|
2530
|
+
const actual = await collectActual(appDir);
|
|
2531
|
+
const issues = await runDoctor(
|
|
2532
|
+
{ appDir, declared, actual },
|
|
2533
|
+
{
|
|
2534
|
+
only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
2535
|
+
skip: opts.skip ? opts.skip.split(",").map((s) => s.trim()).filter(Boolean) : void 0
|
|
2536
|
+
}
|
|
2537
|
+
);
|
|
2538
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
2539
|
+
const warnings = issues.filter((i) => i.level === "warning");
|
|
2540
|
+
if (opts.json) {
|
|
2541
|
+
console.log(JSON.stringify({ errors, warnings, rules: issues }, null, 2));
|
|
2542
|
+
if (errors.length > 0) process.exitCode = 1;
|
|
2543
|
+
return { issues };
|
|
2544
|
+
}
|
|
2545
|
+
if (issues.length === 0) {
|
|
2546
|
+
console.log("\u2705 doctor \u5168\u7EFF\uFF08\u7ED3\u6784\u6821\u9A8C\u901A\u8FC7\uFF09");
|
|
2547
|
+
} else {
|
|
2548
|
+
for (const e of errors) console.error(`\u274C [${e.rule}] ${e.message}`);
|
|
2549
|
+
for (const w of warnings) console.warn(`\u26A0\uFE0F [${w.rule}] ${w.message}`);
|
|
2550
|
+
if (errors.length > 0) {
|
|
2551
|
+
console.error(`doctor \u5931\u8D25: ${errors.length} \u4E2A\u9519\u8BEF`);
|
|
2552
|
+
process.exitCode = 1;
|
|
2553
|
+
} else {
|
|
2554
|
+
console.log(`doctor \u901A\u8FC7\uFF08${warnings.length} \u4E2A\u544A\u8B66\uFF09`);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
return { issues };
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/commands/update.ts
|
|
2561
|
+
import { existsSync as existsSync20 } from "fs";
|
|
2562
|
+
import { readdir as readdir11, readFile as readFile6, unlink, writeFile as writeFile7 } from "fs/promises";
|
|
2563
|
+
import path24 from "path";
|
|
2564
|
+
|
|
2565
|
+
// src/merge3.ts
|
|
2566
|
+
import { diffArrays } from "diff";
|
|
2567
|
+
function computeHunks(base, target) {
|
|
2568
|
+
const changes = diffArrays(base, target);
|
|
2569
|
+
const hunks = [];
|
|
2570
|
+
let baseIdx = 0;
|
|
2571
|
+
let pending = null;
|
|
2572
|
+
for (const c of changes) {
|
|
2573
|
+
if (c.removed) {
|
|
2574
|
+
if (!pending) pending = { start: baseIdx, oldLines: [...c.value], newLines: [] };
|
|
2575
|
+
else pending.oldLines.push(...c.value);
|
|
2576
|
+
baseIdx += c.value.length;
|
|
2577
|
+
} else if (c.added) {
|
|
2578
|
+
if (!pending) pending = { start: baseIdx, oldLines: [], newLines: [...c.value] };
|
|
2579
|
+
else pending.newLines.push(...c.value);
|
|
2580
|
+
} else {
|
|
2581
|
+
if (pending) {
|
|
2582
|
+
hunks.push(pending);
|
|
2583
|
+
pending = null;
|
|
2584
|
+
}
|
|
2585
|
+
baseIdx += c.value.length;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (pending) hunks.push(pending);
|
|
2589
|
+
return hunks;
|
|
2590
|
+
}
|
|
2591
|
+
function merge3(base, ours, theirs) {
|
|
2592
|
+
const baseLines = base === "" ? [] : base.split("\n");
|
|
2593
|
+
const oursLines = ours === "" ? [] : ours.split("\n");
|
|
2594
|
+
const theirsLines = theirs === "" ? [] : theirs.split("\n");
|
|
2595
|
+
const oursHunks = computeHunks(baseLines, oursLines);
|
|
2596
|
+
const theirsHunks = computeHunks(baseLines, theirsLines);
|
|
2597
|
+
const byStart = /* @__PURE__ */ new Map();
|
|
2598
|
+
for (const h of oursHunks) byStart.set(h.start, { ours: h });
|
|
2599
|
+
for (const h of theirsHunks) {
|
|
2600
|
+
const existing = byStart.get(h.start);
|
|
2601
|
+
if (existing) existing.theirs = h;
|
|
2602
|
+
else byStart.set(h.start, { theirs: h });
|
|
2603
|
+
}
|
|
2604
|
+
const out = [];
|
|
2605
|
+
const conflicts = [];
|
|
2606
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
2607
|
+
const applyHunk = (o, t) => {
|
|
2608
|
+
const oldLen = Math.max(o?.oldLines.length ?? 0, t?.oldLines.length ?? 0);
|
|
2609
|
+
if (o && t) {
|
|
2610
|
+
const same = JSON.stringify(o.oldLines) === JSON.stringify(t.oldLines) && JSON.stringify(o.newLines) === JSON.stringify(t.newLines);
|
|
2611
|
+
if (same) {
|
|
2612
|
+
out.push(...o.newLines);
|
|
2613
|
+
} else {
|
|
2614
|
+
conflicts.push([...o.newLines, "--", ...t.newLines].join("\n"));
|
|
2615
|
+
out.push("<<<<<<< ours", ...o.newLines, "=======", ...t.newLines, ">>>>>>> theirs");
|
|
2616
|
+
}
|
|
2617
|
+
} else if (o) {
|
|
2618
|
+
out.push(...o.newLines);
|
|
2619
|
+
} else {
|
|
2620
|
+
out.push(...t.newLines);
|
|
2621
|
+
}
|
|
2622
|
+
return oldLen;
|
|
2623
|
+
};
|
|
2624
|
+
let i = 0;
|
|
2625
|
+
while (i < baseLines.length) {
|
|
2626
|
+
const entry = byStart.get(i);
|
|
2627
|
+
if (!entry || consumed.has(i)) {
|
|
2628
|
+
out.push(baseLines[i]);
|
|
2629
|
+
i++;
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
consumed.add(i);
|
|
2633
|
+
const oldLen = applyHunk(entry.ours, entry.theirs);
|
|
2634
|
+
if (oldLen > 0) i += oldLen;
|
|
2635
|
+
}
|
|
2636
|
+
const tail = byStart.get(baseLines.length);
|
|
2637
|
+
if (tail && !consumed.has(baseLines.length)) {
|
|
2638
|
+
applyHunk(tail.ours, tail.theirs);
|
|
2639
|
+
}
|
|
2640
|
+
return { merged: out.join("\n"), conflicts };
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/commands/update.ts
|
|
2644
|
+
var EXCLUDED = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".tbox"]);
|
|
2645
|
+
function openSourceDir(source) {
|
|
2646
|
+
if (source.endsWith(".tgz") && existsSync20(source)) {
|
|
2647
|
+
const tarball = new TarballSource(source);
|
|
2648
|
+
return { dir: tarball.getDir(), tarball };
|
|
2649
|
+
}
|
|
2650
|
+
return { dir: path24.resolve(source), tarball: null };
|
|
2651
|
+
}
|
|
2652
|
+
async function readPackageFiles(dir) {
|
|
2653
|
+
const map = /* @__PURE__ */ new Map();
|
|
2654
|
+
async function walk(rel) {
|
|
2655
|
+
const full = path24.join(dir, rel);
|
|
2656
|
+
const entries = await readdir11(full, { withFileTypes: true });
|
|
2657
|
+
for (const entry of entries) {
|
|
2658
|
+
if (EXCLUDED.has(entry.name)) continue;
|
|
2659
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
2660
|
+
const childFull = path24.join(full, entry.name);
|
|
2661
|
+
if (entry.isDirectory()) {
|
|
2662
|
+
await walk(child);
|
|
2663
|
+
} else {
|
|
2664
|
+
try {
|
|
2665
|
+
map.set(child, await readFile6(childFull, "utf8"));
|
|
2666
|
+
} catch {
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
await walk("");
|
|
2672
|
+
return map;
|
|
2673
|
+
}
|
|
2674
|
+
async function updateModule(opts) {
|
|
2675
|
+
const appDir = process.cwd();
|
|
2676
|
+
const manifest = readAppManifest(appDir);
|
|
2677
|
+
if (!manifest) throw new Error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F tbox-app \u5E94\u7528\uFF08\u7F3A .tbox/app.json\uFF09");
|
|
2678
|
+
const entry = manifest.npmModules.find((m) => m.id === opts.module);
|
|
2679
|
+
if (!entry) throw new Error(`\u6A21\u5757 ${opts.module} \u672A\u5728 .tbox/app.json \u767B\u8BB0`);
|
|
2680
|
+
if (entry.mode === "sdk") {
|
|
2681
|
+
if (opts.dryRun) {
|
|
2682
|
+
console.log(`[dry-run] pnpm update ${entry.package}`);
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
const { execFileSync: execFileSync4 } = await import("child_process");
|
|
2686
|
+
execFileSync4("pnpm", ["update", entry.package], { stdio: "inherit", cwd: appDir });
|
|
2687
|
+
console.log(`\u2705 \u5DF2\u5347\u7EA7 ${entry.package}\uFF08sdk \u6A21\u5F0F\uFF09`);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
const pkgDir = path24.join(appDir, "packages", opts.module);
|
|
2691
|
+
if (!existsSync20(pkgDir)) throw new Error(`\u7F3A\u5C11 packages/${opts.module}`);
|
|
2692
|
+
const ours = await readPackageFiles(pkgDir);
|
|
2693
|
+
const tarballs = [];
|
|
2694
|
+
try {
|
|
2695
|
+
let theirs;
|
|
2696
|
+
let theirsVersion;
|
|
2697
|
+
if (opts.source) {
|
|
2698
|
+
const src = openSourceDir(opts.source);
|
|
2699
|
+
if (src.tarball) tarballs.push(src.tarball);
|
|
2700
|
+
theirs = await readPackageFiles(src.dir);
|
|
2701
|
+
theirsVersion = src.tarball ? src.tarball.resolveVersion() : readPkgJson(src.dir)?.version ?? entry.version;
|
|
2702
|
+
} else {
|
|
2703
|
+
throw new Error("codegen \u5347\u7EA7\u9700\u8981 --source <\u65B0\u7248\u672C\u5730\u76EE\u5F55\u6216 tgz>\uFF08npm \u62C9\u53D6\u4E3A manual\uFF0C\u771F\u5B9E\u53D1\u5E03\u540E\u56DE\u843D\uFF09");
|
|
2704
|
+
}
|
|
2705
|
+
let base = /* @__PURE__ */ new Map();
|
|
2706
|
+
if (opts.baseSource) {
|
|
2707
|
+
const bsrc = openSourceDir(opts.baseSource);
|
|
2708
|
+
if (bsrc.tarball) tarballs.push(bsrc.tarball);
|
|
2709
|
+
base = await readPackageFiles(bsrc.dir);
|
|
2710
|
+
}
|
|
2711
|
+
const allKeys = /* @__PURE__ */ new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]);
|
|
2712
|
+
let mergedFiles = /* @__PURE__ */ new Map();
|
|
2713
|
+
const conflictList = [];
|
|
2714
|
+
for (const key of allKeys) {
|
|
2715
|
+
const b = base.get(key) ?? "";
|
|
2716
|
+
const o = ours.get(key) ?? "";
|
|
2717
|
+
const t = theirs.get(key) ?? "";
|
|
2718
|
+
if (opts.ours) {
|
|
2719
|
+
if (o) mergedFiles.set(key, o);
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
if (opts.theirs) {
|
|
2723
|
+
if (t) mergedFiles.set(key, t);
|
|
2724
|
+
continue;
|
|
2725
|
+
}
|
|
2726
|
+
const result = merge3(b, o, t);
|
|
2727
|
+
mergedFiles.set(key, result.merged);
|
|
2728
|
+
if (result.conflicts.length > 0) {
|
|
2729
|
+
conflictList.push(`${key}: ${result.conflicts.length} \u5904\u51B2\u7A81`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (opts.dryRun) {
|
|
2733
|
+
console.log(`[dry-run] ${opts.module}: ${allKeys.size} \u6587\u4EF6\u5408\u5E76\uFF0C\u51B2\u7A81 ${conflictList.length} \u5904`);
|
|
2734
|
+
for (const c of conflictList) console.log(` \u26A0\uFE0F ${c}`);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
for (const [key, content] of mergedFiles) {
|
|
2738
|
+
const out = path24.join(pkgDir, key);
|
|
2739
|
+
if (content === "" && existsSync20(out)) {
|
|
2740
|
+
await unlink(out);
|
|
2741
|
+
console.log(` \u{1F5D1} \u5DF2\u5220\u9664 ${key}\uFF08\u5347\u7EA7\u6E90\u5220\u9664\u8BE5\u6587\u4EF6\uFF09`);
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
await writeFile7(out, content, "utf8");
|
|
2745
|
+
}
|
|
2746
|
+
await writeAppManifest(appDir, updateNpmModule(manifest, opts.module, { baseVersion: theirsVersion, version: theirsVersion }));
|
|
2747
|
+
const sync = await syncAll(appDir);
|
|
2748
|
+
const actual = await collectActual(appDir);
|
|
2749
|
+
const issues = await runDoctor({ appDir, declared: sync.declared, actual });
|
|
2750
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
2751
|
+
if (errors.length > 0) {
|
|
2752
|
+
console.error("\u274C doctor \u9519\u8BEF\uFF1A");
|
|
2753
|
+
for (const e of errors) console.error(` - [${e.rule}] ${e.message}`);
|
|
2754
|
+
process.exitCode = 1;
|
|
2755
|
+
}
|
|
2756
|
+
if (conflictList.length > 0) {
|
|
2757
|
+
console.warn("\u26A0\uFE0F \u5347\u7EA7\u5B8C\u6210\uFF0C\u5B58\u5728\u51B2\u7A81\u6807\u8BB0\uFF08<<<<<<< ours / >>>>>>> theirs\uFF09\uFF0C\u8BF7\u4EBA\u5DE5/Agent \u89E3\u51B3\u540E\u8FD0\u884C doctor:");
|
|
2758
|
+
for (const c of conflictList) console.warn(` - ${c}`);
|
|
2759
|
+
} else {
|
|
2760
|
+
console.log(`\u2705 \u5DF2\u5347\u7EA7 ${opts.module} \u2192 ${theirsVersion}`);
|
|
2761
|
+
}
|
|
2762
|
+
} finally {
|
|
2763
|
+
for (const t of tarballs) t.dispose();
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/commands/publish.ts
|
|
2768
|
+
import { existsSync as existsSync22, readFileSync as readFileSync10, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
|
|
2769
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2770
|
+
import path26 from "path";
|
|
2771
|
+
|
|
2772
|
+
// src/commands/pack.ts
|
|
2773
|
+
import { existsSync as existsSync21, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
|
|
2774
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2775
|
+
import path25 from "path";
|
|
2776
|
+
function tarballFileName(name, version) {
|
|
2777
|
+
const plain = name.replace(/^@/, "").replace("/", "-");
|
|
2778
|
+
return `${plain}-${version}.tgz`;
|
|
2779
|
+
}
|
|
2780
|
+
function validatePublishableModule(moduleDir) {
|
|
2781
|
+
if (!existsSync21(moduleDir) || !existsSync21(path25.join(moduleDir, "package.json"))) {
|
|
2782
|
+
throw new Error(`\u6A21\u5757\u76EE\u5F55\u65E0\u6548\uFF08\u7F3A package.json\uFF09: ${moduleDir}`);
|
|
2783
|
+
}
|
|
2784
|
+
let pkg;
|
|
2785
|
+
try {
|
|
2786
|
+
pkg = JSON.parse(readFileSync9(path25.join(moduleDir, "package.json"), "utf8"));
|
|
2787
|
+
} catch {
|
|
2788
|
+
throw new Error(`package.json \u89E3\u6790\u5931\u8D25: ${path25.join(moduleDir, "package.json")}`);
|
|
2789
|
+
}
|
|
2790
|
+
const name = typeof pkg.name === "string" ? pkg.name : "";
|
|
2791
|
+
if (!/^@tbox\.cn\//.test(name)) {
|
|
2792
|
+
throw new Error(`\u53D1\u5E03\u5305\u540D\u5FC5\u987B\u4E3A @tbox.cn/* scope: ${name || "(\u7F3A\u5931)"}`);
|
|
2793
|
+
}
|
|
2794
|
+
const version = typeof pkg.version === "string" ? pkg.version : "";
|
|
2795
|
+
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
|
2796
|
+
throw new Error(`\u7248\u672C\u53F7\u65E0\u6548\uFF08\u9700 semver x.y.z\uFF09: ${version || "(\u7F3A\u5931)"}`);
|
|
2797
|
+
}
|
|
2798
|
+
return { name, version };
|
|
2799
|
+
}
|
|
2800
|
+
function packModule(moduleDir, outDir) {
|
|
2801
|
+
const { name, version } = validatePublishableModule(moduleDir);
|
|
2802
|
+
const absOut = path25.resolve(outDir);
|
|
2803
|
+
execFileSync3("pnpm", ["pack", "--pack-destination", absOut], { stdio: "inherit", cwd: moduleDir });
|
|
2804
|
+
return path25.join(absOut, tarballFileName(name, version));
|
|
2805
|
+
}
|
|
2806
|
+
function detectKind(pkgDir, name) {
|
|
2807
|
+
if (existsSync21(path25.join(pkgDir, "pnpm-workspace.template.yaml"))) return "template";
|
|
2808
|
+
if (existsSync21(path25.join(pkgDir, "tbox.component.json"))) return "module";
|
|
2809
|
+
if (/^@[^/]+\/app-contracts/.test(name)) return "contract";
|
|
2810
|
+
return "generic";
|
|
2811
|
+
}
|
|
2812
|
+
function listAllFiles(dir) {
|
|
2813
|
+
const out = [];
|
|
2814
|
+
function walk(rel) {
|
|
2815
|
+
const full = path25.join(dir, rel);
|
|
2816
|
+
for (const entry of readdirSync3(full, { withFileTypes: true })) {
|
|
2817
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
2818
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
2819
|
+
if (entry.isDirectory()) walk(child);
|
|
2820
|
+
else out.push(child);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
walk("");
|
|
2824
|
+
return out;
|
|
2825
|
+
}
|
|
2826
|
+
function collectExportTargets(exports) {
|
|
2827
|
+
const targets = [];
|
|
2828
|
+
const push = (v) => {
|
|
2829
|
+
if (typeof v === "string") targets.push(v);
|
|
2830
|
+
};
|
|
2831
|
+
if (typeof exports === "string") push(exports);
|
|
2832
|
+
else if (exports && typeof exports === "object") {
|
|
2833
|
+
for (const v of Object.values(exports)) {
|
|
2834
|
+
if (typeof v === "string") push(v);
|
|
2835
|
+
else if (v && typeof v === "object") {
|
|
2836
|
+
for (const t of Object.values(v)) push(t);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return targets;
|
|
2841
|
+
}
|
|
2842
|
+
function verifyPkgDir(pkgDir, kind) {
|
|
2843
|
+
const issues = [];
|
|
2844
|
+
const pkgFile = path25.join(pkgDir, "package.json");
|
|
2845
|
+
let pkg;
|
|
2846
|
+
try {
|
|
2847
|
+
pkg = JSON.parse(readFileSync9(pkgFile, "utf8"));
|
|
2848
|
+
} catch {
|
|
2849
|
+
issues.push({ rule: "pack-manifest", level: "error", message: "\u4EA7\u7269\u7F3A\u5C11\u53EF\u89E3\u6790\u7684 package.json" });
|
|
2850
|
+
return issues;
|
|
2851
|
+
}
|
|
2852
|
+
const kindOf = kind ?? detectKind(pkgDir, pkg.name ?? "");
|
|
2853
|
+
const files = listAllFiles(pkgDir);
|
|
2854
|
+
for (const key of ["dependencies", "peerDependencies", "devDependencies"]) {
|
|
2855
|
+
const deps = pkg[key];
|
|
2856
|
+
if (!deps || typeof deps !== "object") continue;
|
|
2857
|
+
for (const [n, spec] of Object.entries(deps)) {
|
|
2858
|
+
if (typeof spec === "string" && /^(workspace:|catalog:)/.test(spec)) {
|
|
2859
|
+
issues.push({
|
|
2860
|
+
rule: "pack-protocol-residue",
|
|
2861
|
+
level: "error",
|
|
2862
|
+
message: `${n}: \u53D1\u5E03\u6001\u6B8B\u7559 pnpm \u79C1\u6709\u534F\u8BAE "${spec}"\uFF08\u5E94\u5DF2\u5F52\u4E00\u5316\u4E3A\u5B9E\u88C5\u7248\u672C\uFF09`
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (pkg.exports !== void 0) {
|
|
2868
|
+
for (const t of collectExportTargets(pkg.exports)) {
|
|
2869
|
+
if (t.includes("/src/") || t === "src") {
|
|
2870
|
+
issues.push({
|
|
2871
|
+
rule: "pack-exports",
|
|
2872
|
+
level: "error",
|
|
2873
|
+
message: `exports \u6307\u5411\u5F00\u53D1\u6001\u6E90\u7801 "${t}"\uFF08\u53D1\u5E03\u6001\u5E94\u6307\u5411 dist/\uFF09`
|
|
2874
|
+
});
|
|
2875
|
+
} else if (t.startsWith("./") && !existsSync21(path25.join(pkgDir, t))) {
|
|
2876
|
+
issues.push({
|
|
2877
|
+
rule: "pack-exports",
|
|
2878
|
+
level: "error",
|
|
2879
|
+
message: `exports \u76EE\u6807\u7F3A\u5931\uFF08\u4EA7\u7269\u5185\u4E0D\u5B58\u5728\uFF09: ${t}`
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
const filesField = pkg.files;
|
|
2885
|
+
if (Array.isArray(filesField) && filesField.length > 0) {
|
|
2886
|
+
const allow = /* @__PURE__ */ new Set([...filesField.map((w) => String(w)), "package.json"]);
|
|
2887
|
+
const bad = files.filter((f) => ![...allow].some((w) => f === w || f.startsWith(`${w}/`)));
|
|
2888
|
+
if (bad.length > 0) {
|
|
2889
|
+
issues.push({
|
|
2890
|
+
rule: "pack-whitelist",
|
|
2891
|
+
level: "error",
|
|
2892
|
+
message: `\u4EA7\u7269\u542B\u767D\u540D\u5355\u5916\u6587\u4EF6: ${bad.slice(0, 5).join(", ")}${bad.length > 5 ? ` \u7B49 ${bad.length} \u4E2A` : ""}`
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
} else {
|
|
2896
|
+
issues.push({
|
|
2897
|
+
rule: "pack-whitelist",
|
|
2898
|
+
level: "warning",
|
|
2899
|
+
message: "\u672A\u58F0\u660E files \u767D\u540D\u5355\uFF0C\u4EA7\u7269\u8303\u56F4\u4E0D\u53EF\u63A7\uFF08\u5EFA\u8BAE\u58F0\u660E files \u9650\u5236\u6253\u5305\u5185\u5BB9\uFF09"
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
const leaked = files.filter((f) => /(^|\/)\.env$/.test(f) || /(^|\/)\.npmrc$/.test(f));
|
|
2903
|
+
if (leaked.length > 0) {
|
|
2904
|
+
issues.push({ rule: "pack-leak", level: "error", message: `\u4EA7\u7269\u542B\u654F\u611F\u6587\u4EF6: ${leaked.join(", ")}` });
|
|
2905
|
+
}
|
|
2906
|
+
if (kindOf === "module") {
|
|
2907
|
+
for (const required of ["src", "dist", "tbox.component.json"]) {
|
|
2908
|
+
if (!files.includes(required) && !existsSync21(path25.join(pkgDir, required))) {
|
|
2909
|
+
issues.push({ rule: "pack-files", level: "error", message: `\u4E1A\u52A1\u6A21\u5757\u4EA7\u7269\u7F3A\u5C11 ${required}` });
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
} else if (kindOf === "template") {
|
|
2913
|
+
for (const required of [
|
|
2914
|
+
"pnpm-workspace.template.yaml",
|
|
2915
|
+
"apps/server/package.json",
|
|
2916
|
+
"apps/client/package.json",
|
|
2917
|
+
"packages/module-weather/package.json",
|
|
2918
|
+
".tbox/app.json",
|
|
2919
|
+
"tsconfig.base.json",
|
|
2920
|
+
"scripts/verify-generated.mjs"
|
|
2921
|
+
]) {
|
|
2922
|
+
if (!existsSync21(path25.join(pkgDir, required))) {
|
|
2923
|
+
issues.push({ rule: "pack-files", level: "error", message: `\u6A21\u677F\u4EA7\u7269\u7F3A\u5C11 ${required}` });
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
for (const leaked2 of ["pnpm-workspace.yaml", "pnpm-lock.yaml"]) {
|
|
2927
|
+
if (existsSync21(path25.join(pkgDir, leaked2))) {
|
|
2928
|
+
issues.push({
|
|
2929
|
+
rule: "pack-files",
|
|
2930
|
+
level: "error",
|
|
2931
|
+
message: `\u6A21\u677F\u4EA7\u7269\u6CC4\u6F0F ${leaked2}\uFF08\u5E94\u4EC5\u542B pnpm-workspace.template.yaml\uFF09`
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
} else if (kindOf === "contract") {
|
|
2936
|
+
const dts = files.filter((f) => f.startsWith("dist/") && f.endsWith(".d.ts"));
|
|
2937
|
+
if (dts.length === 0) {
|
|
2938
|
+
issues.push({ rule: "pack-files", level: "error", message: "\u5951\u7EA6\u5305\u4EA7\u7269\u7F3A\u5C11 dist/*.d.ts" });
|
|
2939
|
+
}
|
|
2940
|
+
} else {
|
|
2941
|
+
const bin = pkg.bin;
|
|
2942
|
+
const binTargets = typeof bin === "string" ? [bin] : bin && typeof bin === "object" ? Object.values(bin) : [];
|
|
2943
|
+
for (const target of binTargets) {
|
|
2944
|
+
if (typeof target === "string" && !files.includes(target)) {
|
|
2945
|
+
issues.push({ rule: "pack-files", level: "error", message: `bin \u6307\u5411\u6587\u4EF6\u7F3A\u5931: ${target}` });
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
const declaredDocs = Array.isArray(filesField) && filesField.length > 0;
|
|
2950
|
+
for (const doc of ["LICENSE", "README.md"]) {
|
|
2951
|
+
if (!files.includes(doc)) {
|
|
2952
|
+
issues.push({
|
|
2953
|
+
rule: "pack-docs",
|
|
2954
|
+
level: "warning",
|
|
2955
|
+
message: `\u4EA7\u7269\u7F3A\u5C11 ${doc}${declaredDocs ? "\uFF08files \u767D\u540D\u5355\u5DF2\u58F0\u660E\uFF1B\u53D1\u5E03\u5B8C\u5907\u6027 backlog\uFF09" : "\uFF08\u5EFA\u8BAE\u8865\u5145\u4EE5\u5B8C\u5907\u53D1\u5E03\u5143\u6570\u636E\uFF09"}`
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
return issues;
|
|
2960
|
+
}
|
|
2961
|
+
async function verifyTarball(tgzPath) {
|
|
2962
|
+
const tarball = new TarballSource(tgzPath);
|
|
2963
|
+
try {
|
|
2964
|
+
const pkgDir = tarball.getDir();
|
|
2965
|
+
const pkg = JSON.parse(await tarball.readFile("package.json"));
|
|
2966
|
+
const kind = detectKind(pkgDir, pkg.name ?? "");
|
|
2967
|
+
const issues = verifyPkgDir(pkgDir, kind);
|
|
2968
|
+
if (pkg.name && pkg.version) {
|
|
2969
|
+
const expect = tarballFileName(pkg.name, pkg.version);
|
|
2970
|
+
if (path25.basename(tgzPath) !== expect) {
|
|
2971
|
+
issues.push({
|
|
2972
|
+
rule: "pack-filename",
|
|
2973
|
+
level: "error",
|
|
2974
|
+
message: `tgz \u6587\u4EF6\u540D ${path25.basename(tgzPath)} \u4E0E\u5305 ${pkg.name}@${pkg.version} \u671F\u671B ${expect} \u4E0D\u4E00\u81F4`
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
return issues;
|
|
2979
|
+
} finally {
|
|
2980
|
+
tarball.dispose();
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
function printIssues(issues) {
|
|
2984
|
+
for (const i of issues) {
|
|
2985
|
+
console.log(` ${i.level === "error" ? "\u274C" : "\u26A0\uFE0F"} [${i.rule}] ${i.message}`);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
async function packCommand(opts) {
|
|
2989
|
+
const moduleDir = path25.resolve(process.cwd(), opts.module);
|
|
2990
|
+
const tgzPath = packModule(moduleDir, opts.out);
|
|
2991
|
+
console.log(`\u2705 \u5DF2\u6253\u5305 ${path25.basename(tgzPath)} \u2192 ${path25.resolve(opts.out)}`);
|
|
2992
|
+
if (opts.check) {
|
|
2993
|
+
const issues = await verifyTarball(tgzPath);
|
|
2994
|
+
printIssues(issues);
|
|
2995
|
+
if (issues.some((i) => i.level === "error")) process.exitCode = 1;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// src/commands/publish.ts
|
|
3000
|
+
async function publishModule(opts) {
|
|
3001
|
+
const cwd = process.cwd();
|
|
3002
|
+
const moduleDir = path26.resolve(cwd, opts.module);
|
|
3003
|
+
if (!existsSync22(moduleDir) || !existsSync22(path26.join(moduleDir, "package.json"))) {
|
|
3004
|
+
throw new Error(`\u6A21\u5757\u76EE\u5F55\u65E0\u6548\uFF08\u7F3A package.json\uFF09: ${opts.module}`);
|
|
3005
|
+
}
|
|
3006
|
+
let tgzPath;
|
|
3007
|
+
let workDir = null;
|
|
3008
|
+
if (opts.tgz) {
|
|
3009
|
+
tgzPath = path26.resolve(opts.tgz);
|
|
3010
|
+
if (!existsSync22(tgzPath)) throw new Error(`tgz \u4EA7\u7269\u4E0D\u5B58\u5728: ${opts.tgz}`);
|
|
3011
|
+
const tarball = new TarballSource(tgzPath);
|
|
3012
|
+
try {
|
|
3013
|
+
const tgzPkg = JSON.parse(await tarball.readFile("package.json"));
|
|
3014
|
+
const modulePkg = JSON.parse(readFileSync10(path26.join(moduleDir, "package.json"), "utf8"));
|
|
3015
|
+
if (tgzPkg.name !== modulePkg.name) {
|
|
3016
|
+
throw new Error(
|
|
3017
|
+
`--tgz \u5305\u540D ${tgzPkg.name ?? "(\u672A\u77E5)"} \u4E0E\u6A21\u5757 ${modulePkg.name ?? "(\u672A\u77E5)"} \u4E0D\u4E00\u81F4\uFF08\u8BF7\u786E\u8BA4 --tgz \u4E0E\u6A21\u5757\u5339\u914D\uFF09`
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
} finally {
|
|
3021
|
+
tarball.dispose();
|
|
3022
|
+
}
|
|
3023
|
+
} else {
|
|
3024
|
+
workDir = mkdtempSync2(path26.join(tmpdir2(), "tbox-publish-"));
|
|
3025
|
+
tgzPath = packModule(moduleDir, workDir);
|
|
3026
|
+
}
|
|
3027
|
+
try {
|
|
3028
|
+
const issues = await verifyTarball(tgzPath);
|
|
3029
|
+
if (issues.length > 0) printIssues(issues);
|
|
3030
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
3031
|
+
if (errors.length > 0) {
|
|
3032
|
+
throw new Error(`\u4EA7\u7269\u95E8\u7981\u5931\u8D25\uFF08${errors.length} \u5904 error\uFF09\uFF0C\u5DF2\u4E2D\u6B62\u53D1\u5E03: ${path26.basename(tgzPath)}`);
|
|
3033
|
+
}
|
|
3034
|
+
if (!workDir) workDir = mkdtempSync2(path26.join(tmpdir2(), "tbox-publish-"));
|
|
3035
|
+
const { execFileSync: execFileSync4 } = await import("child_process");
|
|
3036
|
+
const registryArgs = opts.registry ? ["--registry", opts.registry] : [];
|
|
3037
|
+
execFileSync4(
|
|
3038
|
+
"pnpm",
|
|
3039
|
+
["publish", tgzPath, "--access", opts.access ?? "public", ...registryArgs, ...opts.dryRun ? ["--dry-run"] : []],
|
|
3040
|
+
{ stdio: "inherit", cwd: workDir }
|
|
3041
|
+
);
|
|
3042
|
+
console.log(
|
|
3043
|
+
`\u2705 \u5DF2\u53D1\u5E03 ${path26.basename(tgzPath)}${opts.registry ? ` \u2192 ${opts.registry}` : ""}${opts.dryRun ? "\uFF08dry-run\uFF09" : ""}`
|
|
3044
|
+
);
|
|
3045
|
+
} finally {
|
|
3046
|
+
if (workDir) {
|
|
3047
|
+
try {
|
|
3048
|
+
rmSync2(workDir, { recursive: true, force: true });
|
|
3049
|
+
} catch {
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// src/index.ts
|
|
3056
|
+
function createProgram() {
|
|
3057
|
+
const program = new Command();
|
|
3058
|
+
program.name("tbox-app").description("tbox \u7EC4\u4EF6\u5316\u5E94\u7528 CLI\uFF1Acreate / module add|remove|list|sync|diff|doctor").version("0.1.0");
|
|
3059
|
+
program.command("create <name>").description("\u4ECE\u6A21\u677F\u751F\u6210\u591A\u5305 workspace \u5E94\u7528\u9AA8\u67B6").option("--source <\u8DEF\u5F84>", "\u6A21\u677F\u6E90\uFF08\u672C\u5730\u76EE\u5F55 / \u6A21\u677F pack tgz / registry \u5305 @scope/pkg[@version]\uFF09").option("--local-deps <tgz\u76EE\u5F55>", "\u6CE8\u5165\u5E73\u53F0 overrides\uFF08file: \u672C\u5730\u53D1\u5E03\u6001 tgz\uFF0Cinstall \u79BB\u7EBF\u9A8C\u8BC1\u6001\uFF1B\u751F\u4EA7 B \u6863\u4E0D\u4F20\uFF09").option("--registry <url>", "\u62C9\u53D6 registry\uFF08registry \u6E90\u65F6\u751F\u6548\uFF1B\u7F3A\u7701\u81EA\u52A8\u89E3\u6790\u73AF\u5883\u955C\u50CF\uFF09").option("--force", "\u8986\u76D6\u5DF2\u5B58\u5728\u76EE\u5F55").action(async (name, opts) => {
|
|
3060
|
+
try {
|
|
3061
|
+
await createApp({ name, source: opts.source ?? "", force: opts.force, localDeps: opts.localDeps, registry: opts.registry });
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
console.error("\u274C", err.message);
|
|
3064
|
+
process.exitCode = 1;
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
const moduleCmd = program.command("module").description("\u6A21\u5757\u7BA1\u7406\uFF08add/remove/list/sync/diff/doctor/update/publish/pack\uFF09");
|
|
3068
|
+
moduleCmd.command("add <pkg>").description("\u5B89\u88C5/\u5C55\u5F00\u6A21\u5757\uFF08--mode local|codegen|sdk\uFF1B--source \u652F\u6301\u76EE\u5F55/tgz/registry \u5305\uFF09").option("--mode <local|codegen|sdk>", "\u5206\u53D1\u6A21\u5F0F", "codegen").option("--source <\u8DEF\u5F84>", "\u6A21\u5757\u6E90\uFF08\u672C\u5730\u76EE\u5F55/tgz/registry \u5305 @scope/pkg[@version]\uFF09").option("--registry <url>", "\u62C9\u53D6 registry\uFF08registry \u6E90\u65F6\u751F\u6548\uFF1B\u7F3A\u7701\u81EA\u52A8\u89E3\u6790\u73AF\u5883\u955C\u50CF\uFF09").option("--no-deps", "\u8DF3\u8FC7\u4F9D\u8D56\u81EA\u52A8\u89E3\u6790\uFF08P3 W3-B \u9003\u751F\u95E8\uFF09").action(async (pkg, opts) => {
|
|
3069
|
+
try {
|
|
3070
|
+
await addModule({ pkg, mode: opts.mode, source: opts.source, registry: opts.registry, noDeps: !opts.deps });
|
|
3071
|
+
} catch (err) {
|
|
3072
|
+
console.error("\u274C", err.message);
|
|
3073
|
+
process.exitCode = 1;
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
moduleCmd.command("remove <id>").description("\u5378\u8F7D\u6A21\u5757\uFF08\u5220\u5305 + \u6E05\u7406\u88C5\u914D\uFF09").option("--force", "\u5FFD\u7565\u53CD\u5411\u4F9D\u8D56\u68C0\u67E5\u5F3A\u5236\u79FB\u9664").action(async (id, opts) => {
|
|
3077
|
+
try {
|
|
3078
|
+
await removeModule(id, { force: opts.force });
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
console.error("\u274C", err.message);
|
|
3081
|
+
process.exitCode = 1;
|
|
3082
|
+
}
|
|
3083
|
+
});
|
|
3084
|
+
moduleCmd.command("list").description("\u5217\u51FA\u5DF2\u88C5\u6A21\u5757").action(async () => {
|
|
3085
|
+
try {
|
|
3086
|
+
await listCommand();
|
|
3087
|
+
} catch (err) {
|
|
3088
|
+
console.error("\u274C", err.message);
|
|
3089
|
+
process.exitCode = 1;
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
moduleCmd.command("sync").description("\u589E\u91CF\u88C5\u914D 4 \u6587\u4EF6\uFF08\u4E0D\u8986\u76D6\uFF1B\u7ED3\u6784\u7834\u574F\u964D\u7EA7\u4E3A\u544A\u8B66\uFF09").option("--force", "\u5168\u91CF\u8986\u76D6\u88C5\u914D\u6587\u4EF6\uFF08002 \xA76.5\uFF1A\u552F\u4E00\u8986\u76D6\u5165\u53E3\uFF09").action(async (opts) => {
|
|
3093
|
+
try {
|
|
3094
|
+
await syncCommand({ force: opts.force });
|
|
3095
|
+
} catch (err) {
|
|
3096
|
+
console.error("\u274C", err.message);
|
|
3097
|
+
process.exitCode = 1;
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
moduleCmd.command("diff [module]").description("\u88C5\u914D\u6F02\u79FB\u53CC\u5411 diff\uFF08module \u53EF\u6307\u5B9A\uFF0C\u9ED8\u8BA4\u5168\u90E8\uFF09").action(async (module) => {
|
|
3101
|
+
try {
|
|
3102
|
+
await diffCommand(module);
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
console.error("\u274C", err.message);
|
|
3105
|
+
process.exitCode = 1;
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
moduleCmd.command("doctor").description("\u7ED3\u6784\u6821\u9A8C\uFF08doctor \u5B8C\u6574\u7248 13 \u6761\u89C4\u5219\uFF09").option("--json", "\u53EA\u8F93\u51FA JSON \u7ED3\u679C").option("--only <\u89C4\u5219\u5217\u8868>", "\u53EA\u8FD0\u884C\u6307\u5B9A\u89C4\u5219\uFF08\u9017\u53F7\u5206\u9694\uFF09").option("--skip <\u89C4\u5219\u5217\u8868>", "\u8DF3\u8FC7\u6307\u5B9A\u89C4\u5219\uFF08\u9017\u53F7\u5206\u9694\uFF09").action(async (opts) => {
|
|
3109
|
+
try {
|
|
3110
|
+
await doctorCommand(opts);
|
|
3111
|
+
} catch (err) {
|
|
3112
|
+
console.error("\u274C", err.message);
|
|
3113
|
+
process.exitCode = 1;
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
moduleCmd.command("update <module>").description("\u5347\u7EA7\u7EC4\u4EF6\uFF08codegen\u21923-way merge\uFF1Bsdk\u2192pnpm update\uFF09").option("--dry-run", "\u53EA\u6253\u5370\u5408\u5E76\u7ED3\u679C\u4E0D\u5199\u56DE").option("--ours", "\u51B2\u7A81\u5FEB\u901F\u53D6\u672C\u5730\u7248\u672C").option("--theirs", "\u51B2\u7A81\u5FEB\u901F\u53D6\u65B0\u7248\u7248\u672C").option("--source <\u8DEF\u5F84>", "\u65B0\u7248\u672C\u5730\u6E90\u76EE\u5F55\u6216 tgz").option("--base-source <\u8DEF\u5F84>", "base \u672C\u5730\u6E90\u76EE\u5F55\u6216 tgz").action(async (module, opts) => {
|
|
3117
|
+
try {
|
|
3118
|
+
await updateModule({ module, ...opts });
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
console.error("\u274C", err.message);
|
|
3121
|
+
process.exitCode = 1;
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
moduleCmd.command("pack <module>").description("\u672C\u5730\u6253\u5305\u4E0D\u53D1\u5E03\uFF08\u6A21\u5757\u76EE\u5F55\u5982 modules/module-member\uFF1Bpnpm pack \u5185\u5EFA\u5F52\u4E00\u5316 \u2192 \u53D1\u5E03\u6001 tgz\uFF09").requiredOption("--out <dir>", "\u4EA7\u7269\u76EE\u5F55\uFF08pnpm \u81EA\u52A8\u521B\u5EFA\uFF0C\u540C\u540D\u8986\u76D6\u5199\uFF09").option("--check", "\u4EA7\u7269\u65AD\u8A00\uFF08\u534F\u8BAE\u6B8B\u7559 / exports / \u767D\u540D\u5355 / \u6CC4\u6F0F / \u6587\u4EF6\u4E00\u81F4\u6027\uFF09").action(async (module, opts) => {
|
|
3125
|
+
try {
|
|
3126
|
+
await packCommand({ module, out: opts.out, check: opts.check });
|
|
3127
|
+
} catch (err) {
|
|
3128
|
+
console.error("\u274C", err.message);
|
|
3129
|
+
process.exitCode = 1;
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
moduleCmd.command("publish <module>").description("\u6A21\u5757\u664B\u5347 npm\uFF08\u6A21\u5757\u76EE\u5F55\u5982 modules/module-member\uFF1B\u6821\u9A8C + \u6253\u5305 + \u4EA7\u7269\u95E8\u7981 + pnpm publish <tgz>\uFF09").option("--tgz <path>", "\u590D\u7528\u5DF2\u9A8C\u8BC1\u4EA7\u7269\uFF08\u7F3A\u7701\u73B0\u573A pack \u5230\u4E34\u65F6\u76EE\u5F55\uFF09").option("--dry-run", "\u6821\u9A8C + \u95E8\u7981 + \u6253\u5370\uFF0C\u4E0D\u771F\u5B9E\u53D1\u5E03").option("--access <public|restricted>", "npm access", "public").option("--registry <url>", "\u53D1\u5E03 registry\uFF08\u7F3A\u7701 pnpm \u914D\u7F6E\u89E3\u6790\uFF1A@tbox.cn scope \u4F18\u5148\uFF09").action(async (module, opts) => {
|
|
3133
|
+
try {
|
|
3134
|
+
await publishModule({
|
|
3135
|
+
module,
|
|
3136
|
+
tgz: opts.tgz,
|
|
3137
|
+
dryRun: opts.dryRun,
|
|
3138
|
+
access: opts.access,
|
|
3139
|
+
registry: opts.registry
|
|
3140
|
+
});
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
console.error("\u274C", err.message);
|
|
3143
|
+
process.exitCode = 1;
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
return program;
|
|
3147
|
+
}
|
|
3148
|
+
var isMain = typeof process.argv[1] === "string" && realpathSync3(fileURLToPath(import.meta.url)) === realpathSync3(process.argv[1]);
|
|
3149
|
+
if (isMain) {
|
|
3150
|
+
createProgram().parseAsync(process.argv);
|
|
3151
|
+
}
|
|
3152
|
+
export {
|
|
3153
|
+
createProgram
|
|
3154
|
+
};
|